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
{
"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_codeA stable, machine-readable code in UPPER_SNAKE_CASE (see the reference below). Branch on this — it is an additive-only contract.error_typeCoarse category for retry/backoff decisions: invalid_request, rate_limit, auth, upstream, or api_error.error_messageA human-readable description of the problem.hintAn actionable next step for resolving the error.docs_urlA link to this exact error code’s docs page (https://docs.billerapi.com/errors/<code>).request_idCorrelation id for the failing request — also returned as the X-Request-Id response header on every response. Quote it in support tickets.retryableWhether retrying or resuming the operation is supported. Older pass-through errors may omit this field during migration.retry_afterOptional. On 429, the number of seconds to wait before retrying (also on the Retry-After header).errorsOptional. On 400 validation failures, an array of per-field problems (see below).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
{
"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" }
]
}paramThe 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.codeA stable reason: required, invalid_type, invalid_format, too_short, too_long, unknown_parameter.messageA 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 | 500 | api_error | An unexpected error occurred on our end. |
| VALIDATION_ERROR | 400 | invalid_request | One or more request fields are invalid. |
| NOT_FOUND | 404 | invalid_request | The requested resource does not exist. |
| CONFLICT | 409 | invalid_request | The request conflicts with the current resource state. |
| INVALID_CURSOR | 400 | invalid_request | The pagination cursor is malformed or stale. |
| IDEMPOTENCY_KEY_MISMATCH | 409 | invalid_request | The Idempotency-Key was reused with a different body. |
| IDEMPOTENCY_KEY_NOT_REPLAYABLE | 409 | invalid_request | The one-time response for this Idempotency-Key cannot be replayed. |
| RATE_LIMITED | 429 | rate_limit | You have exceeded the allowed request rate. |
| SERVICE_UNAVAILABLE | 503 | upstream | An upstream dependency is temporarily unavailable. |
| WEBHOOK_NOT_FOUND | 404 | invalid_request | No webhook with that id. |
| WEBHOOK_ENDPOINT_NOT_FOUND | 404 | invalid_request | No webhook endpoint with that id. |
| INVALID_WEBHOOK_UPDATE | 400 | invalid_request | The webhook patch failed validation. |
| WEBHOOK_REGISTRATION_FAILED | 500 | api_error | The webhook could not be persisted. |
| WEBHOOK_CONFIGURATION_NOT_FOUND | 404 | invalid_request | This client has no webhook configuration for that environment. |
| INVALID_IDEMPOTENCY_KEY | 400 | invalid_request | The Idempotency-Key header is missing or malformed. |
| IDEMPOTENCY_KEY_REUSED | 409 | invalid_request | The Idempotency-Key was reused with different parameters. |
| WEBHOOK_LOOKUP_FAILED | 500 | api_error | The webhook ownership row could not be read. |
| WEBHOOK_UPDATE_FAILED | 500 | api_error | The endpoint update could not be persisted. |
| WEBHOOK_UPDATE_OUTCOME_UNKNOWN | 500 | api_error | The update committed but its stored state could not be read back. |
| WEBHOOK_ROTATION_FAILED | 500 | api_error | The signing-secret rotation could not be persisted. |
| INVALID_WEBHOOK_OUTCOME_ID | 400 | invalid_request | The delivery-outcome identity is missing or malformed. |
| WEBHOOK_OUTCOME_UNKNOWN | 500 | api_error | A prior delivery outcome could not be resolved. |
| WEBHOOK_OUTCOME_ID_REUSED | 409 | invalid_request | The delivery-outcome identity was reused with different parameters. |
| WEBHOOK_DISABLE_NOTIFICATION_PENDING | 500 | api_error | The endpoint was auto-disabled but the owner was not notified. |
| WEBHOOK_UPDATE_CONFLICT | 409 | invalid_request | A concurrent writer updated this endpoint first. |
| WEBHOOK_SAVE_OUTCOME_UNKNOWN | 500 | api_error | The write may or may not have committed. |
| WEBHOOK_REGISTRATION_CONFLICT | 409 | invalid_request | A concurrent registration for this environment won the race. |
| WEBHOOK_REGISTRATION_OUTCOME_UNKNOWN | 500 | api_error | The registration may have committed without a confirmed result. |
| WEBHOOK_ROTATION_CONFLICT | 409 | invalid_request | A concurrent rotation replaced the signing secret first. |
| WEBHOOK_ROTATION_OUTCOME_UNKNOWN | 500 | api_error | The rotation may have committed without a confirmed result. |
| NOT_IMPLEMENTED | 500 | api_error | The operation is declared but not implemented on this service. |
| UNKNOWN | 500 | api_error | The identity provider refused the operation for an unclassified reason. |
| BAD_REQUEST | 400 | invalid_request | A caller-side precondition failed. |
Authentication
| Error Code | HTTP | Type | Summary |
|---|---|---|---|
| UNAUTHORIZED | 401 | auth | Authentication failed or is missing. |
| FORBIDDEN | 403 | auth | Authenticated, but not authorized for this resource. |
| CLIENT_ID_MISMATCH | 403 | auth | The supplied client_id does not match your session. |
| NEEDS_RESIGNIN | 401 | auth | No authenticated client context was resolved. |
| SIGNIN_CREDENTIALS_REJECTED | 401 | auth | Sign-in was refused. |
| AUTHENTICATION_INCOMPLETE | 401 | auth | Sign-in stopped short of issuing tokens. |
| USER_NOT_CONFIRMED | 401 | auth | The account was never confirmed after signup. |
| INVALID_REFRESH_TOKEN | 401 | auth | The refresh token was rejected. |
| INVALID_CONFIRMATION_CODE | 400 | invalid_request | The signup confirmation code did not match. |
| PASSWORD_RESET_FAILED | 400 | invalid_request | The password reset could not be applied. |
| FORGOT_PASSWORD_FAILED | 500 | api_error | The forgot-password flow could not be started. |
| SIGNUP_FAILED | 400 | invalid_request | The account could not be created. |
| WRONG_PASSWORD | 401 | auth | The current password on a change-password call is wrong. |
| WEAK_PASSWORD | 400 | invalid_request | The proposed password fails the password policy. |
| USER_NOT_FOUND | 404 | invalid_request | No user with that id under this client. |
| CLIENT_NOT_FOUND | 404 | invalid_request | No client account with that client_id. |
| EMAIL_ALREADY_VERIFIED | 409 | invalid_request | The address is already verified. |
| VERIFICATION_COOLDOWN | 429 | rate_limit | A verification code was requested too soon after the last one. |
| VERIFICATION_CODE_INVALID | 400 | invalid_request | The email verification code did not match. |
| VERIFICATION_CODE_EXPIRED | 400 | invalid_request | The email verification code has expired. |
| VERIFICATION_SEND_FAILED | 500 | api_error | The verification email could not be sent. |
| EMAIL_NOT_VERIFIED | 400 | invalid_request | Minting a secret requires a verified email. |
| OPERATOR_VERIFICATION_REQUIRED | 400 | invalid_request | Production secrets require completed business verification. |
| SANDBOX_FIXED_SECRET | 400 | invalid_request | The sandbox secret cannot be minted, rotated, or revoked. |
| SECRET_ALREADY_EXISTS | 409 | invalid_request | An active secret already exists for this environment. |
| INVALID_ENVIRONMENT | 400 | invalid_request | The environment value is not recognized. |
| SECRET_NOT_FOUND | 404 | invalid_request | No secret exists for this client and environment. |
| SECRET_EXPIRED | 401 | auth | The client secret is past its expiry. |
| SECRET_ROTATION_FAILED | 500 | api_error | The secret rotation could not be completed. |
| SECRET_REVOCATION_FAILED | 500 | api_error | The secret revocation could not be completed. |
| SECRET_GENERATION_FAILED | 500 | api_error | A new secret value could not be generated or stored. |
| CLIENT_SECRET_ROTATION_CONFLICT | 409 | invalid_request | A concurrent rotate or revoke won the race. |
| CLIENT_SECRET_REVOKE_CONFLICT | 409 | invalid_request | The secret you asked to revoke was already replaced. |
| CLIENT_SECRET_CLEANUP_EXHAUSTED | 500 | api_error | Too many stranded credentials to retire automatically. |
| WEBAUTHN_CHALLENGE_FAILED | 500 | api_error | The passkey challenge could not be issued. |
| WEBAUTHN_VERIFICATION_FAILED | 400 | invalid_request | The passkey ceremony response did not verify. |
| WEBAUTHN_CREDENTIAL_NOT_FOUND | 404 | invalid_request | No registered passkey matches that credential id. |
| VERIFICATION_REQUEST_FAILED | 500 | api_error | The business-verification request could not be recorded. |
| LIVE_ACCESS_REQUEST_FAILED | 500 | api_error | The live-access request could not be recorded. |
| SENDER_NOT_VERIFIED | 500 | api_error | The platform sender domain is not verified with the mail provider. |
| CONFIG_MISSING | 500 | api_error | Email delivery is not configured in this environment. |
| CODE_MISMATCH | 400 | invalid_request | The emailed code did not match. |
| CODE_EXPIRED | 400 | invalid_request | The emailed code is past its expiry window. |
| NO_ACTIVE_CODE | 400 | invalid_request | No code has been requested for this user. |
| LOCKED | 429 | rate_limit | Verification is locked after too many wrong attempts. |
| COOLDOWN | 429 | rate_limit | A code was sent recently — resend is on cooldown. |
| CODE_ROTATED | 409 | invalid_request | A newer code was issued while this attempt was in flight. |
| INVALID_CODE_FORMAT | 400 | invalid_request | The verification code must be exactly 6 digits. |
| LOOKUP_FAILED | 500 | api_error | The stored verification code could not be read. |
| INVALID_PASSWORD | 400 | invalid_request | The new password was rejected by the password policy. |
| TERMS_ACCEPTANCE_REQUIRED | 400 | invalid_request | Signup requires explicit acceptance of the current legal documents. |
| LEGAL_VERSION_OUTDATED | 400 | invalid_request | The accepted legal document versions are no longer current. |
| LEGAL_ACCEPTANCE_INVALID | 400 | invalid_request | The legal acceptance evidence is malformed. |
Bills & statements
| Error Code | HTTP | Type | Summary |
|---|---|---|---|
| BILL_NOT_FOUND | 404 | invalid_request | No bill exists with that id. |
| ACCOUNT_LINK_NOT_FOUND | 404 | invalid_request | The account link does not exist. |
| STATEMENT_NOT_EXTRACTED | 425 | invalid_request | The statement has not been extracted yet. |
| EXTRACTION_FAILED | 503 | upstream | Statement extraction failed upstream. |
| EXTRACTION_UNSUPPORTED_FOR_BILLER | 422 | invalid_request | This biller does not support statement extraction. |
| STATEMENT_TOO_LARGE | 413 | upstream | The statement exceeds the size limit. |
Links & Connect
| Error Code | HTTP | Type | Summary |
|---|---|---|---|
| LINK_NOT_FOUND | 404 | invalid_request | No link exists with that id. |
| LINK_TOKEN_NOT_FOUND | 404 | invalid_request | The link token is unknown or expired. |
| BACKGROUND_NOT_ELIGIBLE | 409 | invalid_request | The link token cannot be backgrounded now. |
| UPDATE_BILLER_MISMATCH | 409 | invalid_request | The update token targets a different biller. |
| INVALID_UPDATE_REASON | 400 | invalid_request | The update reason is not a recognized value. |
| LINK_UPDATE_FORBIDDEN | 403 | auth | The update token belongs to a different client. |
| REDIRECT_URI_NOT_REGISTERED | 400 | invalid_request | The redirect_uri is not in your registered allowlist. |
| LINK_TOKEN_LOOKUP_FAILED | 503 | upstream | The link token could not be read. |
| LINK_TOKEN_EXPIRED | 410 | invalid_request | The link token expired before the flow completed. |
| LINK_TOKEN_NOT_USABLE | 409 | invalid_request | The link token cannot accept this step. |
| LINK_ALREADY_EXISTS | 409 | invalid_request | An active link already exists for this account. |
| REQUEST_TO_LINK_NOT_FOUND | 404 | invalid_request | No request-to-link exists with that id. |
| CLIENT_ID_REQUIRED | 400 | invalid_request | This read refuses to run unscoped. |
| INVALID_PUBLIC_TOKEN | 400 | invalid_request | The public token is invalid or already exchanged. |
| INVALID_CREDENTIALS | 400 | invalid_request | The biller rejected the credentials. |
| BILLER_ACCOUNT_LOCKED | 409 | invalid_request | The biller locked the account. |
| MFA_NO_CHALLENGE | 409 | invalid_request | No MFA challenge is outstanding. |
| MFA_ATTEMPTS_EXHAUSTED | 409 | invalid_request | Every MFA attempt was used. |
| MFA_RESEND_LIMIT_REACHED | 429 | rate_limit | The MFA resend limit was reached. |
| EMPTY_ACCOUNT_SELECTION | 400 | invalid_request | No accounts were selected. |
| NO_DISCOVERED_ACCOUNTS | 409 | invalid_request | Nothing was discovered at the biller. |
| CREDENTIAL_NOT_FOUND | 404 | invalid_request | No vaulted credential with that id. |
| CREDENTIAL_INVALID | 400 | invalid_request | The credential failed validation. |
| CREDENTIAL_OWNER_MISMATCH | 403 | auth | The credential belongs to another owner. |
| CREDENTIAL_REVOKED | 409 | invalid_request | The credential is revoked. |
| CREDENTIAL_TYPE_REQUIRED | 400 | invalid_request | credential_type is missing. |
| CREDENTIAL_NOT_ROTATABLE | 409 | invalid_request | The credential cannot be rotated now. |
| CREDENTIAL_ENCRYPTION_FAILED | 500 | api_error | The credential could not be encrypted or decrypted. |
| CREDENTIAL_PERSIST_FAILED | 500 | api_error | The credential could not be stored. |
| CREDENTIAL_LOOKUP_FAILED | 500 | api_error | The credential store could not be read. |
| SCRAPING_SESSION_KEY_REQUIRED | 400 | invalid_request | The session cache key is incomplete. |
| SCRAPING_SESSION_LOOKUP_FAILED | 500 | api_error | The session cache could not be read. |
| SCRAPING_SESSION_SAVE_FAILED | 500 | api_error | The session cache could not be written. |
| SCRAPING_SESSION_INVALIDATE_FAILED | 500 | api_error | The cached session could not be invalidated. |
| SCRAPING_SESSION_INVALID | 400 | invalid_request | The session snapshot failed validation. |
| MFA_SUBMISSION_INVALID | 400 | invalid_request | The MFA submission failed validation. |
| MFA_SUBMISSION_LOOKUP_FAILED | 500 | api_error | The MFA submission store could not be read. |
| MFA_SUBMISSION_PERSIST_FAILED | 500 | api_error | The MFA submission could not be recorded. |
| MFA_CONTINUATION_UNAVAILABLE | 409 | invalid_request | No live MFA continuation is awaiting this code. |
| MFA_CONTINUATION_STALE | 409 | invalid_request | The continuation_id is out of date. |
| PENDING_MFA_INVALID | 400 | invalid_request | The pending-MFA snapshot failed validation. |
| PENDING_MFA_LOOKUP_FAILED | 500 | api_error | The pending-MFA store could not be read. |
| PENDING_MFA_PERSIST_FAILED | 500 | api_error | The pending-MFA snapshot could not be written. |
| PENDING_MFA_RESEND_FAILED | 500 | api_error | The OTP resend could not be recorded. |
Billers
| Error Code | HTTP | Type | Summary |
|---|---|---|---|
| BILLER_NOT_CONNECT_READY | 409 | invalid_request | The biller is not ready to connect. |
| BILLER_NOT_FOUND | 404 | invalid_request | No biller exists with that id. |
| BILLER_UNSUPPORTED | 422 | invalid_request | This biller cannot be automated. |
Payments
| Error Code | HTTP | Type | Summary |
|---|---|---|---|
| PAYMENT_EXECUTION_NOT_AVAILABLE | 501 | upstream | Payment execution is not yet available. |
Feedback
| Error Code | HTTP | Type | Summary |
|---|---|---|---|
| FEEDBACK_RUN_NOT_FOUND | 404 | invalid_request | The referenced resource does not exist. |
| FEEDBACK_RUN_NOT_OWNED | 403 | auth | The referenced resource belongs to another client. |
| FEEDBACK_RUN_EXPIRED | 410 | invalid_request | The feedback window has closed. |
| FEEDBACK_INVALID_CATEGORY | 422 | invalid_request | The category is not valid for this resource type. |
| FEEDBACK_INVALID_SIGNAL | 400 | invalid_request | The signal is not valid for the resource_type. |
| FEEDBACK_ALREADY_SUBMITTED | 409 | invalid_request | Feedback was already submitted for this resource. |
| FEEDBACK_RATE_LIMITED | 429 | rate_limit | Too many feedback submissions. |
Messaging
| Error Code | HTTP | Type | Summary |
|---|---|---|---|
| MESSAGING_CONSENT_NOT_GRANTED | 403 | invalid_request | The customer has not granted messaging consent. |
| MESSAGING_LIVE_ACCESS_REQUIRED | 403 | auth | Messaging requires production (live) access. |
| MESSAGING_SENDER_NOT_AUTHORIZED | 403 | auth | The sender is not authorized for this account link. |
| MESSAGING_CONTENT_FLAGGED | 422 | invalid_request | The message content was flagged. |
| MESSAGING_RATE_LIMITED | 429 | rate_limit | Messaging rate limit exceeded. |
| MESSAGING_SUPPRESSED | 200 | invalid_request | The recipient is suppressed; message not sent. |
| MESSAGING_AUP_NOT_ACCEPTED | 428 | invalid_request | The messaging Acceptable Use Policy is not accepted. |
| MESSAGING_AUP_REACCEPT_REQUIRED | 412 | invalid_request | The messaging AUP must be re-accepted. |
| MESSAGING_INVALID_PAYLOAD | 400 | invalid_request | The message payload is malformed. |
| MESSAGING_INVALID_CATEGORY | 400 | invalid_request | The message category is not recognized. |
| MESSAGING_PERSIST_FAILED | 503 | upstream | A transient error prevented recording the message. |
| MESSAGING_INTERNAL_ERROR | 500 | upstream | An unexpected messaging error occurred. |
| 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 | 404 | invalid_request | No stored email message with that id. |
| EMAIL_MESSAGE_LOOKUP_FAILED | 503 | upstream | The message store could not be read. |
| EMAIL_MESSAGE_PERSIST_FAILED | 500 | api_error | The message could not be stored. |
| EMAIL_CLIENT_ID_REQUIRED | 400 | invalid_request | client_id is required to read a message. |
| EMAIL_CLASSIFICATION_NOT_FOUND | 404 | invalid_request | No classification for that id or message. |
| EMAIL_CLASSIFICATION_LOOKUP_FAILED | 503 | upstream | The classification store could not be read. |
| EMAIL_CLASSIFICATION_PERSIST_FAILED | 500 | api_error | The classification could not be stored. |
| EMAIL_CLASSIFICATION_INVALID | 400 | invalid_request | The classification aggregate refused the transition. |
| EMAIL_CLASSIFICATION_FORBIDDEN | 403 | auth | The message behind this classification belongs to another client. |
| HUMAN_REVIEW_NOT_FOUND | 404 | invalid_request | No human-review task with that id. |
| HUMAN_REVIEW_LOOKUP_FAILED | 503 | upstream | The review store could not be read. |
| HUMAN_REVIEW_PERSIST_FAILED | 500 | api_error | The review could not be stored. |
| HUMAN_REVIEW_CLAIM_CONFLICT | 409 | invalid_request | Another worker already holds this review. |
| HUMAN_REVIEW_INVALID_STATE | 409 | invalid_request | The review is not in a state this operation accepts. |
| HUMAN_REVIEW_COMPLETION_REJECTED | 409 | invalid_request | The review completion was refused. |
| GMAIL_OAUTH_STATE_INVALID | 400 | auth | The Gmail OAuth state did not verify. |
| GMAIL_TOKEN_EXCHANGE_FAILED | 503 | upstream | Google refused the authorization-code exchange. |
| GMAIL_ACCESS_TOKEN_UNAVAILABLE | 503 | upstream | No usable Gmail access token for this connection. |
| GMAIL_WATCH_NOT_FOUND | 404 | invalid_request | No Gmail watch registration for that mailbox. |
| GMAIL_IMPORT_FAILED | 503 | upstream | Mail could not be imported from Gmail. |
| GMAIL_DISCONNECT_FAILED | 500 | api_error | The Gmail disconnect did not complete. |
| 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 | 404 | invalid_request | No agent-trace run with that run_id. |
| TRACE_LOOKUP_FAILED | 500 | api_error | The trace metadata store could not be read. |
| TRACE_LIST_FAILED | 500 | api_error | The trace list or recent-feed query was rejected. |
| TRACE_PERSIST_FAILED | 500 | api_error | The trace row could not be written. |
| TRACE_INVALID_STATE | 500 | api_error | The stored trace row cannot be acted on. |
| TRACE_ASSET_FORBIDDEN | 403 | invalid_request | The asset key is malformed or outside the run prefix. |
| TRACE_ASSET_READ_FAILED | 500 | api_error | The artifact object could not be read. |
| TRACE_ARTIFACT_REJECTED | 400 | invalid_request | The artifact is refused by credential containment. |
| TRACE_ARTIFACT_UPLOAD_FAILED | 500 | api_error | The artifact could not be written to storage. |
| TRACE_FIXTURE_EXPORT_FAILED | 500 | api_error | The fixture archive could not be built. |
| EVENT_ARCHIVE_QUERY_FAILED | 500 | api_error | The archived-event store could not be queried. |
| EVENT_ARCHIVE_EVENT_NOT_FOUND | 404 | invalid_request | No archived event with that id. |
| DEBUG_PARAMETER_NOT_FOUND | 404 | invalid_request | No configuration parameter at that path. |
| DEBUG_PARAMETER_NAME_INVALID | 400 | invalid_request | The parameter name is outside the writable namespace. |
| DEBUG_PARAMETER_LOOKUP_FAILED | 500 | upstream | Parameter Store could not be read. |
| DEBUG_PARAMETER_UPDATE_FAILED | 500 | upstream | The parameter write was rejected. |
| DEBUG_SERVICE_LIST_FAILED | 500 | upstream | The per-service parameter rollup could not be built. |
| DEBUG_ALARM_QUERY_FAILED | 500 | upstream | Live alarm state could not be read. |
Agent improvement
| Error Code | HTTP | Type | Summary |
|---|---|---|---|
| AGENT_TRACE_NOT_FOUND | 404 | invalid_request | No agent trace for that run. |
| AGENT_TRACE_INVALID | 400 | invalid_request | The agent trace payload is invalid. |
| AGENT_TRACE_LOOKUP_FAILED | 503 | upstream | The agent trace store could not be read. |
| AGENT_TRACE_PERSIST_FAILED | 500 | api_error | The agent trace could not be written. |
| AGENT_TRACE_OUTCOME_EMIT_FAILED | 500 | api_error | The trace outcome event could not be published. |
| AGENT_TRACE_EVENT_BUS_UNAVAILABLE | 503 | upstream | No event bus is wired for trace outcomes. |
| AGENT_AUDIT_LOG_INVALID | 400 | invalid_request | The audit log entry is invalid. |
| AGENT_AUDIT_LOG_PERSIST_FAILED | 500 | api_error | The audit log entry could not be appended. |
| AGENT_AUDIT_LOG_LOOKUP_FAILED | 503 | upstream | The audit log could not be read. |
| PROMPT_VERSION_NOT_FOUND | 404 | invalid_request | No prompt version with that hash. |
| PROMPT_VERSION_INVALID | 400 | invalid_request | The prompt version payload is invalid. |
| PROMPT_VERSION_LOOKUP_FAILED | 503 | upstream | The prompt version store could not be read. |
| PROMPT_VERSION_PERSIST_FAILED | 500 | api_error | The prompt version could not be written. |
| PROMPT_POINTER_NOT_FOUND | 404 | invalid_request | No production prompt pointer for that agent. |
| PROMPT_POINTER_CAS_MISMATCH | 409 | invalid_request | The production pointer moved under you. |
| PROMPT_POINTER_INVALID | 400 | invalid_request | The production pointer payload is invalid. |
| PROMPT_POINTER_FLIP_FAILED | 500 | api_error | The production pointer could not be written. |
| PROMPT_POINTER_LOOKUP_FAILED | 503 | upstream | The production pointer could not be read. |
| PROMPT_POINTER_ROLLBACK_NO_PRIOR | 409 | invalid_request | No prior prompt version to roll back to. |
| PROMPT_POINTER_ROLLBACK_HASH_MISMATCH | 409 | invalid_request | The rollback guard hash does not match. |
| PROMPT_EXPERIMENT_NOT_FOUND | 404 | invalid_request | No prompt experiment with that id. |
| PROMPT_EXPERIMENT_ALREADY_ACTIVE | 409 | invalid_request | An experiment is already running for that agent. |
| PROMPT_EXPERIMENT_VARIANT_NOT_FOUND | 400 | invalid_request | The experiment arm is not a registered prompt version. |
| PROMPT_EXPERIMENT_NO_CONTROL | 400 | invalid_request | The experiment has no control arm. |
| PROMPT_EXPERIMENT_INVALID | 400 | invalid_request | The prompt experiment payload is invalid. |
| PROMPT_EXPERIMENT_LOOKUP_FAILED | 503 | upstream | The prompt experiment store could not be read. |
| PROMPT_EXPERIMENT_PERSIST_FAILED | 500 | api_error | The prompt experiment could not be written. |
| PROMPT_EVAL_SCORE_INVALID | 400 | invalid_request | The prompt eval score is invalid. |
| PROMPT_EVAL_SCORE_PERSIST_FAILED | 500 | api_error | The prompt eval score could not be written. |
| BRAIN_ENTRY_NOT_FOUND | 404 | invalid_request | No brain entry with that id. |
| BRAIN_ENTRY_INVALID | 400 | invalid_request | The brain entry payload is invalid. |
| BRAIN_ENTRY_INVALID_STATE | 409 | invalid_request | The brain entry is in the wrong state for that decision. |
| BRAIN_ENTRY_LOOKUP_FAILED | 503 | upstream | The brain store could not be read. |
| BRAIN_ENTRY_PERSIST_FAILED | 500 | api_error | The brain entry could not be written. |
| BRAIN_EFFECT_LOOKUP_FAILED | 503 | upstream | The brain effect snapshots could not be read. |
| BRAIN_EFFECT_COMPUTE_FAILED | 500 | api_error | The brain entry effect could not be computed. |
| BRAIN_SEARCH_FAILED | 503 | upstream | The brain search could not be executed. |
| RECORDING_VERSION_NOT_FOUND | 404 | invalid_request | No recording version with that hash. |
| RECORDING_VERSION_INVALID | 400 | invalid_request | The recording version payload is invalid. |
| RECORDING_VERSION_LOOKUP_FAILED | 503 | upstream | The recording version store could not be read. |
| RECORDING_VERSION_PERSIST_FAILED | 500 | api_error | The recording version could not be written. |
| RECORDING_POINTER_NOT_FOUND | 404 | invalid_request | No production recording pointer for that biller. |
| RECORDING_POINTER_LOOKUP_FAILED | 503 | upstream | The production recording pointer could not be read. |
| CANARY_ALREADY_RUNNING | 409 | invalid_request | A canary is already running. |
| CANARY_NOT_RUNNING | 409 | invalid_request | No canary is running. |
| CANARY_TERMINAL | 409 | invalid_request | The canary already reached a terminal state. |
| CANARY_CANDIDATE_NOT_FOUND | 404 | invalid_request | The canary candidate recording does not exist. |
| CANARY_NO_BASELINE | 409 | invalid_request | The canary has no baseline to compare against. |
| CANARY_INVALID | 400 | invalid_request | The canary payload is invalid. |
| CANARY_CAS_MISMATCH | 409 | invalid_request | The recording pointer moved during the canary decision. |
| CANARY_LOOKUP_FAILED | 503 | upstream | The canary state could not be read. |
| CANARY_PERSIST_FAILED | 500 | api_error | The canary state could not be written. |
| CANARY_METRICS_UNAVAILABLE | 503 | upstream | The canary metrics could not be computed. |
| AGENT_FREEZE_INVALID | 400 | invalid_request | The agent freeze request is invalid. |
| AGENT_FREEZE_PERSIST_FAILED | 500 | api_error | The agent freeze state could not be written. |
| AGENT_FREEZE_LOOKUP_FAILED | 503 | upstream | The agent freeze state could not be read. |
| CLIENT_FEEDBACK_NOT_FOUND | 404 | invalid_request | No client feedback for that run. |
| CLIENT_FEEDBACK_INVALID | 400 | invalid_request | The client feedback payload is invalid. |
| CLIENT_FEEDBACK_ALREADY_SUBMITTED | 409 | invalid_request | Feedback was already submitted for that run. |
| CLIENT_FEEDBACK_IDEMPOTENCY_CONFLICT | 409 | invalid_request | The idempotency key was reused with different content. |
| CLIENT_FEEDBACK_LOOKUP_FAILED | 503 | upstream | The client feedback store could not be read. |
| CLIENT_FEEDBACK_PERSIST_FAILED | 500 | api_error | The client feedback could not be written. |
| FEEDBACK_INVITED_MARKER_FAILED | 500 | api_error | The feedback-invited marker could not be written. |
| EXECUTION_RUN_NOT_FOUND | 404 | invalid_request | No execution run with that id. |
| EXECUTION_RUN_INVALID | 400 | invalid_request | The execution run payload is invalid. |
| EXECUTION_RUN_INVALID_STATE | 409 | invalid_request | The execution run is in the wrong state for that transition. |
| EXECUTION_RUN_LOOKUP_FAILED | 503 | upstream | The execution run store could not be read. |
| EXECUTION_RUN_PERSIST_FAILED | 500 | api_error | The execution run could not be written. |
| AGENT_REPORT_WINDOW_INVALID | 400 | invalid_request | The report window is invalid. |
| 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
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);
}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
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());
}Related
- Error code reference — a dedicated page per error code
- Feedback errors — the
FEEDBACK_*codes - Rate Limits — rate limit policy and retry strategies
- Authentication — API key and bearer token auth
- Idempotency — webhook dedupe by
event.id