# Error Handling

BillerAPI uses conventional HTTP status codes and one structured error envelope on every non-2xx response, so you can branch on a stable code and show an actionable message.

## The error envelope

Every error response returns this JSON body. It is the exact shape on the wire — no other error dialects.

**Error response**

**JSON**

```json
{
  "error_code": "BILL_NOT_FOUND",
  "error_type": "invalid_request",
  "error_message": "No bill exists with id bill_abc123.",
  "hint": "Verify the bill_id from a recent list call.",
  "docs_url": "https://docs.billerapi.com/errors/BILL_NOT_FOUND",
  "request_id": "d94f5e2a-8c3b-4f1e-9a7d-6b2c1e0f8a34",
  "retryable": false
}
```

- `error_code` — A stable, machine-readable code in `UPPER_SNAKE_CASE` (see the reference below). Branch on this — it is an additive-only contract.
- `error_type` — Coarse category for retry/backoff decisions: `invalid_request`, `rate_limit`, `auth`, `upstream`, or `api_error`.
- `error_message` — A human-readable description of the problem.
- `hint` — An actionable next step for resolving the error.
- `docs_url` — A link to this exact error code's docs page (`https://docs.billerapi.com/errors/<code>`).
- `request_id` — Correlation id for the failing request — also returned as the `X-Request-Id` response header on *every* response. Quote it in support tickets.
- `retryable` — Whether retrying or resuming the operation is supported. Older pass-through errors may omit this field during migration.
- `retry_after` — Optional. On `429`, the number of seconds to wait before retrying (also on the `Retry-After` header).
- `errors` — Optional. On `400` validation failures, an array of per-field problems (see below).

> **Note — Deprecated alias.**
> During the current migration window some responses also include a `documentation_url` field. It duplicates `docs_url` and will be removed — read `docs_url`.

## Validation errors

A `400 VALIDATION_ERROR` carries an `errors` array — one entry per offending field, so you can map problems straight onto a form. Each item names the field in the same `snake_case` you sent it.

**400 VALIDATION_ERROR**

**JSON**

```json
{
  "error_code": "VALIDATION_ERROR",
  "error_type": "invalid_request",
  "error_message": "One or more request fields are invalid. See errors[] for details.",
  "hint": "Inspect errors[] for the offending fields and retry with a corrected request.",
  "docs_url": "https://docs.billerapi.com/errors/VALIDATION_ERROR",
  "request_id": "d94f5e2a-8c3b-4f1e-9a7d-6b2c1e0f8a34",
  "errors": [
    { "param": "given_name", "code": "required", "message": "given_name should not be empty" },
    { "param": "email", "code": "invalid_format", "message": "email must be an email" }
  ]
}
```

- `param` — The offending field, in the snake_case the API accepts. A stray camelCase field is reported with code `unknown_parameter` and a hint about the expected name.
- `code` — A stable reason: `required`, `invalid_type`, `invalid_format`, `too_short`, `too_long`, `unknown_parameter`.
- `message` — A human-readable, single-field explanation.

## HTTP Status Codes

| Code | Meaning | Description |
| --- | --- | --- |
| 200 | OK | Request succeeded. |
| 201 | Created | Resource was created successfully. |
| 202 | Accepted | Request accepted for async processing (e.g., bill sync). |
| 400 | Bad Request | Invalid request body or parameters (see `errors[]`). |
| 401 | Unauthorized | Missing or invalid authentication credentials. |
| 403 | Forbidden | Valid credentials but insufficient permissions. |
| 404 | Not Found | The requested resource does not exist. |
| 429 | Too Many Requests | Rate limit exceeded. See `retry_after` / Retry-After. |
| 500 | Internal Server Error | Something went wrong on our end. Contact support if persistent. |

## Error code reference

The `error_code` field is one of these stable codes. Each links to a dedicated page at `https://docs.billerapi.com/errors/<code>` with cause, fix, and an example. The list is additive-only.

### General

| Error Code | HTTP | Type | Summary |
| --- | --- | --- | --- |
| [`INTERNAL_ERROR`](/docs/errors/INTERNAL_ERROR) | 500 | `api_error` | An unexpected error occurred on our end. |
| [`VALIDATION_ERROR`](/docs/errors/VALIDATION_ERROR) | 400 | `invalid_request` | One or more request fields are invalid. |
| [`NOT_FOUND`](/docs/errors/NOT_FOUND) | 404 | `invalid_request` | The requested resource does not exist. |
| [`CONFLICT`](/docs/errors/CONFLICT) | 409 | `invalid_request` | The request conflicts with the current resource state. |
| [`INVALID_CURSOR`](/docs/errors/INVALID_CURSOR) | 400 | `invalid_request` | The pagination cursor is malformed or stale. |
| [`IDEMPOTENCY_KEY_MISMATCH`](/docs/errors/IDEMPOTENCY_KEY_MISMATCH) | 409 | `invalid_request` | The Idempotency-Key was reused with a different body. |
| [`IDEMPOTENCY_KEY_NOT_REPLAYABLE`](/docs/errors/IDEMPOTENCY_KEY_NOT_REPLAYABLE) | 409 | `invalid_request` | The one-time response for this Idempotency-Key cannot be replayed. |
| [`RATE_LIMITED`](/docs/errors/RATE_LIMITED) | 429 | `rate_limit` | You have exceeded the allowed request rate. |
| [`SERVICE_UNAVAILABLE`](/docs/errors/SERVICE_UNAVAILABLE) | 503 | `upstream` | An upstream dependency is temporarily unavailable. |
| [`WEBHOOK_NOT_FOUND`](/docs/errors/WEBHOOK_NOT_FOUND) | 404 | `invalid_request` | No webhook with that id. |
| [`WEBHOOK_ENDPOINT_NOT_FOUND`](/docs/errors/WEBHOOK_ENDPOINT_NOT_FOUND) | 404 | `invalid_request` | No webhook endpoint with that id. |
| [`INVALID_WEBHOOK_UPDATE`](/docs/errors/INVALID_WEBHOOK_UPDATE) | 400 | `invalid_request` | The webhook patch failed validation. |
| [`WEBHOOK_REGISTRATION_FAILED`](/docs/errors/WEBHOOK_REGISTRATION_FAILED) | 500 | `api_error` | The webhook could not be persisted. |
| [`WEBHOOK_CONFIGURATION_NOT_FOUND`](/docs/errors/WEBHOOK_CONFIGURATION_NOT_FOUND) | 404 | `invalid_request` | This client has no webhook configuration for that environment. |
| [`INVALID_IDEMPOTENCY_KEY`](/docs/errors/INVALID_IDEMPOTENCY_KEY) | 400 | `invalid_request` | The Idempotency-Key header is missing or malformed. |
| [`IDEMPOTENCY_KEY_REUSED`](/docs/errors/IDEMPOTENCY_KEY_REUSED) | 409 | `invalid_request` | The Idempotency-Key was reused with different parameters. |
| [`WEBHOOK_LOOKUP_FAILED`](/docs/errors/WEBHOOK_LOOKUP_FAILED) | 500 | `api_error` | The webhook ownership row could not be read. |
| [`WEBHOOK_UPDATE_FAILED`](/docs/errors/WEBHOOK_UPDATE_FAILED) | 500 | `api_error` | The endpoint update could not be persisted. |
| [`WEBHOOK_UPDATE_OUTCOME_UNKNOWN`](/docs/errors/WEBHOOK_UPDATE_OUTCOME_UNKNOWN) | 500 | `api_error` | The update committed but its stored state could not be read back. |
| [`WEBHOOK_ROTATION_FAILED`](/docs/errors/WEBHOOK_ROTATION_FAILED) | 500 | `api_error` | The signing-secret rotation could not be persisted. |
| [`INVALID_WEBHOOK_OUTCOME_ID`](/docs/errors/INVALID_WEBHOOK_OUTCOME_ID) | 400 | `invalid_request` | The delivery-outcome identity is missing or malformed. |
| [`WEBHOOK_OUTCOME_UNKNOWN`](/docs/errors/WEBHOOK_OUTCOME_UNKNOWN) | 500 | `api_error` | A prior delivery outcome could not be resolved. |
| [`WEBHOOK_OUTCOME_ID_REUSED`](/docs/errors/WEBHOOK_OUTCOME_ID_REUSED) | 409 | `invalid_request` | The delivery-outcome identity was reused with different parameters. |
| [`WEBHOOK_DISABLE_NOTIFICATION_PENDING`](/docs/errors/WEBHOOK_DISABLE_NOTIFICATION_PENDING) | 500 | `api_error` | The endpoint was auto-disabled but the owner was not notified. |
| [`WEBHOOK_UPDATE_CONFLICT`](/docs/errors/WEBHOOK_UPDATE_CONFLICT) | 409 | `invalid_request` | A concurrent writer updated this endpoint first. |
| [`WEBHOOK_SAVE_OUTCOME_UNKNOWN`](/docs/errors/WEBHOOK_SAVE_OUTCOME_UNKNOWN) | 500 | `api_error` | The write may or may not have committed. |
| [`WEBHOOK_REGISTRATION_CONFLICT`](/docs/errors/WEBHOOK_REGISTRATION_CONFLICT) | 409 | `invalid_request` | A concurrent registration for this environment won the race. |
| [`WEBHOOK_REGISTRATION_OUTCOME_UNKNOWN`](/docs/errors/WEBHOOK_REGISTRATION_OUTCOME_UNKNOWN) | 500 | `api_error` | The registration may have committed without a confirmed result. |
| [`WEBHOOK_ROTATION_CONFLICT`](/docs/errors/WEBHOOK_ROTATION_CONFLICT) | 409 | `invalid_request` | A concurrent rotation replaced the signing secret first. |
| [`WEBHOOK_ROTATION_OUTCOME_UNKNOWN`](/docs/errors/WEBHOOK_ROTATION_OUTCOME_UNKNOWN) | 500 | `api_error` | The rotation may have committed without a confirmed result. |
| [`NOT_IMPLEMENTED`](/docs/errors/NOT_IMPLEMENTED) | 500 | `api_error` | The operation is declared but not implemented on this service. |
| [`UNKNOWN`](/docs/errors/UNKNOWN) | 500 | `api_error` | The identity provider refused the operation for an unclassified reason. |
| [`BAD_REQUEST`](/docs/errors/BAD_REQUEST) | 400 | `invalid_request` | A caller-side precondition failed. |

