Skip to main content
Every refused or failed request returns the same JSON shape, with a code you can branch on. This page describes that shape, the errors that every operation can return, the retry rule of each, and the limits on the number of requests.

Error format

The six keys of error are always present. The last three are null when they do not apply. On 400 validation_failed, the code of an issue names the check that failed, for example invalid_type, invalid_value, invalid_format, invalid_union, too_small, too_big, unrecognized_keys or custom. The list is open: read path and message. Two refusals add to their issue: limit_out_of_bounds carries min and max, and webhook_url_not_allowed carries the reason in the code of the issue. On POST /v1/lines/assign, POST /v1/lines/shared and POST /v1/macos-sessions/rent, an Idempotency-Key longer than 255 characters returns an issue on idempotency-key with the code too_big and max. GET /v1/health does not use this format on 503: it returns the health object itself. See GET /v1/health.

Errors

These errors are not specific to one operation. The retry rules, in two sentences:
  • Retry a 429 after Retry-After seconds. Retry a 500 or a 503 up to 2 times, with exponential backoff. Do not retry any other 4xx: fix the request.
  • Retry a write only on an operation that honors Idempotency-Key, with the key of the first attempt.
One 4xx changed nothing and can be sent again once: 409 purchase_in_progress from POST /v1/lines/{id}/release. Idempotency tells you what to do with a write whose response you did not receive. The helpers apply these rules. When a 500 persists, report it. Write to support@isnap.ai with the trace_id and request_id of the response.

Service unavailable

A 503 in the error format carries one of six codes, and its error.retry_after is null. None of these refusals sends a message, delivers an event, starts a trial or changes a pre-order: retry it up to 2 times, even on POST /v1/trial/init, the replay operation and the two pre-order operations, which ignore Idempotency-Key. A refused replay leaves a delivery record with the status failed, and each retry adds one.

Codes of each domain

Each operation adds its own codes. The reference page of an operation lists them, and its guide explains them. Three rules for these codes:
  • Read a code together with the operation that returned it. The same code can come with another status on another operation: line_not_active is a 400 on a send with an API key, a 403 on a send with a trial key, and a 409 on a variant change, and pre_order_not_draft is a 400 or a 409.
  • Do not treat a list of codes as closed. The line_ codes that refuse a reservation are an open set: treat any 409 of a reservation as “choose another line”.
  • For a code your client does not know, apply the retry rules by status.

Rate limits

Both limits are counted per network address, before the key is read. Neither is tied to an API key or to an account:
  • All the keys and all the accounts that call from one network address share one budget.
  • A request with a wrong key, a request without a key and a request to an operation that needs no credential all count. A POST /v1/messages refused with 403 forbidden because its Authorization header carries no API key does not count against the message-operations limit: see How a request is checked.
  • A request refused with 429 counts too. A caller that keeps sending stays refused: stop for 60 seconds.
The paths under /v1/messages/{id} are outside the message-operations limit.

Headers

Every response carries three headers.
Among the headers of the response:

Over the limit

Over the limit, the API returns 429 with error.code rate_limit_exceeded, error.retry_after 60 and the header Retry-After: 60.
To stay under the limit:
  • Receive events by webhook instead of reading the same resource in a loop: see Webhooks overview.
  • Walk lists with limit=100: see Pagination.
  • When several of your systems call from one address, give them one shared budget.

Other 429 codes

Four other limits return 429, each with its own code. They are not the request-rate limit, and the X-RateLimit- headers do not describe them. Each carries error.retry_after and Retry-After. The delay can be long: read it before you wait.