Errors
The shape
Every non-2xx response has the same three fields:
{
"error": "RATE_LIMIT_EXCEEDED",
"message": "Client ingestion rate limit exceeded",
"status_code": 429
}
Branch on error. It is a stable machine-readable code. message is prose
for your logs and may change without notice. status_code repeats the HTTP
status for clients that only surface the body.
info/api_schema_contract.md describes a nested envelope —
{ status: "error", error: { code, message, details, request_id } }. The
gateway does not implement that. The flat three-field shape above is what is
actually returned, and this page is generated against the same source as the
API reference. Build against this.
In particular there is no request_id on error responses, and no
details map for field-level validation errors.
The codes
| Status | error | Cause | Retry? |
|---|---|---|---|
400 | BAD_REQUEST | Body failed validation — see the cases below | No. Fix the request. |
401 | UNAUTHORIZED | Missing, malformed or unrecognised API key | No. Fix the key. |
401 | INVALID_SIGNATURE | A provider webhook signature did not verify | n/a — providers, not you |
409 | IDEMPOTENCY_CONFLICT | Defined for a key reused with a different payload | No. See note below. |
429 | RATE_LIMIT_EXCEEDED | Per-subscriber limit exceeded | Yes, with backoff |
500 | INTERNAL_SERVER_ERROR | Protobuf encoding, Redis or broker failure | Yes, with the same idempotency key |
What produces a 400
| Endpoint | Cause |
|---|---|
POST /v1/events/trigger | The recipient has no contact details for any channel — supply at least one of to.email, to.phone or to.deviceTokens |
POST /v1/events/bulk | An empty events array, or more than 5,000 entries |
PUT /v1/users/{id}/preferences | Neither channels nor categories was supplied |
A batch whose individual events fail validation does not return 400.
The batch returns 200 with those events marked FAILED in the events
array — see Bulk sending.
What is safe to retry
429 — always safe. The rate limiter runs before the idempotency check, so
nothing was enqueued and your Idempotency-Key is still unused. Back off with
jitter; there is no Retry-After header to read. See Rate
limits.
500 — safe with an idempotency key. A 500 means the gateway failed
after authenticating you, possibly after publishing to the broker. Without a
key, a retry may duplicate the notification. With one, it cannot. See
Idempotency.
400 and 401 — never. These are deterministic. Retrying produces the
same result and burns your rate-limit budget.
Network timeout with no response — treat as 500. You cannot tell whether
the event was published. Retry with the same key.
function isRetryable(status: number): boolean {
return status === 429 || status >= 500;
}
Codes you will not see
409 IDEMPOTENCY_CONFLICT is part of the contract but the gateway does not
currently compare payloads — a reused key returns the cached response instead
of conflicting. Handle the code for forward compatibility, but do not rely on
it to catch key-reuse bugs. See
Idempotency.
Errors you will not get
Some failures are silent by design:
- An unrecognised
X-Priority-Overridevalue becomesNORMALwith no error. Check thepriorityfield in the response. - An event with no usable contact details is accepted with
202and then dropped by the worker. There is no synchronous validation that a channel is reachable, and no error response. See Channels & providers. - A
payloadwhose variables are never rendered — see the templating note in Channels & providers.
The pattern: the gateway validates shape, not deliverability. Deliverability shows up in delivery receipts.