### Authentication

| Error Code | HTTP | Type | Summary |
| --- | --- | --- | --- |
| [`UNAUTHORIZED`](/docs/errors/UNAUTHORIZED) | 401 | `auth` | Authentication failed or is missing. |
| [`CLIENT_CREATION_NOT_AVAILABLE`](/docs/errors/CLIENT_CREATION_NOT_AVAILABLE) | 501 | `api_error` | Additional client creation is not available. |
| [`COOKIE_MUTATION_POLICY_REJECTED`](/docs/errors/COOKIE_MUTATION_POLICY_REJECTED) | 403 | `auth` | The browser mutation failed the cookie-session policy. |
| [`FORBIDDEN`](/docs/errors/FORBIDDEN) | 403 | `auth` | Authenticated, but not authorized for this resource. |
| [`CLIENT_ID_MISMATCH`](/docs/errors/CLIENT_ID_MISMATCH) | 403 | `auth` | The supplied client_id does not match your session. |
| [`NEEDS_RESIGNIN`](/docs/errors/NEEDS_RESIGNIN) | 401 | `auth` | No authenticated client context was resolved. |
| [`SIGNIN_CREDENTIALS_REJECTED`](/docs/errors/SIGNIN_CREDENTIALS_REJECTED) | 401 | `auth` | Sign-in was refused. |
| [`AUTHENTICATION_INCOMPLETE`](/docs/errors/AUTHENTICATION_INCOMPLETE) | 401 | `auth` | Sign-in stopped short of issuing tokens. |
| [`USER_NOT_CONFIRMED`](/docs/errors/USER_NOT_CONFIRMED) | 401 | `auth` | The account was never confirmed after signup. |
| [`INVALID_REFRESH_TOKEN`](/docs/errors/INVALID_REFRESH_TOKEN) | 401 | `auth` | The refresh token was rejected. |
| [`INVALID_CONFIRMATION_CODE`](/docs/errors/INVALID_CONFIRMATION_CODE) | 400 | `invalid_request` | The signup confirmation code did not match. |
| [`PASSWORD_RESET_FAILED`](/docs/errors/PASSWORD_RESET_FAILED) | 400 | `invalid_request` | The password reset could not be applied. |
| [`FORGOT_PASSWORD_FAILED`](/docs/errors/FORGOT_PASSWORD_FAILED) | 500 | `api_error` | The forgot-password flow could not be started. |
| [`SIGNUP_FAILED`](/docs/errors/SIGNUP_FAILED) | 400 | `invalid_request` | The account could not be created. |
| [`WRONG_PASSWORD`](/docs/errors/WRONG_PASSWORD) | 401 | `auth` | The current password on a change-password call is wrong. |
| [`WEAK_PASSWORD`](/docs/errors/WEAK_PASSWORD) | 400 | `invalid_request` | The proposed password fails the password policy. |
| [`USER_NOT_FOUND`](/docs/errors/USER_NOT_FOUND) | 404 | `invalid_request` | No user with that id under this client. |
| [`CLIENT_NOT_FOUND`](/docs/errors/CLIENT_NOT_FOUND) | 404 | `invalid_request` | No client account with that client_id. |
| [`EMAIL_ALREADY_VERIFIED`](/docs/errors/EMAIL_ALREADY_VERIFIED) | 409 | `invalid_request` | The address is already verified. |
| [`VERIFICATION_COOLDOWN`](/docs/errors/VERIFICATION_COOLDOWN) | 429 | `rate_limit` | A verification code was requested too soon after the last one. |
| [`VERIFICATION_CODE_INVALID`](/docs/errors/VERIFICATION_CODE_INVALID) | 400 | `invalid_request` | The email verification code did not match. |
| [`VERIFICATION_CODE_EXPIRED`](/docs/errors/VERIFICATION_CODE_EXPIRED) | 400 | `invalid_request` | The email verification code has expired. |
| [`VERIFICATION_SEND_FAILED`](/docs/errors/VERIFICATION_SEND_FAILED) | 500 | `api_error` | The verification email could not be sent. |
| [`EMAIL_NOT_VERIFIED`](/docs/errors/EMAIL_NOT_VERIFIED) | 400 | `invalid_request` | Minting a secret requires a verified email. |
| [`OPERATOR_VERIFICATION_REQUIRED`](/docs/errors/OPERATOR_VERIFICATION_REQUIRED) | 400 | `invalid_request` | Production secrets require completed business verification. |
| [`SANDBOX_CLAIM_NOT_FOUND`](/docs/errors/SANDBOX_CLAIM_NOT_FOUND) | 404 | `invalid_request` | The sandbox claim link is invalid or has expired. |
| [`SANDBOX_FIXED_SECRET`](/docs/errors/SANDBOX_FIXED_SECRET) | 400 | `invalid_request` | The sandbox secret cannot be minted, rotated, or revoked. |
| [`SECRET_ALREADY_EXISTS`](/docs/errors/SECRET_ALREADY_EXISTS) | 409 | `invalid_request` | An active secret already exists for this environment. |
| [`INVALID_ENVIRONMENT`](/docs/errors/INVALID_ENVIRONMENT) | 400 | `invalid_request` | The environment value is not recognized. |
| [`SECRET_NOT_FOUND`](/docs/errors/SECRET_NOT_FOUND) | 404 | `invalid_request` | No secret exists for this client and environment. |
| [`SECRET_EXPIRED`](/docs/errors/SECRET_EXPIRED) | 401 | `auth` | The client secret is past its expiry. |
| [`SECRET_ROTATION_FAILED`](/docs/errors/SECRET_ROTATION_FAILED) | 500 | `api_error` | The secret rotation could not be completed. |
| [`SECRET_REVOCATION_FAILED`](/docs/errors/SECRET_REVOCATION_FAILED) | 500 | `api_error` | The secret revocation could not be completed. |
| [`SECRET_GENERATION_FAILED`](/docs/errors/SECRET_GENERATION_FAILED) | 500 | `api_error` | A new secret value could not be generated or stored. |
| [`CLIENT_SECRET_ROTATION_CONFLICT`](/docs/errors/CLIENT_SECRET_ROTATION_CONFLICT) | 409 | `invalid_request` | A concurrent rotate or revoke won the race. |
| [`CLIENT_SECRET_REVOKE_CONFLICT`](/docs/errors/CLIENT_SECRET_REVOKE_CONFLICT) | 409 | `invalid_request` | The secret you asked to revoke was already replaced. |
| [`CLIENT_SECRET_CLEANUP_EXHAUSTED`](/docs/errors/CLIENT_SECRET_CLEANUP_EXHAUSTED) | 500 | `api_error` | Too many stranded credentials to retire automatically. |
| [`WEBAUTHN_CHALLENGE_FAILED`](/docs/errors/WEBAUTHN_CHALLENGE_FAILED) | 500 | `api_error` | The passkey challenge could not be issued. |
| [`WEBAUTHN_VERIFICATION_FAILED`](/docs/errors/WEBAUTHN_VERIFICATION_FAILED) | 400 | `invalid_request` | The passkey ceremony response did not verify. |
| [`WEBAUTHN_CREDENTIAL_NOT_FOUND`](/docs/errors/WEBAUTHN_CREDENTIAL_NOT_FOUND) | 404 | `invalid_request` | No registered passkey matches that credential id. |
| [`VERIFICATION_REQUEST_FAILED`](/docs/errors/VERIFICATION_REQUEST_FAILED) | 500 | `api_error` | The business-verification request could not be recorded. |
| [`LIVE_ACCESS_REQUEST_FAILED`](/docs/errors/LIVE_ACCESS_REQUEST_FAILED) | 500 | `api_error` | The live-access request could not be recorded. |
| [`SENDER_NOT_VERIFIED`](/docs/errors/SENDER_NOT_VERIFIED) | 500 | `api_error` | The platform sender domain is not verified with the mail provider. |
| [`CONFIG_MISSING`](/docs/errors/CONFIG_MISSING) | 500 | `api_error` | Email delivery is not configured in this environment. |
| [`CODE_MISMATCH`](/docs/errors/CODE_MISMATCH) | 400 | `invalid_request` | The emailed code did not match. |
| [`CODE_EXPIRED`](/docs/errors/CODE_EXPIRED) | 400 | `invalid_request` | The emailed code is past its expiry window. |
| [`NO_ACTIVE_CODE`](/docs/errors/NO_ACTIVE_CODE) | 400 | `invalid_request` | No code has been requested for this user. |
| [`LOCKED`](/docs/errors/LOCKED) | 429 | `rate_limit` | Verification is locked after too many wrong attempts. |
| [`COOLDOWN`](/docs/errors/COOLDOWN) | 429 | `rate_limit` | A code was sent recently — resend is on cooldown. |
| [`CODE_ROTATED`](/docs/errors/CODE_ROTATED) | 409 | `invalid_request` | A newer code was issued while this attempt was in flight. |
| [`INVALID_CODE_FORMAT`](/docs/errors/INVALID_CODE_FORMAT) | 400 | `invalid_request` | The verification code must be exactly 6 digits. |
| [`LOOKUP_FAILED`](/docs/errors/LOOKUP_FAILED) | 500 | `api_error` | The stored verification code could not be read. |
| [`INVALID_PASSWORD`](/docs/errors/INVALID_PASSWORD) | 400 | `invalid_request` | The new password was rejected by the password policy. |
| [`TERMS_ACCEPTANCE_REQUIRED`](/docs/errors/TERMS_ACCEPTANCE_REQUIRED) | 400 | `invalid_request` | Signup requires explicit acceptance of the current legal documents. |
| [`LEGAL_VERSION_OUTDATED`](/docs/errors/LEGAL_VERSION_OUTDATED) | 400 | `invalid_request` | The accepted legal document versions are no longer current. |
| [`LEGAL_ACCEPTANCE_INVALID`](/docs/errors/LEGAL_ACCEPTANCE_INVALID) | 400 | `invalid_request` | The legal acceptance evidence is malformed. |

