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

# Balances and transactions

> Read a client's balance, walk its ledger with a cursor, and export statements, confirmations and receipts.

Read a client through three surfaces: its balance, its ledger, and its exports.

| Surface | Route | Answers |
| - | - | - |
| **Balance** | `GET /dapi/v1/client/balance` | What the client's wallet holds right now, read on chain |
| **Ledger** | `GET /dapi/v1/transactions`, `GET /dapi/v1/transactions/{transactionId}`, `GET /dapi/v1/transactions/summary`, `POST /dapi/v1/transactions/sync` | Every movement in or out, newest first, with totals for the same filters, and an on-demand refresh |
| **Exports** | `GET /dapi/v1/transactions/export` | A file: a transaction list, an account statement, an account confirmation or a receipt |

Send `X-Hevn-Account: cl_…` with your own access token to act for one client. On a resource route
like `GET /banks` the header is optional: omit it and the call acts on your integrator account
instead. The singular `/client*` routes **require** it — the header is the only thing that names the
account — and `/clients*`, `/connect/sessions` and `/escrow*` refuse it.

```bash cURL theme={null}
curl "$HEVN_API/banks" \
  -H "Authorization: Bearer $HEVN_ACCESS_TOKEN" \
  -H "X-Hevn-Account: $HEVN_CLIENT_ID"
```

```python Python theme={null}
northwind = hevn.acting_as(CLIENT_ID)
rails = northwind.get("/banks")
```

```javascript Node theme={null}
const northwind = hevn.actingAs(clientId);
const rails = await northwind.get("/banks");
```

```go Go theme={null}
northwind := api.ActingAs(clientID)
rails, err := northwind.Get("/banks")
```

