Skip to main content
A swap moves money between two accounts the same client owns. Nobody else is paid, so there is no contact and no beneficiary: POST /dapi/v1/swaps prices and reserves the move, POST /dapi/v1/swaps/{swapId}/confirm authorizes it. A client holding euros that owes dollars swaps before it pays out, and the money never leaves its own name.
Examples use the client from HTTP client. acting_as sets the header that picks whose money moves: 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.
The whole table — where the header is optional, where it is required and where it is refused — is in Sessions.

The accounts

Both sides are named from one vocabulary, the same one the balance reads back and a payout’s sourceAccount takes. USDC and EURC are the stablecoins on the client’s wallet; FDIC_USD is dollars a partner bank holds for it: The two sides must differ, and both must be accounts that client actually holds. An account it does not hold is 422 account_unavailable — a client with no custodial virtual account cannot name FDIC_USD. A pair nothing can serve right now is 422 swap_route_unavailable.

Price it first

POST /dapi/v1/swaps/preview takes the same accounts and amount without a publicKey, compares every route that can serve the pair, and answers the best one. It reserves nothing, signs nothing and returns no swapId, so it is safe to call on every keystroke of a form.
Response — 200
Pin one side and one only: amount is what leaves the source account, amountTo is what has to arrive on the destination. Sending both, or neither, is 422 validation_failed. minimumToAmount is the floor a route with slippage guarantees; estimated: true marks a move whose delivered amount is only known on arrival, and then etaHours says how long that takes. A locked price carries expiresAt and nothing else has to be read.

Reserve and sign

POST /dapi/v1/swaps prices the move again, fixes it, and returns the approval to sign. The response tells you what that approval authorizes: Either way you sign approval.payload and post it to the same confirm route. The difference is what HEVN does with the co-signature, not what your code does with the approval.
Response — 201, Location: /dapi/v1/swaps/swp_9f2c1ab84d
When authorization is debit, check debit before you sign — it is decoded out of the operation itself, not echoed from your request, and it is the one cross-check a signer has (Signing).
Then confirm, and keep confirming while it is still in flight:
A confirm answers 200 when settlement landed inside the request and 202 submitted when it did not; in the second case pollUrl names the read route. Confirm is single use: the same signature a second time is 409 funding_attempt_expired on a debit swap and 410 approval_expired on an intent one. Either way the move already happened, and the way to check on it is GET /dapi/v1/swaps/{swapId}, never another confirm.

Read it back

GET /dapi/v1/swaps/{swapId} carries the lifecycle, the accounts it ran between, the terms it was fixed at, and transactionId once the move is on the client’s ledger.
Response — 200
A swap is not a second source of truth about money: both sides of it are ordinary ledger rows, so GET /dapi/v1/transactions shows the result without reading this route at all (Balances and transactions).

Retries

Derive Idempotency-Key from an id your own system already owns, pass it in from the caller, and keep it identical across every retry of the same payment.
Never generate the key inside the helper that sends the request, and never rotate it on a retry — both turn one payment into two. The format and the replay windows are in Conventions. The key names the move. A retry with the same key returns the swap it already created, with Idempotency-Replayed: true; the same key with different accounts or a different amount is 409 idempotency_key_reused, which is the guarantee that a repeated call is never a second conversion.

Paying straight out of the custodial balance

A client that holds FDIC_USD does not have to swap before it pays. POST /dapi/v1/payouts with "sourceAccount": "FDIC_USD" spends that balance directly: the payout is authorized by an intent signature instead of a debit, so the response carries authorization: "intent" and no debit, and confirm is unchanged (Payouts). Use a swap when the client wants the money on its wallet, and a payout when it wants a beneficiary paid.

In the sandbox

Swaps are the one money route the sandbox does not serve. The emulated chain has no exchange venue behind it and the emulator issues no custodial dollar balance, so POST /dapi/v1/swaps there answers a refusal rather than a quote. Build against the contract on this page and exercise it in production (Sandbox).

Next: Balances and transactions

Read a client’s balance, walk its ledger with a cursor, export a statement.