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). On the card endpoints the usual cause is an ARCHIVED card: archiving is terminal, so the card can be neither disabled nor enabled, cannot be reissued, and its holder fields can no longer be edited.Re-read the resource and decide. Blind retries will not clear it — and on an archived card nothing will, because there is no unarchive: issue a new card.
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 one of the 13 endpoints that cannot be narrowed to a location. Two families: every CARD endpoint except issuing one — a card carries no location anywhere in TesserApp, it belongs to a program and programs belong to the whole shop — and the ANALYTICS endpoints whose series are not derived from transactions.Use an unrestricted key for those endpoints. The 403 names the endpoint, and shop-wide data is never silently served to a restricted key instead. GET /transactions is the read that IS site-attributed, so a restricted key can answer "what happened at this site" there.
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.
program_inactive409The card’s program is deleted, switched off (unpublished) or suspended, so the card cannot be brought back: POST /cards/{id}/enable and POST /cards/{id}/reissue refuse it (disabling still works). The shop app gets the same code when it tries to issue a personal card on such a program.Reactivate the program first (publish it, or unsuspend it), then retry. A deleted program cannot be reactivated — issue the card on another program. Not retryable unchanged.
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 is not in a usable state. Since migration 0054 that is overwhelmingly the DISABLED state: fourteen service-layer sites gate on state = 'active', so a card the merchant switched off refuses every scan, redemption, gift card deduction and reversal.Read the card. A status of disabled is undone with POST /cards/{id}/enable and the card returns with its stamps, its expiry and its install intact; a status of archived is TERMINAL, so issue a new card instead. Returned by the shop app's scan paths — the Business API writes no transactions and never returns it.
coupon_not_applicable400The promotion code is unknown, inactive, expired, out of redemptions, or is a CHECKOUT code rather than a subscription one. details.reason says which.Dashboard billing page only. Subscription coupons must carry apply_to: subscription metadata in Stripe.
device_not_bound403A shop-app call that acts as "the device bound to this location" (pairing a kiosk till by QR, listing, renaming or removing the kiosks) came from a session with no live device at a live location.Shop app only. Bind the phone to a location first, then retry.
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.
impersonation_forbidden403An owner-only dashboard action (credentials, other sessions, legal acceptance, account deletion) was attempted while a TesserApp administrator was signed in as the shop for support.Dashboard only; API keys are unaffected. The owner performs the action from their own session.
invalid_credentials401Dashboard sign-in with a wrong email or password.Not reachable over the Business API — there is no login.
invalid_current_password400The dashboard "Change password" form re-typed the current password wrongly. 400 rather than 401: the session is fine.Dashboard only. Retype the current password; repeated failures are rate limited like sign-in.
kiosk_cannot_approve403A kiosk till tried to approve another till’s QR pairing code. Only a non-kiosk device (the phone) can vouch for a kiosk, which then joins that device’s location.Shop app only. Approve the code from the phone bound to the location.
no_active_subscription409A coupon was applied to a shop with no live subscription — there is nothing to discount.Dashboard billing page only.
no_billing_account409The shop has no Stripe customer yet — it has never paid for anything.Dashboard billing page only. Add a location (which creates the customer) first.
password_not_set409A password change was requested for an account that signs in only with Google, Apple or a passkey, so there is no current password.Dashboard only. Set a first password through "Forgot password", which proves control of the login email.
payment_method_is_default409A payment method was removed while it is the one the subscription is billed to.Dashboard billing page only. Set another method as default, then remove this one.
payment_method_last_one409The only payment method on file was removed while a subscription is still live — the next renewal would have nothing to charge.Dashboard billing page only. Add another method first.
points_insufficient_balance422A points redemption exceeded the balance.Shop-app scan path only.
prepaid_insufficient_balance422A gift card (type 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. GET /transactions and the three transaction-derived analytics 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.