Skip to content

Errors

Every error, on every surface, has the same shape:

json
{
  "error": {
    "code": "unauthorized",
    "message": "Missing scope: cards:write",
    "request_id": "req_01J9…"
  }
}
  • Switch on code, never on message. Codes are stable; messages are prose and get better over time.
  • request_id is echoed in the X-Request-Id response header on every response, not just failures. Log it. Quoting it on a support ticket turns "it broke yesterday" into a single indexed lookup.
  • details appears on validation failures and lists the offending fields.

The catalogue below is generated from the server's shared error vocabulary, so a code cannot ship without a row here.

Codes the Business API returns

These are the codes worth handling in an integration.

error.codeHTTPWhen it happensWhat to do
bad_request400The request is well-formed but cannot be satisfied as written (a semantic problem no schema can catch).Read message. Not retryable unchanged.
business_api_required401The shop has no active Business API add-on, and no internal grant standing in for one. Also returned when the whole module is switched off at the deployment.Switch on Business API from Billing in the dashboard, or ask support for a grant. The key is not revoked and is not re-issued — do not mint a new one.
card_expired409 / 422The card instance's expires_at has passed.Reissue the card, or extend the expiry before it lapses.
card_locked409The card instance is installed on a device, which freezes its holder fields and expiry.Reissue the card instead of editing it. This is a state, not a failure — a locked card is a card someone is carrying.
conflict409The write conflicts with the resource's current state (a duplicate, or a transition that is not allowed from here).Re-read the resource and decide. Blind retries will not clear it.
idempotency_conflict409An Idempotency-Key was reused with a different request body, OR the key was claimed by an earlier request that never recorded a response.Do NOT retry with the same key. The message distinguishes the two cases: fix the caller in the first, start over with a fresh key in the second.
internal_error500An unhandled server-side failure. message is deliberately generic.Retry with the same Idempotency-Key — that is exactly what it is for. Quote request_id on a support ticket.
location_disabled402 on authentication, 400 / 422 in service checksThe location the request concerns is deactivated or soft-deleted.Re-enable the location, or target another one.
location_scope_unsupported403A location-restricted key called an analytics endpoint whose series are not derived from transactions, so they cannot be narrowed to one location.Use an unrestricted key for those endpoints. The 403 names the endpoint; shop-wide numbers are never silently served to a restricted key.
mcp_access_required401The credential is fine and REST access may well be working — what is missing is entitlement to the MCP connector specifically, which has its own switch and its own grant.A distinct code precisely so nobody is sent to billing for something billing cannot fix: MCP always also requires API access, so check that first. Ask support to enable the connector.
not_found404The resource does not exist, or belongs to another shop. The two are deliberately indistinguishable.Do not treat a 404 as "deleted" — a key whose location_ids exclude the resource also gets a 404 on some paths and a 403 on others.
program_has_installed_cards409The program has at least one installed card, so its template can no longer be edited.Create a new program for a new design. Existing holders keep the card they installed.
rate_limited429The calling key exceeded its per-minute limit. The limit is per KEY, not per shop.Honour Retry-After and retry with the SAME Idempotency-Key. If two integrations share a key, give each its own.
subscription_suspended402 on authentication, 422 in service checksThe shop's subscription is suspended or cancelled (unpaid invoice, or a cancellation that has taken effect).Stop writing and alert a human. Your key is intact.
unauthenticated401No Authorization header, a malformed one, an unknown key, a revoked key, an expired key — or a key of the wrong class (an MCP tmk_ key presented to the REST API).The message names the mistake, including the wrong-class case. Never retry an unauthenticated call in a loop.
unauthorized403The key is valid but lacks the scope the endpoint requires, or its location_ids restriction excludes the target.Call GET /me — it returns the calling key's own scopes and location restriction, which is the fastest way to resolve a 403. Issue a key with the scope you need.
unprocessable_entity422The request is valid but the domain rejects it.Read message; it names the rule.
validation_failed400A request body, query parameter or path parameter failed schema validation.details lists the offending fields with a stable per-field code. Fix the request; retrying it unchanged will fail identically.

Platform codes

Emitted by the shop app, the consumer app or the dashboard. Listed because the vocabulary is shared and you may see them in a support conversation — an integration does not have to handle them.

