Errors

Every failed request answers with an HTTP status and a JSON body under error:

{
  "error": {
    "message": "Customer not found",
    "code": "NOT_FOUND"
  }
}

message is written to be shown to a person. code is stable and is what your own code should branch on.

Status codes

StatusCodeWhat happened
400VALIDATION_ERRORA parameter or body field is missing, the wrong type, or outside its allowed range.
400BAD_REQUESTThe request is well formed but cannot be carried out, including reaching a plan limit.
403UNAUTHORIZEDNo credential, or one that is not valid or has expired.
403FORBIDDENAuthenticated, but not allowed to do this: a key missing the endpoint's permission, an address off the allowlist, or a session-only endpoint.
404NOT_FOUNDNo such record, at least not in this organization.
409CONFLICTThe record cannot be created or changed because of one that already exists.
413PAYLOAD_TOO_LARGEThe request body is over 10 MB. Split a bulk import into smaller batches, or upload the rows as a CSV import job.
429TOO_MANY_REQUESTSRate limited. Read the Retry-After header.
500INTERNAL_SERVER_ERRORSomething broke on our side. Retrying is reasonable.

A missing or invalid credential answers 403, not 401.

Validation errors

A VALIDATION_ERROR carries an issues array saying exactly which fields were wrong and where they were:

{
  "error": {
    "code": "VALIDATION_ERROR",
    "message": "Field \"rating\": Expected a number.",
    "issues": [
      {
        "code": "invalid_type",
        "expected": "number",
        "path": ["rating"],
        "message": "Expected a number."
      }
    ]
  }
}

message is written to be shown to a person as it is, with one sentence per problem naming the field. Each issue's own message is that sentence without the field name, for showing beside the field it belongs to. path is the full route to the offending field, so ["data", 0, "email"] on an import means the email of the first row you sent.

Filtering has its own validation: an unknown field, an operator mixed with plain keys, or a value that does not fit the column all return 400 describing the problem. See Filtering.

Permission errors

A key that does not hold the endpoint's permission gets the missing scope back in the message, which is usually enough to fix it without reading anything else:

{
  "error": {
    "message": "This API key does not have the `reviews:write` permission.",
    "code": "FORBIDDEN"
  }
}

Update the key's permissions under Settings → API keys, or use a key that already holds it. See Authentication.

Plan limits

Reaching a limit on your plan is a 400 with BAD_REQUEST, worded as what you reached and what to do about it:

{
  "error": {
    "message": "You have reached the limit of 5 sites. Upgrade your plan to add more.",
    "code": "BAD_REQUEST"
  }
}

Some limits block the action outright and some only warn in the dashboard. See Plans and limits.

Rate limiting

A 429 carries a Retry-After header with whole seconds until the request can be afforded again. Wait that long rather than retrying immediately, and prefer larger pages to more requests, because one call for 250 records costs the same as one call for 10.

HTTP/1.1 429 Too Many Requests
Retry-After: 3

See Rate limits for the size of the budget and what spends it.

Retrying

Retry 429 after the interval it gives you, and 500 with a backoff. Do not retry 400, 403, 404, 409 or 413, because the same request will fail the same way until something changes.

If you are retrying a request that records an event, send an idempotency key so a duplicate does not count twice. See Conventions.