Skip to content

Errors

Every error the API returns has the same JSON shape, a stable code to branch on, and a request id to quote to support.

Error response format#

A failed request answers with a 4xx or 5xx status and a JSON body holding a readable error and a machine-readable code. Branch on code and the status: the wording of error may improve over time, the codes will not change.

json
HTTP/1.1 422 Unprocessable Entity
X-Request-Id: 03f82111-6c1e-4a53-9a1f-5d0c8e7b2a44

{
  "error": "Domain 'acme.com' is not verified",
  "code": "unprocessable_entity"
}
errorstring
What went wrong, in words you can show a developer. Validation errors name each failing field as path: message, joined with commas — to.0: must be a valid email address, subject: Subject is required when not using a template.
codestring
One of the codes below. Derived from the status unless the endpoint sets a more specific one.
required, missingstring[]
Only on 403 insufficient_scope: the scopes the endpoint needs and the ones your credentials lack.
missingFieldsstring[]
Only on the 400 for an empty send body: which of from, to, subject and html|text|template_id are absent.

Every response from the API — success or failure — carries an X-Request-Id header. Include it when you contact support; it finds the exact request in our logs.

Not every error is JSON

Two responses come from the proxy in front of the API and have an HTML body with no code: 413 for a body over 36 MB, and 429 when one IP address exceeds the per-IP request rate. An unknown path returns a plain-text 404 Not Found. Check the status before parsing the body.

Batch sends report errors per email

POST /emails/batch answers 202 even when some emails were refused. Each refused element is { "error": "…" } in data, with a code when the refusal was a quota (for example monthly_limit_exceeded) and never an HTTP status — inspect every element. See Send a batch.

Error codes#

CodeStatusWhen
invalid_request400The body is not valid JSON or failed validation. Also the code of any other 4xx that has no code of its own.
unauthorized401No credentials, or the API key or access token is invalid, revoked or expired.
forbidden403Not allowed: missing scope, team suspended or paused, key restricted to another domain, account not verified — or a POST/PATCH/PUT/DELETE without a Bearer header.
not_found404The resource does not exist, belongs to another team, or was deleted by log retention.
conflict409A resource with the same unique value already exists (a contact email, a property name).
attachment_file_missing410An inbound attachment’s stored file no longer exists. Retrying will not help.
payload_too_large413The API refused a body that is too large to process (e.g. an email preview). Bodies over 36 MB are stopped earlier by the proxy with an HTML 413 and no code.
unprocessable_entity422The request is well-formed but cannot be carried out: unverified domain, unpublished template, suppressed recipients, spam block, reused idempotency key.
rate_limit_exceeded429Too many requests for this key and endpoint. Wait Retry-After seconds, then retry.
monthly_limit_exceeded429Your plan’s monthly email allowance is used up. Do not retry: upgrade or wait for the month to roll over.
daily_limit_exceeded429The rolling 24-hour cap on sends to external recipients (new and Free accounts) was reached. Do not retry; it clears as the window rolls.
send_limit_exceeded429Sending from new accounts is paused platform-wide during a safety review. Do not retry; it clears automatically.
internal_error500Something failed on our side (also returned for 502 from an upstream integration).
service_unavailable503Maintenance mode, or a feature whose backing service is not available.

HTTP status codes#

StatusNameMeaning
200OKThe request succeeded; the body holds the result.
201CreatedA resource was created (API keys, contacts, domains, webhooks…).
202AcceptedPOST /emails and POST /emails/batch: accepted and queued; delivery happens afterwards.
400Bad RequestMalformed JSON or failed validation.
401UnauthorizedMissing or invalid credentials.
403ForbiddenCredentials are valid but may not do this.
404Not FoundNo such resource — or no such route (a plain-text 404).
409ConflictDuplicate of an existing resource.
410GoneThe resource existed but is permanently unavailable.
413Payload Too LargeThe body is over the size limit.
422Unprocessable EntityValid request that cannot be carried out in the current state.
429Too Many RequestsRate limit or quota reached.
500Internal Server ErrorUnexpected failure on our side.
502Bad GatewayA third-party service we called on your behalf failed (e.g. a DNS provider).
503Service UnavailableMaintenance, or a dependency is unavailable. Retry later.

Each endpoint on the Emails page lists the exact statuses it returns and what triggers each one.

Common errors#

