Send email

POST /v1/emails sends one email to one or more recipients. Postilon answers with 202 Accepted as soon as the email is queued; delivery happens in the background and is reported by webhooks.

Request

Field Type Description
from string Required. hello@acme.com or Acme <hello@acme.com>. With a live key, the domain must be verified.
to string or string[] Required. One address or a list.
cc string or string[] Copy recipients, shown in the email's Cc header.
bcc string or string[] Blind copy recipients, never shown in the headers.
subject string Required unless template_id is set.
html string HTML body.
text string Plain-text body. Send both html and text for the best delivery.
reply_to string Address that replies go to.
template_id string A template to render.
variables object Values for {{ variable }} placeholders, as strings.
attachments object[] Files to attach; see Attachments.
headers object Extra email headers, such as {"X-Entity-Ref-ID": "order-123"}.
tags object[] [{"name": "category", "value": "receipt"}]. Returned in webhook events.
scheduled_at string RFC 3339 time to send at, such as 2026-11-01T09:00:00Z.
curl https://api.postilon.com/v1/emails \
  -H "Authorization: Bearer $POSTILON_API_KEY" \
  -H "Content-Type: application/json" \
  -H "Idempotency-Key: receipt-order-123" \
  -d '{
    "from": "Acme <billing@acme.com>",
    "to": ["jane@example.com"],
    "reply_to": "support@acme.com",
    "subject": "Receipt for order #123",
    "html": "<p>Thanks for your order.</p>",
    "text": "Thanks for your order.",
    "tags": [{ "name": "category", "value": "receipt" }]
  }'

Response

{
  "id": "msg_4kQ9wZ2xT7bLmN3pR8sV",
  "status": "queued",
  "from": "Acme <billing@acme.com>",
  "to": ["jane@example.com"],
  "environment": "live",
  "credits_used": 1,
  "scheduled_at": null,
  "created_at": "2026-10-25T10:00:00Z"
}

Each recipient in to, cc and bcc uses 1 credit. Credits are taken when the email is accepted. If the email is later suppressed or you cancel it before it is sent, they are returned.

Idempotency

Send an Idempotency-Key header, such as the order or event ID the email is for, so a retried request never sends twice. When Postilon has already accepted an email with the same key in your workspace, it does not send again: it answers with the original 202 Accepted response, plus the header X-Idempotent-Replay: true. If the same key comes with a different request body, the request is refused with 409 idempotency_key_reused, so use one key per email. A key stays in use for as long as its email is kept (90 days).

Templates

Create templates in the dashboard under Templates or with POST /v1/templates, then send with template_id and variables:

{
  "from": "Acme <hello@acme.com>",
  "to": "jane@example.com",
  "template_id": "tmpl_9fG3hJ6kL2mN5pQ8rT1v",
  "variables": { "name": "Jane", "code": "482913" }
}

Placeholders look like {{ name }}. Give a fallback for missing values with {{ name | "there" }}. Variables are filled in the subject, HTML and text; subject, html or text in the request replace the template's own. Values are inserted as they are, so escape any HTML in values that come from users.

Attachments

Each attachment has a filename, a content_type and its content encoded in Base64:

"attachments": [
  {
    "filename": "invoice-123.pdf",
    "content_type": "application/pdf",
    "content": "JVBERi0xLjcKJ…"
  }
]

To attach the same file to many emails, upload it once with POST /v1/files and refer to it with {"file_id": "file_…"} instead of content. Keep emails under 10 MB in total; many mailbox providers refuse larger messages.

Scheduling and cancelling

With scheduled_at in the future, the email gets the status scheduled and is sent at that time. A queued or scheduled email that is not yet being delivered can be cancelled:

curl -X POST https://api.postilon.com/v1/emails/msg_4kQ9wZ2xT7bLmN3pR8sV/cancel \
  -H "Authorization: Bearer $POSTILON_API_KEY"

Cancelling returns its credits. Once delivery has started, the request fails with 409 cannot_cancel.

Batch

POST /v1/emails/batch sends up to 100 emails in one request. They share from, subject, html, text and template_id; each item has its own to and variables.

{
  "from": "Acme <hello@acme.com>",
  "template_id": "tmpl_9fG3hJ6kL2mN5pQ8rT1v",
  "batch": [
    { "to": "jane@example.com", "variables": { "name": "Jane" } },
    { "to": "omar@example.com", "variables": { "name": "Omar" } }
  ]
}

A batch is all or nothing: if it cannot be accepted, for example for lack of credits, no email in it is queued. The response lists the new emails:

{
  "object": "batch_result",
  "count": 2,
  "data": [
    { "id": "msg_4kQ9wZ2xT7bLmN3pR8sV", "to": "jane@example.com", "status": "queued" },
    { "id": "msg_7aB2cD5eF8gH1jK4mN6p", "to": "omar@example.com", "status": "queued" }
  ]
}

Email status

Status Meaning
scheduled Waiting for its scheduled_at time.
queued Accepted and waiting to be sent.
sent Handed to Postilon's mail server for delivery.
delivered The recipient's mail server accepted it.
opened, clicked The recipient opened it or clicked a link (with tracking on).
bounced The recipient's mail server refused it.
complained The recipient marked it as spam.
suppressed Not sent, because a recipient (in to, cc or bcc) is on your suppression list. Credits are returned.
failed Could not be delivered after several attempts.
canceled Cancelled before it was sent. Credits are returned.