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

# Send a batch of transactional emails

`POST /emails/batch`

## Request

- Base URL: `https://api.groundmail.dev`
- URL: `https://api.groundmail.dev/emails/batch`
- Auth: HTTP bearer

Sends each email in order and returns a result per item. Send limits are checked per email: once the plan's daily limit is used up, the remaining items fail with a `plan_limit` error in their item, and once a new account's ramp-up limit is, with `daily_ramp_limit`. The earlier ones are still sent. Each item can name its own `tenant`.

## Request body

- BatchSendEmailRequest
  - `emails` SendEmailRequest[], required: Attachments are rejected for batch sends in v1.
    - `from` union, required
      - string, email
      - object
        - `email` string, email, required
        - `name` string
    - `to` union, required
      - string, email
      - string[]
    - `subject` string: Required unless using `template` (optional override when sending with a template).
    - `html` string
    - `htmlBody` string: Alias for html
    - `html_body` string: Alias for html
    - `text` string
    - `textBody` string: Alias for text
    - `text_body` string: Alias for text
    - `plain_body` string: Alias for text
    - `template` SendTemplateRef: Reference a published template by UUID or alias. Mutually exclusive with html/text.
      - `id` string, required: Template UUID or alias
      - `variables` object: Values for `{{variable}}` placeholders (HTML-escaped in html body)
    - `reply_to` union
      - string, email
      - string[]
    - `replyTo` union: Alias for reply_to
      - string, email
      - string[]
    - `cc` union
      - string
      - string[]
    - `bcc` union
      - string
      - string[]
    - `tags` union: Key/value tags stored with the email. Groundmail derives a single tag for delivery metadata.
      - EmailTag[]
        - `name` string, required
        - `value` string, required
      - object
    - `tag` string: Optional delivery tag override.
    - `headers` object: Custom headers after server-side safety filtering.
    - `attachments` EmailAttachment[]: Not supported for batch sends or scheduled sends in v1.
      - `name` string, required: Filename sent to the recipient.
      - `filename` string: Alias for name.
      - `content_type` string, required
      - `contentType` string: Alias for content_type.
      - `data` string, byte, required: Base64-encoded attachment content (10MB decoded max).
      - `content` string, byte: Alias for data (base64 string expected over HTTP).
      - `content_id` string: Optional Content-ID for inline attachments.
      - `contentId` string: Alias for content_id.
    - `scheduled_at` string, date-time: Schedule delivery for a future ISO 8601 timestamp. Returns status `scheduled`.
    - `scheduledAt` string, date-time: Alias for scheduled_at.
    - `unsubscribe` boolean: Opt in to managed unsubscribe for this send. Injects RFC 8058 `List-Unsubscribe` headers and replaces `{{unsubscribe_url}}` in subject/html/text. When sending with a template, inherits the template's `unsubscribe_enabled` setting unless overridden here.
    - `category` 'transactional' | 'product' | 'newsletter': Optional send-level category override. Defaults to the template's category when sending with a template, otherwise `transactional`. `product` (onboarding, lifecycle and notification email) supports category-scoped opt-out and auto-enables managed unsubscribe. Transactional and product email are sent from separate IP pools. `newsletter` (opt-in newsletters) is accepted only for approved accounts: it is sent from its own IPs, separate from transactional and product email, always carries `List-Unsubscribe` and one-click unsubscribe (`unsubscribe: false` is a `validation_error`), honours category opt-outs, and has its own daily ramp-up limit. On an account that is not approved it fails with 403 `newsletter_not_enabled` and nothing is sent.
    - `tenant` string: Send as one of your tenants (its `external_id` or `id`). The send uses that tenant's domains, suppression list, sending ramp, daily limit and pause switch. Readers who unsubscribe from a tenant's email are suppressed or opted out for that tenant only, so other tenants can still mail them. Omit it to send from the account as a whole; a send from a domain a tenant owns is that tenant's even without `tenant`. A key limited to one tenant always sends as its tenant. On an account without tenants, naming one returns 403 `tenants_not_enabled`.

## Response `200`

Index-aligned per-message results

- BatchSendEmailResponse
  - `data` object[], required
    - `index` integer, required
    - `id` string
    - `status` string
    - `held_back` HeldBackRecipient[]: Recipients held back by Groundmail validation; present only when some were.
      - `email` string, required: The held-back address (trimmed, lowercased)
      - `reason` 'invalid_address' | 'no_mail_server' | 'previously_bounced' | 'mailbox_not_found', required: `invalid_address`: the address cannot exist. `no_mail_server`: the domain does not exist or takes no email (no MX or A record, or a null MX). `previously_bounced`: the address hard-bounced (mailbox does not exist) for a Groundmail sender in the last 90 days. `mailbox_not_found`: Groundmail validation already knows the mailbox does not exist. The last two can be turned off per account in Settings → Suppressions.
    - `test` true: Present (and `true`) on a test send; nothing was delivered.
    - `error` object
      - `message` string
      - `code` string
      - `status_code` integer
      - `details` unknown

## Other responses

- `400`: Invalid batch wrapper
- `401`: Missing or invalid API key
- `503`: Mail server not configured

---

[API](https://skmtc.dev/groundmail/apis/groundmail-api.md) · [All operations](https://skmtc.dev/groundmail/apis/groundmail-api/llms.txt) · [OpenAPI document](https://skmtc.dev/groundmail/apis/groundmail-api/revisions/479821c9fecf?raw)
