> ## Documentation Index
> Fetch the complete documentation index at: https://docs.gethevn.com/llms.txt
> Use this file to discover all available pages before exploring further.

> ## Agent Instructions
> HEVN accounts are non-custodial in both products. Self-serve (the General tab): the person who owns the email holds the only key and signs for themselves. Whitelabel (the Whitelabel and REST API tabs): the integrator's customer creates and owns their account through Sign in with HEVN, a HEVN-hosted popup on the integrator's site; the integrator's backend holds a developer key, operates linked accounts and books their payouts, and the account holder approves every payout in that popup. The developer key signs only for the integrator's own wallet; it never signs a client's money.
> Use HEVN's terms exactly. A rail is a bank route a client can request. A virtual account (bnk_) is an opened rail. Account details are what a payer needs in order to wire money, and arrive in a field named requisites. A client is a customer's own account linked to the integrator through Sign in with HEVN (POST /dapi/v1/connect/sessions, then POST /dapi/v1/clients/link); there is no API call that creates one. The business behind it is the integrator's customer. A contact is a saved destination; a beneficiary is the party at the far end of a fiat payout. Every departure of money is a payout, to a bank account or to a wallet alike.
> Never name a banking partner. Say 'a licensed banking partner'.
> There are no published fee rates, spreads or minimums. Prices come from a quote on a specific payment; direct pricing questions to HEVN rather than inventing numbers.
> The whitelabel API is at /dapi/v1 and takes a developer-key session plus an X-Hevn-Account header. Developer keys have an IP allowlist and no scopes. Remote transfers are at /api/v1/remote_spend_limit and take a remote transfer key (rsl_) in X-Api-Key, bounded by an onchain spend permission the account holder grants. They are different credentials and neither works on the other surface. The HEVN CLI is discontinued.

# Errors

> One envelope and every error slug, grouped by resource, with what to do about each.

Every refusal from `/dapi/v1` has the same body. `code` is a stable lowercase slug — branch on it, and never on `message`.

```json theme={null}
{
  "error": {
    "code": "already_funded",
    "message": "This payment was already funded.",
    "details": { "payoutId": "po_4c8a…", "transactionHash": "0x4f1c…" }
  }
}
```

| Key | Contract |
| - | - |
| `code` | One of the slugs below. Stable across releases. Lowercase, `snake_case`, never a class name. |
| `message` | One sentence, safe to log or show an operator. Rewritten whenever it can be made clearer — do not parse it. |
| `details` | camelCase, and present only when there is something to carry: the id the key already owns, the fields that failed, the amount available. On a `5xx` it also carries `requestId`. |

Every response — success or failure — carries an `X-Request-ID` header. Log it on anything you cannot resolve yourself; it is the first thing support asks for.

## Field errors

A `422 validation_failed` names every field that failed, in wire spelling, with the validator's own code:

```json theme={null}
{
  "error": {
    "code": "validation_failed",
    "message": "The request body is invalid.",
    "details": {
      "fields": [
        { "field": "people[0].taxIdCountry", "code": "missing", "message": "Field required" },
        { "field": "maxAmount", "code": "amount_precision", "message": "USDC allows 6 decimals." }
      ]
    }
  }
}
```

`details.location` appears when the failure is not in the body — `query`, `path` or `header`. Unknown keys are rejected rather than ignored, so a typo in a field name is a `422` with `code: "extra_forbidden"`, not a silently dropped value.

## Status codes

