API Error Codes
Every v1 API error carries a stable machine-readable code. Branch on the code — never on the message.
The error shape
Errors return a non-2xx HTTP status and a JSON body with a human-readable error message and a stable code:
{
"error": "No barcodes remaining in this pass's pool.",
"code": "pool_exhausted"
}The code values below are a contract: they never change meaning and are only ever added to. The error strings are for humans and may be reworded — don't match on them. Some errors carry extra context fields (e.g. matchCount on ambiguous_barcode, retryable: true on retry-safe 500s).
Tracing a call
Every response carries an X-Request-Id header, and error bodies also include it as requestId. Quote that value when you contact support and we can find the exact call.
You can supply your own instead: send an X-Request-Id header with any request — your log id, up to 64 characters (letters, digits, ., _, -). We record it alongside ours and echo it back, so a failure in your logs matches ours exactly. If you don't send one, we generate it.
Codes
| Code | Status | Meaning |
|---|---|---|
missing_authorization | 401 | No Bearer token in the Authorization header. |
invalid_api_key | 401 | The API key is unknown or has been revoked. |
forbidden | 403 | The resource exists but belongs to another account. |
invalid_request | 400 | Malformed body or parameters — the error message says exactly what to fix. |
pass_not_found | 404 / 400 | No pass with that ID on your account (400 from Process Order when the passId is inactive). |
instance_not_found | 404 | No pass instance matches that instanceId or barcode. |
holder_not_found | 404 | No loyalty holder matches that identifier. |
no_reward_pending | 409 | POST /loyalty/redeem on a membership card that runs a loyalty program: no reward is pending on the card (the response carries balance and threshold). Rewards on program cards unlock at the threshold; use POST /loyalty/points to award. |
insufficient_points | 409 | The membership card's POINTS balance is below the reward's cost (POST /loyalty/redeem). The response carries balance and cost; nothing was deducted. |
partner_not_found | 404 | No partner with that id on this workspace (unknown, deleted, or another workspace's). |
pass_archived | 400 | Archived passes can't issue new instances. |
fields_not_editable | 400 | This pass has no editable fields — add fields in the studio first. |
template_owned_field | 400 | The key is template-owned (title, details, terms, expiry) — update it for ALL holders instead. |
unknown_field_key | 400 | The field key doesn't exist on the pass; the message lists the valid keys. |
no_loyalty_program | 400 | Points operations on a pass with no loyalty program. |
program_inactive | 400 | The loyalty program exists but is not active. |
program_not_launched | 400 | The loyalty program has not been launched yet. |
fields_update_unsupported | 409 | This holder's card predates per-holder addressing or ownership tracking — per-holder values (or clearing one with null) aren't possible on it; re-adding the pass mints an addressable card. |
pool_exhausted | 409 | No barcodes remain in the pass's pool — upload more codes. |
ambiguous_barcode | 409 | A shared Simple-mode code matches several instances; the response carries matchCount and how to disambiguate. |
asset_fetch_failed | 422 | An image URL could not be used — slot names the parameter and reason says why: invalid_url (not https), blocked_host (private network, localhost, or cloud metadata address), unreachable, timeout (10 s), too_many_redirects (max 3), not_an_image, unsupported_type (PNG, JPEG, WebP only), too_small (below the slot's minimum). Nothing was created or changed; host the file publicly and retry. |
payload_too_large | 413 | An image URL points to a file over 20 MB. Nothing was created or changed; resize the file and retry. |
insufficient_credits | 402 | Not enough credits — top up in Billing and re-run. |
insufficient_scope | 403 | The API key is read-only, or restricted to other passes. Use a key with the right scope; see Key scopes and rotation. |
rate_limited | 429 | Request rate exceeded for this API key. Honor Retry-After, then retry. See Rate limits. |
internal_error | 500 | Something failed on our side. When retrying is safe the response carries retryable: true. |
Retry guidance
- 202 — saved but not delivered: the update applied, but the wallet push was skipped for lack of credits (
pushed: false,pushSkipReason: "insufficient_credits", plus awarningsentry). Top up in Billing and re-send (with a freshexternalRefif you use them) to deliver. - 4xx — don't retry unchanged; fix the request. The exceptions:
insufficient_credits(retry after topping up) andrate_limited(retry with backoff). - 500 with retryable: true — safe to retry: the operation released its claim, and idempotent endpoints (issue, points with externalRef, Process Order) return the original result instead of double-applying.
- 500 without retryable — contact support with the request details.
See also Rate limits and the API & integrations guide.