> ## 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 is payments software: an API and app over a Base smart wallet. HEVN is not a bank and holds no key to the wallet. Virtual accounts, fiat payins and fiat payouts are provided by partner banks, which hold fiat while it is in transit. Self-serve (the General tab): the person who controls the email signs for the wallet. Developers integrate /dapi/v1 in one of two ways. HEVN Connect (recommended, the HEVN Connect tab): the integrator's customer creates or links a HEVN account in a HEVN-hosted sign-in window on the integrator's site, the integrator's backend reads the linked account and books its payouts, and the account holder approves each payout in that window. External wallets (the External wallets tab, enabled per integrator): the integrator creates a client through the API, registers a wallet address the customer holds as an external account, and requests virtual accounts that deliver to that address. The customer's own wallet sends each transfer that funds a payout. The developer key signs for the integrator's wallet and its sub accounts and cannot sign for a client's wallet.
> Use HEVN's terms as written. A product is the region a client requests. A rail is the bank route HEVN picks for the product. A virtual account (bnk_) is the record a partner bank opens for a client. Account details are what a payer needs to send a bank transfer, returned in a field named requisites. A client (cl_) is a customer's account: linked to the integrator through Sign in with HEVN (POST /dapi/v1/connect/sessions, then POST /dapi/v1/clients/link), or created with POST /dapi/v1/clients for an external-wallets integration. An external account (acc_, kind external) is a wallet address the client holds and signs for. A sub account (acc_, kind sub) is a further wallet of the integrator's own account on Base, spent with the developer key. Write 'sub account' as two words. A contact is a saved destination. A beneficiary is the party that receives a fiat payout. Money leaving an account is a payout, whether it goes to a bank account or to a wallet.
> Do not name a bank, payment institution, card issuer, identity-verification vendor or stablecoin issuer. Say 'the partner bank'. Do not describe HEVN as licensed, regulated or insured.
> HEVN publishes no fee rates, spreads or minimums. Prices come from a quote on a specific payment. Direct pricing questions to HEVN and do not invent numbers.
> The 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. The two credentials are separate and neither works on the other API. The HEVN CLI is discontinued.

# Sandbox and going live

