# Send an email

> Send email with POST /emails, covering sender rules, recipients, content, templates, tracking, the response, webhook events and every error code.

This guide explains each part of a `POST /emails` request and what Emailit does with it, from the From address to the errors you can get back. For the complete parameter reference, see [Send an email](/docs/api-reference/emails/send/) in the API reference.

## Before you begin

- A verified sending domain in your workspace. See [Add a domain](/docs/domains/add-a-domain/).
- An API key with **Full Access** or **Sending Only** scope. See [API keys](/docs/developers/api-keys/).
- Production access if you send to anyone other than your workspace members. See [Production access](/docs/workspaces/production-access/).
- Enough credits for every recipient (1 credit each).

## Send a basic email

**cURL**

```bash
curl https://api.emailit.com/v2/emails \
  -H "Authorization: Bearer $EMAILIT_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "from": "Acme Billing <billing@acme.com>",
    "to": ["ada@example.com", "Grace Hopper <grace@example.com>"],
    "cc": "accounts@example.com",
    "reply_to": "support@acme.com",
    "subject": "Your invoice for October",
    "html": "<p>Your invoice is ready.</p>",
    "text": "Your invoice is ready."
  }'
```

**Node.js**

```javascript
import { Emailit } from '@emailit/node';

const emailit = new Emailit(process.env.EMAILIT_API_KEY);

const email = await emailit.emails.send({
  from: 'Acme Billing <billing@acme.com>',
  to: ['ada@example.com', 'Grace Hopper <grace@example.com>'],
  cc: 'accounts@example.com',
  reply_to: 'support@acme.com',
  subject: 'Your invoice for October',
  html: '<p>Your invoice is ready.</p>',
  text: 'Your invoice is ready.',
});
```

**Python**

```python
import os
from emailit import EmailitClient

client = EmailitClient(os.environ["EMAILIT_API_KEY"])

email = client.emails.send({
    "from": "Acme Billing <billing@acme.com>",
    "to": ["ada@example.com", "Grace Hopper <grace@example.com>"],
    "cc": "accounts@example.com",
    "reply_to": "support@acme.com",
    "subject": "Your invoice for October",
    "html": "<p>Your invoice is ready.</p>",
    "text": "Your invoice is ready.",
})
```

**PHP**

```php
$emailit = Emailit::client(getenv('EMAILIT_API_KEY'));

$email = $emailit->emails()->send([
    'from' => 'Acme Billing <billing@acme.com>',
    'to' => ['ada@example.com', 'Grace Hopper <grace@example.com>'],
    'cc' => 'accounts@example.com',
    'reply_to' => 'support@acme.com',
    'subject' => 'Your invoice for October',
    'html' => '<p>Your invoice is ready.</p>',
    'text' => 'Your invoice is ready.',
]);
```

## Set the From address

`from` is required and takes one address in either form:

- `billing@acme.com`
- `Acme Billing <billing@acme.com>`, or with quotes, `"Acme, Inc." <billing@acme.com>`

The domain after the `@` must be a verified sending domain in the same workspace:

- **The match is exact.** Domains are compared without regard to case, but `mail.acme.com` and `acme.com` are different domains. Add and verify every subdomain you send from.
- **Any local part works.** You don't need a mailbox for `billing@` or `no-reply@`.
- **Pending domains can't send.** A domain that is still awaiting review (**Pending verification**) is treated as not verified.
- **Restricted keys stay on their domain.** A **Sending Only** key restricted to one domain can only send from that domain.
- **Paused domains are blocked.** If [sending health](/docs/deliverability/sending-health/) paused the domain, sends from it are rejected until the pause is lifted.

## Add recipients

`to` is required. `cc` and `bcc` are optional. Each field accepts a string or an array of strings, with or without display names, and holds up to 50 addresses. A string can contain several comma-separated addresses; use an array when a display name itself contains a comma.

Emailit removes duplicates across `to`, `cc` and `bcc` (ignoring case), then creates **one email per unique recipient**, each with its own `em_` ID. Every copy carries the same `To` and `Cc` headers, so recipients see the conversation as usual, and `Bcc` recipients never appear in any copy's headers.

When a request has more than one recipient, the response includes an `ids` map from recipient to email ID. `id` is the email of the first recipient.

```json
{
  "object": "email",
  "id": "em_33VtK8mRq1xZp7LwN4cY2bHsDfa",
  "ids": {
    "ada@example.com": "em_33VtK8mRq1xZp7LwN4cY2bHsDfa",
    "grace@example.com": "em_33VtK8nB5rTq0YxW3mJk9PdLsUe",
    "accounts@example.com": "em_33VtK8nQ2wErT6yU8iOp4AsDf7g"
  }
}
```