### Bills & statements

| Error Code | HTTP | Type | Summary |
| --- | --- | --- | --- |
| [`BILL_NOT_FOUND`](/docs/errors/BILL_NOT_FOUND) | 404 | `invalid_request` | No bill exists with that id. |
| [`ACCOUNT_LINK_NOT_FOUND`](/docs/errors/ACCOUNT_LINK_NOT_FOUND) | 404 | `invalid_request` | The account link does not exist. |
| [`STATEMENT_NOT_EXTRACTED`](/docs/errors/STATEMENT_NOT_EXTRACTED) | 425 | `invalid_request` | The statement has not been extracted yet. |
| [`EXTRACTION_FAILED`](/docs/errors/EXTRACTION_FAILED) | 503 | `upstream` | Statement extraction failed upstream. |
| [`EXTRACTION_UNSUPPORTED_FOR_BILLER`](/docs/errors/EXTRACTION_UNSUPPORTED_FOR_BILLER) | 422 | `invalid_request` | This biller does not support statement extraction. |
| [`STATEMENT_TOO_LARGE`](/docs/errors/STATEMENT_TOO_LARGE) | 413 | `upstream` | The statement exceeds the size limit. |

### Links & Connect

| Error Code | HTTP | Type | Summary |
| --- | --- | --- | --- |
| [`LINK_NOT_FOUND`](/docs/errors/LINK_NOT_FOUND) | 404 | `invalid_request` | No link exists with that id. |
| [`LINK_TOKEN_WRONG_IDENTIFIER`](/docs/errors/LINK_TOKEN_WRONG_IDENTIFIER) | 400 | `invalid_request` | The path carries a link_token_id where the link_token value belongs. |
| [`LINK_TOKEN_NOT_FOUND`](/docs/errors/LINK_TOKEN_NOT_FOUND) | 404 | `invalid_request` | The link token is unknown or expired. |
| [`BACKGROUND_NOT_ELIGIBLE`](/docs/errors/BACKGROUND_NOT_ELIGIBLE) | 409 | `invalid_request` | The link token cannot be backgrounded now. |
| [`UPDATE_BILLER_MISMATCH`](/docs/errors/UPDATE_BILLER_MISMATCH) | 409 | `invalid_request` | The update token targets a different biller. |
| [`INVALID_UPDATE_REASON`](/docs/errors/INVALID_UPDATE_REASON) | 400 | `invalid_request` | The update reason is not a recognized value. |
| [`LINK_UPDATE_FORBIDDEN`](/docs/errors/LINK_UPDATE_FORBIDDEN) | 403 | `auth` | The update token belongs to a different client. |
| [`REDIRECT_URI_NOT_REGISTERED`](/docs/errors/REDIRECT_URI_NOT_REGISTERED) | 400 | `invalid_request` | The redirect_uri is not in your registered allowlist. |
| [`LINK_TOKEN_LOOKUP_FAILED`](/docs/errors/LINK_TOKEN_LOOKUP_FAILED) | 503 | `upstream` | The link token could not be read. |
| [`LINK_TOKEN_EXPIRED`](/docs/errors/LINK_TOKEN_EXPIRED) | 410 | `invalid_request` | The link token expired before the flow completed. |
| [`LINK_TOKEN_NOT_USABLE`](/docs/errors/LINK_TOKEN_NOT_USABLE) | 409 | `invalid_request` | The link token cannot accept this step. |
| [`LINK_ALREADY_EXISTS`](/docs/errors/LINK_ALREADY_EXISTS) | 409 | `invalid_request` | An active link already exists for this account. |
| [`REQUEST_TO_LINK_NOT_FOUND`](/docs/errors/REQUEST_TO_LINK_NOT_FOUND) | 404 | `invalid_request` | No request-to-link exists with that id. |
| [`CLIENT_ID_REQUIRED`](/docs/errors/CLIENT_ID_REQUIRED) | 400 | `invalid_request` | This read refuses to run unscoped. |
| [`INVALID_PUBLIC_TOKEN`](/docs/errors/INVALID_PUBLIC_TOKEN) | 400 | `invalid_request` | The public token is invalid or already exchanged. |
| [`INVALID_CREDENTIALS`](/docs/errors/INVALID_CREDENTIALS) | 400 | `invalid_request` | The biller rejected the credentials. |
| [`BILLER_ACCOUNT_LOCKED`](/docs/errors/BILLER_ACCOUNT_LOCKED) | 409 | `invalid_request` | The biller locked the account. |
| [`MFA_NO_CHALLENGE`](/docs/errors/MFA_NO_CHALLENGE) | 409 | `invalid_request` | No MFA challenge is outstanding. |
| [`MFA_ATTEMPTS_EXHAUSTED`](/docs/errors/MFA_ATTEMPTS_EXHAUSTED) | 409 | `invalid_request` | Every MFA attempt was used. |
| [`MFA_RESEND_LIMIT_REACHED`](/docs/errors/MFA_RESEND_LIMIT_REACHED) | 429 | `rate_limit` | The MFA resend limit was reached. |
| [`EMPTY_ACCOUNT_SELECTION`](/docs/errors/EMPTY_ACCOUNT_SELECTION) | 400 | `invalid_request` | No accounts were selected. |
| [`NO_DISCOVERED_ACCOUNTS`](/docs/errors/NO_DISCOVERED_ACCOUNTS) | 409 | `invalid_request` | Nothing was discovered at the biller. |
| [`CREDENTIAL_NOT_FOUND`](/docs/errors/CREDENTIAL_NOT_FOUND) | 404 | `invalid_request` | No vaulted credential with that id. |
| [`CREDENTIAL_INVALID`](/docs/errors/CREDENTIAL_INVALID) | 400 | `invalid_request` | The credential failed validation. |
| [`CREDENTIAL_OWNER_MISMATCH`](/docs/errors/CREDENTIAL_OWNER_MISMATCH) | 403 | `auth` | The credential belongs to another owner. |
| [`CREDENTIAL_REVOKED`](/docs/errors/CREDENTIAL_REVOKED) | 409 | `invalid_request` | The credential is revoked. |
| [`CREDENTIAL_TYPE_REQUIRED`](/docs/errors/CREDENTIAL_TYPE_REQUIRED) | 400 | `invalid_request` | credential_type is missing. |
| [`CREDENTIAL_NOT_ROTATABLE`](/docs/errors/CREDENTIAL_NOT_ROTATABLE) | 409 | `invalid_request` | The credential cannot be rotated now. |
| [`CREDENTIAL_ENCRYPTION_FAILED`](/docs/errors/CREDENTIAL_ENCRYPTION_FAILED) | 500 | `api_error` | The credential could not be encrypted or decrypted. |
| [`CREDENTIAL_PERSIST_FAILED`](/docs/errors/CREDENTIAL_PERSIST_FAILED) | 500 | `api_error` | The credential could not be stored. |
| [`CREDENTIAL_LOOKUP_FAILED`](/docs/errors/CREDENTIAL_LOOKUP_FAILED) | 500 | `api_error` | The credential store could not be read. |
| [`SCRAPING_SESSION_KEY_REQUIRED`](/docs/errors/SCRAPING_SESSION_KEY_REQUIRED) | 400 | `invalid_request` | The session cache key is incomplete. |
| [`SCRAPING_SESSION_LOOKUP_FAILED`](/docs/errors/SCRAPING_SESSION_LOOKUP_FAILED) | 500 | `api_error` | The session cache could not be read. |
| [`SCRAPING_SESSION_SAVE_FAILED`](/docs/errors/SCRAPING_SESSION_SAVE_FAILED) | 500 | `api_error` | The session cache could not be written. |
| [`SCRAPING_SESSION_INVALIDATE_FAILED`](/docs/errors/SCRAPING_SESSION_INVALIDATE_FAILED) | 500 | `api_error` | The cached session could not be invalidated. |
| [`SCRAPING_SESSION_INVALID`](/docs/errors/SCRAPING_SESSION_INVALID) | 400 | `invalid_request` | The session snapshot failed validation. |
| [`MFA_SUBMISSION_INVALID`](/docs/errors/MFA_SUBMISSION_INVALID) | 400 | `invalid_request` | The MFA submission failed validation. |
| [`MFA_SUBMISSION_LOOKUP_FAILED`](/docs/errors/MFA_SUBMISSION_LOOKUP_FAILED) | 500 | `api_error` | The MFA submission store could not be read. |
| [`MFA_SUBMISSION_PERSIST_FAILED`](/docs/errors/MFA_SUBMISSION_PERSIST_FAILED) | 500 | `api_error` | The MFA submission could not be recorded. |
| [`MFA_CONTINUATION_UNAVAILABLE`](/docs/errors/MFA_CONTINUATION_UNAVAILABLE) | 409 | `invalid_request` | No live MFA continuation is awaiting this code. |
| [`MFA_CONTINUATION_STALE`](/docs/errors/MFA_CONTINUATION_STALE) | 409 | `invalid_request` | The continuation_id is out of date. |
| [`PENDING_MFA_INVALID`](/docs/errors/PENDING_MFA_INVALID) | 400 | `invalid_request` | The pending-MFA snapshot failed validation. |
| [`PENDING_MFA_LOOKUP_FAILED`](/docs/errors/PENDING_MFA_LOOKUP_FAILED) | 500 | `api_error` | The pending-MFA store could not be read. |
| [`PENDING_MFA_PERSIST_FAILED`](/docs/errors/PENDING_MFA_PERSIST_FAILED) | 500 | `api_error` | The pending-MFA snapshot could not be written. |
| [`PENDING_MFA_RESEND_FAILED`](/docs/errors/PENDING_MFA_RESEND_FAILED) | 500 | `api_error` | The OTP resend could not be recorded. |