> What the sandbox emulates, the /dapi/v1/sandbox/* routes that drive it, and what changes in production.

The sandbox is the production API running against one emulated partner bank on Base Sepolia. Fund a client with `POST /dapi/v1/sandbox/deposits`, drive KYB, payins and payouts with the routes below, then [go live](#go-live).

| Environment | API base URL | App | 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 |

Developer keys, clients and tokens are per environment.

The examples read these 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, keep it out of your repo
export HEVN_CLIENT_ID="cl_7YQ2Kf3mN8"                # the client a call acts for
```

| Derived variable | Value |
| - | - |
| `HEVN_ACCESS_TOKEN` | The `accessToken` returned by [login](https://docs.gethevn.com/developers/authentication.md#sessions). |
| `HEVN_USER_ID` | Your `cl_…` id, the `userId` returned by login. |
| `HEVN_PUBLIC_KEY` | The base64 public key of the PEM, sent as `publicKey` to `POST /dapi/v1/payouts`. |

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

`X-Hevn-Account` selects the client ([Acting as a client](https://docs.gethevn.com/developers/authentication.md#acting-as-a-client)).

| Integration kind | Test clients |
| - | - |
| [HEVN Connect](https://docs.gethevn.com/connect/overview.md) | Clients link through [Sign in with HEVN](https://docs.gethevn.com/connect/sign-in-with-hevn.md) with a real sign-in. Each needs a Google account or email address with no HEVN sandbox account. Add your test origin, for example `http://localhost:<port>`, under **Allowed sites** in the sandbox app. |
| [External wallets](https://docs.gethevn.com/external-wallets/overview.md) | Create clients with `POST /dapi/v1/clients` and register testnet addresses you hold. |

## What HEVN must enable

HEVN grants these in both environments. No API call sets them.

| Grant | Without it | When to ask |
| - | - | - |
| An active integrator profile that may link clients | `403 client_creation_not_enabled` on `POST /dapi/v1/connect/sessions` and `POST /dapi/v1/clients/link`. `403 account_forbidden` on calls that act for an existing client. | Before you write production code. |
| Client creation for external wallets | `403 client_creation_not_enabled` on `POST /dapi/v1/clients` | After you show HEVN that your wallet infrastructure is non-custodial ([External wallets](https://docs.gethevn.com/external-wallets/overview.md)). |
| Escrow operation | Escrow actions are refused. | When funds must be locked on chain between your customers. |
| The rails you need | `403 rail_not_available` on `POST /dapi/v1/banks` | Name the currencies and countries you sell into. Availability is set per integrator and per client. |

## Environment

| Area | Sandbox |
| - | - |
| Chain | Wallets, balances, signatures and transaction hashes are real. Tokens are testnet USDC and EURC. |
| Partner bank | One emulator for all rails. Account details are checksum-valid: `SBOX…` BICs, DE, AE and GB IBANs, ABA routing numbers, a PIX key, an 18-digit SPEI number. |
| FX | Fixed rates, for example USD 1, EUR 1.08, GBP 1.27 and AED 0.2723. USDC is pegged to USD, EURC to EUR. |
| Verification | No registry or document check. Made-up companies, people and documents pass once the KYB document is `ready: true` and you [approve the application](#decide-a-kyb-application). |
| Background work | Each emulated credit and status change writes its rows before the call returns. The exception is the matching of an external account's payout transfer, which runs about every 30 seconds ([Accounts](#accounts)). |
| Swaps | `POST /dapi/v1/swaps` and `POST /dapi/v1/swaps/preview` between two account keys return `422 swap_route_unavailable`. A [move](https://docs.gethevn.com/developers/swaps.md#sub-account-moves) that names a sub account is served. The sandbox issues no dollar balance held at the partner bank. |
| Partner-issued cards, cross-chain routing | Disabled. Not part of `/dapi/v1`. |
| Source IP | Checked against the key's [allowlist](https://docs.gethevn.com/developers/authentication.md#the-ip-allowlist). An unlisted address gets `403` on all routes. |

## Read the faucet account

`GET /dapi/v1/sandbox/treasury` returns the shared testnet faucet account that pays emulated credits and receives the funds of emulated payouts.

```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" }, … ] }
```

When the faucet account is out of tokens or gas, credits return `503 provider_unavailable` with `details.treasuryAddress` and `details.token`.

## Mint money

`POST /dapi/v1/sandbox/deposits` credits a client. With `bankId` it books an unquoted payin to that virtual account at the fixed rate. Without it, the faucet sends `token` to the client's wallet, or to the sub account named in `accountId`, within the faucet [limits](#limits). Fields: [Emulate an incoming deposit](https://docs.gethevn.com/api-reference/sandbox/emulate-an-incoming-deposit:-an-account-transfer-with-bankid-a-token-transfer-without.md).

```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-client-001" \
  -H "Content-Type: application/json" \
  -d '{"amount":"5.00","token":"USDC"}'
```

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

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

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

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

`202` means the faucet transfer is unconfirmed. Repeat the same request until `status` is `settled`. The amount is credited once.

Identical requests without an [`Idempotency-Key`](https://docs.gethevn.com/developers/conventions#idempotency-key) replay the first credit at any interval. Send a new key to credit the same amount again.

## Accounts

Sub accounts and external accounts run in the sandbox as in production, with these differences.

| Flow | Sub account | External account |
| - | - | - |
| Create | `asset: "RD"` returns `409 account_destination_unsupported`. | `balance` is read on the Base and Arbitrum testnets and on no other network. |
| Virtual account with `accountId` | Products `us` and `swift`. Others return `409 account_destination_unsupported`. | The same products, for an account on Base. |
| Credit with `bankId` | Delivered to the sub account's address | Delivered to the external address |
| Faucet credit with `accountId` | Sends the token the account holds to its address, within the faucet [limits](#limits) | `409 account_destination_unsupported`. Fund the address from a public testnet faucet. |
| Quoted payin with `destinationAccount` | No on-chain route exists in the sandbox. | No on-chain route exists in the sandbox. |
| Payout | `authorization: "debit"`, at most `1` of the account's token | `authorization: "wallet"`. `transfer.toAddress` is the faucet account of `GET /dapi/v1/sandbox/treasury`, and the transfer is matched within about 30 seconds. |
| Move | Served, with no amount cap | `404 account_not_found`, as in production |

`accountId` together with `bankId` returns `422 validation_failed`: a credit with `bankId` lands on the account its virtual account was opened for. The same `Idempotency-Key` with another `accountId` returns `409 idempotency_key_reused`.

The responses of `POST /dapi/v1/sandbox/deposits` and `POST /dapi/v1/sandbox/payouts/{payoutId}/status` have `accountId` when the credit or the payout belongs to an `acc_…` account. After a payout from an external account, wait for the transfer to be matched before you [move the payout](#move-a-payout). Until then `POST /dapi/v1/sandbox/payouts/{payoutId}/status` returns `409 payout_not_fundable` for `submitted`, `settled` and `refunded`.

## Decide a KYB application

A completed KYB submission stays `pending` until you call `POST /dapi/v1/sandbox/kyb/status`. The call sets the status of the client's latest application.

```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 = client.post("/sandbox/kyb/status", {"status": "approved"})
```

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

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

The statuses, what each needs and the refusals are on [Decide the client's latest KYB application](https://docs.gethevn.com/api-reference/sandbox/decide-the-clients-latest-kyb-application-as-the-compliance-reviewer-would.md). After `rfi`, [answer the RFI](https://docs.gethevn.com/developers/clients.md#answer-an-rfi) and complete the submission again, then decide the new application. After `rejected`, a new attempt needs a new submission. For an individual client, `approved` sets `verifiedAt`.

## Settle a payin

`POST /dapi/v1/sandbox/payins/{payinId}/complete` emulates the payer's bank transfer for a quoted payin.

```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 = client.post(f"/sandbox/payins/{payin['id']}/complete", {"remitter": "ACME GmbH"})
```

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

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

Open the payin with `POST /dapi/v1/payins/{payinId}/instructions` first. The body and refusals are on [Emulate the arrival of the funds](https://docs.gethevn.com/api-reference/sandbox/emulate-the-arrival-of-the-funds-a-pay-in-is-waiting-for.md).

## Move a payout

Book and sign a payout as in production, then set its status with `POST /dapi/v1/sandbox/payouts/{payoutId}/status`.

```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 = client.post(f"/sandbox/payouts/{payout['id']}/status", {"status": "settled"})
```

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

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

The statuses, which need a funded payout and the refusals are on [Move an emulated outgoing transfer](https://docs.gethevn.com/api-reference/sandbox/move-an-emulated-outgoing-transfer-to-its-next-reported-status.md). A `settled` payout's row in `GET /dapi/v1/transactions` has `status: "success"`. `failed` does not return the debited amount.

A return is matched to a refunded payout by amount, within `0.20`. Keep test payouts more than `0.20` apart.

## Limits

Production has none of these limits.

| Limit | Value | Refusal |
| - | - | - |
| One payout | At most `1` of the debited account. A bank payout starts at `0.10` and costs the rail's `feeBps`, with no fixed fee. | `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 (with `bankId`, or a completed payin) | `0.10` to `1` (`maxCredit`) of the credited token | `422 amount_below_minimum`, `422 sandbox_amount_limit_exceeded` |
| One faucet credit (without `bankId`) | `0.10` to `5`, while the wallet holds less than `1` of that token | `422 amount_below_minimum`, `422 sandbox_amount_limit_exceeded`, `409 sandbox_faucet_balance_too_high` |
| Calls to `/sandbox/deposits`, `/sandbox/payins/{payinId}/complete` and `/sandbox/payouts/{payoutId}/status` | 60 per account per hour | `429 rate_limited` |

`sandbox_amount_limit_exceeded` returns `details.operation` (`payout`, `escrow`, `payin` or `faucet`) and `details.maximum`, plus `details.amount` and `details.currency` for a payout or escrow. `sandbox_faucet_balance_too_high` returns `details.balance`, `threshold` and `token`.

## Go live

Production has the same routes, bodies and error codes as the sandbox, with partner banks in place of the emulator. Ask HEVN to [enable your integrator account](#what-hevn-must-enable), then create a production developer key.

### Re-create per environment

| Object | In production |
| - | - |
| Developer key | Create one in the production app (`https://app.gethevn.com`) with your production egress addresses. The [allowlist](https://docs.gethevn.com/developers/authentication.md#the-ip-allowlist) cannot be edited, so list all calling hosts first. |
| Sign-in window | Add your production callback origins under **Allowed sites**. An unlisted `redirectUri` returns `403 redirect_uri_not_allowed`. Ask HEVN to brand the production window. Check that you [bind `state` to the browser](https://docs.gethevn.com/connect/sign-in-with-hevn#3-bind-the-session-to-this-browser). |
| Clients | A sandbox `cl_…` does not exist in production, and neither do its KYB, virtual accounts, contacts or escrow deals. Your customers link again, or you create the clients again. |
| External accounts and sub accounts | An `acc_…` id exists in one environment. Register the addresses and open the sub accounts again. |
| Tokens | Access and refresh tokens are valid in one environment. |
| Virtual accounts | Product codes are the same. The products a client can request and the rail behind each differ. Read `GET /dapi/v1/banks` per client and do not store `rail`. |
| Idempotency keys | Scoped to your account and the environment. |

A key used against the other environment's host fails at login with `401 invalid_credentials`. The response does not name the environment, so check `HEVN_API` first.

### Timing

In production, the steps you trigger yourself in the sandbox wait on a reviewer or the partner bank. Poll as described in [Status codes](https://docs.gethevn.com/developers/conventions.md#status-codes).

| Step | Sandbox | Production |
| - | - | - |
| KYB decision | You [decide the application](#decide-a-kyb-application). | A review. Poll `GET /dapi/v1/client/kyb` and handle `rfi`. |
| Virtual account | `active` with account details once no blocker remains, with no review. | `preparing` until the partner bank publishes the account details. Poll `GET /dapi/v1/banks`. |
| Payin and payout settlement | You [settle the payin](#settle-a-payin) and [move the payout](#move-a-payout). | Timing is set by the payer's bank and the partner bank. A `settled` payout is accepted and debited. Delivery can come later. |

### Production refusals

Production returns these for causes the emulator does not have.

| Signal | Cause | Action |
| - | - | - |
| `422 contact_payment_details_invalid` | The receiving bank does not recognise the beneficiary, usually the name. | Correct the contact with `PATCH /dapi/v1/contacts/{contactId}` and book again. |
| A virtual account stays `preparing` | The partner bank is reviewing it. | Read `blockers[]` on `GET /dapi/v1/banks/{product}`. If it is empty, wait. |
| `422 amount_below_minimum`, `422 amount_above_maximum` | Rail limits replace the sandbox minimum of `0.10`. | Change the amount. |
| `attention: {"kind": "rfi"}` on a payout | The partner bank holds the payout until a question is answered. | Answer the question. Signing again does not release the payout. |
| `503 provider_unavailable` | The partner bank is unavailable. | Retry with backoff. Do not book again. |
| `429 rate_limited` | A [rate limit](https://docs.gethevn.com/developers/conventions.md#rate-limits) was reached. The counters are enforced in production. | Wait for `Retry-After`. |

With HEVN Connect, also handle the sign-in window results `access_denied`, `already_registered`, `already_linked` and `expired`. Handle the [funding codes](https://docs.gethevn.com/developers/reference/errors.md#funding-a-payment) for payouts from your integrator account.

### Checklist

**1. Switch the host**

Set `HEVN_API` to `https://api.hevn.finance/dapi/v1` and load the production key.

**2. Remove sandbox calls**

`/dapi/v1/sandbox/*` returns `404` in production. Poll for each state in [Timing](#timing).

**3. Handle 429, 401 and 5xx**

Handle `429`, `401` and `5xx` as in [Errors](https://docs.gethevn.com/developers/reference/errors.md). Log `X-Request-ID` and include it in support requests.

**4. Rehearse key rotation**

Create a second key whose allowlist covers your hosts. Log in with it and book a payout from your integrator account with its `publicKey`, then delete the old key ([Rotate a key](https://docs.gethevn.com/developers/authentication.md#rotate-a-key)).

**5. Run one production payment**

A payout debits the paying account and an escrow refund debits your wallet, so fund both. Link or create one client, pay a small amount to an account you control and reconcile it from `GET /dapi/v1/transactions`.

> [card] [Next: Rails and fees](https://docs.gethevn.com/developers/reference/rails.md)


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