Skip to content

Errors ​

Every failure — including a 405 from an unsupported method and a 500 from a bug — comes back as JSON in the same envelope. You never have to parse an HTML error page or branch on content-type.

The envelope ​

json
{
  "error": {
    "code": "INVALID_INPUT",
    "message": "Send at most 500 rows per request",
    "path": ["rows"]
  }
}
FieldAlways presentMeaning
codeYesA stable, machine-readable identifier. Branch on this.
messageYesHuman-readable, and may change without notice. Do not parse it.
pathNoPresent on validation failures; points at the offending field.

path is an array of strings and numbers forming a path into your request body, with numbers being array indices. ["rows", 12, "phone"] means the phone field of the thirteenth row. Use it to point a human at the row they got wrong instead of making them bisect a 500-row payload.

The envelope is built by json_err in src/lib/utils/api/response.util.ts.

Codes ​

CodeStatusWhen
INVALID_INPUT400The body or query failed validation, or an idempotency-key was reused for a different body. Usually carries path.
UNAUTHORIZED401No key, an unparseable key, or a key we do not recognise.
PAYMENT_REQUIRED402Your organization's prepaid credit cannot cover the request — a QA review. Top up, then retry.
FORBIDDEN403The key is valid but lacks a scope, or the org is the wrong kind.
NOT_FOUND404No such route, or no such record in your organization.
METHOD_NOT_ALLOWED405Real route, wrong verb. Carries an allow header.
TIMEOUT408The request took too long to process.
DUPLICATE409The record already exists.
CONFLICT409The record is still in use, or a request with the same idempotency-key is still running.
TOO_LARGE413The body exceeded the endpoint's limit.
UNSUPPORTED_MEDIA_TYPE415Missing or wrong content-type.
TOO_MANY_REQUESTS429A rate limit was hit. See Rate limits.
INTERNAL_SERVER_ERROR500Our fault. Quote your x-request-id.

Defined in src/lib/const/error.const.ts. Two codes share 409, so branch on code, not on status — DUPLICATE and CONFLICT are different situations with different fixes.

This list can grow within v1. Treat an unrecognised code as a failure of its status class rather than crashing on it.

The two you will hit first ​

Getting the content type wrong:

json
{
  "error": {
    "code": "UNSUPPORTED_MEDIA_TYPE",
    "message": "Send a JSON body with `content-type: application/json`."
  }
}

A charset parameter is accepted — application/json; charset=utf-8 is fine. A missing header is not.

Sending something that is not JSON:

json
{
  "error": {
    "code": "INVALID_INPUT",
    "message": "Request body is not valid JSON."
  }
}

TOO_LARGE is checked before authentication, from the content-length header, so an oversized body is refused without your key being verified or any rate-limit token being spent. A body sent without content-length (chunked) is measured as it is read instead, after your key is verified: that 413 spends the request's tokens, as any other refusal after the limiter does.

What to retry ​

CodeRetry?
TOO_MANY_REQUESTSYes — wait retry-after seconds, add jitter.
CONFLICT on an idempotency-keyYes — shortly, with the same key. The first request is still running.
INTERNAL_SERVER_ERRORYes — exponential backoff, a few attempts, then alert.
TIMEOUTYes — with backoff.
Everything elseNo. Retrying will produce the same answer; fix the request.

Before retrying anything that writes, make sure the retry carries the same idempotency-key as the first attempt, so it cannot apply twice — see Idempotency.

Getting help ​

Every response carries x-request-id, echoed from your request if you sent a usable one (see the overview), minted otherwise. Capture it and quote it. With a request id we can find the exact request; without one, a timestamp and a rough description is all anybody has to work with.