### Billers

| Error Code | HTTP | Type | Summary |
| --- | --- | --- | --- |
| [`BILLER_NOT_CONNECT_READY`](/docs/errors/BILLER_NOT_CONNECT_READY) | 409 | `invalid_request` | The biller is not ready to connect. |
| [`BILLER_NOT_FOUND`](/docs/errors/BILLER_NOT_FOUND) | 404 | `invalid_request` | No biller exists with that id. |
| [`BILLER_CORRECTION_NOT_AVAILABLE`](/docs/errors/BILLER_CORRECTION_NOT_AVAILABLE) | 501 | `api_error` | Public biller correction proposals are not available. |
| [`BILLER_UNSUPPORTED`](/docs/errors/BILLER_UNSUPPORTED) | 422 | `invalid_request` | This biller cannot be automated. |

### Payments

| Error Code | HTTP | Type | Summary |
| --- | --- | --- | --- |
| [`PAYMENT_EXECUTION_NOT_AVAILABLE`](/docs/errors/PAYMENT_EXECUTION_NOT_AVAILABLE) | 501 | `upstream` | Bill payment execution is not available in this environment. |

### Feedback

| Error Code | HTTP | Type | Summary |
| --- | --- | --- | --- |
| [`FEEDBACK_RUN_NOT_FOUND`](/docs/errors/FEEDBACK_RUN_NOT_FOUND) | 404 | `invalid_request` | The referenced resource does not exist. |
| [`FEEDBACK_RUN_NOT_OWNED`](/docs/errors/FEEDBACK_RUN_NOT_OWNED) | 403 | `auth` | The referenced resource belongs to another client. |
| [`FEEDBACK_RUN_EXPIRED`](/docs/errors/FEEDBACK_RUN_EXPIRED) | 410 | `invalid_request` | The feedback window has closed. |
| [`FEEDBACK_INVALID_CATEGORY`](/docs/errors/FEEDBACK_INVALID_CATEGORY) | 422 | `invalid_request` | The category is not valid for this resource type. |
| [`FEEDBACK_INVALID_SIGNAL`](/docs/errors/FEEDBACK_INVALID_SIGNAL) | 400 | `invalid_request` | The signal is not valid for the resource_type. |
| [`FEEDBACK_ALREADY_SUBMITTED`](/docs/errors/FEEDBACK_ALREADY_SUBMITTED) | 409 | `invalid_request` | Feedback was already submitted for this resource. |
| [`FEEDBACK_RATE_LIMITED`](/docs/errors/FEEDBACK_RATE_LIMITED) | 429 | `rate_limit` | Too many feedback submissions. |

