Emails
Send one message or a batch, retrieve what was sent, and cancel a scheduled send before it goes out.
Updated
Send an email
POST /v1/emails
sending or full_access key.curl -X POST https://api.sendpository.com/v1/emails \
-H "Authorization: Bearer $SENDPOSITORY_API_KEY" \
-H "Content-Type: application/json" \
-H "Idempotency-Key: welcome-4821" \
-d '{
"from": "Acme <hello@mail.yourdomain.com>",
"to": ["customer@example.com"],
"subject": "Welcome to Acme",
"html": "<p>Your account is ready.</p>",
"text": "Your account is ready."
}'Body
| Field | Type | Required | Notes |
|---|---|---|---|
from | string | Yes | Must be on a verified domain. Name <addr> accepted. |
to | string[] | Yes | A single string is accepted. to, cc and bcc together take at most 50 recipients. |
subject | string | Yes | Up to 998 characters. |
html | string | One of | HTML body. |
text | string | One of | Plain-text body. Send both when you can. |
cc | string[] | No | |
bcc | string[] | No | Never appears in the delivered headers. |
reply_to | string[] | No | Where replies go. Up to 10 addresses. |
headers | object | No | Custom headers, string values. |
tags | object | No | String key/values, for filtering in the dashboard. |
attachments | array | No | See Attachments. |
scheduled_at | string | No | ISO 8601. See Scheduling. |
track_opens | boolean | No | Adds a tracking pixel to the HTML body. Overrides your account default either way. See Open tracking. |
track_clicks | boolean | No | Rewrites links so clicks are recorded. See Click tracking. |
list_unsubscribe | boolean | No | Adds RFC 8058 one-click unsubscribe headers. Single-recipient messages only. Put {{unsubscribe_url}} in html or text for a visible link - it turns this on for the message. See Unsubscribe. |
At least one of html or text is required.
Headers
| Header | Notes |
|---|---|
Authorization | Bearer sp_.... Required. |
Idempotency-Key | Replaying returns the original id. See Idempotency. |
Response
{ "id": "d67e39ed-96be-4b35-ab22-853163d92c92" }Send a batch
POST /v1/emails/batch
Send an array of the same objects. Each message succeeds or fails on its own, so one bad recipient never discards the rest.
curl -X POST https://api.sendpository.com/v1/emails/batch \
-H "Authorization: Bearer $SENDPOSITORY_API_KEY" \
-H "Content-Type: application/json" \
-d '[
{ "from": "Acme <hello@mail.yourdomain.com>", "to": ["a@example.com"], "subject": "Hi", "html": "<p>1</p>" },
{ "from": "Acme <hello@mail.yourdomain.com>", "to": ["b@example.com"], "subject": "Hi", "html": "<p>2</p>" }
]'Response
Results are returned in the order you sent them, so index 0 is your first message:
{
"data": [
{ "id": "a1aeaca2-2b41-458e-a0da-887bcc65b802" },
{ "error": { "type": "domain_not_verified", "message": "The domain `nope.test` is not registered on this account." } }
]
}Check every entry
The request returns 200 even when some messages failed. Iterate the array and
look for error - don't assume a 200 means all sent.
If your quota runs out mid-batch, processing stops there - remaining entries are simply absent.
Retrieve an email
GET /v1/emails/{id}
curl https://api.sendpository.com/v1/emails/d67e39ed-96be-4b35-ab22-853163d92c92 \
-H "Authorization: Bearer $SENDPOSITORY_API_KEY"{
"id": "d67e39ed-96be-4b35-ab22-853163d92c92",
"from": "Acme <hello@mail.yourdomain.com>",
"to": ["customer@example.com"],
"subject": "Welcome to Acme",
"status": "delivered",
"created_at": "2026-08-30T09:12:41.203Z",
"sent_at": "2026-08-30T09:12:41.640Z",
"delivered_at": "2026-08-30T09:12:42.881Z",
"track_opens": true,
"opened_at": "2026-08-30T09:19:07.412Z",
"open_count": 2,
"track_clicks": true,
"clicked_at": "2026-08-30T09:19:31.006Z",
"click_count": 1,
"events": [
{ "type": "queued", "occurred_at": "2026-08-30T09:12:41.203Z" },
{ "type": "sent", "occurred_at": "2026-08-30T09:12:41.640Z" },
{ "type": "delivered", "occurred_at": "2026-08-30T09:12:42.881Z" },
{ "type": "opened", "occurred_at": "2026-08-30T09:19:07.412Z" }
]
}open_count is 0 on a message sent without track_opens, which is not the
same as nobody having read it - check track_opens before drawing either
conclusion, and read Open tracking on what the
number is worth.
Statuses
| Status | Meaning |
|---|---|
queued | Accepted, not yet handed to the provider. |
scheduled | Held for a future time. |
sent | With the provider. |
delivered | Accepted by the recipient's mailbox. |
delayed | Temporary failure; being retried. |
bounced | Permanent failure. Address suppressed. |
complained | Marked as spam. Address suppressed. |
failed | Couldn't be sent. |
cancelled | A scheduled message you cancelled. |
List emails
GET /v1/emails
| Parameter | Notes |
|---|---|
limit | 1-100. Default 25. |
offset | For paging. |
status | One of the statuses above. |
created_after | ISO 8601. Only messages created at or after this. |
created_before | ISO 8601. Only messages created at or before this. |
curl "https://api.sendpository.com/v1/emails?created_after=2026-08-29T00:00:00Z&status=bounced" \
-H "Authorization: Bearer $SENDPOSITORY_API_KEY"{
"data": [
{
"id": "d67e39ed-...",
"to": ["customer@example.com"],
"subject": "Welcome to Acme",
"status": "delivered",
"created_at": "2026-08-30T09:12:41.203Z"
}
],
"total": 412,
"limit": 25,
"offset": 0
}total counts everything matching the filter, not just this page - so
"how many bounced in the last 24 hours" is one call rather than paging to the
end and counting.
Cancel a scheduled email
POST /v1/emails/{id}/cancel
scheduled.curl -X POST https://api.sendpository.com/v1/emails/{id}/cancel \
-H "Authorization: Bearer $SENDPOSITORY_API_KEY"{ "id": "169a3e78-...", "status": "cancelled" }Returns validation_error if the message has already gone to the provider.