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

# Sending email

> Send a message, read its status, receive the email.* webhooks, and handle every error the e-mail endpoints answer.

## Send

`POST /v1/email/messages` sends one message to one recipient, with the application's live key and the `email:send` scope.

```bash theme={"dark"}
curl -X POST https://tawked.com/v1/email/messages \
  -H "Authorization: Bearer $TAWKED_KEY" \
  -H "Content-Type: application/json" \
  -H "Idempotency-Key: order-1042-confirmation" \
  -d '{
    "from": { "name": "متجر أكمي", "address": "orders@acme.sa" },
    "to": "customer@example.com",
    "subject": "طلبك رقم 1042 قيد التجهيز",
    "html": "<p>مرحبًا سارة،</p><p>استلمنا طلبك رقم 1042 وهو قيد التجهيز الآن.</p>",
    "text": "مرحبًا سارة، استلمنا طلبك رقم 1042 وهو قيد التجهيز الآن.",
    "reply_to": "support@acme.sa",
    "reference": "order-1042"
  }'
```

| Field | Required | Notes |
| - | - | - |
| `from` | No | `{ "name", "address" }`. `address` must be on a verified domain of this application; any local part works. Left out, the domain's default from name and address are used (see [Sender domains](/email/senders#the-default-from-address)). |
| `to` | Yes | One valid e-mail address. One recipient per call. |
| `subject` | Yes | 1 to 255 characters. |
| `html` | One of the two | The HTML body. |
| `text` | One of the two | The plain-text body. Send both for the best delivery: clients that do not show HTML read this one. |
| `reply_to` | No | Where replies go. Any valid address. |
| `reference` | No | Your own reference, 1 to 64 printable characters, echoed back, carried by the webhooks and searchable on **Email → Messages**. |

`html` and `text` together are at most 512 KiB. There are no attachments, no templates and no list of recipients: render the message on your side and call once per recipient.

The answer is `202 Accepted`:

```json theme={"dark"}
{
  "id": "3d9a7f2e-1c4b-4e8a-9f6d-2b5c8e1a7d40",
  "status": "accepted",
  "from": { "name": "متجر أكمي", "address": "orders@acme.sa" },
  "to": "customer@example.com",
  "subject": "طلبك رقم 1042 قيد التجهيز",
  "reference": "order-1042",
  "price_halalas": 1,
  "created_at": "2026-10-09T12:00:00+03:00"
}
```

`price_halalas` is what this message took from your balance.