Each recipient costs 1 credit and counts toward your [rate limits](/docs/api-reference/rate-limits/). A recipient with a `recipient`-type [suppression](/docs/suppressions/) is accepted and then marked `suppressed` instead of being delivered.

## Write the content

| Field | Rules |
| --- | --- |
| `subject` | Required, unless a template provides it. Non-ASCII characters are encoded for you. |
| `html` | The HTML body. You need `html`, `text` or both, unless a template provides them. |
| `text` | The plain-text body. Send it alongside `html`: some clients and spam filters prefer messages with both. |
| `reply_to` | A string or an array of addresses where replies should go. |

If `reply_to` names the same address as `from`, Emailit drops the `Reply-To` header because it adds nothing and some spam filters penalize it.

## Send with a template

Set `template` to a template alias or a `tem_` ID, and pass `variables` for the [Temple](/docs/templates/temple/) placeholders in it.

- **An alias** sends the version that is currently published for that alias. If no version is published, the request fails with `404`.
- **A `tem_` ID** sends that exact version, published or not. Use it to test a draft version before you publish it.

Fields in the request take precedence over the template: a `subject`, `html` or `text` you send replaces the template's value. If you don't send `reply_to`, the template's Reply-To is used. `from` is always required in the request. See [Template versions](/docs/templates/versions/) for how publishing works.

**cURL**

```bash
curl https://api.emailit.com/v2/emails \
  -H "Authorization: Bearer $EMAILIT_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "from": "Acme <hello@acme.com>",
    "to": "ada@example.com",
    "template": "welcome-email",
    "variables": {
      "first_name": "Ada",
      "plan": "Pro",
      "activation_url": "https://acme.com/activate?token=8f3k2"
    }
  }'
```

**Node.js**

```javascript
const email = await emailit.emails.send({
  from: 'Acme <hello@acme.com>',
  to: 'ada@example.com',
  template: 'welcome-email',
  variables: {
    first_name: 'Ada',
    plan: 'Pro',
    activation_url: 'https://acme.com/activate?token=8f3k2',
  },
});
```

**Python**

```python
email = client.emails.send({
    "from": "Acme <hello@acme.com>",
    "to": "ada@example.com",
    "template": "welcome-email",
    "variables": {
        "first_name": "Ada",
        "plan": "Pro",
        "activation_url": "https://acme.com/activate?token=8f3k2",
    },
})
```

**PHP**

```php
$email = $emailit->emails()->send([
    'from' => 'Acme <hello@acme.com>',
    'to' => 'ada@example.com',
    'template' => 'welcome-email',
    'variables' => [
        'first_name' => 'Ada',
        'plan' => 'Pro',
        'activation_url' => 'https://acme.com/activate?token=8f3k2',
    ],
]);
```

`variables` also works without a template: Emailit renders Temple placeholders in the `subject`, `html` and `text` you send inline.

## Control tracking

By default, each email follows its sending domain's **Track loads** and **Track clicks** settings. Override them per email with `tracking`:

- `"tracking": true` or `false` turns both load (open) and click tracking on or off.
- `"tracking": { "loads": true, "clicks": false }` sets each one separately.

Tracking only works when the domain's tracking CNAME is verified. Without it, the email is sent untracked and the request still succeeds. The `tracking` object in the response shows the settings that were actually applied. See [Open and click tracking](/docs/tracking/).

## Add headers and metadata

Use `headers` for custom email headers, such as `List-Unsubscribe`, and `meta` for your own string key-value pairs. Emailit stores `meta` with the email and includes it in webhook events. See [Headers and metadata](/docs/email-api/headers-and-metadata/).

To attach files, schedule the send, or make retries safe, see [Attachments](/docs/email-api/attachments/), [Scheduling](/docs/email-api/scheduling/) and [Idempotency](/docs/email-api/idempotency/).

## Read the response

A successful request returns `200`:

| Field | Description |
| --- | --- |
| `object` | Always `email`. |
| `id` | The `em_` ID of the first recipient's email. |
| `ids` | Map of recipient address to email ID. Present only when there is more than one recipient. |
| `token` | Internal token of the first email, also used in its Message-ID. |
| `message_id` | The `Message-ID` header of the first email, in the form `<token@your-domain>`. |
| `from` | The From address as you sent it. |
| `to` | The `to` addresses, without display names. |
| `cc`, `bcc` | The `cc` and `bcc` addresses. Present only when you sent them. |
| `subject` | The final subject, after template rendering. |
| `status` | `accepted`, or `scheduled` when the email has a future send time. |
| `scheduled_at` | The send time in ISO 8601, or `null`. |
| `created_at` | When the email was created. |
| `tracking` | The applied `loads` and `clicks` settings. |