### Messaging

| Error Code | HTTP | Type | Summary |
| --- | --- | --- | --- |
| [`MESSAGING_CONSENT_NOT_GRANTED`](/docs/errors/MESSAGING_CONSENT_NOT_GRANTED) | 403 | `invalid_request` | The customer has not granted messaging consent. |
| [`MESSAGING_LIVE_ACCESS_REQUIRED`](/docs/errors/MESSAGING_LIVE_ACCESS_REQUIRED) | 403 | `auth` | Messaging requires production (live) access. |
| [`MESSAGING_SENDER_NOT_AUTHORIZED`](/docs/errors/MESSAGING_SENDER_NOT_AUTHORIZED) | 403 | `auth` | The sender is not authorized for this account link. |
| [`MESSAGING_CONTENT_FLAGGED`](/docs/errors/MESSAGING_CONTENT_FLAGGED) | 422 | `invalid_request` | The message content was flagged. |
| [`MESSAGING_RATE_LIMITED`](/docs/errors/MESSAGING_RATE_LIMITED) | 429 | `rate_limit` | Messaging rate limit exceeded. |
| [`MESSAGING_SUPPRESSED`](/docs/errors/MESSAGING_SUPPRESSED) | 200 | `invalid_request` | The recipient is suppressed; message not sent. |
| [`MESSAGING_AUP_NOT_ACCEPTED`](/docs/errors/MESSAGING_AUP_NOT_ACCEPTED) | 428 | `invalid_request` | The messaging Acceptable Use Policy is not accepted. |
| [`MESSAGING_AUP_REACCEPT_REQUIRED`](/docs/errors/MESSAGING_AUP_REACCEPT_REQUIRED) | 412 | `invalid_request` | The messaging AUP must be re-accepted. |
| [`MESSAGING_INVALID_PAYLOAD`](/docs/errors/MESSAGING_INVALID_PAYLOAD) | 400 | `invalid_request` | The message payload is malformed. |
| [`MESSAGING_INVALID_CATEGORY`](/docs/errors/MESSAGING_INVALID_CATEGORY) | 400 | `invalid_request` | The message category is not recognized. |
| [`MESSAGING_PERSIST_FAILED`](/docs/errors/MESSAGING_PERSIST_FAILED) | 503 | `upstream` | A transient error prevented recording the message. |
| [`MESSAGING_INTERNAL_ERROR`](/docs/errors/MESSAGING_INTERNAL_ERROR) | 500 | `upstream` | An unexpected messaging error occurred. |
| [`MESSAGING_NOT_FOUND`](/docs/errors/MESSAGING_NOT_FOUND) | 404 | `invalid_request` | The referenced message or thread does not exist. |

### Email & discovery

| Error Code | HTTP | Type | Summary |
| --- | --- | --- | --- |
| [`EMAIL_MESSAGE_NOT_FOUND`](/docs/errors/EMAIL_MESSAGE_NOT_FOUND) | 404 | `invalid_request` | No stored email message with that id. |
| [`EMAIL_MESSAGE_LOOKUP_FAILED`](/docs/errors/EMAIL_MESSAGE_LOOKUP_FAILED) | 503 | `upstream` | The message store could not be read. |
| [`EMAIL_MESSAGE_PERSIST_FAILED`](/docs/errors/EMAIL_MESSAGE_PERSIST_FAILED) | 500 | `api_error` | The message could not be stored. |
| [`EMAIL_CLIENT_ID_REQUIRED`](/docs/errors/EMAIL_CLIENT_ID_REQUIRED) | 400 | `invalid_request` | client_id is required to read a message. |
| [`EMAIL_CLASSIFICATION_NOT_FOUND`](/docs/errors/EMAIL_CLASSIFICATION_NOT_FOUND) | 404 | `invalid_request` | No classification for that id or message. |
| [`EMAIL_CLASSIFICATION_LOOKUP_FAILED`](/docs/errors/EMAIL_CLASSIFICATION_LOOKUP_FAILED) | 503 | `upstream` | The classification store could not be read. |
| [`EMAIL_CLASSIFICATION_PERSIST_FAILED`](/docs/errors/EMAIL_CLASSIFICATION_PERSIST_FAILED) | 500 | `api_error` | The classification could not be stored. |
| [`EMAIL_CLASSIFICATION_INVALID`](/docs/errors/EMAIL_CLASSIFICATION_INVALID) | 400 | `invalid_request` | The classification aggregate refused the transition. |
| [`EMAIL_CLASSIFICATION_FORBIDDEN`](/docs/errors/EMAIL_CLASSIFICATION_FORBIDDEN) | 403 | `auth` | The message behind this classification belongs to another client. |
| [`HUMAN_REVIEW_NOT_FOUND`](/docs/errors/HUMAN_REVIEW_NOT_FOUND) | 404 | `invalid_request` | No human-review task with that id. |
| [`HUMAN_REVIEW_LOOKUP_FAILED`](/docs/errors/HUMAN_REVIEW_LOOKUP_FAILED) | 503 | `upstream` | The review store could not be read. |
| [`HUMAN_REVIEW_PERSIST_FAILED`](/docs/errors/HUMAN_REVIEW_PERSIST_FAILED) | 500 | `api_error` | The review could not be stored. |
| [`HUMAN_REVIEW_CLAIM_CONFLICT`](/docs/errors/HUMAN_REVIEW_CLAIM_CONFLICT) | 409 | `invalid_request` | Another worker already holds this review. |
| [`HUMAN_REVIEW_INVALID_STATE`](/docs/errors/HUMAN_REVIEW_INVALID_STATE) | 409 | `invalid_request` | The review is not in a state this operation accepts. |
| [`HUMAN_REVIEW_COMPLETION_REJECTED`](/docs/errors/HUMAN_REVIEW_COMPLETION_REJECTED) | 409 | `invalid_request` | The review completion was refused. |
| [`GMAIL_OAUTH_STATE_INVALID`](/docs/errors/GMAIL_OAUTH_STATE_INVALID) | 400 | `auth` | The Gmail OAuth state did not verify. |
| [`GMAIL_TOKEN_EXCHANGE_FAILED`](/docs/errors/GMAIL_TOKEN_EXCHANGE_FAILED) | 503 | `upstream` | Google refused the authorization-code exchange. |
| [`GMAIL_ACCESS_TOKEN_UNAVAILABLE`](/docs/errors/GMAIL_ACCESS_TOKEN_UNAVAILABLE) | 503 | `upstream` | No usable Gmail access token for this connection. |
| [`GMAIL_WATCH_NOT_FOUND`](/docs/errors/GMAIL_WATCH_NOT_FOUND) | 404 | `invalid_request` | No Gmail watch registration for that mailbox. |
| [`GMAIL_IMPORT_FAILED`](/docs/errors/GMAIL_IMPORT_FAILED) | 503 | `upstream` | Mail could not be imported from Gmail. |
| [`GMAIL_DISCONNECT_FAILED`](/docs/errors/GMAIL_DISCONNECT_FAILED) | 500 | `api_error` | The Gmail disconnect did not complete. |
| [`GMAIL_MAILBOX_ALREADY_CONNECTED`](/docs/errors/GMAIL_MAILBOX_ALREADY_CONNECTED) | 409 | `invalid_request` | This Gmail mailbox is already connected to another account. |