ErrorStatusWhat to do
Unauthorized401A GET was sent without an Authorization header (other methods get Invalid request origin instead). Add Authorization: Bearer sk_live_….
Invalid API key401The key is mistyped, revoked, rotated, or was restricted to a domain that has since been deleted. Create a new key.
Invalid request origin403A POST, PATCH, PUT or DELETE was sent without an Authorization: Bearer header. Add the header — without it the request is treated as a browser request.
insufficient_scope403The key or token lacks a permission this endpoint needs; the missing field lists it. Use a full-access key or grant the scope.
Verify your email address before sending email.403The team owner has not confirmed their email address. Click the link in the verification email.
API key is restricted to a different domain403The key was created for another domain than the one in from. Send from the key’s domain or use an unrestricted key.
Sending is paused for this team403Sending was paused (for example during a safety review). Contact support.
Domain 'example.com' not found for this team422The from domain has not been added to this team. Add it under Domains — or check you are using the right team’s key.
Domain 'example.com' is not verified422Finish the domain’s DNS records and verify it before sending.
Template is not published422Publish the template (POST /templates/:id/publish) before sending with it.
All recipients are suppressed422Every recipient (or every to recipient) is on your suppression list. Remove the address from Suppressions if it should receive mail again.
Email rejected by spam filter (score 11.40)422The content scored at or above the block threshold. Check it with POST /emails/spam-preview and remove what the symbols point at.
This idempotency key was already used for a different request. Use a new key to send a different email.422Each distinct email needs its own idempotency_key.
Contact with this email already exists409Update the existing contact instead of creating it again.
Rate limit exceeded. Please try again later.429Wait for the number of seconds in Retry-After, then retry.
Monthly email limit reached. Upgrade your plan.429Code monthly_limit_exceeded. Your plan’s monthly allowance is used up. Retrying will not help until you upgrade or the month rolls over.
Request body is missing required fields: from, to, subject, html|text|template_id. See https://poststack.dev/docs/emails for a working example.400The send body was empty or had neither recipients nor content. Check that you send JSON with Content-Type: application/json.
to.0: must be a valid email address400Recipients must be bare addresses. Only from accepts Name <address>.
emails: Too big: expected array to have <=100 items400Split the batch into requests of 100 emails or fewer.

Handling errors in code#

The SDKs raise a typed error for any non-2xx response, carrying the status, the error message, the code and the request id.

import { PostStack, PostStackError } from '@poststack.dev/sdk';

try {
  await poststack.emails.send({ /* … */ });
} catch (err) {
  if (err instanceof PostStackError) {
    err.statusCode; // 422
    err.code;       // "unprocessable_entity"
    err.message;    // "Domain 'acme.com' is not verified"
    err.requestId;  // "03f82111-6c1e-…" — quote it to support
  }
  throw err;
}

Retries#

Retry network errors, 408, 429 and 5xx with exponential backoff. Do not retry other 4xx responses: the same request will fail the same way until you change it.

All three SDKs do this for you — up to 3 retries by default, with full-jitter exponential backoff starting at 250 ms — and send an Idempotency-Key with every POST, so a retried email send can never deliver twice. Change the count with maxRetries (TypeScript), max_retries (Python) or WithMaxRetries (Go).

Not every 429 clears with time

Sending quotas are also 429, with their own code: monthly_limit_exceeded (plan allowance used up), daily_limit_exceeded (the daily cap on sends to external addresses) and send_limit_exceeded (sending from new accounts paused platform-wide). Only rate_limit_exceeded is worth waiting out. The SDKs throw the quota codes at once instead of retrying them; branch on code in your own client too.

Rate limits#

This is a summary; Rate limits covers every limit and quota in detail.

Limits are counted per API key (or OAuth token, or dashboard user) and per endpoint, over fixed windows — one minute for most endpoints. Free-plan teams get half of each limit. The send endpoints allow 60 requests per minute for POST /emails and 10 for POST /emails/batch; see Rate limits → Limits by endpoint for the rest. Every rate-limited endpoint returns these headers, so you can slow down before you hit the limit:

HeaderMeaning
X-RateLimit-LimitRequests allowed in the current window.
X-RateLimit-RemainingRequests left in the current window.
X-RateLimit-ResetUnix time (seconds) when the window resets.
Retry-AfterOn a 429 only: seconds to wait before retrying.
bash
HTTP/1.1 429 Too Many Requests
X-RateLimit-Limit: 60
X-RateLimit-Remaining: 0
X-RateLimit-Reset: 1790686842
Retry-After: 47
Content-Type: application/json

{
  "error": "Rate limit exceeded. Please try again later.",
  "code": "rate_limit_exceeded"
}

In front of the API, the proxy also limits each source IP address to 30 requests per second (10 per second on /emails/batch), with a short burst allowance. Its 429 has no JSON body and no Retry-After; back off for a second and retry. Spreading one integration over many API keys does not raise this limit.

Troubleshooting#

401 Invalid API key with a key that used to work

The key was revoked or rotated (rotation deletes the old key immediately), or it was restricted to a domain that has since been deleted — deleting a domain expires the keys restricted to it. Create a new key under API keys in the dashboard.

403 insufficient_scope

A sending-only key called an endpoint that manages resources, or an OAuth token lacks the scope. The missing field names what is needed. See Permissions.

403 Invalid request origin

A POST, PATCH, PUT or DELETE arrived without an Authorization: Bearer … header, so it was treated as a browser request and failed the cross-site check before authentication ran. Add the header (the scheme is case-sensitive: Bearer). Calls from a browser should go through your own server — an API key must never reach the browser.

500 on every retry

Note the X-Request-Id of one failed request and contact support with it. The SDKs already retried three times before surfacing the error.

Next steps

Was this page helpful?

Related