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
Two more names can appear in
availableActions and are not verbs you send. approve is prepared by POST /dapi/v1/escrow and is the buyer’s to sign with their own key. reclaim is the buyer’s own way to take an uncaptured hold back after holdUntil. Sending either as action answers 422 validation_failed. Sending an amount with void 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.
HEVN 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 operations against an emulated partner on Base Sepolia.