API error reference

Every error the Abundera API returns is actionable: a stable code to branch on, one sentence on how to fix it, a link back to this page, and a machine-actionable hint a coding agent can apply. This page applies to every Abundera product API — the vocabulary is identical across abundera.ai, Sign, and QR Pro.

The error envelope

Errors return HTTP 4xx/5xx with this JSON body. error.code is the stable contract your code branches on; error.message is human-facing and may change. request_id always matches the X-Request-ID header — quote it to support.

{
  "ok": false,
  "error": {
    "code": "payment_required",
    "message": "Pro tier required for dynamic codes",
    "remediation": "The account's tier does not include this feature. Upgrade the subscription, then retry the same request.",
    "doc_url": "https://abundera.ai/docs/errors/#payment_required",
    "fix": { "kind": "upgrade_tier" }
  },
  "request_id": "req_a096cbe8"
}

A handler may sharpen the generic remediation or fix with request-specific detail (a concrete upgrade URL, the exact invalid fields). The shape stays the same.

For agents: branch on fix.kind. The kinds are frozen contract: fix_request_fields, set_header, upgrade_tier, grant_scope, verify_resource_id, refetch_and_retry, retry_after, retry_with_backoff.

validation_error — 400

The request body or parameters are malformed or fail the operation's schema. When the handler can name them, error.fields lists the offending fields.

Fix: correct the fields named in error.fields (or re-check the operation's request schema in the API reference) and resend. fix.kind = fix_request_fields, with fix.source pointing at the error detail.

Returned remediation: Correct the fields named in the error (or re-check the operation's request schema), then resend.

unauthorized — 401

No credential, or an invalid/expired one. 401 means "who are you?" — distinct from 403's "I know you, you can't do this."

Fix: send Authorization: Bearer <API key or session JWT>. Keys are minted in the product dashboard under Settings → API keys (the raw key is shown once). fix.kind = set_header with fix.header and a fix.value_template.

Returned remediation: Send a valid credential: Authorization: Bearer <API key or session JWT>. Keys are minted under Settings then API keys.

payment_required — 402

The authenticated account's tier doesn't include this feature or has exhausted a tier-bound quota.

Fix: upgrade the subscription in the product dashboard, then retry the identical request. When the handler knows it, fix.upgrade_url gives the exact billing page. fix.kind = upgrade_tier.

Returned remediation: The account's tier does not include this feature. Upgrade the subscription, then retry the same request.

forbidden — 403

The credential is valid but lacks the required scope, role, or ownership of the resource.

Fix: mint a key carrying the needed scope (Settings → API keys shows each key's scopes), or act from an account that holds the required role. fix.kind = grant_scope.

Returned remediation: The credential is valid but lacks the required scope or role. Mint a key with the needed scope, or act from an account that holds it.

not_found — 404

The resource doesn't exist — or doesn't belong to the authenticated account (we don't reveal which).

Fix: verify the resource id, then list the parent collection to discover valid ids. fix.kind = verify_resource_id.

Returned remediation: Check the resource id and that it belongs to the authenticated account; list the collection to discover valid ids.

conflict — 409

The request conflicts with current state: a duplicate create, a stale update, or a race another writer won.

Fix: re-read the resource's current state and reapply your change on top of it; for duplicate creates, reuse the existing resource instead of retrying. fix.kind = refetch_and_retry.

Returned remediation: Re-read the resource's current state and reapply the change; for duplicate-creation conflicts, reuse the existing resource.

rate_limited — 429

Too many requests for the credential or IP in the current window. The response carries RateLimit-Limit / RateLimit-Remaining / RateLimit-Reset headers.

Fix: wait the seconds given by RateLimit-Reset (or Retry-After), then retry with backoff. fix.kind = retry_after with fix.source naming the header to read.

Returned remediation: Wait the number of seconds in the RateLimit-Reset (or Retry-After) header, then retry with backoff.

internal_error — 500

An unexpected server fault. Nothing about your request was necessarily wrong.

Fix: retry idempotent requests with jittered backoff (writes that carried an Idempotency-Key are safe to retry as-is). If it persists, contact support quoting the request_id. fix.kind = retry_with_backoff with fix.idempotent_only = true.

Returned remediation: Retry idempotent requests with jittered backoff. If it persists, contact support quoting the request_id.