### Agent traces & debug

| Error Code | HTTP | Type | Summary |
| --- | --- | --- | --- |
| [`TRACE_NOT_FOUND`](/docs/errors/TRACE_NOT_FOUND) | 404 | `invalid_request` | No agent-trace run with that run_id. |
| [`TRACE_LOOKUP_FAILED`](/docs/errors/TRACE_LOOKUP_FAILED) | 500 | `api_error` | The trace metadata store could not be read. |
| [`TRACE_LIST_FAILED`](/docs/errors/TRACE_LIST_FAILED) | 500 | `api_error` | The trace list or recent-feed query was rejected. |
| [`TRACE_PERSIST_FAILED`](/docs/errors/TRACE_PERSIST_FAILED) | 500 | `api_error` | The trace row could not be written. |
| [`TRACE_INVALID_STATE`](/docs/errors/TRACE_INVALID_STATE) | 500 | `api_error` | The stored trace row cannot be acted on. |
| [`TRACE_ASSET_FORBIDDEN`](/docs/errors/TRACE_ASSET_FORBIDDEN) | 403 | `invalid_request` | The asset key is malformed or outside the run prefix. |
| [`TRACE_ASSET_READ_FAILED`](/docs/errors/TRACE_ASSET_READ_FAILED) | 500 | `api_error` | The artifact object could not be read. |
| [`TRACE_ARTIFACT_REJECTED`](/docs/errors/TRACE_ARTIFACT_REJECTED) | 400 | `invalid_request` | The artifact is refused by credential containment. |
| [`TRACE_ARTIFACT_UPLOAD_FAILED`](/docs/errors/TRACE_ARTIFACT_UPLOAD_FAILED) | 500 | `api_error` | The artifact could not be written to storage. |
| [`TRACE_FIXTURE_EXPORT_FAILED`](/docs/errors/TRACE_FIXTURE_EXPORT_FAILED) | 500 | `api_error` | The fixture archive could not be built. |
| [`EVENT_ARCHIVE_QUERY_FAILED`](/docs/errors/EVENT_ARCHIVE_QUERY_FAILED) | 500 | `api_error` | The archived-event store could not be queried. |
| [`EVENT_ARCHIVE_EVENT_NOT_FOUND`](/docs/errors/EVENT_ARCHIVE_EVENT_NOT_FOUND) | 404 | `invalid_request` | No archived event with that id. |
| [`DEBUG_EVENT_QUERY_UNINDEXED`](/docs/errors/DEBUG_EVENT_QUERY_UNINDEXED) | 400 | `invalid_request` | The event query named no indexed predicate. |
| [`DEBUG_EVENT_CURSOR_INVALID`](/docs/errors/DEBUG_EVENT_CURSOR_INVALID) | 400 | `invalid_request` | The pagination cursor is not valid for this query. |
| [`DEBUG_PARAMETER_NOT_FOUND`](/docs/errors/DEBUG_PARAMETER_NOT_FOUND) | 404 | `invalid_request` | No configuration parameter at that path. |
| [`DEBUG_PARAMETER_NAME_INVALID`](/docs/errors/DEBUG_PARAMETER_NAME_INVALID) | 400 | `invalid_request` | The parameter name is outside the writable namespace. |
| [`DEBUG_PARAMETER_LOOKUP_FAILED`](/docs/errors/DEBUG_PARAMETER_LOOKUP_FAILED) | 500 | `upstream` | Parameter Store could not be read. |
| [`DEBUG_PARAMETER_UPDATE_FAILED`](/docs/errors/DEBUG_PARAMETER_UPDATE_FAILED) | 500 | `upstream` | The parameter write was rejected. |
| [`DEBUG_SERVICE_LIST_FAILED`](/docs/errors/DEBUG_SERVICE_LIST_FAILED) | 500 | `upstream` | The per-service parameter rollup could not be built. |
| [`DEBUG_ALARM_QUERY_FAILED`](/docs/errors/DEBUG_ALARM_QUERY_FAILED) | 500 | `upstream` | Live alarm state could not be read. |

### Agent improvement

