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. |