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

# Sandbox

> The one place /dapi/v1/sandbox/* is documented: read the faucet account, mint money, settle a payin and move a payout.

The sandbox is the same API against one emulated partner on Base Sepolia. Same paths, same bodies, same slugs, same signatures — only the counterparty is fake, and five extra routes let you play it: every operation production serves, plus these five.

| Environment | API base URL | App, in a browser | Chain |
| - | - | - | - |
| Sandbox | `https://sandbox-api.hevn.finance/dapi/v1` | `https://sandbox.gethevn.com` | Base Sepolia |
| Production | `https://api.hevn.finance/dapi/v1` | `https://app.gethevn.com` | Base |

Every example on these pages reads the same variables:

```bash theme={null}
export HEVN_API="https://sandbox-api.hevn.finance/dapi/v1"
export HEVN_EMAIL="integrator@example.com"
export HEVN_KEY_PEM="$HOME/.hevn/developer-key.pem"  # mode 0600, never in your repo
export HEVN_CLIENT_ID="cl_7YQ2Kf3mN8"                # the client a call acts for
```

Three more come out of the first two: `HEVN_ACCESS_TOKEN`, the one-hour token
[logging in](https://docs.gethevn.com/whitelabel/sessions.md) returns; `HEVN_USER_ID`, your own account's `cl_…` id, which the
same response returns as `userId` and which the `/client*` routes need in `X-Hevn-Account` when you
read your own account; and `HEVN_PUBLIC_KEY`, the base64 public half of the PEM, which
`POST /dapi/v1/payouts` takes as `publicKey`.

```bash theme={null}
export HEVN_PUBLIC_KEY=$(openssl ec -in "$HEVN_KEY_PEM" -pubout -outform DER 2>/dev/null \
  | base64 | tr -d '\n')
```

Nothing crosses between the two environments: developer keys, clients and tokens are
per-environment, and a sandbox token is refused in production.

## What is real and what is emulated

| | In the sandbox |
| - | - |
| Chain | Base Sepolia. Balances, wallets, signatures and transaction hashes are real; the tokens are testnet USDC and EURC. |
| Fiat partner | One emulator answers for every rail. It issues checksum-valid account details — `SBOX…` BICs, DE/AE/GB IBANs, ABA routing numbers, a PIX key, an 18-digit SPEI number. |
| Money in and out | Paid by a shared testnet faucet account on demand, through the routes below. Nothing arrives on its own. |
| FX | A fixed table: USD 1, EUR 1.08, GBP 1.27, AED 0.2723, and so on, with USDC pegged to USD and EURC to EUR. |
| Verification | Nothing is checked against a registry or the papers' contents. Any made-up company, people and documents that bring the KYB document to `ready: true` will do, so let your coding agent generate them. A submitted KYB application waits for a reviewer as in production, and you are the reviewer: [decide it](#decide-a-kyb-application) yourself. Rails open without a compliance queue. |
| Background work | Off. Every call projects its own rows before it returns, so there is nothing to wait for that you did not ask for. |
| Swaps | Not served. The emulated chain has no exchange venue behind it and no custodial dollar balance is issued, so `POST /dapi/v1/swaps` and `POST /dapi/v1/swaps/preview` answer `422 swap_route_unavailable` rather than a quote ([Swaps](https://docs.gethevn.com/whitelabel/swaps.md)). |
| Cards, cross-chain routing | Disabled. They are not part of the whitelabel API. |
| Your source IP | As real as in production. Every call is checked against the allowlist you registered with the developer key, so a sandbox host on a new egress IP answers `403` on every route. |

> [!NOTE]
>
> The sandbox is the same API against one emulated partner on Base Sepolia. It runs on shared testnet money, so it is capped: **at most 1 per payout, escrow or payin**, a faucet that sends **at most 5, only to a wallet holding less than 1**, and **60 money calls per account per hour**. Full table in [Limits](https://docs.gethevn.com/whitelabel/reference/limits.md#sandbox).

Three of these five routes move a client's money, one decides its verification, and the header is how you pick which client. `GET /dapi/v1/sandbox/treasury` accepts it and reports the same shared faucet account either way.

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

None of these routes exist in production; there they answer `404`.

## What the sandbox does not grant

Being an integrator that may link clients, and being allowed to operate escrow, are flags on your account, and nothing in the API sets them — in the sandbox as in production, HEVN does. There is no self-enrolment call. Until the first is on, `POST /dapi/v1/connect/sessions` answers `403 client_creation_not_enabled`; a session that comes back with a `url` is the cheapest confirmation that it is.

Clients are linked through [Sign in with HEVN](https://docs.gethevn.com/whitelabel/sign-in-with-hevn.md) here too, with real sign-ins — Google, or a one-time code to an email you pass as `login_hint`. Each test client needs a Google account or an email address that has no HEVN sandbox account of its own, and your test page's origin — `http://localhost:<port>` works — under **Allowed sites** in the sandbox app.

## Read the faucet account

Every emulated credit is paid by one account, and every emulated payout is booked against it.

```bash cURL theme={null}
curl -s "$HEVN_API/sandbox/treasury" -H "Authorization: Bearer $HEVN_ACCESS_TOKEN"
```

```python Python theme={null}
treasury = hevn.get("/sandbox/treasury")
```

```javascript Node theme={null}
const treasury = await hevn.get("/sandbox/treasury");
```

```go Go theme={null}
treasury, err := api.Get("/sandbox/treasury")
```

```json Response — 200 theme={null}
{ "chainId": 84532, "address": "0x7d4E5E29e6d40735D4f9bB74a0234383068f8A21",
  "gasBalance": "0.0412", "maxCredit": "1",
  "balances": [ { "token": "USDC", "tokenAddress": "0x036CbD…", "balance": "842.51" },
                { "token": "EURC", "tokenAddress": "0x808456…", "balance": "310.00" } ] }
```

`maxCredit` is the live per-call ceiling for a bank deposit or a completed payin. The token faucet has its own ceiling of `5` ([Limits](#limits)). When the faucet account is empty or out of gas, credits answer `503 provider_unavailable` with `details.treasuryAddress` and `details.token` — that is a faucet problem, not your bug.

## Mint money

`POST /dapi/v1/sandbox/deposits` has two arms, and `bankId` picks between them.

* **With `bankId`** it is an unsolicited bank transfer into that rail's account details: `amount` is in the rail's currency, the emulator converts at the fixed rate and credits the client's account. `remitter` and `paymentReference` are the payer's name and message.
* **Without `bankId`** it is the token faucet: a transfer straight to the client's wallet. `token` is `USDC` (default) or `EURC`, and it is the fastest way to fund a client before a payout test. It sends at most `5` per call, and only while the wallet holds less than `1` of that token; a fuller wallet answers `409 sandbox_faucet_balance_too_high`.

```bash cURL theme={null}
curl -s -X POST "$HEVN_API/sandbox/deposits" \
  -H "Authorization: Bearer $HEVN_ACCESS_TOKEN" \
  -H "X-Hevn-Account: $HEVN_CLIENT_ID" \
  -H "Idempotency-Key: fund-northwind-001" \
  -H "Content-Type: application/json" \
  -d '{"amount":"5.00","token":"USDC"}'
```

```python Python theme={null}
northwind = hevn.acting_as(CLIENT_ID)
credit = northwind.post("/sandbox/deposits", {"amount": "5.00", "token": "USDC"},
                        idempotency_key="fund-northwind-001")
```

```javascript Node theme={null}
const northwind = hevn.actingAs(clientId);
const credit = await northwind.post("/sandbox/deposits",
  { amount: "5.00", token: "USDC" },
  { idempotencyKey: "fund-northwind-001" });
```

```go Go theme={null}
northwind := api.ActingAs(clientID)
credit, err := northwind.Post("/sandbox/deposits", hevn.Body{"amount": "5.00", "token": "USDC"},
	hevn.IdempotencyKey("fund-northwind-001"))
```

```json Response — 200 theme={null}
{ "depositId": "sbx_cd_1b8e0f7a2c", "status": "settled",
  "token": "USDC", "amount": "5.000000", "currency": "USDC",
  "idempotencyKey": "fund-northwind-001",
  "credited": "5.000000", "txHash": "0x08fa…", "transactionId": "txn_9e4…" }
```

A bank deposit answers the same shape with the fiat side filled in — `amount` and `currency` in the rail's currency, `credited` in the account it landed in, plus `fee`, `feeCurrency` and `rate`. When the credit settles a payin you quoted, `payinId` names it.

The key is optional here, as everywhere. Send one and a retry replays it (`200` with `Idempotency-Replayed: true`); send none and HEVN derives one from the request and echoes it as `idempotencyKey`. Two identical un-keyed deposits always replay, whatever the gap between them, so pass an explicit key when you want a second identical credit.

`202` means the faucet account's transfer has not confirmed yet. Repeat the same request until it is `settled`; nothing is ever credited twice.

## Decide a KYB application

A completed KYB submission waits for a reviewer, and in the sandbox there is none but you. This route takes the reviewer's decision on the client's latest application, through the same transition HEVN compliance uses in production.

```bash cURL theme={null}
curl -s -X POST "$HEVN_API/sandbox/kyb/status" \
  -H "Authorization: Bearer $HEVN_ACCESS_TOKEN" \
  -H "X-Hevn-Account: $HEVN_CLIENT_ID" \
  -H "Content-Type: application/json" \
  -d '{"status":"rfi","comment":"The proof of address for Jane Doe is older than three months."}'
```

```python Python theme={null}
kyb = northwind.post("/sandbox/kyb/status", {"status": "approved"})
```

```javascript Node theme={null}
const kyb = await northwind.post("/sandbox/kyb/status", { status: "approved" });
```

```go Go theme={null}
kyb, err := northwind.Post("/sandbox/kyb/status", hevn.Body{"status": "approved"})
```

| `status` | Needs | What the client sees |
| - | - | - |
| `approved` | a `pending` application | `GET /dapi/v1/client/kyb` reads `approved`. |
| `rfi` | a `pending` or `approved` application, and a `comment` | `rfi`, with your `comment` in `application.comment`. Answer it as in production: fix the document, complete again, and decide the next attempt. |
| `rejected` | a `pending` or `approved` application, and a `comment` | `rejected`. A new attempt needs a new submission. |

The response is the same body `GET /dapi/v1/client/kyb` returns. Asking for the status the application already has replays it. A client with no application answers `409 conflict` with `details.status: "notStarted"`; deciding a `rfi` or `rejected` one answers `409 conflict` with `details.current` and `details.target` — complete the submission again first. `rfi` and `rejected` without a `comment` answer `422 validation_failed`.

## Settle a payin

A quoted payin waits for a bank transfer that will never arrive on its own. This route is that transfer.

```bash cURL theme={null}
curl -s -X POST "$HEVN_API/sandbox/payins/pi_2b7f14c0a9/complete" \
  -H "Authorization: Bearer $HEVN_ACCESS_TOKEN" \
  -H "X-Hevn-Account: $HEVN_CLIENT_ID" \
  -H "Content-Type: application/json" \
  -d '{"remitter":"ACME GmbH"}'
```

```python Python theme={null}
settled = northwind.post(f"/sandbox/payins/{payin['id']}/complete", {"remitter": "ACME GmbH"})
```

```javascript Node theme={null}
const settled = await northwind.post(`/sandbox/payins/${payin.id}/complete`,
  { remitter: "ACME GmbH" });
```

```go Go theme={null}
settled, err := northwind.Post("/sandbox/payins/"+payin.Str("id")+"/complete",
	hevn.Body{"remitter": "ACME GmbH"})
```

The body is optional and `remitter` is the only field in it — it becomes the payer name on the resulting income row, which is what you want if you are testing reconciliation. The payin must be open: one you never opened with `POST /dapi/v1/payins/{payinId}/instructions` answers `409 quote_not_submitted`.

## Move a payout

Booking and signing a payout work exactly as in production; the partner is emulated, so nothing settles on its own. You drive it.

```bash cURL theme={null}
curl -s -X POST "$HEVN_API/sandbox/payouts/po_4c8a13d7f2/status" \
  -H "Authorization: Bearer $HEVN_ACCESS_TOKEN" \
  -H "X-Hevn-Account: $HEVN_CLIENT_ID" \
  -H "Content-Type: application/json" \
  -d '{"status":"settled"}'
```

```python Python theme={null}
moved = northwind.post(f"/sandbox/payouts/{payout['id']}/status", {"status": "settled"})
```

```javascript Node theme={null}
const moved = await northwind.post(`/sandbox/payouts/${payout.id}/status`, { status: "settled" });
```

```go Go theme={null}
moved, err := northwind.Post("/sandbox/payouts/"+payout.Str("id")+"/status",
	hevn.Body{"status": "settled"})
```

| `status` | Needs the payout funded | What it does |
| - | - | - |
| `submitted` | yes | The partner accepted the payment. |
| `settled` | yes | The payout is done and the ledger row becomes a success. |
| `failed` | no | Allowed at any point. The money does **not** come back. |
| `refunded` | yes | The status flips, then the faucet account returns the source token to the client's wallet. |

"Funded" means the on-chain debit has landed — signed by the client's holder in the sign-in window, or by your key on your own account. Asking for anything but `failed` before that answers `409 payout_not_fundable`. An illegal move — `settled` back to `submitted` — answers `409 conflict`. `refunded` answers `202` until the return lands; call it again until `refundTxHash` is set, and the same send is resumed, never duplicated.

`status` shares its type with a payout's own status, so the schema also admits `awaitingSignature` — the one value this route never accepts. Sending it answers `422 validation_failed` with `details.fields[0].code = "value_error"` and the four legal values in the message; the four above are the whole vocabulary.

Keep two test payouts more than `0.20` apart in amount: returned money is attributed to a refunded payout by amount, within a `0.20` tolerance.

## Limits

The sandbox runs on shared testnet infrastructure — one faucet account, one gas budget — so every integrator gets a small, fixed share of it. The tokens are real testnet USDC and EURC, so that every transfer behaves as it does in production, and their free supply is finite. Production has none of these limits.

To test money moving between parties, link two or three clients, fund one from the faucet and pay the others with amounts below `1`.

| Limit | Value | Refusal |
| - | - | - |
| One payout | at most `1` of the debited account (USDC or EURC). A bank payout starts at `0.10` and carries no fixed fee here, only the rail's `feeBps` | `422 sandbox_amount_limit_exceeded`, `400 invalid_request` below the minimum |
| One escrow | `maxAmount` at most `1`; its actions cannot exceed it | `422 sandbox_amount_limit_exceeded` |
| One payin credit — a bank deposit, or a completed payin | `0.10` to `1` of the credited token, live as `maxCredit` | `422 amount_below_minimum`, `422 sandbox_amount_limit_exceeded` |
| One faucet credit — a deposit without `bankId` | `0.10` to `5`, only while the wallet holds less than `1` of that token | `422 sandbox_amount_limit_exceeded`, `409 sandbox_faucet_balance_too_high` |
| Money calls | 60 per account per hour | `429 rate_limited` |

Every refusal says which limit it hit and by how much, in the message and in `details`:

```json Response — 422 theme={null}
{ "error": { "code": "sandbox_amount_limit_exceeded",
  "message": "The sandbox moves at most 1 USDC per payout; this request asks for 5.00 USDC.",
  "details": { "operation": "payout", "maximum": "1", "amount": "5.00", "currency": "USDC" } } }
```

```json Response — 409 theme={null}
{ "error": { "code": "sandbox_faucet_balance_too_high",
  "message": "The sandbox faucet only tops up a wallet holding less than 1 USDC; this wallet holds 3.2 USDC",
  "details": { "balance": "3.2", "threshold": "1", "token": "USDC" } } }
```

A client that holds `1` or more already has enough to test with: pay it out, or move it to another client, before asking the faucet again.

## What the sandbox does for you

Two things the production path makes you wait for happen here without a call: a rail you open becomes `active` with account details as soon as the partner is asked, and every row exists by the time the call returns. A third, the KYB review, is one call of yours instead of a reviewer's. That is the difference that will bite when you switch hosts — [Going live](https://docs.gethevn.com/whitelabel/going-live.md) is the list.

> [card] [Next: Going live](https://docs.gethevn.com/whitelabel/going-live.md)
>
> What HEVN must enable, what you re-create per environment, and what production starts refusing.


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