/v1/messages
Ask us to send one email. We render it from a template, tag it with the product that sent it (an Application), and hand it to your own mail server (a transport). One accepted call = one message against your plan; retries of the same message never count again.
Authentication
Keys are prefixed spw_, belong to a named person, and are shown exactly once. Abilities are chosen at mint and never widen. This endpoint requires messages:send. No key may carry both messages:send and messages:read-content — a leaked sending key can send mail in your name, but it can never open a stored message body.
Parameters
Worked example
{
"id": "2ee35675-6bae-42be-aa5e-6d4618d0c61d",
"status": "queued",
"application_id": 1,
"over_monthly_quota": false,
"attachments_captured": 0
}The failures, documented like they matter
You'll decide whether to trust this API by reading this list — that's what it's for. Every error is JSON with a human message. Transport failures also carry a stable type of no_usable_transport.
{ "message": "Unauthenticated" }{
"message": "This API key does not have the \"messages:send\" ability. Abilities are chosen when the key is minted and cannot be widened — mint a new key that includes this ability.",
"type": "forbidden_ability",
"required_ability": "messages:send"
}{
"message": "This key is bound to the Application \"legacy-mailer\", which is archived. Archived Applications accept no new sends; use a key for a live Application.",
"type": "application_archived",
"application": "legacy-mailer"
}{ "message": "No template with slug \"auth/password-resett\" on this account. Sync it from git or create it in the panel before sending against it." }{ "message": "Idempotency-Key was already used with a different request body" }{ "message": "Missing template variables: reset_url, expires_minutes" }HTTP/1.1 429 · Retry-After: 60 { "message": "Daily send limit exceeded: plan daily send limit — used 1,000 of 1,000. Upgrade your plan to raise the daily send cap, or wait until midnight (UTC).", "type": "limit_exceeded", "limit": { "dimension": "daily_send", "binding": "plan", "limit": 1000, "used": 1000, "lift": "Upgrade your plan to raise the daily send cap, or wait until midnight (UTC)." } }
{
"message": "no_usable_transport: No transport is configured for this application, or all configured transports are disabled. Add or re-enable a transport in the panel, then retry with the same idempotency key. The message was not queued.",
"type": "no_usable_transport",
"last_transport_error": "connection refused to smtp.example:587"
}Idempotency
Send the same Idempotency-Key header (or idempotency_key body field) and the message is created at most once. A replay within the retention window returns the original 202 — same UUID id, same body. Keys are retained for 24 hours, then may be reused. Build the key from your own domain facts: pwreset-usr_8812-20260802T1402 beats a random UUID you can't reconstruct after a crash. Full contract:
Reusing a key with a different body returns a 409 — see the failures above.
What happens next
The 202 is where most APIs stop documenting and where this product starts. The message enters your archive under the UUID id from the response — the same id the panel shows — attributed to its Application, and its delivery record fills in as each attempt runs — every attempt, every verbatim server response, the template version it rendered from, and the body under your retention policy. Open Message Log with that id; the log is searchable by anyone you've given a free viewer seat. The same archive is readable over the API: , , and (with a separate key) .