---
title: "Send an email"
method: POST
path: "/v1/send"
tags: ["Send"]
---

# Send an email

`POST /v1/send`

## Request

- Base URL: `https://api.volanea.com`
- URL: `https://api.volanea.com/v1/send`
- Auth: HTTP bearer

Send one message (to one address or up to 50). Runs the full pipeline: suppression check, contact upsert, template render, tracking instrumentation, and dispatch. Supports scheduling via `sendAt` and safe retries via the `Idempotency-Key` header.

**Reading a failure** — every failure of the send pipeline keeps the top-level `code` `send_failed`, at whatever HTTP status fits (404 for a missing template, 403 for an unverified from domain). The specific cause is in `details[0].code` — e.g. `template_not_found`. Branch on `code === "send_failed"` for "the send did not happen", then read `details[0].code` when you need to tell a wrong template id in your config from a transient provider problem worth retrying. `details` is absent when there is no cause more specific than the message.

**A 200 does not mean delivered.** Each entry in `messages` is either an accepted message (with an `id` and a `status`) or a skip (`{ to, skipped }` — suppressed, unsubscribed, over quota). Skips are part of a 200 body. Accepted messages are queued: the provider round trip happens after this response returns, so track final delivery with `GET /v1/emails/{id}` or the `email.*` webhooks, not from this call.

## Headers

- `Idempotency-Key` string

## Request body

- SendMessage: A single message. Provide inline `html`/`text` content or a `templateId`; `subject` is required unless the template supplies one.
  - `to` union, required: Recipient address (optionally with a display name), or an array of 1–50 recipients (each gets its own message).
    - string, email
    - object
      - `email` string, email, required
      - `name` string
    - union[]
      - union
        - string, email
        - object
          - `email` string, email, required
          - `name` string
  - `subject` string: Subject line. Supports `{{variable}}` placeholders, with fallbacks: `{{firstName | "there"}}`.
  - `html` string: HTML body. Supports `{{variable}}` placeholders, with fallbacks: `{{firstName | "there"}}`.
  - `text` string: Plain-text body (wrapped in `<pre>` when no HTML is given).
  - `templateId` string: Send using a stored template instead of inline content.
  - `variables` object: Values for `{{variable}}` placeholders in the subject and body. A name you omit falls back to the same-named contact field, then to the placeholder's own `| "default"` if it has one, then to nothing.
  - `from` string, email: From address. Must belong to a verified domain (unless in test mode). Defaults to the template's from address, or hello@ your first verified domain.
  - `fromName` string
  - `replyTo` string, email: Sets a Reply-To header.
  - `headers` object: Custom email headers to include on the message.
  - `attachments` object[]: Up to 10 attachments per message. Total request size is capped at 10 MiB.
    - `filename` string, required
    - `content` string, required: Base64-encoded file content.
    - `contentType` string
  - `sendAt` string, date-time: Schedule delivery for a future time (ISO 8601 with offset). The message is stored `queued` with a `scheduledFor` — list the pending ones with `GET /v1/emails?status=scheduled`, stop one with `DELETE /v1/emails/{id}`.
  - `createContact` boolean: Set `false` to skip auto-creating a contact for unknown recipients (default `true`).
  - `type` 'transactional' | 'marketing': Marketing messages get the unsubscribe footer and skip contacts who have unsubscribed. Defaults to the template's type, or `transactional` for inline content — so promotional copy sent as inline `html` must set this explicitly or it bypasses the unsubscribe check.

## Response `200`

Per-recipient results.

- object
  - `messages` SendResult[], required
    - union: Per-recipient outcome: either an accepted message or a skip reason.
      - object
        - `id` string, required: Email id — use with `GET /v1/emails/{id}`.
        - `to` string, email, required
        - `status` 'sent' | 'scheduled' | 'queued', required
      - object
        - `to` string, email, required
        - `skipped` 'suppressed' | 'unsubscribed' | 'limit_reached' | 'reputation_paused' | 'disabled' | 'free_tier_exceeded' | 'plan_quota_exceeded' | 'provider_suppressed' | 'domain_blocked', required
  - `testMode` boolean, required
  - `replay` true: Present and `true` ONLY on an idempotent replay — absent on a first send, never `false`. Nothing was dispatched for this request; `messages` is the stored response from the original call, so the ids in it are the original ids. Test `data.replay === true`, or read the `X-Idempotent-Replay` header.

## Other responses

- `401`: Missing or invalid API key.
- `403`: The from domain is not verified, or the project has no verified domain at all. `code` is `send_failed` — the domain is named in `error`.
- `404`: Template not found. `code` is `send_failed`; `details[0].code` is `template_not_found`.
- `409`: `Idempotency-Key` reused with a different body (`idempotency_key_reused`) — nothing was sent.
- `422`: Request validation failed (code `validation_error`, 422).
- `429`: Per-project send rate limit (`rate_limited`). Retry after the `Retry-After` header.

---

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