Skip to main content
Six actions, one resource. POST /dapi/v1/escrow/{escrowId}/actions prepares one and returns a payload to sign; POST /dapi/v1/escrow/{escrowId}/actions/{idempotencyKey}/confirm submits the signature. This page is the matrix.

The matrix

approve is not a verb you send: it is prepared for you by POST /dapi/v1/escrow and confirmed under the create’s own key. Sending an amount with void or reclaim answers 409 amount_not_allowed; omitting it on the four that need it answers 422 validation_failed. charge is authorize and capture in one on-chain action — the money goes straight to the seller with no hold in between. Use it when there is nothing to wait for.

Preparing an action

Request — POST /dapi/v1/escrow/esc_5Qb4e17c9d/actions, Idempotency-Key: order-A-1187-capture
201 for a new action, 200 with Idempotency-Replayed: true when the key replays one. A replay returns the same record — with its approval if that is still open, without it once the action was submitted. The same key with a different body answers 409 idempotency_key_reused.
Response — 201
The idempotency key you sent is the action’s address. Send no key and HEVN derives one, echoes it as action.idempotencyKey, and that derived value works in the confirm URL exactly the same way. Either way, a lost prepare response costs you nothing: you already know where to confirm.

Confirming an action

Sign the decoded approval.payload bytes — never the base64 string, never a re-serialized JSON object.
Request — POST /dapi/v1/escrow/esc_5Qb4e17c9d/actions/order-A-1187-capture/confirm
publicKey is optional and only skips a scan of your registered keys. HEVN verifies the signature locally, against the payload it stored, before it consumes the approval — a wrong signature costs nothing but the call. This confirm needs the escrow:sign scope; a key without it answers 403 forbidden with details.requiredScope, before anything is verified. HEVN then re-checks the developer key behind the session immediately before it co-signs: a key deleted between prepare and confirm, or a confirm sent from an IP outside the key’s allowlist, answers 403 and signs nothing. The prepared action is untouched, so a confirm from an allowlisted host still works.

One action in flight

A signing wallet runs one on-chain action at a time — across every deal it signs for, not just this one. While one of yours is signing or submitted, the next prepare or confirm answers 409 action_in_flight. The refusal names no deal, so a worker that drives many deals has to know which one it left in flight: serialise your actions per signing wallet and keep the in-flight deal id on your side, then POST /dapi/v1/escrow/{escrowId}/sync it before retrying.

Statuses

The deal’s status is a label for the last thing that landed. Decide with availableActions, capturableAmount and refundableAmount.

Reads

GET /dapi/v1/escrow/{escrowId} returns one deal with its last action. GET /dapi/v1/escrow lists the deals you operate, newest first, cursor-paged:
status repeats, so it goes in the path rather than the client’s parameter map, which holds one value per name. Filters are status, senderClientId and receiverClientId. The envelope is { "items": [...], "nextCursor": "…" }; pass nextCursor back as cursor until it is absent. A cursor from another query answers 400 invalid_cursor. POST /dapi/v1/escrow/{escrowId}/sync reconciles the deal against the chain and returns the same shape plus consistent. It reads the chain at most once every 15 seconds per deal; calls in between answer the stored projection, which is why polling it in a tight loop buys nothing. There is no separate action list: the deal read carries the last action, and every action is addressable by the key you chose.

Refusals

action_in_flight, escrow_state_changed and escrow_action_not_found are the three that change your control flow, and all three are handled above. The full registry, with every status code, is in Errors. One shape worth knowing: a closed window or an over-large amount arrives today as a bare 400 invalid_request — the contract-level reason (preApprovalExpiry has been reached, amount exceeds available amount) is logged on our side but not published, so message is the generic one and details is absent. Treat it exactly like window_closed and amount_out_of_range: re-read the deal and pick from availableActions, which is computed from the same windows and amounts.

Next: Sandbox

The same 45 operations against an emulated partner on Base Sepolia.