# Transactional Send

Send one message that a person's own action asked for, such as a password reset, a receipt or a confirmation code. It goes to one to five recipients. For anything bulk, use a [campaign](/docs/api/campaigns).

```text
POST /api/v1/send
```

Needs a bearer API key (see [API Overview](/docs/api/overview)).

```json
{
  "to": "ada@example.com",
  "subject": "Your receipt",
  "html": "<p>Thanks for your order.</p>",
  "text": "Thanks for your order.",
  "replyTo": "support@example.com"
}
```

| Field     | Notes                                                                                                         |
| --------- | ------------------------------------------------------------------------------------------------------------- |
| `to`      | Required. One address or an array of up to 5. Duplicates are removed. Any invalid address rejects the request |
| `subject` | Required. Control characters are stripped and the subject is cut at 500 characters                            |
| `html`    | The HTML body. Send `html`, `text`, or both                                                                   |
| `text`    | The plain text body. If you send only `text`, it is wrapped in a minimal HTML page                            |
| `replyTo` | Optional address. Defaults to the workspace reply-to, if one is set                                           |

The message comes from your workspace's sending address: your verified custom domain if you have one, otherwise the shared Flomailr address, with your workspace's from name.

## Response

```json
{ "data": { "sent": 1, "failed": 0, "blocked": [] } }
```

`blocked` lists recipients that were skipped and why, as `{ "email": "...", "reason": "..." }`. Reasons are `SUPPRESSED_HARD_BOUNCE`, `SUPPRESSED_COMPLAINT`, `SUPPRESSED_MANUAL` and `BLOCKED_DOMAIN`.

If every recipient is blocked the request returns `422`. If every send attempt fails it returns `502`.

## How it differs from campaign mail

Transactional mail follows different rules on purpose:

- No unsubscribe footer, no `List-Unsubscribe` header, and no postal address requirement.
- No open or click tracking is added.
- A marketing unsubscribe does not block it. Someone who left your newsletter can still get a password reset.
- Quiet hours and the daily send cap do not apply.

These still apply:

- Addresses suppressed for a hard bounce, a spam complaint or a manual block are skipped.
- Domains in your blocked-domains list are skipped.
- The workspace must not be suspended, and the guardrails master switch must be on (otherwise `403`).
- The message counts toward your monthly sending limit. A send that would pass it returns `402` with code `QUOTA_EXCEEDED`.

See [Compliance](/docs/concepts/compliance) for where the line between transactional and marketing mail sits.

## Idempotency

Send an `Idempotency-Key` header to make retries safe:

```bash
curl -X POST https://flomailr.com/api/v1/send \
  -H "Authorization: Bearer $FLOMAILR_KEY" \
  -H "Idempotency-Key: receipt-1042" \
  -H "Content-Type: application/json" \
  -d '{"to":"ada@example.com","subject":"Your receipt","text":"Thanks for your order."}'
```

- A repeat with the same key and the same request returns the original result without sending again, and the response includes `"replayed": true`.
- The same key with a different request returns `409` with code `CONFLICT`.
- The same key while the first request is still running returns `409`.

## Errors

| HTTP | Code               | When                                                              |
| ---- | ------------------ | ----------------------------------------------------------------- |
| 400  | `VALIDATION_ERROR` | The body is not valid JSON                                        |
| 422  | `VALIDATION_ERROR` | Missing or invalid recipients, subject or body, or all blocked    |
| 402  | `QUOTA_EXCEEDED`   | The send would pass the monthly limit                             |
| 403  | `FORBIDDEN`        | The workspace is suspended or guardrails are off                  |
| 409  | `CONFLICT`         | Idempotency-Key reused with a different request, or still running |
| 502  | `INTERNAL_ERROR`   | Every recipient failed to send                                    |
