---
title: "Send a batch of emails"
method: POST
path: "/v1/emails/batch"
tags: ["Emails"]
---

# Send a batch of emails

`POST /v1/emails/batch`

## Request

- Base URL: `https://smtpfa.st/api`
- URL: `https://smtpfa.st/api/v1/emails/batch`
- Auth: HTTP bearer

Queue up to 100 emails in a single request. The body is a JSON array of email objects, each using the same shape as the single-send endpoint. Validation runs on every item before any are queued — a malformed item rejects the whole batch with 400. Suppressed recipients (to, cc and bcc) are dropped per row; a row left with no deliverable To recipient is recorded with status "failed". The batch is accepted whole or not at all: every email is recorded and queued, or none is, so there is no partial success to reconcile. A 4xx means nothing was queued. After a 5xx or a lost response, retry with the same Idempotency-Key: you get the first answer if the batch was accepted, or a fresh attempt if it was not.

## Headers

- `Idempotency-Key` string

## Request body

- SendEmailRequest[]: JSON array of email objects (1 to 100). Each item accepts the same fields as POST /v1/emails.
  - `template` object: Send a published template instead of html and text, as in Resend. The published version is rendered with the variables, then every normal send rule applies. from, subject and reply_to in the request win over the template's defaults; a subject in the request is rendered with the same variables.
    - `id` string, required: The template id or alias.
    - `variables` object: Values by variable key. A missing value uses the inline fallback ({{{KEY|default}}}), then the variable's fallback_value. A declared variable with neither must be given a value: otherwise the send answers 422 and lists the keys in missing_variables, before anything is charged or queued. Values are inserted as text: HTML-escaped, and never parsed as Markdown. FIRST_NAME, LAST_NAME and EMAIL fill the contact tags of the same name.
  - `attachments` object[]: Up to 10 files, base64 encoded, 25 MB per message in total. A content_id makes the part inline: reference it from the HTML as cid:<content_id>.
    - `filename` string, required
    - `content` string, required: Base64-encoded file content
    - `content_type` string
    - `content_id` string: Letters, digits, . _ - with an optional @domain; sent inline as multipart/related
    - `inline` boolean: Send inline even without a content_id (a Content-ID is derived from the filename)
  - `from` string, email: Sender email address (must belong to a verified domain)
  - `to` union, required: One recipient address, or a list of them
    - string, email
    - string[]
  - `cc` union: Cc recipients
    - string, email
    - string[]
  - `bcc` union: Bcc recipients
    - string, email
    - string[]
  - `reply_to` union: Reply-To address or addresses
    - string, email
    - string[]
  - `subject` string: Email subject line
  - `tags` object[]: Up to 50 name/value pairs, returned on events and webhooks and filterable in logs
    - `name` string, required
    - `value` string, required
  - `headers` object: Custom headers, e.g. X-Entity-Ref-ID
  - `scheduled_at` string, date-time: Send later, up to 30 days ahead (ISO 8601)
  - `category` 'transactional' | 'marketing': Defaults to marketing. Label receipts, password resets and other transactional mail transactional so a marketing opt-out does not block them; unlabelled mail is treated as marketing. Over SMTP, set the X-SMTPfast-Category header instead; it is removed before sending. An email whose html or text (for a template, the rendered body) contains an unsubscribe placeholder is always marketing. A marketing email carries the one-click List-Unsubscribe header and is not sent to anyone who unsubscribed. A transactional email has no List-Unsubscribe header and still goes to addresses whose suppression has scope marketing (a recipient's own unsubscribe, or a manual suppression created with scope marketing), but not to addresses with scope all: hard bounces, complaints, and manual suppressions, which block everything by default. Transactional means mail the recipient needs because of an action or account; labelling promotions transactional is not allowed. Any other value is refused with 400.
  - `track_clicks` boolean: Not in Resend, which sets tracking per domain. Rewrite this email's links through the click tracker (true) or send them exactly as written (false). Left out, the account's tracking settings apply, including the setting for transactional email, except that a contact-form relay (every recipient on the From domain or a parent or subdomain of it, Reply-To on an unrelated domain, not sent from an inbox) gets no click tracking. Over SMTP, set the X-SMTPfast-Track-Clicks header to true or false instead; it is removed before sending. To keep one link out of tracking, such as a password reset link with a token, add data-smtpfast-track="off" to its <a> tag; the attribute is removed before sending. null is the same as leaving it out; any other value that is not true or false is refused with 400.
  - `track_opens` boolean: Not in Resend. Add the open-tracking pixel to this email (true) or not (false). Left out, the account's tracking settings apply. Over SMTP, set the X-SMTPfast-Track-Opens header to true or false instead; it is removed before sending. null is the same as leaving it out; any other value that is not true or false is refused with 400.
  - `html` string: HTML body of the email. Include `{{unsubscribe_url}}` (or the Resend-compatible `{{{RESEND_UNSUBSCRIBE_URL}}}`) anywhere in the body and SMTPfast substitutes the per-recipient unsubscribe link at send time. An email with the placeholder is always marketing, and a marketing email gets the same URL on the RFC 8058 `List-Unsubscribe` header.
  - `text` string: Plain text fallback body. Supports the same `{{unsubscribe_url}}` placeholder as the HTML body.

## Response `200`

Batch accepted

- BatchSendResponse
  - `batch_id` string, required: Identifier for the batch as a whole
  - `count` integer, required: Number of emails created (matches input length)
  - `emails` object[], required: Per-row result. status is "queued" for normal sends, "scheduled" if scheduled_at is in the future, or "failed" if the row has no deliverable To recipient left after suppression (cc and bcc alone do not send).
    - `id` string, required
    - `status` 'queued' | 'scheduled' | 'failed', required
  - `data` object[], required: The same ids in Resend's batch response shape: one { id } per item, in request order.
    - `id` string, required

## Other responses

- `400`: Bad request - check your payload
- `401`: Invalid or missing API key
- `403`: Forbidden - domain not verified or insufficient permissions
- `409`: Conflict - the resource already exists or changed meanwhile
- `422`: The request is well formed but cannot be carried out in the current state, for example an unpublished template
- `429`: Rate limit exceeded - retry after the period indicated in the Retry-After header
- `500`: Internal server error

---

[API](https://skmtc.dev/smtpfa/apis/smtpfast-api.md) · [All operations](https://skmtc.dev/smtpfa/apis/smtpfast-api/llms.txt) · [OpenAPI document](https://skmtc.dev/smtpfa/apis/smtpfast-api/revisions/79dda742f53e?raw)