| Status | What it means for your code |
| - | - |
| `400` | The request could not be read: a malformed id, cursor or header. Fix the call. |
| `401` | No session, or an expired one. Refresh the access token and retry once. |
| `403` | The session is real but may not do this. Never retry unchanged. |
| `404` | No such resource — including a resource that exists but is not yours. |
| `405` | Wrong method on a real path. |
| `409` | The resource's state refuses this request. Read the slug: some are success in disguise (`already_funded`), some ask you to wait (`funding_in_progress`), most ask you to re-read and choose again. |
| `410` | The thing you are pointing at expired. Create a new one. |
| `413` | The upload is too large. |
| `422` | The body parsed but a value is wrong, or a precondition on the data is unmet. |
| `429` | Rate limited. Sleep for `Retry-After` seconds, then retry. See [Limits](https://docs.gethevn.com/whitelabel/reference/limits.md#rate-limits). |
| `500` | Our fault. `details.requestId` identifies the failure. |
| `501` | A mode that exists in the schema but is not enabled here. |
| `503` | A partner, the chain infrastructure (bundler, paymaster) or the database is temporarily unavailable. Retry with backoff; retrying a confirm is safe. |

No refusal is a `502`. A `502` with an HTML body comes from the network in front of the API, not from HEVN: it carries no slug, so treat it like a timeout.

## Request and transport

| Slug | Status | What to do |
| - | - | - |
| `invalid_request` | 400 | The request is not valid in a way no single field covers. `message` says what. |
| `validation_failed` | 422 | Fix the fields in `details.fields`. |
| `invalid_cursor` | 400 | The cursor belongs to another query or another sort order. Restart the walk with no `cursor`. |
| `invalid_id` | 400 | A body or query field carries an id from another namespace. `details.expectedPrefix` names the right one. |
| `idempotency_key_invalid` | 400 | The header is outside `^[A-Za-z0-9._:-]{1,128}$`. |
| `idempotency_key_reused` | 409 | The same key already names different terms. `details` carries the id it owns (`payoutId`, `escrowId`). Replay the exact body, or use a new key. |
| `unauthenticated` | 401 | Send `Authorization: Bearer <accessToken>`. |
| `token_expired` | 401 | Re-mint with `POST /dapi/v1/auth/refresh` and retry once. |
| `forbidden` | 403 | This session may not perform this operation. On `/dapi/v1` it is also what a non-platform token, a deleted developer key and an off-allowlist source IP answer — see [Developer keys](#developer-keys). |
| `not_found` | 404 | No such resource. |
| `method_not_allowed` | 405 | Wrong verb for this path. |
| `conflict` | 409 | A state conflict with no narrower slug. Read `message`, re-read the resource. |
| `gone` | 410 | The resource is no longer available. |
| `payload_too_large` | 413 | Send a smaller upload. |
| `rate_limited` | 429 | Sleep `Retry-After` seconds, then retry. |
| `provider_unavailable` | 503 | A partner is down or the sandbox faucet account cannot pay. Retry with backoff. |
| `database_unavailable` | 503 | Retry with backoff. |
| `internal_error` | 500 | Log `details.requestId` and contact support. |

## Authentication and signing

| Slug | Status | What to do |
| - | - | - |
| `invalid_credentials` | 401 | The email, the key or the proof did not check out. One code for all three, deliberately. |
| `request_expired` | 400 | `requestExpiry` is in the past or more than five minutes out. Sign a fresh proof. |
| `challenge_not_found` | 404 | Unknown `challengeId`. Start a new challenge. |
| `challenge_consumed` | 409 | A challenge is single-use. Start a new one. |
| `challenge_expired` | 410 | Start a new challenge and sign it promptly. |
| `signature_invalid` | 400 | The signature does not verify against the stored payload. Nothing was consumed — sign the decoded payload bytes and try again. |
| `key_not_registered` | 403 | This public key is not one of the account's developer keys. See [Developer key](https://docs.gethevn.com/whitelabel/developer-key.md). |
| `signer_not_attached` | 409 | The developer signer is not attached to the account's wallet. |
| `wallet_not_linked` | 409 | The account has no wallet yet. For a client, wait for `status: "ready"`. |
| `approval_consumed` | 409 | This approval already signed something. Prepare the operation again. |
| `approval_expired` | 410 | The approval's window closed. Prepare again and sign the new payload. |
| `approval_mismatch` | 409 | The approval does not belong to this operation. Do not reuse a payload across resources. |
| `signing_policy_refused` | 403 | The signing policy refused the operation. Take it to support with `X-Request-ID`. |
| `login_unavailable` | 503 | Login is temporarily unavailable. Retry with backoff. |
| `login_mode_unavailable` | 501 | This login mode is not enabled here. `details.use` names the one that is. |

## Developer keys

A platform session is established by one developer key, and every `/dapi/v1` request re-reads it: the key must still
exist, and the request must arrive from an address inside the key's allowlist. Both checks are repeated on `POST /dapi/v1/auth/refresh` and again immediately before HEVN co-signs, so
deleting a key stops the sessions it already minted rather than waiting for them to expire.

| Slug | Status | What to do |
| - | - | - |
| `unauthenticated` | 401 | No bearer, or one that does not verify. `/dapi/v1` takes a **platform** access token and nothing else. |
| `forbidden` | 403 | With `details.reason = "developerSessionRequired"`: the token is an ordinary API token, a `platform-refresh` token on a business route, or a platform token with no `developer_key_id`. Log in through `POST /dapi/v1/auth/challenge` and `POST /dapi/v1/auth/token`. |
| `forbidden` | 403 | With no `details`: the key behind the session was deleted, or the call left an address outside its allowlist. Resume from an allowlisted host, or create a key that covers this one. |
| `key_not_registered` | 403 | The public key you named in a signature is not one of this account's keys. |
| `invalid_credentials` | 401 | At login only. A deleted key, an off-allowlist IP, an unknown email and a bad signature are all one code, deliberately. |
| `developer_key_not_found` | 404 | A `dk_…` id that is not yours was given to the key-management routes. |
| `developer_key_already_exists` | 409 | That public key already belongs to a key on this account. Keys are immutable and additive — create a second one and delete the first. |

The last two come from the key-management routes under `/api/v1`, which take an ordinary owner session and carry the
ordinary API's own error shape. Your integration never calls them: keys are created and deleted in the HEVN app. They
are deliberately absent from `/dapi/v1`.

## Acting as a client

| Slug | Status | What to do |
| - | - | - |
| `account_header_invalid` | 400 | `X-Hevn-Account` is not a `cl_…` id. |
| `account_scope_conflict` | 400 | The header was sent to a self-scoped route — `POST /clients/link`, `GET /clients`, `POST /connect/sessions` or any escrow route — or the token itself is delegated to one client. `details.reason` is `selfScopedRoute` in both cases. |
| `account_not_found` | 404 | Unknown client, or a client that is not yours. The two are indistinguishable by design. |
| `account_forbidden` | 403 | The client exists but cannot be acted for right now — typically a paused integrator profile. |
| `account_read_only` | 403 | This credential may read the client but not write. `details.credential` names it. |
| `account_not_controlled` | 403 | This session does not control the account's wallet, so it cannot sign for it: a confirm on a client's payout, a swap for a client, or an escrow deal whose buyer is a linked client. A client's holder signs in the [sign-in window](https://docs.gethevn.com/whitelabel/sign-in-with-hevn.md#approve-a-payout). |

## Clients and the sign-in window

| Slug | Status | What to do |
| - | - | - |
| `client_creation_not_enabled` | 403 | Your integrator account is not active, or may not link clients. HEVN switches it on; nothing in the API does. |
| `redirect_uri_not_allowed` | 403 | The `redirectUri` of a sign-in window session is not on an origin under **Allowed sites**. Add it in the app's **Sign-in window** card. |
| `payout_not_awaiting_signature` | 409 | A `sign` session for a payout that no longer waits for its holder. Read the payout. |
| `link_code_invalid` | 400 | The code from the sign-in window is malformed, belongs to another integrator, or its account is gone. Start a new sign-in. |
| `link_code_expired` | 410 | The code is older than five minutes. Start a new sign-in. |
| `client_already_linked` | 409 | The account is already connected to another integrator, or can no longer be connected to you. |
| `client_not_found` | 404 | Unknown client id, or not one of yours. |
| `verification_not_applicable` | 409 | A `verify` sign-in window session for a business client. A business verifies through `PUT /dapi/v1/client/kyb`. |
| `profile_locked` | 409 | The field is frozen after verification. `details.fields` names them. |

The sign-in window's own refusals — `access_denied`, `already_registered`, `already_linked`, `not_found`, `unsupported`, `expired` and `failed` — are not error slugs. They arrive as `error` on your callback page; [Sign in with HEVN](https://docs.gethevn.com/whitelabel/sign-in-with-hevn.md#errors) has what each one means.

## Rails and payins

| Slug | Status | What to do |
| - | - | - |
| `rail_not_found` | 404 | No such virtual account — a `bnk_…` id that is unknown or not this client's. |
| `rail_not_available` | 403 | Nothing can serve this product for this client — its residency, its type or no partner in that region. `details.product` names it. What still blocks a product that *is* available is never a refusal: it is `blockers[]` on the record ([Virtual accounts](https://docs.gethevn.com/whitelabel/virtual-accounts.md#what-still-blocks-it)). |
| `payin_not_available` | 422 | This client cannot be paid in that currency over that method. `details.options` lists the pairs that work. |
| `payin_expired` | 410 | The price no longer holds. Quote the payin again. |
| `payin_not_found` | 404 | Unknown payin id, or one belonging to another client. |
| `transaction_not_found` | 404 | Unknown transaction id for this account. |

## Payouts

Booking a payout — to a bank account or to a wallet — and the contacts it pays. The refusals that arrive at `confirm` are in [Funding a payment](#funding-a-payment) below.

| Slug | Status | What to do |
| - | - | - |
| `contact_details_invalid` | 422 | The contact's account details or address are incomplete for the method, at create or update time. `details.fields` names them. |
| `contact_in_use` | 409 | A payout still references this contact. It cannot be deleted. |
| `transfer_policy_denied` | 403 | The account's own transfer policy refuses this payout — for example, the holder has paused outgoing transfers. `message` says which rule. Nothing to retry until the holder changes it. |
| `contact_not_payable` | 409 | The contact cannot be paid on this rail. `details.blockers` is the same array `capabilities` returns — the codes are listed under [Contact blockers](#contact-blockers). |
| `contact_payment_details_invalid` | 422 | The partner refused the contact's payment details — most often a beneficiary name the bank does not recognise. Correct the contact and book again. Not to be confused with `contact_details_invalid` above, which is our own refusal of an incomplete contact body. |
| `amount_below_minimum` | 422 | Below the route's minimum. `POST /dapi/v1/payouts/preview` reports it as `minAmount`. |
| `amount_above_maximum` | 422 | Above the route's maximum. |
| `amount_precision_unsupported` | 422 | More decimals than the asset allows. Amounts are refused, never rounded. |
| `account_unavailable` | 422 | The client holds no such account. A client with no custodial virtual account cannot name `FDIC_USD`. |
| `account_not_supported` | 422 | The account cannot be used for this operation: a payin cannot settle into `FDIC_USD`, and a payout from it needs the rail that holds it. |
| `payout_not_found` | 404 | Unknown payout id for this client. |
| `payout_not_fundable` | 409 | This payout can no longer be funded. Book a new one. With `details.reason = "holder_signature_unsupported"`, a client's holder cannot approve this kind of payout in the sign-in window — a `FDIC_USD` payout, for now. |
| `quote_expired` | 409 | The price expired. Book again with the same `Idempotency-Key` to get a fresh one. |
| `swap_not_found` | 404 | Unknown swap id for this client. |
| `swap_route_unavailable` | 422 | Nothing can move money between these two accounts right now. The sandbox answers it for every swap: it has no exchange venue. |
| `swap_not_confirmable` | 409 | The swap is not waiting for a signature. Read it before confirming again. |
| `intent_not_signable` | 409 | This quote is no longer authorized by a signature — it was already spent, or it is funded by a debit. |
| `bundler_rejected` | 503 | The chain rejected the operation. Confirm again with backoff. |
| `bundler_unavailable` | 503 | Chain infrastructure is unreachable. Confirm again with backoff. |

Four more slugs are in the published enum and never reach you. `contact_requires_quote` and
`direct_transfer_unsupported` are both folded into `409 contact_not_payable` before the response is written.
`contact_chain_unsupported` and `contact_token_unsupported` have no raise site at all: a contact off Base, or
expecting another token, is [routed cross-chain](https://docs.gethevn.com/whitelabel/payouts.md#a-routed-payout-is-priced) rather than
refused, and an account without routing gets `409 payout_not_fundable`. Generated clients will carry all four;
branch on `contact_not_payable`.

## Contact blockers

`GET /dapi/v1/contacts/{contactId}/capabilities` answers a `blockers[]` array rather than a refusal, and
`409 contact_not_payable` carries the same array as `details.blockers`. Each entry has a `code`, a `message` safe to
show an operator, and sometimes the `field` it is about. These codes are their own vocabulary — they are not error
slugs, and they never appear as `error.code`.

| Code | What it means |
| - | - |
| `contact_details_missing` | The contact has no payment account for this method yet. |
| `contact_address_incomplete` | The method needs the beneficiary's address and part of it is missing. |
| `contact_country_restricted` | The partner does not pay this beneficiary country. |
| `contact_holder_type_unsupported` | The partner does not pay this holder type — a company where only people are paid, or the reverse. |
| `account_type_unsupported` | The partner cannot pay an account of this shape at all. |
| `routing_unresolved` | The identifiers do not resolve to a route the partner recognises. |
| `receiving_account_unavailable` | The client has no active account to send from. Request a virtual account first. |
| `payment_method_unavailable` | None of the client's accounts sends over this method. |
| `currency_mismatch` | The account holds one currency and the transfer is in another. |
| `transfer_feature_disabled` | This transfer kind is switched off on the sending account. |
| `transfer_feature_on_request` | This transfer kind exists but must be opened for the account first. |
| `channel_not_configured` | The channel that would carry it is not configured here. |
| `sender_residency_ineligible` | The sending client's residency rules this route out. |
| `sender_category_restricted` | The sending client's business category rules this route out. |
| `endorsement_required` | The route needs an endorsement the sending client does not hold. |
| `channel_unavailable` | The partner reports the channel as unavailable right now. |
| `amount_below_minimum` | The amount you asked about is under the route's minimum. |
| `amount_above_maximum` | The amount you asked about is over the route's maximum. |

## Funding a payment

These arrive at `POST /dapi/v1/payouts/{payoutId}/confirm`; two of them — `quote_not_submitted` and `payout_not_fundable` — also answer the sandbox settlement routes, where they mean the same thing. The decision table for the four that branch is on [Payouts](https://docs.gethevn.com/whitelabel/payouts.md#when-a-payout-is-refused).

| Slug | Status | What to do |
| - | - | - |
| `already_funded` | 409 | **Success.** The money moved; `details.transactionHash` proves it. Never re-open. |
| `funding_in_progress` | 409 | An attempt is still in flight. Wait, then confirm again. |
| `funding_attempt_expired` | 409 | The approval expired unsigned. Re-open with the same `Idempotency-Key` and sign the new payload. |
| `payment_slot_consumed` | 409 | This payout's on-chain slot is spent — an operation for it landed, including one that reverted. Nothing you sign for it can move money. Book a new payout with a new key. |
| `insufficient_funds` | 409 | The client's balance does not cover the debit. `details.available` and `details.required` are atomic units. On an escrow action, `details.party` says whose wallet is short — `payer` for `authorize` and `charge`, `operator` (yours) for `refund` — with `token`, `balanceAtomic` and `amountAtomic`. |
| `contact_changed` | 409 | The contact was edited after the payout was booked. Book again. |
| `contact_not_found` | 404 | The contact was deleted. |
| `quote_not_submitted` | 409 | The payout was never booked with the partner. Book it again. |
| `quote_not_fundable` | 409 | The booking is no longer fundable. Request a new one. |
| `funding_mode_unsupported` | 409 | This route cannot be funded with a developer key. |
| `funding_token_unsupported` | 409 | The payout's `sourceAccount` is not one a developer key can fund on this route. |
| `smart_wallet_not_deployed` | 409 | The client's wallet is not on chain yet. Wait for `status: "ready"`. |
| `wallet_owner_unverified` | 409 | The wallet's owner set does not yet include the signer. Contact support. |
| `user_operation_rejected` | 503 | The bundler rejected the operation. Confirm again. |
| `user_operation_unavailable` | 503 | The bundler or paymaster is unavailable. `details.reason` carries its answer. Confirm again with backoff. |

## KYB and documents

| Slug | Status | What to do |
| - | - | - |
| `kyb_incomplete` | 422 | The submission was completed with gaps. `GET /dapi/v1/client/kyb` lists `missing[]`. |
| `kyb_documents_processing` | 409 | Some uploaded documents are still being read. `details.documents` lists their ids. Complete again in a few seconds. |
| `kyb_roster_conflict` | 409 | The people on file conflict with this write — usually a person removed while still owning shares. |
| `kyb_subject_conflict` | 409 | The subject conflicts with the verification data already on file. |
| `document_not_found` | 404 | Unknown `doc_…` id for this client. |
| `document_type_unsupported` | 422 | That slot does not accept this document type. |
| `invalid_enum_value` | 422 | The value is not one of the accepted ones. See [KYB fields](https://docs.gethevn.com/whitelabel/reference/kyb-fields.md). |
| `entity_not_found` | 404 | Unknown person or company in the verification data. |

## Escrow

| Slug | Status | What to do |
| - | - | - |
| `escrow_not_found` | 404 | Unknown deal id, or a deal you do not operate. |
| `escrow_action_not_found` | 404 | No action on this deal carries that idempotency key. The confirm URL takes the **raw** key you sent to `POST /actions`. |
| `escrow_not_operated_by_you` | 403 | You are a party to the deal but not its operator. |
| `escrow_state_changed` | 409 | The deal moved since the action was prepared: nothing is capturable, the payment was already collected, or the approval already landed. Read the deal and pick from `availableActions`. |
| `action_in_flight` | 409 | One signing wallet runs one action at a time, across every deal. The refusal carries no `details`, so track the deal you left in flight yourself, sync it, then retry. |
| `simulation_failed` | 409 | The action would revert on chain, or the gas sponsor refused it. `details.reason` carries the chain's own answer. Check balances and windows, then prepare again. |
| `stale_nonce` | 409 | The wallet's nonce moved between prepare and confirm. Prepare again with the same key. |
| `amount_not_allowed` | 409 | `void` takes no amount. |
| `amount_out_of_range` | 422 | The amount is outside what the deal allows. An over-large `capture` or `authorize` currently arrives as a bare `400 invalid_request` instead, with no `details` and the generic message. |
| `window_closed` | 409 | The action's window has passed. A closed window currently arrives as a bare `400 invalid_request` instead — handle both the same way: read the deal, pick from `availableActions`. |
| `token_not_supported` | 422 | Escrow settles in `USDC` or `EURC`, by symbol. |
| `receiver_not_a_client` | 422 | Both parties must be clients linked to you through Sign in with HEVN. An account you only referred is never an escrow party. |
| `conflict` | 409 | A state conflict with no narrower slug — on escrow it is what an unclassified deal error becomes. Read `message`, re-read the deal. An approval that closed is `410 approval_expired`, one already used is `409 approval_consumed`. |
| `bundler_rejected`, `bundler_unavailable` | 503 | Chain infrastructure. Confirm again with backoff. `bundler_unavailable` carries `details.reason`. |

## Sandbox

Only the sandbox answers these. Each one names the limit in `message` and `details`; see [Sandbox limits](https://docs.gethevn.com/whitelabel/sandbox.md#limits).

| Slug | Status | What to do |
| - | - | - |
| `sandbox_amount_limit_exceeded` | 422 | A payout, escrow, payin credit or faucet credit above the sandbox cap. `details.maximum` is the cap and `details.operation` names which one. |
| `sandbox_faucet_balance_too_high` | 409 | The faucet only tops up a wallet holding less than `details.threshold` of the token. Spend what the client has first. |

## Handling a refusal in code

Write the envelope into a typed error once, at the bottom of your HTTP layer: read `error.code`, keep `error.details`, honour `Retry-After` on a `429`, re-mint the access token once on a `401`, and log `details.requestId` on anything `5xx`. Every example in these guides calls that one helper, and it is written out in full on [HTTP client](https://docs.gethevn.com/whitelabel/reference/client.md).

> [card] [Next: Limits](https://docs.gethevn.com/whitelabel/reference/limits.md)
>
> Every number that can refuse a request: rate limits, expiry windows, page sizes, sandbox caps.


This documentation is built and hosted on [Mintlify](https://mintlify.com), a developer documentation platform.