> ## Documentation Index
> Fetch the complete documentation index at: https://docs.instantcampaign.ai/llms.txt
> Use this file to discover all available pages before exploring further.

# Transactional email API

> Send one email per call — receipts, password resets, notifications — from a stored template or inline HTML, with Liquid data and idempotency.

| Endpoint                                   | Scope                 | Limit               |
| ------------------------------------------ | --------------------- | ------------------- |
| `POST /v1/transactional/send`              | `transactional:write` | 100 requests/minute |
| `POST /v1/transactional/validate`          | `transactional:write` | 100 requests/minute |
| `GET /v1/transactional`                    | `transactional:read`  | 60 requests/minute  |
| `GET /v1/transactional/{messageId}/status` | `transactional:read`  | 100 requests/minute |

## Sending

```bash theme={null}
curl -X POST https://instantcampaign.ai/api/v1/transactional/send \
  -H "Authorization: Bearer ic_YOUR_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "to": "jane@example.com",
    "templateId": "YOUR_TEMPLATE_ID",
    "subject": "Your order {{ data.order.id }}",
    "externalId": "order-10422",
    "data": { "order": { "id": "10422", "total": "£42.00" } }
  }'
```

```json theme={null}
{ "id": "…", "status": "sent", "to": "jane@example.com", "messageId": "…", "externalId": "order-10422", "contactId": "…", "deduplicated": false, "warnings": [] }
```

`to` and `subject` are required, plus either `templateId` or `htmlContent`. If you send both, `htmlContent` is used.

* **Liquid.** The subject, preheader and body are rendered with Liquid: `{{ data.x }}` (also `{{ x }}`), `{{ contact.first_name }}`, `{{ customField.plan }}`, and loops. Every value is HTML-escaped. `data` is limited to 256 KB and 32 levels of nesting.
* **Idempotency.** A repeated `externalId` returns the original message with `deduplicated: true`, and nothing is sent.
* **Contact.** Pass `contact` to create or update the recipient first. If the plan's contact limit blocks that, the email is still sent. The recipient's language, or the `language` field, picks a template translation.
* **Send policy.** `sendPolicy` (a key or ID from **Settings → Send policies**) decides the delivery route and default sender. If you leave it out, the workspace's Transactional policy applies. That policy delivers to people who unsubscribed from marketing, but never to bounced, complained or erased addresses (`422`). An unknown policy answers `400 SEND_POLICY_NOT_FOUND`.
* **Missing variables.** Variables the template marks as required always block a send, and `strict: true` blocks on any unresolved variable. Either way the answer is `422 MISSING_VARIABLES` with `missing[]`. Unresolved variables that don't block are returned in `warnings`.
* **Attachments.** Up to 20, base64-encoded, at most 3 MB each and 10 MB in total. Also available: `cc`/`bcc` (up to 10 each), `replyTo`, and `fromEmail`, which must be on a domain the workspace owns.

## Checking before you send

`POST /v1/transactional/validate` renders exactly as a send would, but sends and stores nothing. It returns the rendered `subject`, every variable path the template reads, the ones this payload leaves `unresolved`, and `valid: false` when a send would be refused. It is useful in CI.

## Delivery status

A message moves from `queued` to `sent`. It then becomes `delivered`, `bounced` or `complained` as the provider reports back, or it ends as `failed`. Look it up by our ID or by your key:

```bash theme={null}
curl https://instantcampaign.ai/api/v1/transactional/ext:order-10422/status \
  -H "Authorization: Bearer ic_YOUR_KEY"
```

`GET /v1/transactional` lists messages newest first. You can filter by `to`, `status` and `externalId`, and page through the results with `before` / `nextBefore`.
