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

# Send a transactional email

`POST /emails`

## Request

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

## Headers

- `Idempotency-Key` string

## Request body

- SendEmailRequest: `subject` is required unless `template` is provided (subject comes from the published template). Provide `html` and/or `text`, or `template` (mutually exclusive). React Email is compiled client-side by the Node SDK (`react` option) — do not send `react` to the HTTP API.
  - `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`

Email accepted for delivery

- SendEmailResponse
  - `id` string, required: Public message ID (`msg_…`)
  - `status` string, required: `held` when content scanning stopped the email for review before sending (new senders only); you get `email.delivered` or `email.failed` once it is reviewed.
  - `held_back` HeldBackRecipient[]: Present only when Groundmail validation held some recipients back. The email went to the others.
    - `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: a test key (`gm_test_…`) or only simulator addresses. Nothing was delivered; the outcome and webhooks are simulated.

## Other responses

- `400`: Invalid request (including `category` set to `newsletter`)
- `401`: Missing or invalid API key
- `403`: From domain not verified, sandbox/verified-domain sender used for non-account recipients, send to other recipients without a paid plan, or sending paused (for the workspace, or for its `product` or `newsletter` email only after high bounce or complaint rates). `newsletter_not_enabled` when the send uses `category: newsletter` and the account is not approved for newsletter sending. For tenants: `tenants_not_enabled` when the send names a tenant on a plan without tenants; `forbidden` when the tenant is paused, is `transactional_only` and the email is product or newsletter email, or a key limited to one tenant names another; `domain_not_allowed` when `from` is on another tenant's domain.
- `404`: The send names a `tenant` that does not exist in this account
- `409`: Idempotency key conflict (payload mismatch or concurrent request)
- `422`: Recipient is suppressed or opted out of the email category, or Groundmail validation held back every `to` recipient (`details.held_back`). When only some recipients are held back, the email goes to the rest and the 200 response lists them in `held_back`.
- `429`: Send limit reached. `plan_limit`: the Free plan's daily or monthly limit (paid plans meter overage instead). `daily_ramp_limit`: this send would go over a new account's daily ramp-up limit for its stream, or over its tenant's own ramp or `daily_limit` (then `details.tenant` is set); it has no `upgrade_url` and `details.resets_at` says when to send the rest.
- `502`: Upstream mail server error
- `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)
