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

# Conventions

> The seven rules that hold for every operation: money, ids, casing, idempotency, pagination, status codes and errors.

Seven rules hold for every operation in this API. Read them once and no endpoint will surprise you.

| Rule | In one line |
| - | - |
| [Money](#money) | A decimal string in the asset's major unit, never a number |
| [Ids](#ids) | Prefixed and opaque: `cl_`, `po_`, `pi_`, `txn_`, `ct_`, `bnk_`, `doc_`, `kyb_`, `esc_`, `dk_` |
| [Casing](#casing) | camelCase everywhere, unknown keys refused, null fields omitted |
| [`Idempotency-Key`](#idempotency-key) | Accepted on every write, required nowhere |
| [Pagination](#pagination) | A cursor and a limit; no offset and no total |
| [Status codes](#status-codes) | `201` creates, `200` replays, `202` polls, and every response has a body |
| [Errors](#errors) | One envelope; branch on `code`, never on `message` |

## Money

Every amount on the wire is a **decimal string in the asset's major unit**: `"500.00"`, `"0.9247"`,
`"1204.500000"`. Not a JSON number, not an atomic-unit integer, never exponent notation.

Precision is refused, never rounded. More fractional digits than the asset allows answers
`422 validation_failed` with `details.fields[0].code = "amount_precision"` — so a rounding error can
never reach a payment. Rates and fees follow the same rule; `feeBps` is the one exception and is an
integer number of basis points.

Where an operation has two sides, you pin one and HEVN derives the other. Send `amount` to pin what
leaves the balance or `amountTo` to pin what the beneficiary receives — one of them, never both.

> [!WARNING]
>
> A decimal arrives in JavaScript and Go as a **string**. A `typeof x === "number"` guard silently
> skips every amount this API returns; parse with a decimal type, not a float.

## Ids

Every id is prefixed, opaque and safe to store as text:

| Prefix | Resource | Prefix | Resource |
| - | - | - | - |
| `cl_` | client (and your own account) | `bnk_` | bank record of an open rail |
| `po_` | payout, over a bank rail or on chain | `doc_` | document |
| `pi_` | payin | `kyb_` | KYB application |
| `txn_` | transaction | `esc_` | escrow deal |
| `ct_` | contact | `dk_` | developer key |

A `dk_…` is the one id you never send to this API: it names a developer key, which is created and
deleted in the HEVN app ([Developer key](https://docs.gethevn.com/whitelabel/developer-key.md)).

Do not parse them, derive them or assume a length. An id of the wrong kind in a body or query field
is `400 invalid_id` with the expected prefix in `details`; in a path it is that resource's own `404`,
so an id from another namespace is never confirmable.

Products — `eu`, `us`, `uk` — are the exception that proves the rule: they are fixed region codes, not
ids, and they key a client's virtual account in `POST /dapi/v1/banks` and in the path. The `rail`
string next to them is informational and never a key. See [Rails](https://docs.gethevn.com/whitelabel/reference/rails.md).

## Casing

**camelCase everywhere**: request bodies, query parameters, response fields and the keys inside
`error.details`. Two things are deliberately not field names. An `error.code` slug is always
lowercase `snake_case`. An enum member usually is too — `business_revenue` — but an enum member is a
literal, not a naming convention: `HighRiskActivity` members are SCREAMING\_SNAKE\_CASE
(`MONEY_SERVICES`, `PRECIOUS_METALS_STONES_JEWELRY`), and `FiatTransactionCount` members are the
ranges they read as (`<10`, `10-50`, `50-200`, `200-1,000`, `1,000+`, punctuation included). Send and
compare enum values exactly as the spec prints them, and never case-fold them.

Two consequences worth knowing before your first `422`:

* **Unknown keys are refused.** A body with a field this version does not define answers
  `422 validation_failed` with `code: "extra_forbidden"` rather than ignoring it, so a typo cannot
  silently drop a payment reference. The two nested objects that predate this rule — a contact's
  `address` and a requisite's `bank`/`payer` block — still ignore a key they do not know, so a
  misspelt `streetAddress` there resurfaces one step later as `422 contact_details_invalid`
  naming the field it never received.
* **Null fields are omitted.** "Absent" and "null" are the same thing; a response never prints
  `"phone": null`. Treat a missing key as unset.

One corner is not camelCase and cannot be: the keys inside `fields` on
`PUT /dapi/v1/documents/{documentId}/content` are the extractor's own field names —
`registration_number`, `address.street_address` — because they are values read off a page rather
than fields of this API. They are the keys the refusal that asks for them already named. See
[Onboard a client](https://docs.gethevn.com/whitelabel/onboarding.md#fill-a-documents-fields-by-hand).

Timestamps are RFC 3339 in UTC with a `Z` suffix: `2026-09-17T10:04:11Z`.

## Idempotency-Key

Every write accepts `Idempotency-Key`, matching `^[A-Za-z0-9._:-]{1,128}$`. No operation requires it.

Send one whenever a retry must not become a second payment. Derive it from an id your own system
owns, and keep it identical across every retry of the same request.

Derive `Idempotency-Key` from an id your own system already owns, pass it in from the caller, and
keep it identical across every retry of the same payment.

```python Python theme={null}
key = f"payout-{invoice.id}"  # "payout-INV-2026-114"
northwind.post("/payouts", payout, idempotency_key=key)
```

```javascript Node theme={null}
const key = `payout-${invoice.id}`;
await northwind.post("/payouts", payout, { idempotencyKey: key });
```

```go Go theme={null}
key := "payout-" + invoice.ID
northwind.Post("/payouts", payout, hevn.IdempotencyKey(key))
```

Never generate the key inside the helper that sends the request, and never rotate it on a retry —
both turn one payment into two. The format and the replay windows are in
[Conventions](https://docs.gethevn.com/whitelabel/conventions.md#idempotency-key).

When you omit it, HEVN derives a fingerprint from the request itself, answers with it as
`idempotencyKey`, and treats an identical repeat as a replay. The difference is the window: a key you
chose anchors the request for **7 days**, a derived one for **2 hours** — long enough for a retry,
short enough that two genuinely separate payments of the same amount both happen.

A key is scoped to your account and the operation, so two integrators can never collide and the same
key on `POST /payouts` and `POST /contacts` is not a replay. Reusing one with a different body is
`409 idempotency_key_reused`, and `details` names the resource the key already belongs to. A
malformed one is `400 idempotency_key_invalid`.

## Why a retry cannot pay twice

Deduplication happens twice, and the two layers answer different questions.

**Your key deduplicates the request.** `Idempotency-Key` decides whether two HTTP calls are the same
intent. It is what makes a lost response safe to retry: the second `POST /dapi/v1/payouts` answers
`200` with `Idempotency-Replayed: true` and the payout the first one booked, rather than booking a
second one. This layer is ours, it lives in our database, and it is the one you control.

**The chain deduplicates the money.** Every payout owns exactly one slot in its client's smart
wallet — an ERC-4337 nonce derived from the payout itself, not a counter — and a slot can be spent
once. Two signatures for one payout are not two payments: the first operation to land takes the slot,
and the second is rejected by the network itself. You can re-open an expired approval as often as you
like, sign it twice, or race two processes that both retry; there is one slot, so there is one
payment.

That is why the refusals read the way they do:

| You see | What actually happened |
| - | - |
| `Idempotency-Replayed: true` | The request was a duplicate. Nothing new was booked. |
| `409 already_funded` | The slot was spent by an operation that settled. `details.transactionHash` is the proof. |
| `409 payment_slot_consumed` | The slot was spent by an operation that landed and reverted. Nothing you sign for this payout can move money; book a new one. |
| `409 funding_in_progress` | An operation is signed and in flight. Confirm again — that is what resolves it. |

The practical rule stays small: keep the key identical across every retry of one payment, confirm
until you get a `200`, and never invent a new key to "get past" a refusal. A new key is a new
payment, a new slot and a second debit — the one thing neither layer can undo.

The same mechanism covers escrow actions and money the client's own app sends: an operation prepared
by your integration and one prepared in the app for the same payment compete for the same slot
instead of both landing.

## Pagination

Every list is a cursor page:

```json theme={null}
{ "items": [ … ], "nextCursor": "eyJ2IjoiMjAyNi0wOS0xN1QxMDowNDoxMVoiLCJpIjoi…" }
```

Pass `limit` (1 to 100, default 50) and `cursor`. Pass back the `nextCursor` you were given, verbatim
and unparsed; when it is absent you have reached the end. There is no `offset` and no total count
anywhere, and a cursor that does not belong to the query it is sent to is `400 invalid_cursor`.

Aggregates are their own route rather than a field on page one —
`GET /dapi/v1/transactions/summary` takes the same filters as `GET /dapi/v1/transactions`.

## Status codes

| Code | When | What to read |
| - | - | - |
| `200` | A read, an update, or a **replay** of a create | `Idempotency-Replayed: true` marks the replay |
| `201` | A resource now exists | `Location` points at it |
| `202` | Accepted, still settling | `status` and `pollUrl` in the body |
| `4xx` / `5xx` | Refused | the error envelope below |

There is no `204`: every response has a body. `Location` and `pollUrl` are paths from the host root
and already include the `/dapi/v1` prefix. One of them names no resource: a client's `pollUrl` is
literally `/dapi/v1/client`, so poll it with the `cl_…` you were just given in `X-Hevn-Account` —
the URL alone identifies nothing.

## Errors

One envelope, for every refusal, on every route:

```json theme={null}
{
  "error": {
    "code": "insufficient_funds",
    "message": "The account's USDC balance does not cover this transfer.",
    "details": { "available": "3000000", "required": "5000000" }
  }
}
```

`code` is a stable lowercase slug — branch on it. `message` is written for a human reading a log and
may be reworded at any time. `details` is optional, camelCased, and carries the fields that make the
refusal actionable: `blockers` on a rail, `fields` on a validation failure, `transactionHash` on a
payment that already went through, `requestId` on a `5xx` — quote that one to support.

> [!WARNING]
>
> `details` is diagnostic, not part of the money contract: the funding refusals report
> `available` and `required` in **atomic units** (`"3000000"` is 3 USDC), which is the one place an
> amount is not a major-unit decimal string. Read them for a log line, not for arithmetic.

Field-level failures collapse into one shape, so you can render them next to your form inputs:

```json theme={null}
{ "error": { "code": "validation_failed", "message": "The request body is invalid.",
  "details": { "fields": [ { "field": "phone", "code": "string_pattern_mismatch", "message": "Use E.164, for example +13125550142." } ] } } }
```

The whole slug registry, grouped by resource, is in [Errors](https://docs.gethevn.com/whitelabel/reference/errors.md); the
numbers behind every limit are in [Limits](https://docs.gethevn.com/whitelabel/reference/limits.md).

> [card] [Next: Signing](https://docs.gethevn.com/whitelabel/signing.md)
>
> Prepare, check, sign, confirm — the one mechanism behind every payment your key signs.


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