| Error Code | HTTP | Type | Summary |
| --- | --- | --- | --- |
| [`AGENT_TRACE_NOT_FOUND`](/docs/errors/AGENT_TRACE_NOT_FOUND) | 404 | `invalid_request` | No agent trace for that run. |
| [`AGENT_TRACE_INVALID`](/docs/errors/AGENT_TRACE_INVALID) | 400 | `invalid_request` | The agent trace payload is invalid. |
| [`AGENT_TRACE_LOOKUP_FAILED`](/docs/errors/AGENT_TRACE_LOOKUP_FAILED) | 503 | `upstream` | The agent trace store could not be read. |
| [`AGENT_TRACE_PERSIST_FAILED`](/docs/errors/AGENT_TRACE_PERSIST_FAILED) | 500 | `api_error` | The agent trace could not be written. |
| [`AGENT_TRACE_OUTCOME_EMIT_FAILED`](/docs/errors/AGENT_TRACE_OUTCOME_EMIT_FAILED) | 500 | `api_error` | The trace outcome event could not be published. |
| [`AGENT_TRACE_EVENT_BUS_UNAVAILABLE`](/docs/errors/AGENT_TRACE_EVENT_BUS_UNAVAILABLE) | 503 | `upstream` | No event bus is wired for trace outcomes. |
| [`AGENT_AUDIT_LOG_INVALID`](/docs/errors/AGENT_AUDIT_LOG_INVALID) | 400 | `invalid_request` | The audit log entry is invalid. |
| [`AGENT_AUDIT_LOG_PERSIST_FAILED`](/docs/errors/AGENT_AUDIT_LOG_PERSIST_FAILED) | 500 | `api_error` | The audit log entry could not be appended. |
| [`AGENT_AUDIT_LOG_LOOKUP_FAILED`](/docs/errors/AGENT_AUDIT_LOG_LOOKUP_FAILED) | 503 | `upstream` | The audit log could not be read. |
| [`PROMPT_VERSION_NOT_FOUND`](/docs/errors/PROMPT_VERSION_NOT_FOUND) | 404 | `invalid_request` | No prompt version with that hash. |
| [`PROMPT_VERSION_INVALID`](/docs/errors/PROMPT_VERSION_INVALID) | 400 | `invalid_request` | The prompt version payload is invalid. |
| [`PROMPT_VERSION_LOOKUP_FAILED`](/docs/errors/PROMPT_VERSION_LOOKUP_FAILED) | 503 | `upstream` | The prompt version store could not be read. |
| [`PROMPT_VERSION_PERSIST_FAILED`](/docs/errors/PROMPT_VERSION_PERSIST_FAILED) | 500 | `api_error` | The prompt version could not be written. |
| [`PROMPT_POINTER_NOT_FOUND`](/docs/errors/PROMPT_POINTER_NOT_FOUND) | 404 | `invalid_request` | No production prompt pointer for that agent. |
| [`PROMPT_POINTER_CAS_MISMATCH`](/docs/errors/PROMPT_POINTER_CAS_MISMATCH) | 409 | `invalid_request` | The production pointer moved under you. |
| [`PROMPT_POINTER_INVALID`](/docs/errors/PROMPT_POINTER_INVALID) | 400 | `invalid_request` | The production pointer payload is invalid. |
| [`PROMPT_POINTER_FLIP_FAILED`](/docs/errors/PROMPT_POINTER_FLIP_FAILED) | 500 | `api_error` | The production pointer could not be written. |
| [`PROMPT_POINTER_LOOKUP_FAILED`](/docs/errors/PROMPT_POINTER_LOOKUP_FAILED) | 503 | `upstream` | The production pointer could not be read. |
| [`PROMPT_POINTER_ROLLBACK_NO_PRIOR`](/docs/errors/PROMPT_POINTER_ROLLBACK_NO_PRIOR) | 409 | `invalid_request` | No prior prompt version to roll back to. |
| [`PROMPT_POINTER_ROLLBACK_HASH_MISMATCH`](/docs/errors/PROMPT_POINTER_ROLLBACK_HASH_MISMATCH) | 409 | `invalid_request` | The rollback guard hash does not match. |
| [`PROMPT_EXPERIMENT_NOT_FOUND`](/docs/errors/PROMPT_EXPERIMENT_NOT_FOUND) | 404 | `invalid_request` | No prompt experiment with that id. |
| [`PROMPT_EXPERIMENT_ALREADY_ACTIVE`](/docs/errors/PROMPT_EXPERIMENT_ALREADY_ACTIVE) | 409 | `invalid_request` | An experiment is already running for that agent. |
| [`PROMPT_EXPERIMENT_VARIANT_NOT_FOUND`](/docs/errors/PROMPT_EXPERIMENT_VARIANT_NOT_FOUND) | 400 | `invalid_request` | The experiment arm is not a registered prompt version. |
| [`PROMPT_EXPERIMENT_NO_CONTROL`](/docs/errors/PROMPT_EXPERIMENT_NO_CONTROL) | 400 | `invalid_request` | The experiment has no control arm. |
| [`PROMPT_EXPERIMENT_INVALID`](/docs/errors/PROMPT_EXPERIMENT_INVALID) | 400 | `invalid_request` | The prompt experiment payload is invalid. |
| [`PROMPT_EXPERIMENT_LOOKUP_FAILED`](/docs/errors/PROMPT_EXPERIMENT_LOOKUP_FAILED) | 503 | `upstream` | The prompt experiment store could not be read. |
| [`PROMPT_EXPERIMENT_PERSIST_FAILED`](/docs/errors/PROMPT_EXPERIMENT_PERSIST_FAILED) | 500 | `api_error` | The prompt experiment could not be written. |
| [`PROMPT_EVAL_SCORE_INVALID`](/docs/errors/PROMPT_EVAL_SCORE_INVALID) | 400 | `invalid_request` | The prompt eval score is invalid. |
| [`PROMPT_EVAL_SCORE_PERSIST_FAILED`](/docs/errors/PROMPT_EVAL_SCORE_PERSIST_FAILED) | 500 | `api_error` | The prompt eval score could not be written. |
| [`BRAIN_ENTRY_NOT_FOUND`](/docs/errors/BRAIN_ENTRY_NOT_FOUND) | 404 | `invalid_request` | No brain entry with that id. |
| [`BRAIN_ENTRY_INVALID`](/docs/errors/BRAIN_ENTRY_INVALID) | 400 | `invalid_request` | The brain entry payload is invalid. |
| [`BRAIN_ENTRY_INVALID_STATE`](/docs/errors/BRAIN_ENTRY_INVALID_STATE) | 409 | `invalid_request` | The brain entry is in the wrong state for that decision. |
| [`BRAIN_ENTRY_LOOKUP_FAILED`](/docs/errors/BRAIN_ENTRY_LOOKUP_FAILED) | 503 | `upstream` | The brain store could not be read. |
| [`BRAIN_ENTRY_PERSIST_FAILED`](/docs/errors/BRAIN_ENTRY_PERSIST_FAILED) | 500 | `api_error` | The brain entry could not be written. |
| [`BRAIN_EFFECT_LOOKUP_FAILED`](/docs/errors/BRAIN_EFFECT_LOOKUP_FAILED) | 503 | `upstream` | The brain effect snapshots could not be read. |
| [`BRAIN_EFFECT_COMPUTE_FAILED`](/docs/errors/BRAIN_EFFECT_COMPUTE_FAILED) | 500 | `api_error` | The brain entry effect could not be computed. |
| [`BRAIN_SEARCH_FAILED`](/docs/errors/BRAIN_SEARCH_FAILED) | 503 | `upstream` | The brain search could not be executed. |
| [`RECORDING_VERSION_NOT_FOUND`](/docs/errors/RECORDING_VERSION_NOT_FOUND) | 404 | `invalid_request` | No recording version with that hash. |
| [`RECORDING_VERSION_INVALID`](/docs/errors/RECORDING_VERSION_INVALID) | 400 | `invalid_request` | The recording version payload is invalid. |
| [`RECORDING_VERSION_LOOKUP_FAILED`](/docs/errors/RECORDING_VERSION_LOOKUP_FAILED) | 503 | `upstream` | The recording version store could not be read. |
| [`RECORDING_VERSION_PERSIST_FAILED`](/docs/errors/RECORDING_VERSION_PERSIST_FAILED) | 500 | `api_error` | The recording version could not be written. |
| [`RECORDING_POINTER_NOT_FOUND`](/docs/errors/RECORDING_POINTER_NOT_FOUND) | 404 | `invalid_request` | No production recording pointer for that biller. |
| [`RECORDING_POINTER_LOOKUP_FAILED`](/docs/errors/RECORDING_POINTER_LOOKUP_FAILED) | 503 | `upstream` | The production recording pointer could not be read. |
| [`CANARY_ALREADY_RUNNING`](/docs/errors/CANARY_ALREADY_RUNNING) | 409 | `invalid_request` | A canary is already running. |
| [`CANARY_NOT_RUNNING`](/docs/errors/CANARY_NOT_RUNNING) | 409 | `invalid_request` | No canary is running. |
| [`CANARY_TERMINAL`](/docs/errors/CANARY_TERMINAL) | 409 | `invalid_request` | The canary already reached a terminal state. |
| [`CANARY_CANDIDATE_NOT_FOUND`](/docs/errors/CANARY_CANDIDATE_NOT_FOUND) | 404 | `invalid_request` | The canary candidate recording does not exist. |
| [`CANARY_NO_BASELINE`](/docs/errors/CANARY_NO_BASELINE) | 409 | `invalid_request` | The canary has no baseline to compare against. |
| [`CANARY_INVALID`](/docs/errors/CANARY_INVALID) | 400 | `invalid_request` | The canary payload is invalid. |
| [`CANARY_CAS_MISMATCH`](/docs/errors/CANARY_CAS_MISMATCH) | 409 | `invalid_request` | The recording pointer moved during the canary decision. |
| [`CANARY_LOOKUP_FAILED`](/docs/errors/CANARY_LOOKUP_FAILED) | 503 | `upstream` | The canary state could not be read. |
| [`CANARY_PERSIST_FAILED`](/docs/errors/CANARY_PERSIST_FAILED) | 500 | `api_error` | The canary state could not be written. |
| [`CANARY_METRICS_UNAVAILABLE`](/docs/errors/CANARY_METRICS_UNAVAILABLE) | 503 | `upstream` | The canary metrics could not be computed. |
| [`AGENT_FREEZE_INVALID`](/docs/errors/AGENT_FREEZE_INVALID) | 400 | `invalid_request` | The agent freeze request is invalid. |
| [`AGENT_FREEZE_PERSIST_FAILED`](/docs/errors/AGENT_FREEZE_PERSIST_FAILED) | 500 | `api_error` | The agent freeze state could not be written. |
| [`AGENT_FREEZE_LOOKUP_FAILED`](/docs/errors/AGENT_FREEZE_LOOKUP_FAILED) | 503 | `upstream` | The agent freeze state could not be read. |
| [`CLIENT_FEEDBACK_NOT_FOUND`](/docs/errors/CLIENT_FEEDBACK_NOT_FOUND) | 404 | `invalid_request` | No client feedback for that run. |
| [`CLIENT_FEEDBACK_INVALID`](/docs/errors/CLIENT_FEEDBACK_INVALID) | 400 | `invalid_request` | The client feedback payload is invalid. |
| [`CLIENT_FEEDBACK_ALREADY_SUBMITTED`](/docs/errors/CLIENT_FEEDBACK_ALREADY_SUBMITTED) | 409 | `invalid_request` | Feedback was already submitted for that run. |
| [`CLIENT_FEEDBACK_IDEMPOTENCY_CONFLICT`](/docs/errors/CLIENT_FEEDBACK_IDEMPOTENCY_CONFLICT) | 409 | `invalid_request` | The idempotency key was reused with different content. |
| [`CLIENT_FEEDBACK_LOOKUP_FAILED`](/docs/errors/CLIENT_FEEDBACK_LOOKUP_FAILED) | 503 | `upstream` | The client feedback store could not be read. |
| [`CLIENT_FEEDBACK_PERSIST_FAILED`](/docs/errors/CLIENT_FEEDBACK_PERSIST_FAILED) | 500 | `api_error` | The client feedback could not be written. |
| [`FEEDBACK_INVITED_MARKER_FAILED`](/docs/errors/FEEDBACK_INVITED_MARKER_FAILED) | 500 | `api_error` | The feedback-invited marker could not be written. |
| [`EXECUTION_RUN_NOT_FOUND`](/docs/errors/EXECUTION_RUN_NOT_FOUND) | 404 | `invalid_request` | No execution run with that id. |
| [`EXECUTION_RUN_INVALID`](/docs/errors/EXECUTION_RUN_INVALID) | 400 | `invalid_request` | The execution run payload is invalid. |
| [`EXECUTION_RUN_INVALID_STATE`](/docs/errors/EXECUTION_RUN_INVALID_STATE) | 409 | `invalid_request` | The execution run is in the wrong state for that transition. |
| [`EXECUTION_RUN_LOOKUP_FAILED`](/docs/errors/EXECUTION_RUN_LOOKUP_FAILED) | 503 | `upstream` | The execution run store could not be read. |
| [`EXECUTION_RUN_PERSIST_FAILED`](/docs/errors/EXECUTION_RUN_PERSIST_FAILED) | 500 | `api_error` | The execution run could not be written. |
| [`AGENT_REPORT_WINDOW_INVALID`](/docs/errors/AGENT_REPORT_WINDOW_INVALID) | 400 | `invalid_request` | The report window is invalid. |
| [`AGENT_REPORT_DATA_UNAVAILABLE`](/docs/errors/AGENT_REPORT_DATA_UNAVAILABLE) | 503 | `upstream` | The report data could not be read. |