The whole table — where the header is optional, where it is required and where it is refused — is
in [Sessions](https://docs.gethevn.com/whitelabel/sessions.md#acting-as-a-client).

The balance route is the strictest of the three: no client id appears in the path, so `X-Hevn-Account` is **required** rather than optional. Omit it and the call answers `422 validation_failed` for a missing header — reading your own account means sending your own `cl_…`.

## Balance

```bash cURL theme={null}
curl -s "$HEVN_API/client/balance" \
  -H "Authorization: Bearer $HEVN_ACCESS_TOKEN" \
  -H "X-Hevn-Account: cl_7YQ2Kf3mN8"
```

```python Python theme={null}
balance = northwind.get("/client/balance")
```

```javascript Node theme={null}
const balance = await northwind.get("/client/balance");
```

```go Go theme={null}
balance, err := northwind.Get("/client/balance")
```

```json Response — 200 theme={null}
{ "clientId": "cl_7YQ2Kf3mN8",
  "baseSmartWallet": "0x2f1c…",
  "accounts": [ { "account": "USDC", "balance": "1204.500000", "usdValue": "1204.50" },
                { "account": "EURC", "balance": "459.310000", "usdValue": "496.55" } ],
  "totalUsd": "1701.05" }
```

A client holds one balance per **account**. Two of them are the stablecoins on its wallet and are named after the token they hold; the others are money a partner bank holds for it, in dollars or in euros:

| Account | Where the money is | Currency |
| - | - | - |
| `USDC` | the client's Base smart wallet | USD |
| `EURC` | the same smart wallet | EUR |
| `FDIC_USD` | a virtual account a partner bank holds for the client | USD |
| `FDIC_EUR` | a virtual account a partner bank holds for the client | EUR |

That is the same name a payout's `sourceAccount` takes, the same name an incoming transfer's
`destinationAccount` settles into, and the same name a [swap](https://docs.gethevn.com/whitelabel/swaps.md) moves between — one
vocabulary end to end. A client with no custodial balance has no `FDIC_USD` or `FDIC_EUR` row, and a row that is
there is read from the partner bank at the moment you ask.

Every number is a decimal string in the major unit — no atomic units, no floats. `balance` carries the account's own precision, `usdValue` and `totalUsd` are rounded to cents. `baseSmartWallet` is the address funds live at and the address a crypto payer sends to; it is the only address on this surface.

The read hits the chain, so it is the truth about spendable funds — and it is what `POST /dapi/v1/payouts` checks before anything is signed. A payout refused with `insufficient_funds` and a balance that looks sufficient means the difference is an in-flight debit, not a stale cache.

## Walk the ledger

`GET /dapi/v1/transactions` is cursor-paged, newest first. Pass `nextCursor` back as `cursor` until it is gone. There is no `offset` and no total.

```bash cURL theme={null}
curl -s "$HEVN_API/transactions?limit=50&incomeOnly=true" \
  -H "Authorization: Bearer $HEVN_ACCESS_TOKEN" \
  -H "X-Hevn-Account: $HEVN_CLIENT_ID"
```

```python Python theme={null}
page = northwind.get("/transactions", params={"limit": 50, "incomeOnly": True})
while page["nextCursor"]:
    page = northwind.get("/transactions", params={"limit": 50, "cursor": page["nextCursor"]})
```

```javascript Node theme={null}
let page = await northwind.get("/transactions", { limit: 50, incomeOnly: true });
while (page.nextCursor) {
  page = await northwind.get("/transactions", { limit: 50, cursor: page.nextCursor });
}
```

```go Go theme={null}
page, err := northwind.Get("/transactions", hevn.Query{"limit": "50", "incomeOnly": "true"})
for page.Str("nextCursor") != "" {
	page, err = northwind.Get("/transactions", hevn.Query{"limit": "50", "cursor": page.Str("nextCursor")})
}
```

```json Response — 200 theme={null}
{ "items": [
    { "id": "txn_9e4…", "type": "sepa", "status": "success", "isIncome": false,
      "from": { "amount": "500.00", "currency": "USD", "usdRate": "1.00" },
      "to": { "amount": "459.31", "currency": "EUR", "usdRate": "1.0815" },
      "fee": { "amount": "1.00", "currency": "USD" },
      "counterparty": "Ardenne Fabrication SARL",
      "paymentReference": "INV-2026-114",
      "txHash": "0x4f1c…", "hasAttachments": false,
      "createdAt": "2026-09-17T10:04:39Z" } ],
  "nextCursor": "eyJ0IjoiMjAyNi0w…" }
```

`from` is what left the source side and `to` what arrived at the destination; on a same-asset movement they are equal. `isIncome` is the direction, `counterparty` is the resolved name of the other party, and `txHash` is the on-chain leg where there is one. A cursor belongs to the query that produced it: change a filter and start again, or the call answers `400 invalid_cursor`.

### Filters

Every filter is a query parameter, camelCase, and the list-shaped ones take comma-separated values.

| Parameter | Takes |
| - | - |
| `type` | Transaction types: the rail method slugs (`sepa`, `ach`, `swift`, `fedwire`, …), `crypto` for on-chain movement, `refund` for a returned payment |
| `status` | `pending`, `rfi`, `success`, `failed`, `refunded` |
| `incomeOnly` | `true` for money in only |
| `contactId` | One or more `ct_…`, the contacts you paid |
| `bankId` | One or more `bnk_…`, the rails money arrived on |
| `tag` | A category on the row, from the fixed set the ledger uses (`PAYROLL`, `CONTRACTORS`, `BANK_FEES`, …), or `UNTAGGED` |
| `q` | Free text, at least two characters |
| `from`, `to` | ISO-8601 timestamps bounding `createdAt` |
| `limit`, `cursor` | Page size 1–100, default 50, and the opaque cursor |

Values are matched exactly, case included. A value outside the set answers `400 invalid_request` with the offending one in `details.reason`, rather than returning a silently narrower page.

### Totals for the same filters

`GET /dapi/v1/transactions/summary` takes the same filters and answers the aggregate, in USD, settled and pending kept apart:

```json Response — 200 theme={null}
{ "inflowUsd": "12040.50", "inflowCount": 9,
  "outflowUsd": "8300.00", "outflowCount": 14,
  "pendingInflowUsd": "2000.00", "pendingInflowCount": 1,
  "pendingOutflowUsd": "0.00", "pendingOutflowCount": 0 }
```

It is its own route, so an aggregate is never folded into a page and never depends on which page you asked for.

### One transaction

`GET /dapi/v1/transactions/{transactionId}` returns the same row plus `description`, `explorerUrl`, `traceNumber` and `attachmentIds` (`doc_…`). Use it when you have an id — from a payout's `transactionId`, from a sandbox credit, or from a row you are reconciling.

## Exports

`GET /dapi/v1/transactions/export` streams a file. The name arrives in `Content-Disposition`; there is no JSON body and no document to fetch afterwards.

| `kind` | Also needs | `format` | Contents |
| - | - | - | - |
| omitted | — | `csv` (default), `xlsx`, `pdf` | The filtered transaction list, exactly what `GET /dapi/v1/transactions` would return |
| `statement` | `bankId` | `pdf`, `xlsx` | An account statement for that rail over the period |
| `confirmation` | `bankId` | `pdf`, `xlsx` | A confirmation that the virtual account exists, with its account details |
| `receipt` | `transactionId` | `pdf` | A receipt for one transaction |

```bash cURL theme={null}
curl -s "$HEVN_API/transactions/export?kind=statement&bankId=bnk_88Hq3T&format=pdf&from=2026-09-01&to=2026-09-30" \
  -H "Authorization: Bearer $HEVN_ACCESS_TOKEN" \
  -H "X-Hevn-Account: $HEVN_CLIENT_ID" \
  -o statement.pdf
```

```python Python theme={null}
northwind.download("/transactions/export", {
    "kind": "statement", "bankId": "bnk_88Hq3T", "format": "pdf",
    "from": "2026-09-01", "to": "2026-09-30",
}, "statement.pdf")
```

```javascript Node theme={null}
await northwind.download("/transactions/export", {
  kind: "statement", bankId: "bnk_88Hq3T", format: "pdf",
  from: "2026-09-01", to: "2026-09-30",
}, "statement.pdf");
```

```go Go theme={null}
_, err := northwind.Download("/transactions/export", hevn.Query{
	"kind": "statement", "bankId": "bnk_88Hq3T", "format": "pdf",
	"from": "2026-09-01", "to": "2026-09-30",
}, "statement.pdf")
```

`download` is the one method that writes bytes instead of parsing JSON —
[the reference client](https://docs.gethevn.com/whitelabel/reference/client.md#download-an-export) has it.

Every ledger filter applies to an export as well, so a statement can be narrowed the same way a page can. A statement with no `from`/`to` covers the current month to now. Asking for a combination the table does not list — a `receipt` as `xlsx`, a `statement` without `bankId` — answers `422 validation_failed` naming the parameter at fault.

## Reconciling

The ledger is the durable record of everything the other pages produce: a confirmed payout, a transfer, a settled payin, a sandbox credit. Reconcile with a watermark rather than a re-read:

1. Keep the newest `createdAt` you have processed, per client.
2. Ask for `from=<watermark>` and walk the cursor to the end.
3. Match your own records on `id`, and a payout on the `transactionId` its read returns — `txHash` identifies the on-chain leg, not the payment.
4. Move the watermark only after the whole walk succeeded. Ids are stable, so replaying an overlap costs a comparison, and skipping a row costs a missing payment.

Rows appear when the bank reports or the chain confirms, not when you call. A `pending` row is not an error; a row that never appears is, and its payin or payout id is where to look.

### Forcing a refresh

`POST /dapi/v1/transactions/sync` refreshes one client's ledger from the banks and chains its money moved over, and answers what changed:

```json Response — 200 theme={null}
{ "syncedAt": "2026-09-19T12:04:11Z", "written": 3 }
```

Reads never do this on their own — `GET /dapi/v1/transactions` answers from the ledger as it stands — so this is the call for the one case a watermark walk cannot cover: a transfer made outside your integration that has to be visible *before* you read. `written` is how many entries the refresh created or updated; zero is a normal answer, not a refusal.

It reaches out to every source the client has, so it is rate-limited and wrong to call per request. In a reconciliation loop, call it once before the walk, not once per page.

> [card] [Next: Escrow](https://docs.gethevn.com/whitelabel/escrow.md)
>
> Lock one client's funds in an on-chain contract while another delivers: the marketplace order, end to end.


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