Store the `id` (or the `ids` map) so you can match later webhook events and look the email up with [Retrieve an email](/docs/api-reference/emails/get/).

## Events

Every recipient's email emits its own events:

1. [`email.accepted`](/docs/webhooks/events/email/accepted/) right after the request, or [`email.scheduled`](/docs/webhooks/events/email/scheduled/) if it has a future send time.
2. Delivery events as the email moves through the pipeline: [`email.delivered`](/docs/webhooks/events/email/delivered/), [`email.attempted`](/docs/webhooks/events/email/attempted/) (temporary failure, will retry), [`email.bounced`](/docs/webhooks/events/email/bounced/), [`email.failed`](/docs/webhooks/events/email/failed/), [`email.rejected`](/docs/webhooks/events/email/rejected/) or [`email.suppressed`](/docs/webhooks/events/email/suppressed/). An email held for review emits `email.held`.
3. Engagement events, if tracking is on: [`email.loaded`](/docs/webhooks/events/email/loaded/) and [`email.clicked`](/docs/webhooks/events/email/clicked/). Spam reports emit [`email.complained`](/docs/webhooks/events/email/complained/).

See [Email statuses](/docs/logs/email-statuses/) for what each status means.

## Errors

Validation errors return a list of every problem found:

```json
{
  "error": "Validation failed",
  "validation_errors": [
    "Missing required field: subject",
    "Invalid to email address at index 1: grace@"
  ]
}
```

| Status | `error` | Cause | Fix |
| --- | --- | --- | --- |
| `400` | `Validation failed` | A required field is missing, an address is malformed, a field has more than 50 recipients, or an attachment is invalid. | Fix each item in `validation_errors`. |
| `400` | `Invalid Idempotency-Key` | The `Idempotency-Key` header has a bad format. | Use 1–256 letters, digits, `-` or `_`. See [Idempotency](/docs/email-api/idempotency/). |
| `401` | `Unauthorized` | The API key is missing or invalid. | Send `Authorization: Bearer` with a current key. |
| `402` | `Insufficient credits` | The workspace can't pay for every recipient. | [Buy credits](/docs/billing/credits/) or turn on [auto-refill](/docs/billing/auto-refill/). |
| `403` | `Workspace not verified` | The workspace is in sandbox mode and a recipient isn't a workspace member. `code` is `unverified_workspace_recipient` and `blocked_recipients` lists the addresses. | [Request production access](/docs/workspaces/production-access/), or test with members' addresses. |
| `403` | `Domain not authorized` | The API key is restricted to a different sending domain. | Send from the key's domain, or use a key without a domain restriction. |
| `403` | `Domain paused` | Sending health paused the From domain. | See [Sending health](/docs/deliverability/sending-health/). |
| `404` | `Template not found` | The alias has no published version, or the `tem_` ID doesn't exist in this workspace. | Publish a version or check the ID. |
| `409` | `Idempotency key in progress` | Another request with the same key is still running. | Wait, then retry with the same key. |
| `413` | `Message too large` | The encoded message is larger than 40 MB. | Send fewer or smaller attachments, or link to large files. |
| `422` | `Domain not verified` | The From domain isn't a verified sending domain in this workspace. | Verify the domain, or check for a subdomain or typo. |
| `422` | `Attachment error` | An attachment URL couldn't be downloaded or is larger than 25 MB. | See [Attachments](/docs/email-api/attachments/). |
| `429` | `Rate limit exceeded` or `Daily limit exceeded` | You're over the per-second or daily sending limit. | Wait for `retry-after` seconds, or request a higher limit. |
| `503` | `Idempotency unavailable` | The idempotency store couldn't be reached. | Retry with the same key. |

A suspended workspace gets `403` with `Workspace is suspended` on every send. For the general error format, see [Errors](/docs/api-reference/errors/).

## Related

- [Send an email](/docs/api-reference/emails/send/) API reference
- [Templates](/docs/templates/)
- [Email statuses](/docs/logs/email-statuses/)
- [Webhook event types](/docs/webhooks/event-types/)
- [Why didn't my email arrive?](/docs/kb/email-not-delivered-checklist/)

---
Source: https://emailit.com/docs/email-api/send-email/
