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

> ## Agent Instructions
> HEVN has two products and they authorize money differently. 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 backend holds a developer key, signs on behalf of client accounts it created, and HEVN co-signs. Never mix the two models in an answer.
> 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 an account the integrator created; 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. The CLI uses an app API key in X-Api-Key. They are different credentials and neither works on the other surface.

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

## 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](/whitelabel/developer-key)).

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.

Rail ids — `sepa_named-bank_a`, a stand-in for a real one — are the exception that proves the rule:
they are opaque strings you copy from `GET /dapi/v1/banks`, they carry no prefix, and their shape is
not a contract. See [Rails](/whitelabel/reference/rails).

## 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](/whitelabel/onboarding#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.

<CodeGroup>
  ```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))
  ```
</CodeGroup>

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](/whitelabel/conventions#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.
</Warning>

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](/whitelabel/reference/errors); the
numbers behind every limit are in [Limits](/whitelabel/reference/limits).

<Card title="Next: Signing" icon="pen-tool" href="/whitelabel/signing">
  Prepare, check, sign, confirm — the one mechanism behind every payment.
</Card>
