Error contract

One body for every error, one stable identifier to branch on.

Every API error returns the same JSON body, without exception — including validation errors, which would otherwise come back in a different, framework-imposed shape.

json
{
  "detail": "invalid or revoked API key",
  "error_code": "invalid_api_key"
}

Fields that don't apply are omitted, never returned as null.

FieldAlways presentPurpose
error_codeyesStable, language-independent identifier. The only field to branch code on.
detailyesHuman text, for logs and display. Its wording and language may change.
scopenorate_limit_exceeded only: "ip", "account" or "email".
retry_after_secondsnorate_limit_exceeded only. Also returned as the standard Retry-After HTTP header.
quota_used, quota_limitnoquota_exceeded only.
field_errorsnovalidation_error only: the per-field detail.
The code, not the message
Branch on error_code, never on detail. detail is human text: its wording and language can change without notice. error_code cannot.

Codes reachable with an API key

This is your real surface: a script authenticated by API key can only ever hit these seven codes. The others, listed further down, come from the dashboard or from webhooks.

error_codeHTTPWhenWhat to do
missing_bearer_prefix401Authorization header present but missing the Bearer prefix.Fix the header. Configuration error, not transient.
invalid_api_key401Unknown or revoked key, or deleted account.Hard failure. Don't retry: regenerate the key and update your CI secret.
validation_error422Malformed request body, or an unknown field.Read field_errors: loc gives the path to the offending field. Hard failure.
rate_limit_exceeded429Rate exceeded (60 requests/hour per account).Retry after retry_after_seconds. The only case where an automatic retry makes sense.
quota_exceeded402Monthly simulation quota exhausted. No audit is created.Don't insist: nothing frees up before the next cycle.
audit_not_found404Unknown audit id.Check the id returned at creation.
report_not_found404Report requested before the audit finished.Wait for status: "done". Not an error — you polled too early.

Why 402 and not 429 for quota

A 429 invites a retry. An exhausted monthly quota doesn't clear by retrying — hence a distinct, machine-readable code. And no audit is created for that request: no partial score quietly handed to a pipeline using it as a blocking gate. A loud failure beats a degraded result you believe is complete.

quota_used and quota_limit come with the error. That is today the only consumption signal available from a script, since account endpoints don't accept API keys yet.

The other codes

Reachable from a dashboard session or from billing webhooks. Listed so the reference is complete: an API key does not produce them.

error_codeHTTPContext
session_required401Account endpoint called without a session token.
invalid_session401Unknown, revoked or expired session.
internal_token_required401Endpoint reserved for the application's internal proxy.
invalid_magic_link_token400Sign-in link invalid, expired or already used.
api_key_not_found404Revocation requested while no key is active.
session_not_found404Revoking an unknown or already-revoked session.
badge_token_not_found404Revoking an unknown or already-revoked badge.
badge_token_already_exists409An active badge already exists for this store — revoke it first.
already_subscribed409Opening a checkout while a subscription is already active.
no_billing_account404Opening the portal with no associated billing customer.
invalid_stripe_signature400Invalid webhook signature.
billing_provider_error502Billing provider temporarily unavailable. Transient.

Message language

detail is in English by default, unlike the rest of the product — audit reports and emails default to French. That's deliberate: the audience for these messages is an integrator, not a merchant.

French is available by passing language: "fr" in the request body, but only for errors raised after that body has been validated. This is a structural limit, not an oversight: authentication and rate limiting run before the body is bound to its schema. At that point, no language signal exists yet.

Always EnglishHonors language
missing_bearer_prefix, invalid_api_key, validation_error, the 404s, and rate_limit_exceeded on audit creationquota_exceeded, and rate_limit_exceeded on the sign-in link request

field_errors is never translated: those messages are produced by the schema validators themselves.

Handling errors properly in CI

bash
response=$(curl -sS -w '\n%{http_code}' \
  -X POST https://api.agent-readiness.com/audits \
  -H "Authorization: Bearer $CABFY_API_KEY" \
  -H 'Content-Type: application/json' \
  -d '{"business": "https://my-store.com"}')

status=$(tail -n1 <<< "$response")
body=$(sed '$d' <<< "$response")
code=$(jq -r '.error_code // empty' <<< "$body")

case "$code" in
  "")                  ;;  # success
  rate_limit_exceeded) sleep "$(jq -r .retry_after_seconds <<< "$body")" ;;
  quota_exceeded)      echo "Quota exhausted: $(jq -r .quota_used <<< "$body")/$(jq -r .quota_limit <<< "$body")" >&2; exit 1 ;;
  *)                   echo "Failed ($status): $(jq -r .detail <<< "$body")" >&2; exit 1 ;;
esac

Three behaviours, only three: retry while respecting the advertised delay (429), fail loudly because nothing is coming back this month (402), fail (everything else). The command line already applies exactly this logic — this page describes what it does, for those integrating the API directly.