Error catalog
The exhaustive error-code catalog, grouped by category, each with its error type, HTTP status, and exactly when it fires.
This is the complete catalog of code values the public API
(api.acute.network/v1) can return. Each code is paired with the ApiError.type
it surfaces under, the HTTP status, and the condition that triggers it. For the
envelope and how to handle these, see the Errors guide.
Branch on code; the type groups it
type is the broad category for the failure (e.g. every 422 is
unprocessable_error, every 409 is conflict_error). The granular code is
the stable thing to branch on; it names the specific failure.
| HTTP status | error.type |
|---|---|
| 400 | validation_error |
| 401 | authentication_error |
| 403 | authorization_error |
| 404 | not_found_error |
| 409 | conflict_error |
| 422 | unprocessable_error |
| 429 | rate_limit_error |
| 500 | internal_error |
| 502 / 503 / 504 | internal_error |
Bearer acuinf_{test|live}_…. A bad or missing key never reaches a handler.
code | Type | Status | When |
|---|---|---|---|
API_KEY_MISSING | authentication_error | 401 | No Authorization: Bearer header on a non-public route. |
API_KEY_INVALID | authentication_error | 401 | The key doesn't resolve, is revoked, or is malformed. |
API_KEY_ENVIRONMENT_MISMATCH | authentication_error | 401 | The key's environment doesn't match this host (e.g. a live key sent to the sandbox host, or a test key sent to the live host). |
API_KEY_SCOPE_FORBIDDEN | authorization_error | 403 | The key is valid but lacks the required scope. details.requiredScopes / providedScopes are populated. |
The scope→endpoint map: wallet, payment, transfer, payout. See
Authentication.
The six money POSTs require an Idempotency-Key. See Idempotency.
code | Type | Status | When |
|---|---|---|---|
IDEMPOTENCY_KEY_MISSING | validation_error | 400 | A money POST was sent without an Idempotency-Key header. |
IDEMPOTENCY_KEY_REUSED | conflict_error | 409 | The key was already used with a different request body. |
IDEMPOTENCY_IN_PROGRESS | conflict_error | 409 | A request with this key is still running. Back off and retry the same key. |
code | Type | Status | When |
|---|---|---|---|
VALIDATION_FAILED | validation_error | 400 | The body/query/params failed validation. details.fields[] lists each bad field. |
INVALID_CURSOR | validation_error | 400 | The ?cursor= token is malformed or tampered. See Pagination. |
code | Type | Status | When |
|---|---|---|---|
WALLET_NOT_FOUND | not_found_error | 404 | No wallet matches the :id reference (under your org/environment). |
WALLET_EMAIL_TAKEN | conflict_error | 409 | An end-user wallet already exists for that email in this org. |
WALLET_KYC_REQUIRED | unprocessable_error | 422 | The action (fund/withdraw/transfer/balance) needs tier1 KYC, but the wallet is none. Submit KYC. |
WALLET_TIER1_LIMIT_EXCEEDED | unprocessable_error | 422 | The move would breach the tier-1 held-balance or per-transaction cap. |
WALLET_INSUFFICIENT_FUNDS | unprocessable_error | 422 | The source wallet's balance can't cover amount + fees. |
code | Type | Status | When |
|---|---|---|---|
TRANSFER_SELF | unprocessable_error | 422 | Source and destination wallet are the same. |
TRANSFER_NOT_FOUND | not_found_error | 404 | No transfer matches the reference. |
(WALLET_INSUFFICIENT_FUNDS also applies to transfers; the sender is
debited amount + fee.)
code | Type | Status | When |
|---|---|---|---|
WITHDRAWAL_NOT_FOUND | not_found_error | 404 | No withdrawal matches the reference. |
WITHDRAWAL_NAME_VERIFICATION_FAILED | unprocessable_error | 422 | The destination account name couldn't be verified at quote time. |
A withdrawal is created 201 then resolves asynchronously: a later failure on the
rail surfaces via the withdrawal.failed webhook (and failureReason), not a
synchronous error. The debited funds are restored. See
Withdrawals.
code | Type | Status | When |
|---|---|---|---|
PAYMENT_NOT_FOUND | not_found_error | 404 | No payment matches the reference. |
PAYMENT_ORG_NOT_PROVISIONED | unprocessable_error | 422 | The org's deposit account isn't provisioned yet (collections can't be created). Common in fresh test orgs before provisioning completes. |
PAYMENT_TARGET_WALLET_NOT_FOUND | not_found_error | 404 | The targetWalletId doesn't resolve to a wallet in this org. |
PAYMENT_NOT_PAYABLE | unprocessable_error | 422 | The payment can't be settled in its current status (already settled/expired/failed, or not a virtual_wallet payment). |
code | Type | Status | When |
|---|---|---|---|
PAYOUT_NOT_FOUND | not_found_error | 404 | No payout matches the reference. |
PAYOUT_EMPTY | validation_error / unprocessable_error | 400 / 422 | The batch has no items. |
A payout returns 202 Accepted (async) and processes item-by-item; partial
failures surface per item and via payout.partially_completed. See
Payouts.
code | Type | Status | When |
|---|---|---|---|
REFUND_NOT_FOUND | not_found_error | 404 | No refund matches the reference. |
REFUND_PAYMENT_NOT_SETTLED | unprocessable_error | 422 | The payment isn't in a refundable (settled / partially_refunded) state. |
REFUND_AMOUNT_EXCEEDS | unprocessable_error | 422 | The refund amount exceeds the settled, not-yet-refunded balance. |
These surface on the webhook-management surface (registering/redelivering), and the inbound-verification code applies when you verify a delivered signature.
code | Type | Status | When |
|---|---|---|---|
WEBHOOK_ENDPOINT_NOT_FOUND | not_found_error | 404 | No active endpoint configured for this org/environment (e.g. on a redelivery request). |
WEBHOOK_EVENT_NOT_FOUND | not_found_error | 404 | No webhook event matches the reference (e.g. redelivering an unknown event). |
WEBHOOK_SIGNATURE_INVALID | authentication_error / unprocessable_error | 401 / 422 | A signature failed verification. |
code | Type | Status | When |
|---|---|---|---|
RATE_LIMIT_EXCEEDED | rate_limit_error | 429 | More than 600 requests/minute for this API key (fixed window). |
The response includes a Retry-After header and details:
{
"success": false,
"statusCode": 429,
"error": {
"type": "rate_limit_error",
"code": "RATE_LIMIT_EXCEEDED",
"message": "Rate limit exceeded for this API key",
"details": { "limit": 600, "remaining": 0, "resetAt": "2026-06-24T09:42:00.000Z", "retryAfterSeconds": 37 }
},
"meta": { "requestId": "req_1a3c5e7b9d2f4a6c8b0e1d33" }
}These are platform-level codes. The ledger codes indicate an internal invariant was protected (a money move was refused rather than applied wrongly); the system codes are the generic fallbacks.
code | Type | Status | When |
|---|---|---|---|
LEDGER_TRANSACTION_UNBALANCED | unprocessable_error / internal_error | 422 / 500 | A posting's legs didn't sum to zero; refused rather than applied wrongly. |
LEDGER_INVALID_ENTRY | unprocessable_error / internal_error | 422 / 500 | A malformed ledger entry was rejected. |
LEDGER_ACCOUNT_NOT_FOUND | internal_error | 500 | A referenced ledger account doesn't exist. |
LEDGER_TRANSACTION_NOT_FOUND | not_found_error | 404 | A referenced ledger transaction doesn't exist. |
LEDGER_TRANSACTION_ALREADY_REVERSED | conflict_error | 409 | A ledger transaction was already reversed. |
UPSTREAM_ERROR | internal_error (upstream_unavailable) | 502 / 503 / 504 | An upstream provider errored. details.retryable is true. |
INTERNAL_ERROR | internal_error (internal) | 500 | An unexpected error on Acute's side. The real message stays in our logs; quote the requestId. details.retryable is false. |
When you hit a 5xx on a money POST
Retry with the same Idempotency-Key. A 500/503
is ambiguous about whether money moved: the replay returns the original outcome
instead of double-applying.
Every error carries meta.requestId (also the X-Request-Id header), formatted
req_<24 hex>. It pins the exact request in our logs. Include it in any support
request and we can trace the call end-to-end.