The API
Base URL https://api.postrun.dev. Authenticate with
Authorization: Bearer <your API key>.
Only that header — never a query parameter. A credential in a URL ends up in access logs, browser history and referrer headers.
Errors
Every failure has the same shape:
{ "name": "validation_error", "message": "`subject` is required" }
name is stable and machine-readable. message is prose and may be reworded,
so do not match on it.
name | HTTP | Meaning | Retry? |
|---|---|---|---|
unauthorized | 401 | The key is missing, wrong or revoked | No |
validation_error | 422 | The request is malformed | No |
domain_not_found | 422 | from is not one of your sending domains | No |
domain_not_verified | 422 | The domain is yours but its DNS is not verified yet | No |
key_not_permitted | 403 | This key may not do this: a sending-access key reading, or a key scoped to another domain | No |
email_unconfirmed | 403 | Confirm your email address before sending | No |
account_suspended | 403 | The account may not send | No |
suppressed | 422 | Every recipient is on your suppression list | No |
already_sent | 422 | The message has already left, so it cannot be changed or cancelled | No |
not_found | 404 | No such message or contact | No |
already_exists | 409 | on_conflict: "error" and that contact is already on your list | No |
forbidden | 403 | The account may not do this | No |
failed_precondition | 422 | A state the request cannot argue with, e.g. re-subscribing an address that complained | No |
rate_limit_exceeded | 429 | Too fast. Retry-After says how long | Yes |
quota_exceeded | 402 | An allowance spent: the daily send limit, or the plan’s contact ceiling | No — see below |
internal_error | 500 | Ours | Yes, with an Idempotency-Key |
The 403s are about the account or the key, not the request: your JSON is
fine and re-sending it will not help. email_unconfirmed in particular is the
one a new account meets first — spend the link in the mail we sent at signup and
the same request works.
domain_not_found is returned for a domain that does not exist here and for
one belonging to somebody else, with identical wording. Which of the two it is
is not something this endpoint will tell you.
quota_exceeded is deliberately not a 429. A quota does not clear in a
moment, and telling a client to retry shortly would be a lie they would act on.
A 500 does not mean the message was not accepted. Sending is several steps, and a failure in a later one — recording the send against your usage, say — leaves a message that is already stored and will still go out. Discarding it to make the error tidier would trade a duplicate for a message that silently never arrives.
So retry with an Idempotency-Key: it is the key that makes
the retry safe, not the failure.
What is here, and what is not
Everything about sending and reading email is on this API: send, batch, retrieve, list, reschedule, cancel, and the webhook stream that tells you what happened afterwards.
Your audience is here too. Contacts can be created, listed, updated, deleted and unsubscribed with a key, because the people on your list arrive in your own application and a list you can only update by uploading a CSV is a list that is out of date by definition. Those are the pages that follow.
Everything else is the dashboard. Domains, segments, broadcasts, templates,
suppression lists, webhook endpoints and API keys are managed there and have no
HTTP API a key can reach — the control surface is an RPC surface that takes a
signed-in session, not a bearer key. So a full_access key is “full access to
the email API and the audience”, not to the account.
The two permissions
A key is full_access or sending_access, and both are about this API alone — a
script that needs to add a domain cannot, whichever it holds.
sending_access may send and nothing else: it is the credential for the
machine you trust least. Reading the log, or touching contacts, answers
403 key_not_permitted.
full_access may also read the message log and manage the audience.