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.