# Send Test Email

> Send one real email through a campaign's sending account.

Requires an API key with the `write` scope.

**This sends a real email** from one of the campaign's sending accounts to the address you give. It is not a dry run: use [Preview Campaign Email](/api-reference/campaigns/preview-campaign-email) to see copy without sending.

The email counts against the account's daily limit and the workspace's monthly limit, is logged as a campaign email, and updates the campaign's sent counters. `{spin|syntax}` and `{{merge}}` tags are filled in. The same checks run as for a message sent from the app: the address is refused when it belongs to a lead that has bounced, unsubscribed or failed verification, or when it is on the block list. A List-Unsubscribe header is added when the campaign needs one.

A delivery failure at the mail server is not an HTTP error: you get `200` with `success: false` and the `error`.


## Path Parameters

<ParamField path="campaign_id" type="string" required>The campaign.</ParamField>

## Request Body

<ParamField body="toEmail" type="string" required>Recipient. Any address; a real email goes to it.</ParamField>
<ParamField body="accountId" type="string" required>A sending account assigned to this campaign. It must be active and connected.</ParamField>
<ParamField body="subject" type="string" required>Subject, up to 500 characters.</ParamField>
<ParamField body="body" type="string" required>Body, up to 200,000 characters.</ParamField>
<ParamField body="isHtml" type="boolean" default="false">Send as HTML instead of plain text.</ParamField>
<ParamField body="preheader" type="string">Preview text, up to 300 characters. HTML sends only.</ParamField>
<ParamField body="leadId" type="string">Fill merge tags from this lead in the campaign.</ParamField>

## Response

<ResponseField name="data" type="object">
  <Expandable title="Result">
    <ResponseField name="success" type="boolean">Whether the mail server accepted the email.</ResponseField>
    <ResponseField name="messageId" type="string | null">The Message-ID when it was sent.</ResponseField>
    <ResponseField name="error" type="string | null">The mail server error when it was not.</ResponseField>
  </Expandable>
</ResponseField>

<RequestExample>
```bash cURL
curl -X POST "https://api.foxreach.io/api/v1/campaigns/cmcamp1/test-email" \
  -H "X-API-Key: otr_your_key" \
  -H "Content-Type: application/json" \
  -d '{"toEmail": "you@example.com", "accountId": "cmacct1", "subject": "Test", "body": "Hello"}'
```

```python Python SDK
from foxreach import FoxReach

client = FoxReach(api_key="otr_your_key")
result = client.campaigns.send_test_email(
    "cmcamp1", to_email="you@example.com", account_id="cmacct1", subject="Test", body="Hello",
)
```

```typescript TypeScript SDK
import { FoxReach } from "foxreach";

const client = new FoxReach({ apiKey: "otr_your_key" });
const result = await client.campaigns.sendTestEmail("cmcamp1", {
  toEmail: "you@example.com", accountId: "cmacct1", subject: "Test", body: "Hello",
});
```
</RequestExample>

<ResponseExample>
```json
{
  "data": { "success": true, "messageId": "<abc123@mail.example.com>", "error": null }
}
```
</ResponseExample>

## Errors

| Status | Description |
|--------|-------------|
| `401` | Missing, invalid, revoked or expired API key |
| `403` | API key does not have the required scope: `write` |
| `400` | The account is not on this campaign, not active or not connected, or the address bounced, unsubscribed, failed verification or is blocked |
| `404` | Campaign or sending account not found in this workspace |
| `429` | The account's daily limit or the workspace's monthly send limit is reached, or the `expensive` rate limit is |
| `503` | The block list could not be checked; try again |

## Rate Limit

- **100 requests per minute, shared by every `expensive` call in your workspace.** Fixed 60-second window.
- Every response includes `X-RateLimit-Limit`, `X-RateLimit-Remaining`, `X-RateLimit-Reset` (unix epoch seconds) and `X-RateLimit-Bucket` (`expensive`).
- A `429` response includes a `Retry-After` header (seconds until the bucket resets).

For client-side patterns (`Retry-After`, exponential backoff, monitoring `X-RateLimit-Remaining`), see [Rate Limiting](/rate-limiting).
