Skip to main content
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. Every example on these pages reads the same variables:
Three more come out of the first two: HEVN_ACCESS_TOKEN, the one-hour token logging in 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.
Nothing crosses between the two environments: developer keys, clients, tokens and rail ids are per-environment, and a sandbox token is refused in production.

What is real and what is emulated

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.
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.
The whole table — where the header is optional, where it is required and where it is refused — is in Sessions. 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 here too, with real Google sign-ins. Each test client needs a Google account 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.
Response — 200
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). 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.
Response — 200
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.
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.
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.
“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. Production has none of these limits. Every refusal says which limit it hit and by how much, in the message and in details:
Response — 422
Response — 409
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 is the list.

Next: Going live

What HEVN must enable, what you re-create per environment, and what production starts refusing.