Skip to main content

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.

If you read the internal schema contract

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

StatuserrorCauseRetry?
400BAD_REQUESTBody failed validation — see the cases belowNo. Fix the request.
401UNAUTHORIZEDMissing, malformed or unrecognised API keyNo. Fix the key.
401INVALID_SIGNATUREA provider webhook signature did not verifyn/a — providers, not you
409IDEMPOTENCY_CONFLICTDefined for a key reused with a different payloadNo. See note below.
429RATE_LIMIT_EXCEEDEDPer-subscriber limit exceededYes, with backoff
500INTERNAL_SERVER_ERRORProtobuf encoding, Redis or broker failureYes, with the same idempotency key

What produces a 400

EndpointCause
POST /v1/events/triggerThe recipient has no contact details for any channel — supply at least one of to.email, to.phone or to.deviceTokens
POST /v1/events/bulkAn empty events array, or more than 5,000 entries
PUT /v1/users/{id}/preferencesNeither 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-Override value becomes NORMAL with no error. Check the priority field in the response.
  • An event with no usable contact details is accepted with 202 and then dropped by the worker. There is no synchronous validation that a channel is reachable, and no error response. See Channels & providers.
  • A payload whose 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.

Next