Skip to main content
Nothing you sign is constructed by you. HEVN builds the operation, echoes its terms, and verifies the same terms again at confirm — your job is to check the echo and sign, not to assemble a transaction.

Prepare

You call the opening route — POST /dapi/v1/payouts or POST /dapi/v1/escrow/{escrowId}/actions. HEVN prices the payment, books it, builds the on-chain operation and stores an approval. The response carries quote, approval and debit.

Check

Compare debit.amount, debit.token and debit.address with the payment your own system intended, and refuse if they differ. This is the one guarantee the design cannot give you.

Sign

Base64-decode approval.payload and sign those exact bytes: ECDSA P-256 over SHA-256, DER-encoded, base64 out.

Confirm

Post the signature to the matching confirm route inside the approval window. HEVN verifies it locally, co-signs, and submits.

Where the mechanism is used

Payouts act on a client — send X-Hevn-Account. Escrow is always operated by you, so it refuses that header. Every confirm body is the same two fields:
publicKey is optional; it only saves HEVN a scan over your account’s keys. A key that is not one of them is 403 key_not_registered — see Developer key. Every other call in these guides carries a cURL tab. The confirms do not: a signature is produced by your key over bytes HEVN just handed you, so a curl block could only show a stale one. The prepare call has a cURL tab, the confirm is Python, Node and Go.

What you sign

approval.payload is base64 of a canonical JSON document — the authorization HEVN will execute on your behalf. Decoded, one from a payout looks like this (formatted here to read; the real bytes carry no whitespace):
Sign base64decode(payload) verbatim. Never parse the JSON and re-serialize it. Any library that reorders keys, adds a space after :, escapes non-ASCII differently or appends a newline produces different bytes and a signature that is refused with 400 signature_invalid.

Signature format

Five ways to get it wrong, all of them answering 400 signature_invalid:
  • Raw r‖s. WebCrypto and Java’s SHA256withECDSAinP1363Format produce a fixed 64-byte signature. Convert it to DER before encoding.
  • base64url. - and _ are rejected; keep the standard alphabet and the = padding.
  • Whitespace. Do not wrap the base64 at 64 columns, and do not let a shell pipeline append a newline.
  • Signing the string. Sign the decoded bytes, not the base64 text.
  • Hashing twice. Most libraries hash for you. Do not pre-hash and then sign the digest as a message.
Sign on your server. The key can authorize a spend from every client wallet you control, so a browser is the wrong place for it.

What HEVN binds, and what it verifies

You are not trusted to describe the operation, and you are not asked to. At prepare, HEVN:
  • loads the target itself — the booked payout, the contact you named, the deal — and refuses anything that is not in a fundable state;
  • checks that your account controls the client’s wallet and that the wallet’s live on-chain owner is your signer;
  • builds a single stablecoin transfer from the client’s wallet, with the amount and destination taken from what it just booked, and checks the balance covers it;
  • re-validates that built operation against the stored terms before it hands you anything to sign;
  • pins an idempotency key of its own on the authorization, so the same signature cannot be replayed as a different request;
  • binds the approval to your account, the target, the wallet and your signer, and stores it with an expiry.
At confirm, your signature is verified locally against the stored bytes before anything is consumed. A wrong signature is a refusal that costs you nothing: the approval survives, and you can fix the signing and confirm again. The credential is re-checked at both ends. Every request on this API re-reads the developer key behind your session, the address the request came from and the scopes the key holds, and the confirm re-checks them once more immediately before HEVN co-signs — so a key deleted between the prepare and the confirm, or a confirm sent from an address outside its allowlist, is a 403 and nothing is signed. See Developer key.
Check the echo anyway. debit.amount, debit.amountAtomic, debit.token, debit.chainId and debit.address are decoded from the very operation you are about to sign — comparing them against your own record is the only check HEVN cannot make for you.

Approvals are single-use and short-lived

An approval is live for 120 seconds (60 for escrow actions), and approval.expiresAt is in the response. Three rules follow:
  • Prepare, check, sign and confirm in one pass. Never queue a payload for a human to approve later — it will expire.
  • Re-opening is free and idempotent. Call the opening route again with the same Idempotency-Key: an expired-and-unused approval is replaced with a fresh one on the same booked payment, at the same price. It does not re-quote and does not pay twice.
  • Re-open as often as you need. There is no attempt budget, because there is nothing to budget: every re-opened approval prepares an operation in the same on-chain slot, and a slot can only be spent once. See Why a retry cannot pay twice.

Retrying a confirm

Confirm is designed to be repeated, and it never pays twice. This table is the whole policy:
After a 202, a 502 or a timeout, confirm again rather than re-opening — a signed operation may still be in flight, and confirming is what resolves it. Re-open only on an explicit failed or funding_attempt_expired. Re-opening after a signed operation is not dangerous — the chain refuses the second one — it just answers payment_slot_consumed instead of telling you what happened.
The same policy as code, in one place:

Next: Clients

Create a client company, poll it to ready, and keep one id.