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

{
  "error": {
    "message": "insufficient funds for this payout",
    "code": "insufficient_balance"
  }
}
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.
{
  "error": {
    "message": "validation failed",
    "code": "validation_error",
    "details": [
      {
        "field": "source_amount",
        "message": "must be greater than zero"
      },
      {
        "field": "dest_currency",
        "message": "is required"
      },
      {
        "field": "individual.date_of_birth",
        "message": "must be exactly 10 characters"
      }
    ]
  }
}
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.
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.
HTTP/1.1 500 Internal Server Error
Content-Type: application/json
X-Request-ID: cu1h4o9s4g8p2m6f3v4g

{"error":{"message":"internal server error","code":"internal_error"}}

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: Bearer <token>.
401unauthorizedinvalid Authorization header formatHeader is not Bearer <token>.Use the Bearer prefix.
401unauthorizedinvalid or expired tokenJWT signature, expiry, or issuer is invalid.Have the portal session refresh.
401unauthorizedAPI keys are not accepted on this endpointUsed an API key on a JWT-only endpoint.Use a WorkOS portal JWT.
401invalid_api_keyinvalid API key formatToken does not begin with ak_.Use a valid ak_live_... or ak_test_... key.
401invalid_api_keyinvalid API keyKey was not found, is revoked, expired, or has been deleted.Rotate via the dashboard.
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 API keys are not allowed in productionAn ak_test_... key was used against the production or staging environment.Use a live key in production; keep test keys to sandbox.

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.
403self_modification_forbiddencannot change your own role / cannot deactivate your own accountA user tried to modify their own role or active status via the team-management endpoints.Have a different admin perform the change.
403last_admin_protectioncannot change the last admin's role… / cannot deactivate the last admin…The merchant must always have at least one active admin; the request would have left zero.Promote another user to admin first, then retry.
403current_session_forbiddencannot revoke your current session — sign out insteadThe user attempted to revoke the same session that authenticated the request.Sign out via the normal flow so the cookie clears cleanly.
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 activeOnboarding/provisioning is still in progress.Finish onboarding; only /v1/onboarding/*, /v1/documents/*, and /v1/rfis/* are callable during this stage.
403merchant_not_foundmerchant not foundThe authenticated merchant ID could not be resolved.Re-authenticate; confirm the merchant exists.
403user_not_founduser not found or inactiveThe JWT subject does not resolve to an active user for this merchant.Re-authenticate; confirm the user is active.

MFA enforcement

HTTPcodeMeaningRemediation
403mfa_enrollment_requiredThe merchant has enforced MFA, the grace period has expired, and the calling user has no active TOTP factor.Have the user enroll a TOTP factor via /v1/users/me/mfa/enroll. The portal converts this code into a forced-enrollment flow.

Validation

HTTPcodeMeaningRemediation
422validation_errorOne or more fields failed validation. The details array carries per-field errors. Endpoints that previously returned individual 400s for each field (notably PATCH /v1/merchant/branding) now collect all errors into this single envelope so the merchant can fix the form in one round trip.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_roleRole value is not one of the supported merchant roles (admin, accounting, technical, operator, viewer).Use a valid role slug.
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_thresholdNumeric value (velocity threshold, FX amount, simulator amount) is missing, non-numeric, or not positive.Send a valid positive decimal.
400invalid_actionVelocity rule action is not one of block, review, alert.Use a valid action.
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_typeBranding asset or avatar upload had an unsupported Content-Type. Branding assets accept PNG and SVG; avatars accept PNG, JPEG, and WebP.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_asset_kindBranding asset path parameter is not a recognized asset kind.Use a documented asset kind.
400invalid_svg_contentUploaded SVG contains scripts or event handlers (XSS-prevention policy).Strip scripts/event handlers from the SVG before uploading.
400invalid_intentAdmin Portal link request used an unsupported intent. Allowed values: sso, dsync, domain_verification.Use a valid intent.
400invalid_hostnamecustom_domain does not look like a hostname.Send a valid hostname (e.g. pay.example.com).
400invalid_colorprimary_color or accent_color is not a valid hex color.Send a #RRGGBB hex value.
400invalid_urlsupport_url, terms_url, or privacy_url is not a valid HTTPS URL.Use an HTTPS URL.
400invalid_csscustom_css exceeds the 64 KB cap.Trim the CSS to under 64 KB.
400invalid_sender_nameemail_sender_name exceeds 100 characters.Shorten the sender name.
400invalid_sender_emailemail_sender_address is not a valid email.Use a valid email.
400invalid_display_namedisplay_name exceeds 100 characters, or beneficiary display_name is missing/invalid.Use a shorter / valid display name.
400invalid_support_emailsupport_email is not a valid email.Use a valid email.
400invalid_statement_descriptorStatement descriptor failed validation (length / charset).Fix per the statement descriptor rules.
400invalid_scopescope query param on sandbox reset was not one of the allowed values.Use balances, all, delete-all, or full-reset.
400weak_passwordNew password is shorter than 12 characters.Use a password with at least 12 characters.
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, GET /v1/payouts/{id}/velocity-results, GET /v1/payouts/{id}/engine-verdict
404beneficiary_not_foundAll /v1/beneficiaries/{id}* reads/writes; instrument creation under a beneficiary
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_foundGET /v1/merchant, merchant security/users surfaces
404user_not_found/v1/users/me, /v1/users/{user_id}/*
404session_not_found/v1/users/me/sessions/{session_id}/revoke
404domain_not_found/v1/merchant/security/domains/{id}*
404invitation_not_found/v1/users/invitations/{id}*
404channel_not_found/v1/users/me/notifications/channels/{id}*, /v1/merchant/notifications/routes/{id}*
404rule_not_found/v1/velocity/rules/{id}*
404rfi_not_found/v1/rfis/{id}*
404document_not_found/v1/documents/{id}*
404application_not_found/v1/onboarding, /v1/onboarding/status/{token}, /v1/onboarding/submit
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.

Merchant onboarding

HTTPcodeMeaningRemediation
409merchant_existsPOST /v1/register was called with an email that already has an account.Direct the user to sign in instead.

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.
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.

Batches

HTTPcodeMeaningRemediation
409duplicate_fileA batch file with the same SHA-256 hash is already pending, validating, 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.

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.

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/....
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.

Settings — branding, security, notifications, preferences, users

These codes are returned by the merchant portal settings endpoints (/v1/merchant/branding, /v1/merchant/security, /v1/merchant/notifications, /v1/merchant/preferences, /v1/users/*).
HTTPcodeReturned by
422branding_update_failedPATCH /v1/merchant/branding
422branding_asset_save_failedPOST /v1/merchant/branding/assets/{kind}
422security_update_failedPATCH /v1/merchant/security
422domain_add_failedPOST /v1/merchant/security/domains (commonly: domain claimed by another organization)
422domain_verify_failedPOST /v1/merchant/security/domains/{id}/verify (DNS TXT not yet detected)
422domain_remove_failedDELETE /v1/merchant/security/domains/{id}
412merchant_workos_unlinkedMerchant is not linked to a WorkOS organization (precondition for SSO/dsync/portal-link routes).
422workos_org_not_configuredMerchant org is not configured (e.g. user-invite when the merchant has no WorkOS org).
503workos_integration_unavailableWorkOS integration is not configured or unreachable.
503storage_unavailableObject storage is not configured (branding asset / avatar uploads).
503mfa_service_unavailableMFA service is not configured (/v1/users/me/mfa/*).
503session_service_unavailableSession service is not configured (/v1/users/me/sessions*).
503password_service_unavailablePassword service is not configured (POST /v1/users/me/password).
503portal_link_failedPOST /v1/merchant/security/portal-link could not generate an Admin Portal link.
422notifications_update_failedGeneric notifications-settings update failure (/v1/merchant/notifications/global, /v1/merchant/notifications/beneficiary, /v1/users/me/notifications/preferences, etc.).
422notifications_route_create_failedPOST /v1/merchant/notifications/routes
422notifications_route_delete_failedDELETE /v1/merchant/notifications/routes/{id}
422preferences_update_failedPATCH /v1/merchant/preferences
422channel_create_failedPOST /v1/users/me/notifications/channels
422channel_update_failedPUT /v1/users/me/notifications/channels/{id}
422channel_delete_failedDELETE /v1/users/me/notifications/channels/{id}
422channel_test_failedPOST /v1/users/me/notifications/channels/{id}/test
422channel_verify_failedChannel verification step inside the test endpoint.
422profile_update_failedPUT /v1/users/me, PATCH /v1/merchant/profile
422profile_complete_failedPOST /v1/users/me/profile/complete (welcome flow).
422password_change_failedPOST /v1/users/me/password
422avatar_save_failedPOST /v1/users/me/avatar
422mfa_enroll_failedPOST /v1/users/me/mfa/enroll, GET /v1/users/me/mfa
422mfa_verify_failedPOST /v1/users/me/mfa/verify (challenge step)
422mfa_invalid_codePOST /v1/users/me/mfa/verify (code mismatch)
422session_list_failedGET /v1/users/me/sessions
422session_revoke_failedPOST /v1/users/me/sessions/{session_id}/revoke
422session_ownership_failedInternal scope check inside the session-revoke flow.
422invitation_failedPOST /v1/users/invite
422invitation_resend_failedPOST /v1/users/invitations/{id}/resend
422invitation_revoke_failedDELETE /v1/users/invitations/{id}
422user_role_update_failedPUT /v1/users/{user_id}/role
422user_deactivate_failedPOST /v1/users/{user_id}/deactivate
422user_activate_failedPOST /v1/users/{user_id}/activate
422apikey_create_failedPOST /v1/api-keys
422apikey_revoke_failedPOST /v1/api-keys/{id}/revoke

Onboarding and compliance

HTTPcodeReturned by
409application_lockedTried to edit an onboarding application that is no longer in draft or info_needed.
422registration_failedPOST /v1/register failure not covered by merchant_exists.
422application_update_failedPUT /v1/onboarding
422application_submit_failedPOST /v1/onboarding/submit
422rfi_respond_failedPOST /v1/rfis/{id}/respond
422document_upload_failedDocument upload (multipart or metadata-only) failure.
422document_download_failedGET /v1/documents/{id}/download could not generate a presigned URL.

Velocity (merchant-scoped rules)

HTTPcodeReturned by
422rule_create_failedPOST /v1/velocity/rules
422rule_update_failedPATCH /v1/velocity/rules/{id}
422rule_delete_failedDELETE /v1/velocity/rules/{id}
422simulation_failedPOST /v1/velocity/simulate

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_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.
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

const res = await fetch("https://api.antonpayments.dev/v1/payouts", {
  method: "POST",
  headers: {
    Authorization: `Bearer ${apiKey}`,
    "Content-Type": "application/json",
    "Idempotency-Key": idempotencyKey,
  },
  body: JSON.stringify(payout),
});

if (!res.ok) {
  const { error } = await res.json();
  const requestId = res.headers.get("X-Request-ID");

  switch (error.code) {
    case "validation_error":
      // error.details is an array of { field, message }
      break;
    case "insufficient_balance":
      // prompt user to top up
      break;
    case "velocity_blocked":
      // inspect /velocity-results
      break;
    case "rate_limit_exceeded": {
      const retryAfter = Number(res.headers.get("Retry-After") ?? 1);
      await new Promise((r) => setTimeout(r, retryAfter * 1000));
      break;
    }
    case "idempotency_conflict":
      // generate a new key; do not reuse the old one
      break;
    default:
      if (res.status >= 500) {
        // retry with backoff; include requestId in logs
      }
  }
}
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.