Skip to main content
Every non-2xx response returned by /v1/* carries the same JSON error envelope describing what went wrong. Every error — whether it originates in a handler or in middleware (authentication, permissions, rate limiting, idempotency, request timeouts) — includes a human-readable message and a machine-readable code your client can branch on.

Standard error envelope

FieldTypeAlways presentDescription
error.messagestringYesHuman-readable description. Safe to log; never contains PII. For 5xx errors, this is always the string internal server error — the detailed error is recorded server-side and can be correlated via X-Request-ID.
error.codestringYesMachine-readable snake_case identifier. Safe to branch on. Every merchant-facing error returned by the API carries a code; the codes listed in the catalog below are the complete public surface.
error.detailsarray or objectNoAdditional context. Present on validation errors (see below). May be omitted from other error types.

Validation error envelope

Requests that fail field-level validation return 422 Unprocessable Entity with a details array. Each entry identifies the offending field and what it needs.
The field is dotted-path notation for nested objects (for example individual.address.country). Fix every entry in details and retry.

HTTP status reference

StatusMeaning in the Anton API
400 Bad RequestRequest body could not be parsed, contained unknown fields, or a URL parameter/path was malformed.
401 UnauthorizedMissing, malformed, expired, or revoked credential. Re-authenticate before retrying.
403 ForbiddenAuthenticated but not permitted. Caused by insufficient role permissions, suspended/terminated merchant status, test-key-in-production, MFA enforcement, or a velocity block.
404 Not FoundResource does not exist under the calling merchant’s scope. May also mean the resource exists but belongs to a different merchant — the API never distinguishes the two, to prevent tenant enumeration.
409 ConflictThe resource’s current state disallows the requested transition (for example, cancelling a completed payout), or an idempotency key was replayed with a different payload.
410 GoneThe endpoint has been deprecated and removed. Follow the migration note in the response message.
413 Payload Too LargeRequest body exceeds the endpoint’s limit (1 MB on most routes, 26 MB on document upload routes).
422 Unprocessable EntityRequest was syntactically valid but semantically rejected — validation failure, insufficient balance, or a business-rule refusal.
429 Too Many RequestsPer-merchant rate limit exceeded. Respect Retry-After. See Rate limits.
500 Internal Server ErrorAn unexpected server error. Safe to retry with exponential backoff. The detailed error is never exposed to clients — correlate via X-Request-ID.
501 Not ImplementedThe endpoint exists but is not enabled in this environment due to missing configuration (for example, the engine admin token is not set on this API instance). Not a caller mistake.
503 Service UnavailableA downstream dependency (rail provider, FX provider, Basis Theory, WorkOS) is degraded or circuit-broken. Retry with backoff.
504 Gateway TimeoutThe request exceeded the 25-second handler timeout. Retry; consider whether your payload can be split.

The X-Request-ID header

Every response includes an X-Request-ID header. This ID is generated by the API when a request arrives (or echoed from the client when you set one on the request). Include this value when opening a support ticket — Anton retains the server-side log for every request keyed by this ID, and we can trace the failure without asking you to reproduce it.

Error code catalog

The following codes are returned via the code field of the error envelope. Codes not listed here do not currently exist in the API — the merchant-facing surface has a small, deliberate set.

Authentication

HTTPcodeTypical error.messageCauseRemediation
401unauthorizedmissing Authorization headerNo Authorization header sent.Add Authorization: DPoP <access_token> plus a per-request DPoP: <proof> header. See Authentication.
401unauthorizedinvalid Authorization header formatHeader scheme is neither DPoP nor Bearer.Use Authorization: DPoP <token> for OAuth, Bearer <jwt> for portal JWT.
401invalid_tokeninvalid or expired tokenOAuth access token signature, expiry, or audience is invalid.Mint a new token via POST /oauth/token.
401invalid_dpop_proofinvalid DPoP proofThe DPoP header proof failed signature, htm, htu, iat, jti, ath, or jkt verification.See the troubleshooting matrix on the Authentication page.
401static_api_keys_disabledstatic API keys are no longer acceptedAn Authorization: Bearer ak_* header was sent.Migrate to OAuth 2.0 + DPoP. See Authentication.
401not_authenticatednot authenticatedA handler ran without a resolved merchant or user context. Defense-in-depth check after the auth middleware.Re-authenticate. The portal should redirect to the login flow.
401merchant_context_requiredmerchant context requiredToken validated, but Anton could not resolve a merchant from it.Re-authenticate; confirm the merchant binding on your portal user.
403key_environment_mismatchtest tokens are not allowed in productionA test-environment OAuth token was used against the production or staging API.Use the right environment: production credentials issue tokens for api.antonpayments.com; sandbox credentials for api.antonpayments.dev.
503oauth_unavailableOAuth authentication is not configured on this instanceMisconfigured deploy: OAuth signing key not set. Production refuses to start in this state, so this only surfaces in misconfigured non-prod environments.Contact support.

Authorization and access

HTTPcodeerror.messageCauseRemediation
403insufficient_permissionsinsufficient permissionsThe caller’s role does not grant the required permission on this endpoint.Have a user with the right role perform the action, or adjust team role assignments.
403role_forbiddenvarious role-specific phrases (e.g. only admin users can…, access denied)A handler-level role gate refused the action — admin-only fields, owner-only resources, technical/admin-only branding writes, etc. Distinct from insufficient_permissions (which is the middleware-level RBAC gate).Have a user with the required role take the action. The UI should hide the affected control.
403sandbox_onlysandbox … is not available in this environmentA /v1/merchant/sandbox/* endpoint was called against production.Only call sandbox endpoints in the sandbox environment.
403merchant_suspendedmerchant account is suspendedMerchant account is in the suspended state.Contact Anton support.
403merchant_terminatedmerchant account is terminatedMerchant account is in the terminated state.Contact Anton support.
403merchant_not_activemerchant account is not yet activeMerchant provisioning is still in progress.Complete onboarding from the merchant dashboard. Contact Anton support if this persists.
403merchant_not_foundmerchant not foundThe authenticated merchant ID could not be resolved.Re-authenticate; confirm the merchant exists.

Validation

HTTPcodeMeaningRemediation
422validation_errorOne or more fields failed validation. The details array carries per-field errors.Inspect error.details[] and fix each entry.
400bad_requestReturned by the idempotency middleware when the request body could not be read.Retry the request; verify the body is well-formed JSON and under 1 MB.
400invalid_request_bodyThe request body could not be JSON-decoded — malformed JSON, unknown fields (the decoder rejects them), or wrong content type.Confirm the body matches the documented schema.
400missing_required_fieldAn ad-hoc required field was missing — for example, currency, id, domain, or email.Add the missing field.
400invalid_emailEmail field is not a valid RFC 5322 address.Fix the email value.
400invalid_country_codeCountry value is not a 2-letter ISO 3166-1 alpha-2 code.Use a valid country code (e.g. US, GB).
400invalid_fileMultipart upload was missing the file field.Include a file under the file form field.
400invalid_file_formatBatch upload file is not .csv or .xlsx.Resave the file in a supported format.
400invalid_file_typeDocument or batch upload had an unsupported Content-Type.Re-export the asset in a supported format.
400invalid_multipart_formThe multipart form could not be parsed.Confirm the request is a properly-encoded multipart/form-data body.
400invalid_template_formatGET /v1/batches/template?format= was called with anything other than xlsx or csv.Use one of the supported template formats.
400invalid_urlsupport_url, terms_url, or privacy_url is not a valid HTTPS URL.Use an HTTPS URL.
400invalid_display_namedisplay_name exceeds 100 characters.Use a shorter / valid display name.
400invalid_scopescope query param on sandbox reset was not one of the allowed values.Use balances, all, delete-all, or full-reset.
400file_hash_mismatchClient-supplied file_hash does not match the server-computed SHA-256.Recompute the hash on the file as it is being sent and resubmit.

Resource not found

404 Not Found responses always carry a code identifying the missing resource. Anton never distinguishes “does not exist” from “exists but belongs to another merchant” — both return the same *_not_found envelope to prevent tenant enumeration.
HTTPcodeReturned by
404payout_not_foundGET /v1/payouts/{id}, GET /v1/payouts/{id}/events
404beneficiary_not_foundAll /v1/beneficiaries/{id}* reads/writes; instrument creation under a beneficiary
404engine_beneficiary_not_foundPOST /v1/beneficiaries/{id}/rescore when the engine has no profile for this beneficiary yet (typically a brand-new beneficiary that has not yet been scored by the engine’s background consumer)
404instrument_not_foundGET/PUT/DELETE /v1/instruments/{id}
404batch_not_foundAll /v1/batches/{id}* endpoints
404template_not_foundGET /v1/batches/template when the template file is missing on the server
404webhook_subscription_not_found/v1/webhooks/{id}* reads, secret rotation, test, deactivate
404webhook_event_not_foundGET /v1/webhooks/events/{id}
404merchant_not_foundInternal resolution of the authenticated merchant (rare — implies a token/session drift).
404pricing_plan_not_foundPOST /v1/pricing/quote when no plan applies
404account_not_found/v1/accounts/{currency}*
404balance_not_foundGET /v1/balances/{currency}
404country_not_supportedGET /v1/instruments/methods?country=<CC> for unsupported countries
404quote_not_foundPOST /v1/fx/exchange with a quote_id that does not exist or is not owned by the merchant

Idempotency

HTTPcodeMeaningRemediation
400missing_idempotency_keyThe endpoint requires an Idempotency-Key header and none was supplied.Add a unique Idempotency-Key header. See Idempotency.
409idempotency_conflictAn Idempotency-Key was reused with a different request payload.Either retry the original payload with the same key, or use a new key for the new payload. Never mix.

Rate limiting

HTTPcodeMeaningRemediation
429rate_limit_exceededPer-merchant rate limit has been exceeded (1,000 requests/minute on /v1/*; tighter limits apply to /v1/fx/quote, /v1/fx/exchange, and sensitive endpoints).Honor the Retry-After header. Back off with jitter. See Rate limits.

Payouts

HTTPcodeMeaningRemediation
422insufficient_balanceThe source balance cannot cover the payout amount plus fees. (Also returned by POST /v1/fx/exchange when the sell-side balance is too low.)Top up the relevant currency balance, or reduce the amount.
403velocity_blockedThe payout was rejected by Anton’s real-time risk policy (velocity/thresholds/geo).Inspect GET /v1/payouts/{id}/velocity-results for the triggered rules. A payout.velocity_blocked webhook is also dispatched with the reason.
422payout_rejectedCatch-all for payout creation failures that are not validation, balance, or velocity related.Inspect the message; correct the input or retry. See the payout_rejected sub-causes table below for the specific conditions this code covers.
422payout_not_cancellableThe payout’s current state does not allow cancellation (already submitted to a rail, completed, etc.).Re-fetch the payout; cancellation is only valid before submission.

payout_rejected sub-causes

payout_rejected is a stable, coarse-grained code — your integration should pattern-match on error.code, not on error.message. Today it rolls up the following conditions in the API response. The message string distinguishes them, and the operational runbook (api/docs/internal/integrations/) carries the full mapping. Future API revisions may split any of these into a dedicated code; when that happens it will be announced as a breaking change with a minimum 90-day deprecation window on the catch-all.
Sub-causeWhen it firesRemediation
Archived or inactive beneficiaryThe beneficiary_id exists but is archived or inactive.Unarchive or activate the beneficiary via the merchant portal, or pick a different beneficiary.
Archived or inactive instrumentThe instrument_id is not in active status.Activate or replace the instrument.
Wrong-currency instrumentThe instrument’s currency doesn’t match the payout’s dest_currency.Select an instrument whose currency matches the destination.
Merchant not active / approvedThe merchant is not in active status (still onboarding, suspended, etc.).Complete onboarding or contact support. State-changes to active arrive via webhook.
Invalid corridorThe source / destination currency pair is not enabled on your account.Call GET /v1/corridors to enumerate enabled corridors. Contact support to request a new corridor.
Pricing service unavailableTransient backend failure computing the quote for this payout.Retry with back-off. The same Idempotency-Key is safe to reuse once the service recovers.

Batches

HTTPcodeMeaningRemediation
400invalid_cursorGET /v1/batches, GET /v1/batches/{id}/payouts, or GET /v1/ops/batches was called with a cursor that is malformed or has been tampered with.Drop the cursor and start from the first page, or use the next_cursor returned on the previous page verbatim.
400file_hash_mismatchClient-supplied file_hash multipart field does not match the server-computed SHA-256 of the uploaded file.Recompute the SHA-256 on the exact bytes being uploaded.
409duplicate_fileA batch file with the same SHA-256 hash is already uploaded, validating, validated, confirmed, or processing for this merchant.Wait for the pending batch to finish, or cancel it. Use the pending_batches array on a non-error upload response to correlate.
409batch_not_validatedPOST /v1/batches/{id}/confirm was called before the batch finished validating.Poll for validated status before confirming.
409batch_confirmation_expiredThe batch validation result expired before confirm was called.Re-upload the file and retry.
409batch_not_cancellableThe batch is not in a cancellable state.Re-fetch the batch; cancellation is only valid in early states.
422batch_upload_failedGeneric upload failure not covered by duplicate_file or invalid_file_format.Retry; if persistent, file a support ticket with the X-Request-ID.
422batch_confirm_failedGeneric confirmation failure.Inspect the message and retry.
422batch_cancel_failedGeneric cancellation failure.Inspect the message and retry.
501not_configuredGET /v1/batches/{id}/beneficiaries was called on an API instance where the beneficiary service is not wired into the batch handler. Not a caller mistake.Environment-level misconfiguration; contact support. Sandbox and production have this enabled.
Uploads larger than the 32 MB limit currently surface as 400 file_too_large (the BodySizeLimit middleware rejects oversize requests before the handler runs). The catalog entry in the Payload size section reflects the intended long-term shape (413 file_too_large); an internal fix is tracked to align the status.

Beneficiaries

HTTPcodeMeaningRemediation
422duplicate_beneficiaryA beneficiary with the same fingerprint already exists for the merchant.Reuse the existing beneficiary.
422invalid_display_nameThe supplied display name is missing or invalid.Provide a valid display name.
422missing_beneficiary_detailsNeither individual nor business details were provided.Include the appropriate detail block for the beneficiary type.
422beneficiary_create_failedCatch-all for creation failures (Basis Theory tokenization, database). The detailed cause is logged server-side.Retry; share the X-Request-ID if persistent.
422beneficiary_update_failedGeneric update failure.Inspect the message; retry.
422beneficiary_pii_update_failedPII update via Basis Theory failed.Retry; if persistent, share the X-Request-ID.
422beneficiary_archive_failedGeneric archive failure.Inspect the message; retry.
422beneficiary_restore_failedGeneric restore failure.Inspect the message; retry.
422beneficiary_delete_failedGeneric delete failure.Inspect the message; retry.

Beneficiary rescore

Codes returned only by POST /v1/beneficiaries/{id}/rescore.
HTTPcodeMeaningRemediation
404engine_beneficiary_not_foundThe beneficiary exists under this merchant, but the Anton Engine has not yet seeded a profile for it. Typically seen on brand-new beneficiaries before the first payout.Wait for the engine’s beneficiary-events consumer to write the profile (usually seconds; definitely after the next payout), then retry.
409beneficiary_scoring_lockedThe beneficiary is in a terminal scoring state (permanently locked by the engine) and cannot be rescored.Do not retry. Contact support if a rescore is required.
429rescore_rate_limitedA rescore for this beneficiary completed recently — the engine applies a cooldown to prevent churn. Distinct from rate_limit_exceeded, which is the per-merchant request quota.Honor the Retry-After header before retrying.
501rescore_not_configuredManual rescore is not enabled on this API instance (the engine admin token is not configured). Environment-level configuration issue, not a caller mistake.Contact support — the environment needs the engine admin token provisioned.

Instruments

HTTPcodeMeaningRemediation
410deprecated_endpointThe flat POST /v1/instruments route is deprecated.Use POST /v1/beneficiaries/{id}/instruments.
422duplicate_instrumentAn instrument with the same fingerprint already exists for the beneficiary.Reuse the existing instrument.
422invalid_credentialsThe instrument credentials (IBAN check digit, account number, routing number) failed validation.Fix the input.
422method_not_supportedThe requested payment method is not supported for the country.Use a method valid for the destination country.
422instrument_create_failedCatch-all creation failure (Basis Theory, database).Retry; share the X-Request-ID if persistent.
422instrument_update_failedGeneric update failure.Inspect the message; retry.
422instrument_delete_failedGeneric delete failure.Inspect the message; retry.

FX

HTTPcodeMeaningRemediation
400currency_not_supportedOne or both currencies in the quote/exchange request are not supported.Use a supported corridor.
410quote_expiredThe locked quote referenced by quote_id has expired.Generate a new quote and retry.
400quote_not_lockedPOST /v1/fx/exchange was called with a quote_id that points to an indicative (non-lockable) quote.Use a locked quote — call POST /v1/fx/quote first.
422no_funding_accountThe merchant has no funding account in the sell currency.Fund the account first via /v1/accounts/....
422no_buy_accountPOST /v1/fx/exchange — the merchant has no funding account in the buy_currency, so the exchange cannot be credited. Previously surfaced as 500; now deterministic per ANT-55.Provision the buy-side account (contact ops) and retry.
502fx_rates_unavailableThe upstream FX rate provider is degraded or unavailable.Retry with backoff.
503locked_rate_unavailableThe provider returned an indicative rate when a locked rate was requested.Retry shortly.
500fx_execution_failedExchange execution failed in a way that does not map to insufficient balance or a quote issue. The message is sanitized; the real cause is logged with the X-Request-ID.Retry with backoff; share the X-Request-ID if persistent.

Webhook subscriptions

Structured codes returned by POST /v1/webhooks:
HTTPcodeMeaningRemediation
400webhook_url_requiredurl field was missing or empty.Send a non-empty url.
400webhook_url_invalidurl field could not be parsed as a URL.Send a syntactically valid URL.
400webhook_url_not_httpsThe url field used a non-https:// scheme. HTTP is rejected in production, staging, and sandbox — it is only permitted against local development APIs.Use https:// and a valid TLS certificate.
400webhook_url_private_addressThe url host is a loopback, RFC1918/ULA private address, link-local address, .local / .internal hostname, or a cloud metadata endpoint (for example 169.254.169.254). Anton refuses to register these targets (SSRF and credential-exfiltration defense).Use a publicly reachable, internet-routable endpoint.

Payload size

HTTPcodeMeaningRemediation
413file_too_largeUpload exceeds the per-endpoint cap (2 MB for branding assets, 5 MB for user avatars, 25 MB for documents, 32 MB for batches).Reduce the file size. The message includes the applicable limit.

Sandbox-only

These codes are returned only by the /v1/merchant/sandbox/* routes, which are registered only in sandbox-class environments. Production merchants will never see them.
HTTPcodeMeaningRemediation
422no_beneficiariesPOST /v1/merchant/sandbox/seed-payouts was called before any beneficiaries existed for the merchant.Call /v1/merchant/sandbox/seed-beneficiaries first.
429rate_limited_sandbox_resetSandbox reset is capped at 10 invocations per merchant per hour.Wait for the counter to reset.
429rate_limited_sandbox_seedSandbox seeding is capped at 10 invocations per merchant per hour.Wait for the counter to reset.
503sandbox_seeder_unavailableSandbox account seeder is not configured.Surface a support ticket; this is an environment misconfiguration.
503beneficiary_seeder_unavailableBeneficiary seeder is not configured.Same as above.
503payout_seeder_unavailablePayout seeder is not configured.Same as above.
500sandbox_reset_failedSandbox reset failed during wipe or reseed.Retry; share the X-Request-ID if persistent.
500seed_beneficiaries_failedSample beneficiary seeder failed — unexpected error during tokenization or persistence. Distinct from duplicate_beneficiary / duplicate_instrument (422), which indicate the seeder already ran.Retry; share the X-Request-ID if persistent.
500seed_payouts_failedSample payout seeder failed.Retry; share the X-Request-ID if persistent.

Server and infrastructure

HTTPcodeMeaningRemediation
500internal_errorUnexpected server error. message is always internal server error — the real cause is logged with the X-Request-ID.Retry with exponential backoff. If the failure persists, share the X-Request-ID with support.
504timeoutHandler exceeded the 25-second request timeout.Retry. For large batches or reports, prefer asynchronous endpoints.

Retry semantics

Not every error is safe to retry. The table below is a safe default — apply stricter rules where your integration’s correctness depends on it.
CategorySafe to retry?Notes
500, 502, 503, 504YesUse exponential backoff with jitter. Always include the same Idempotency-Key on mutating retries so a partially-processed request is not duplicated.
429 rate_limit_exceededYesWait at least the Retry-After value before the first retry.
429 rescore_rate_limitedYesBeneficiary-specific rescore cooldown. Honor Retry-After.
501 rescore_not_configuredNoEnvironment is missing the engine admin token. Contact support.
409 beneficiary_scoring_lockedNoBeneficiary is permanently locked. Retrying will fail identically.
404 engine_beneficiary_not_foundConditionalRetry only after the engine has seeded a profile (typically after the next payout).
Transient network errors (connection reset, DNS failure, TLS reset)YesTreat the same as a 5xx. Retry with the same Idempotency-Key.
400, 404, 410, 413, 422 validation_errorNoFix the request. Retrying will fail identically.
401NoRe-authenticate first. Do not loop on 401.
403 insufficient_permissions, 403 mfa_enrollment_required, 403 velocity_blockedNoPolicy or permission issue. Resolve upstream before retrying.
409 idempotency_conflictNoYou sent a new payload under a used key. Generate a new key.
409 state conflicts (batch, payout, approval)ConditionalRe-fetch the resource; retry only if the current state actually permits the action.
422 insufficient_balanceNoTop up first.
422 beneficiary/instrument service errors (duplicate_beneficiary, invalid_display_name, missing_beneficiary_details, duplicate_instrument, invalid_credentials, method_not_supported, etc.)NoFix input, then retry with a fresh idempotency key.
422 quote_expired, 422 fx_rates_unavailable, 503 locked_rate_unavailableYes (re-quote)Generate a fresh quote and retry the exchange.
When retrying a mutating request (POST, PUT, PATCH), always send the same Idempotency-Key you used on the original attempt. Without it, the server cannot distinguish a retry from a new request, and you risk double-processing. See Idempotency.

Handling errors in code

When contacting Anton support about a failed request, include the X-Request-ID header value from the response. Anton retains a full server-side trace keyed by that ID and can diagnose without asking you to reproduce.