<Tip>
  Send an `Idempotency-Key` per business event and retry a `502` or a network error with the same key. A replay returns the original `202` with `Idempotent-Replayed: true` and sends nothing. A `502 send_failed` releases the key, so the retry sends. See [Reliability and limits](/reliability#idempotency).
</Tip>

## Read a message

`GET /v1/email/messages/{id}` needs `email:read` (which `email:send` includes) and answers for a message of the key's application; any other id answers `404 not_found`.

```json theme={"dark"}
{
  "id": "3d9a7f2e-1c4b-4e8a-9f6d-2b5c8e1a7d40",
  "status": "bounced",
  "from": { "name": "متجر أكمي", "address": "orders@acme.sa" },
  "to": "customer@example.com",
  "subject": "طلبك رقم 1042 قيد التجهيز",
  "reference": "order-1042",
  "price_halalas": 1,
  "created_at": "2026-10-09T12:00:00+03:00",
  "error": { "code": "bounced", "message": "smtp; 550 5.1.1 user unknown" },
  "sent_at": "2026-10-09T12:00:01+03:00",
  "delivered_at": null,
  "bounced_at": "2026-10-09T12:00:03+03:00",
  "complained_at": null,
  "failed_at": null
}
```

## Statuses

`accepted` then `sent` then `delivered`; `bounced` or `failed` end it. Statuses only move forward.

| Status | Meaning | Price |
| - | - | - |
| `accepted` | Tawked recorded the message and charged it. | Charged |
| `sent` | The message left Tawked for the recipient's mail server. | Charged |
| `delivered` | The recipient's mail server accepted it. | Charged |
| `bounced` | The recipient's server refused it. A permanent bounce suppresses the address; a transient one does not. `error.code` is `bounced` and `error.message` the server's reason. | Refunded once |
| `failed` | Rejected before delivery (`error.code` `rejected`) or not renderable. | Refunded once |

A bounce can arrive after `delivered`, when a mailbox accepts the message and refuses it later: the status becomes `bounced`. A **complaint**, the recipient marking the message as spam, sets `complained_at` on the delivered message and suppresses the address; the status and the price stay.

## The suppression list

Each account keeps one list of addresses it will not send to: every permanent bounce and every complaint, from any of its applications. A send to a listed address answers `422 recipient_suppressed` before anything is recorded or charged.

**Email → Suppressed** lists every address with its reason and the message that caused it. An address that bounced by mistake can be removed there; an address that complained cannot.

## Webhooks

The application's [webhook](/webhooks) receives four events, signed and retried like every other:

| Event | When |
| - | - |
| `email.delivered` | The recipient's mail server accepted the message. |
| `email.bounced` | The server refused it, permanently or for now. `data.bounce` has the `type` (`Permanent`, `Transient`) and the `subtype`. The price is refunded. |
| `email.complained` | The recipient marked it as spam. The address is suppressed; the price stays. |
| `email.failed` | The message was rejected before delivery or could not be rendered. The price is refunded. |

```json theme={"dark"}
{
  "event": "email.bounced",
  "data": {
    "id": "3d9a7f2e-1c4b-4e8a-9f6d-2b5c8e1a7d40",
    "status": "bounced",
    "application_id": "c2a10f3e-8b7a-4d2e-9c1a-1a2b3c4d5e6f",
    "from": { "name": "متجر أكمي", "address": "orders@acme.sa" },
    "to": "c***@example.com",
    "subject": "طلبك رقم 1042 قيد التجهيز",
    "reference": "order-1042",
    "price_halalas": 1,
    "error": { "code": "bounced", "message": "smtp; 550 5.1.1 user unknown" },
    "bounce": { "type": "Permanent", "subtype": "General" },
    "occurred_at": "2026-10-09T09:00:03.512Z",
    "created_at": "2026-10-09T09:00:01.087Z"
  },
  "sent_at": "1791536403601"
}
```

`to` is masked to its first character and its domain: match on `id` or your `reference`. `error` is `null` on `email.delivered`, and `bounce` is `null` on every event but `email.bounced`. The [event catalog](/webhooks/events#email-events) has each event.

## Errors

The checks every endpoint shares come first: the rate limit, the key, its [access](/authentication#key-access) (`403 insufficient_scope` names `email:send` or `email:read`), the account's state and the request's shape (`400 invalid_request` for a missing `to` or `subject`, neither `html` nor `text`, or a subject over 255 characters). Then, in this order:

| HTTP | Error | Meaning | Retry |
| - | - | - | - |
| 403 | `live_key_required` | A `tk_test_` key. There is no e-mail sandbox; use the application's live key. | No |
| 403 | `email_early_access` | Tawked Email is early access; apply from the console. | Later |
| 409 | `no_verified_domain` | The application has no verified sender domain yet. | No |
| 422 | `sender_not_verified` | `from.address` is not on a verified domain of this application. | No |
| 422 | `invalid_destination` | `to` is not one valid e-mail address. | No |
| 422 | `recipient_suppressed` | The address is on the account's suppression list. Nothing was recorded or charged. | No |
| 413 | `message_too_large` | `html` and `text` together are over 512 KiB. | No |
| 429 | `daily_cap_reached` | The account sent its 2,000 messages for the day (Riyadh time). Nothing was recorded or charged. | Later |
| 402 | `insufficient_credits` | The balance is below the price of a message. The body adds `balance_halalas` and `price_halalas`. | Later |
| 503 | `email_paused` | Sending is paused for the account, after a high bounce or complaint rate or by Tawked's team. Nothing was sent or charged; contact support. | Later |
| 502 | `send_failed` | The send could not be completed. The body adds `provider_code` and `message`; the price is refunded and the Idempotency-Key released. | Yes |

`GET /v1/email/messages/{id}` answers `live_key_required` and `404 not_found`.


This documentation is built and hosted on [Mintlify](https://mintlify.com), a developer documentation platform.