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
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 decodedapproval.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 issigning 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 withavailableActions, 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.