Every example on these pages reads the same variables:
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.
What is real and what is emulated
The sandbox is the same API against one emulated partner on Base Sepolia. Its money comes from a shared faucet account: at most 100 tokens per credit, at least
0.10, and 60 money calls per account per hour. GET /dapi/v1/sandbox/treasury reports the live ceiling. Full table in Limits.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 and /escrow* refuse it.
404.
What the sandbox does not grant
Being an integrator, being allowed to create 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 flags are on,POST /dapi/v1/clients answers 403 integrator_inactive or 403 client_creation_not_enabled; GET /dapi/v1/clients answering an empty page is the cheapest confirmation that they are.
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. 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
bankIdit is an unsolicited bank transfer into that rail’s account details:amountis in the rail’s currency, the emulator converts at the fixed rate and credits the client’s account.remitterandpaymentReferenceare the payer’s name and message. - Without
bankIdit is a token transfer straight to the client’s wallet.tokenisUSDC(default) orEURC, and it is the fastest way to fund a client before a payout test.
Response — 200
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.
Settle a payin
A quoted payin waits for a bank transfer that will never arrive on its own. This route is that transfer.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 you signed has landed. 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.
What the sandbox does for you
Three things the production path makes you wait for happen here without a call: a submitted KYB application is approved immediately, a rail you open becomesactive with account details as soon as the partner is asked, and every row exists by the time the call returns. 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.