## Handling Errors

Always check the HTTP status code and parse the error envelope for details.

**Error handling**

**JavaScript**

```javascript
try {
  const response = await fetch('https://sandbox.api.billerapi.com/v1/billers', {
    headers: {
      Authorization: `Bearer ${process.env.BILLERAPI_SECRET_KEY}`,
    },
  });

  if (!response.ok) {
    const error = await response.json();
    console.error(`[${response.status}] ${error.error_code}: ${error.error_message}`);
    console.error('Fix:', error.hint, '·', error.docs_url);

    if (error.error_code === 'VALIDATION_ERROR') {
      for (const field of error.errors ?? []) {
        console.error(`  ${field.param} (${field.code}): ${field.message}`);
      }
    }

    if (error.error_type === 'rate_limit') {
      // Prefer the body field; fall back to the Retry-After header.
      const retryAfter = error.retry_after ?? response.headers.get('Retry-After');
      console.log(`Retry after ${retryAfter} seconds`);
    }
    return;
  }

  const data = await response.json();
  console.log(data);
} catch (err) {
  console.error('Network error:', err.message);
}
```

**Python**

```python
import requests
import os

try:
    response = requests.get(
        'https://sandbox.api.billerapi.com/v1/billers',
        headers={'Authorization': f"Bearer {os.environ['BILLERAPI_SECRET_KEY']}"},
    )
    response.raise_for_status()
    data = response.json()
    print(data)

except requests.exceptions.HTTPError as e:
    error = e.response.json()
    print(f"[{e.response.status_code}] {error['error_code']}: {error['error_message']}")
    print("Fix:", error.get('hint'), "·", error.get('docs_url'))

    if error['error_code'] == 'VALIDATION_ERROR':
        for field in error.get('errors', []):
            print(f"  {field['param']} ({field['code']}): {field['message']}")

    if error['error_type'] == 'rate_limit':
        # Prefer the body field; fall back to the Retry-After header.
        retry_after = error.get('retry_after') or e.response.headers.get('Retry-After')
        print(f"Retry after {retry_after} seconds")

except requests.exceptions.RequestException as e:
    print(f"Network error: {e}")
```

## 404 after a webhook is normal

BillerAPI's webhook bodies are intentionally minimal — they carry the envelope plus a small `data.object` with the resource ID and a few routing keys. To get the full resource you call `GET /v1/<resource>/<id>` after handling the webhook.

That follow-up GET can return `404 Not Found` even though you just received an event for the resource. This is a normal consequence of two facts:

- Webhook delivery is asynchronous; minutes can pass between the event being emitted and your handler running.
- Some resources are short-lived (a link can be disconnected, a request-to-link can be cancelled, a bill can be re-extracted with a new id).

**Treat 404 as "gone is gone":** log the event id and return `200 OK` from your webhook endpoint. Do *not* return non-2xx — that triggers BillerAPI to retry the same webhook, which will hit the same 404 on the next attempt and burn your retry budget. The state the event reflected (e.g. `link.disconnected`) is still actionable from the envelope alone.

**Tolerating 404 on the follow-up GET**

**JavaScript**

```javascript
async function handleBillCreated(envelope) {
  const billId = envelope.data.object.id;
  const res = await fetch(`https://api.billerapi.com/v1/bills/${billId}`, {
    headers: { Authorization: `Bearer ${linkScopedToken}` },
  });

  if (res.status === 404) {
    // Resource gone between webhook fan-out and our follow-up GET.
    // Acknowledge so we are not retried; we already know the bill id.
    logger.info('bill_gone_at_consume_time', { event_id: envelope.id, bill_id: billId });
    return; // caller returns 200 OK
  }

  if (!res.ok) throw new Error(`unexpected ${res.status}`);
  await persistBill(await res.json());
}
```

**Python**

```python
def handle_bill_created(envelope):
    bill_id = envelope['data']['object']['id']
    res = requests.get(
        f'https://api.billerapi.com/v1/bills/{bill_id}',
        headers={'Authorization': f'Bearer {link_scoped_token}'},
    )

    if res.status_code == 404:
        # Resource gone between webhook fan-out and our follow-up GET.
        logger.info('bill_gone_at_consume_time', extra={
            'event_id': envelope['id'], 'bill_id': bill_id,
        })
        return  # caller returns 200 OK

    res.raise_for_status()
    persist_bill(res.json())
```

## Related

- [Error code reference](/docs/errors) — a dedicated page per error code
- [Feedback errors](/docs/concepts/errors/feedback-errors) — the `FEEDBACK_*` codes
- [Rate Limits](/docs/concepts/rate-limits) — rate limit policy and retry strategies
- [Authentication](/docs/guides/authentication) — API key and bearer token auth
- [Idempotency](/docs/concepts/idempotency) — webhook dedupe by `event.id`