error.codeHTTPWhen it happensWhat to do
amount_below_min_stamp400A spend amount was too small to earn one stamp.Shop-app scan path only.
cannot_reverse_completed_cycle422 / 500A reversal would undo a final-prize redemption that already reset the card.Shop-app reversal path only.
card_full_no_repeat422A stamp scan hit a full card on a program with repeat turned off.Shop-app scan path only; the Business API never writes transactions.
card_suspended422The card instance is suspended and cannot be used.Unsuspend it from the dashboard.
email_not_verified401A dashboard account has not verified its email address.Dashboard only; API keys are unaffected.
feature_disabled401 / 404A deployment-level feature switch is off (e.g. a sign-in method).Not reachable through the Business API.
gone410A one-time resource (an install link, an enrollment token) has already been used.Issue a new one.
invalid_credentials401Dashboard sign-in with a wrong email or password.Not reachable over the Business API — there is no login.
points_insufficient_balance422A points redemption exceeded the balance.Shop-app scan path only.
prepaid_insufficient_balance422A prepaid deduction exceeded the remaining balance.Shop-app scan path only.
spend_not_enabled400A scan sent an amount to a program that is not spend-based.Shop-app scan path only.
spend_required400 / 422A scan sent a manual stamp count to a spend-based program.Shop-app scan path only.
threshold_already_claimed422A prize at that threshold was already redeemed this cycle.Shop-app redeem path only.
token_invalid401 / 403 / 422A one-time link or verification token (email verification, password reset, passkey challenge, social sign-in) is invalid.Dashboard flows only.

Reserved codes

Declared in the shared vocabulary but not constructed by any handler today. Handle them defensively if you switch on error.code; do not build a flow that depends on one.

error.codeHTTPWhen it happensWhat to do
already_exists409Reserved. Duplicates are reported as conflict.Treat as conflict.
dependency_unavailable503Reserved for a dependency outage.Retry with the same idempotency key after a backoff.
invalid_input400Reserved. No handler constructs it today; validation_failed covers input problems.Treat as validation_failed if you ever see it.
password_reset_required401Reserved for a forced-reset policy.Dashboard only.
token_expired401Reserved. Expired API keys are reported as unauthenticated.Rotate the key.
totp_invalid401Reserved. Two-factor prompts are dashboard-side.Dashboard only.
totp_required401Reserved. Two-factor prompts are dashboard-side.Dashboard only.
upstream_error502Reserved for a third-party failure.Retry with the same idempotency key.

Recoverable failures

These are the errors an unattended integration will meet in normal operation. None of them means your credential is gone.

  • business_api_required — RECOVERABLE. Keys are suspended, never destroyed, when access lapses: disabled_by_plan_at is stamped and cleared again on restore, so the same key resumes working and the customer never re-keys their CRM.
  • location_disabled — RECOVERABLE. Re-enabling the location restores the call with no change on your side.
  • location_scope_unsupported — RECOVERABLE by calling with an unrestricted key. The three transaction-derived endpoints work with a restricted key as-is.
  • mcp_access_required — RECOVERABLE. Like business_api_required, MCP keys are suspended rather than destroyed and resume working the moment access is restored.
  • rate_limited — RECOVERABLE by waiting. It is the one error a correct client resolves entirely on its own.
  • subscription_suspended — RECOVERABLE. Nothing about the key changed: the moment the subscription is settled in the dashboard, the same key works again. Back off, retry periodically, and do not re-key your integration.

Handling errors, in order of what actually happens

  1. 429 and 5xx: retry. With the same Idempotency-Key, honouring Retry-After. See Idempotency — this is the single highest -value thing to get right.
  2. business_api_required, mcp_access_required, subscription_suspended: back off and alert a human. All three are recoverable and none of them means your credential is gone. Do not re-key, and do not delete your stored key — the same key resumes working when the shop's access is restored.
  3. 409 idempotency_conflict: stop. Retrying cannot help. Either your caller reused a key with a different body, or an earlier attempt claimed the key and never finished — the message says which.
  4. 400 and 422: fix the request. The same request will fail identically forever.
  5. 401: read error.code before you touch the credential. A 401 is not always a bad key. unauthenticated is — check the prefix first; the message will tell you if you presented an MCP key to the REST API. But business_api_required and mcp_access_required also return 401, and there the key is intact and only the shop's entitlement is missing. Branch on the code, never on the status alone.

Requires the Business API add-on.