# Test Emails

## Use case

Check your email copy, saved signature or banner, and attachments by sending repeated previews to **hello@clodo.ai**. This restricted endpoint is available only to the three verified Clodo accounts belonging to Sid, Rithvik, and Natalie. Teammates, sender aliases, and other accounts with matching email domains do not gain access.

These are real emails to the shared inbox. Supply finished copy; Clodo does not generate a draft or wait for approval. The POST queues the send immediately. Test records are separate from prospect outreach: they neither create prospect correspondence or reservations nor clear existing outreach history. Real-prospect duplicate protections are unchanged.

## Endpoint

```http
POST https://api-public.clodo.ai/api/public/v1/test-emails/
```

Send your API key in the `x-api-key` header. See [Authentication](https://docs.clodo.ai/api-reference/authentication).

> This endpoint returns `202 Accepted`. Poll the resource to follow its progress. Outreach does not require a webhook signing secret.

## Pricing

See [Credits & Pricing](https://docs.clodo.ai/guides/credits-and-pricing) for credit costs and charging behavior.

## Errors

For error responses and retry guidance, see [Handling Errors](https://docs.clodo.ai/api-reference/errors).

## Create — `POST /test-emails/`

Use the base URL `https://api-public.clodo.ai/api/public/v1/` and authenticate with your `x-api-key`. Select one of your own connected [Sending Accounts](https://docs.clodo.ai/api-reference/endpoint/sending-accounts).

| Field | Type | Notes |
|---|---|---|
| `from_account` | string | Required `ea_...` account ID owned by the authenticated user. Its selected sending address or alias is used. |
| `subject` | string | Required, nonempty, at most 300 characters. Control characters are rejected. |
| `body_plain` | string | Nonempty finished text, at most 50,000 characters. Supply exactly one body format. |
| `body_html` | string | Nonempty finished HTML, at most 50,000 characters. Sanitized before sending; supply instead of `body_plain`. |
| `attachments` | array | Optional, default `[]`. At most 5 files and 1 MiB decoded bytes combined. See Attachments below. |

All other fields are rejected, including `to`, `cc`, `bcc`, `reply_to`, `review`, and scheduling fields. The recipient cannot be changed, even to another personal test address.

The selected account must be connected and usable. Clodo snapshots its sending address and resolved saved signature when creating the record; the signature, including any saved banner, is appended during sending. Supply only your message body to avoid duplicating the signature. An unavailable or changed sending account blocks submission rather than silently choosing another sender.

### Required idempotency header

Every POST requires `Idempotency-Key`: 1–128 ASCII letters, digits, periods, underscores, colons, or hyphens. Keys belong to the authenticated user, across that user's API keys.

- Reuse the **same key and request content** after a timeout or lost response. Clodo returns the original record; it does not start another send once the worker has claimed it.
- Reusing the same key with different validated content returns `409 conflict`.
- Use a new key only for a deliberately new preview. A new key can send another real email, even when the content matches a previous preview.
- If the result is `unknown`, **do not retry with a new key**. The email may already have been sent. Inspect the shared inbox and contact support to reconcile the original record.

Requests are compared after input validation and normalization. Preserve your original payload when retrying. This endpoint's durable per-user retry behavior is specific to test emails.

The following examples are equivalent. Replace the sample account ID with one returned for your own account. Keep the key unchanged when retrying this preview; running these examples with a different key requests another send.

```bash
curl -X POST https://api-public.clodo.ai/api/public/v1/test-emails/ \
  -H "x-api-key: ck_live_..." \
  -H "Idempotency-Key: signature-preview-001" \
  -H "Content-Type: application/json" \
  -d '{
    "from_account": "ea_0123456789abcdef",
    "subject": "Signature and attachment preview",
    "body_plain": "This is a preview for the shared test inbox.",
    "attachments": [{
      "filename": "notes.txt",
      "content_type": "text/plain",
      "content_base64": "SGVsbG8="
    }]
  }'
```
```python
import requests

response = requests.post(
    "https://api-public.clodo.ai/api/public/v1/test-emails/",
    headers={
        "x-api-key": "ck_live_...",
        "Idempotency-Key": "signature-preview-001",
    },
    json={
        "from_account": "ea_0123456789abcdef",
        "subject": "Signature and attachment preview",
        "body_plain": "This is a preview for the shared test inbox.",
        "attachments": [{
            "filename": "notes.txt",
            "content_type": "text/plain",
            "content_base64": "SGVsbG8=",
        }],
    },
    timeout=30,
)
response.raise_for_status()
print(response.json())
```
```javascript
const response = await fetch("https://api-public.clodo.ai/api/public/v1/test-emails/", {
  method: "POST",
  headers: {
    "x-api-key": "ck_live_...",
    "Idempotency-Key": "signature-preview-001",
    "Content-Type": "application/json",
  },
  body: JSON.stringify({
    from_account: "ea_0123456789abcdef",
    subject: "Signature and attachment preview",
    body_plain: "This is a preview for the shared test inbox.",
    attachments: [{
      filename: "notes.txt",
      content_type: "text/plain",
      content_base64: "SGVsbG8=",
    }],
  }),
});
if (!response.ok) throw new Error(`Request failed: ${response.status}`);
console.log(await response.json());
```

New records return `202 Accepted`. Same-key requests return `202` while queued or sending, otherwise `200`, with the existing record. A successful POST acknowledges the record; poll its status to check submission.

## Read — `GET /test-emails/{id}/`

The ID is a UUID. Only the user who created the test can read it; sharing the recipient inbox does not grant access to another user's API records.

```bash
curl https://api-public.clodo.ai/api/public/v1/test-emails/3c20cc6e-1a2f-4f88-86bf-9030e192309d/ \
  -H "x-api-key: ck_live_..."
```

POST and GET return the same fields. Example queued response:

```json
{
  "id": "3c20cc6e-1a2f-4f88-86bf-9030e192309d",
  "status": "queued",
  "to": "hello@clodo.ai",
  "from_account": "ea_0123456789abcdef",
  "from_email": "you@yourcompany.com",
  "subject": "Signature and attachment preview",
  "body_html": "<p>This is a preview for the shared test inbox.</p>",
  "signature_html": "<p>Your saved signature</p>",
  "attachments": [{"filename": "notes.txt", "content_type": "text/plain", "size_bytes": 5}],
  "created_at": "2026-10-15T12:00:00Z",
  "sent_at": null,
  "message_id": null,
  "internet_message_id": null,
  "error": null
}
```

| Status | Meaning and next step |
|---|---|
| `queued` | Waiting for the sending worker. Poll; a same-key POST can safely re-enqueue an unclaimed record after a queue interruption. |
| `sending` | The worker has claimed the one submission attempt. Poll without creating another request. |
| `sent` | The sending service accepted the email and returned a message identifier. `sent_at` and at least one of `message_id` or `internet_message_id` are populated. This is **not proof of inbox delivery**; check hello@clodo.ai to verify rendering and files. |
| `failed` | Blocked before submission, for example by an unavailable sending account. `error` describes this state. The original request is not automatically retried. |
| `unknown` | Submission outcome is uncertain, including an interrupted or timed-out send. No automatic resend. Check the shared inbox and reconcile this record before requesting another preview. |

If a record remains `queued` after a queue or publish outage, repeat the original POST with the same key and content to safely republish it. Automatic recovery from a queue outage is not guaranteed. This recovery applies only before the worker claims the attempt; never use a new key to retry an `unknown` outcome.

A send still in progress after ten minutes is reported as `unknown`; it may subsequently become `sent` if its original attempt completes and is recorded. There is no edit, approve, cancel, or reply operation on test records.

## Attachments

Files use the existing private attachment pilot: attachment access must also be enabled for the authenticated account. Each file requires exactly `filename`, `content_type`, and `content_base64`.

- Maximum 5 nonempty files; each file and all decoded contents combined must fit within 1 MiB (1,048,576 bytes).
- Supported types: `application/pdf` (`.pdf`), `image/png` (`.png`), `image/jpeg` (`.jpg` or `.jpeg`), `text/plain` (`.txt`), and `text/csv` (`.csv`). Text must be UTF-8. File bytes must match the declared type.
- Use standard base64 without whitespace or a data-URL prefix. Filenames must be at most 180 characters, with no paths, control characters, or leading/trailing whitespace.
- These checks validate format, not malware safety. URLs, executable files, and inline attachment images are unsupported.

Contents remain on the private test record. Responses expose only filename, content type, and decoded size; there is no file-content or download endpoint. The saved signature's banner is separate from these file attachments.

## Limits and charging

Each allowed user may create at most **10 new tests per rolling hour and 50 per rolling day**, across all their API keys and sending accounts. Replaying an existing key does not consume another test allowance. This test allowance is separate from prospect-outreach daily counters and history; previews do not consume or reset those counters. Normal API gateway request limits still apply to POSTs and reads. Until these routes are explicitly registered in the gateway, they share its catch-all limit of **1 request per minute, burst 2**. Poll no more than once per minute and use jittered backoff on `429`; see [Rate Limits](https://docs.clodo.ai/api-reference/rate-limits).

No AI drafting runs and no drafting credits are charged for test emails, including failed requests. The endpoint sends the supplied copy through the existing email delivery service.

## Error responses

| HTTP | Meaning |
|---|---|
| `400` | Invalid or missing fields/header, unsupported destination fields, invalid attachment, or `from_account` not owned by the caller. |
| `401` | Missing, invalid, or revoked API authentication. |
| `403` | The authenticated account is outside the three-account pilot or lacks required API access. |
| `404` | Test record missing or owned by another user. |
| `409` | Idempotency key reused with different content, selected sender unusable, or incompatible development recipient redirection. |
| `429` | Per-user test allowance or API gateway request limit reached. |
| `5xx` | Request could not complete reliably. Recover using the same key and content; never assume this means no email was sent. |

Application errors use the shared [error envelope](https://docs.clodo.ai/api-reference/errors); gateway throttling can instead return `{"message":"Too Many Requests"}`. A send failure after the POST is represented by the record's status and `error`, not a later HTTP error on GET. Do not treat a generic retry suggestion as permission to submit a new key for an uncertain send.