Error format
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
429afterRetry-Afterseconds. Retry a500or a503up to 2 times, with exponential backoff. Do not retry any other4xx: fix the request. - Retry a write only on an operation that honors
Idempotency-Key, with the key of the first attempt.
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
A503 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_activeis a400on a send with an API key, a403on a send with a trial key, and a409on a variant change, andpre_order_not_draftis a400or a409. - Do not treat a list of codes as closed. The
line_codes that refuse a reservation are an open set: treat any409of 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/messagesrefused with403 forbiddenbecause itsAuthorizationheader carries no API key does not count against the message-operations limit: see How a request is checked. - A request refused with
429counts too. A caller that keeps sending stays refused: stop for 60 seconds.
/v1/messages/{id} are outside the message-operations limit.
Headers
Every response carries three headers.Over the limit
Over the limit, the API returns429 with error.code rate_limit_exceeded, error.retry_after 60 and the header Retry-After: 60.
- 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.