Valta Docs
Escrow
Available now, via
POST /api/v1/escrow/create|fund|release|dispute. Not yet wrapped by the SDK. Currently scoped to a single account's own agent wallets — cross-account escrow (a real buyer/seller relationship between two different Valta accounts) is not yet supported.
An escrow holds funds between two agent wallets until a release or dispute resolves them. Its state machine:
pending → funded → released
→ disputed
released and disputed are mutually exclusive terminal states — real ones, not just documentation. Whichever request wins is decided by a guarded compare-and-swap on the escrow row itself (UPDATE escrow_agreements SET status = 'released' WHERE status = 'funded' — an update against a row that's already moved past funded affects zero rows and is rejected). If a release and a dispute are fired at the same moment, exactly one succeeds; the other gets a real blocked_reason, not a race condition.
Why this needs to be real, not just documented
A naive implementation checks the escrow's status, then updates it — two separate steps. Two concurrent requests can both pass the check before either has written its update, and both proceed: a double-release (funds credited twice) or a release racing a dispute with no defined winner. Valta's escrow routes lock the escrow row (and the wallet balances they touch) inside one database transaction before ever writing to it, so the second of two concurrent calls genuinely waits for the first to finish, then sees its real, already-committed outcome — not a stale read.
The four calls
| Call | Effect |
|---|---|
POST /api/v1/escrow/create | Opens a pending escrow between two wallets. No funds move yet. |
POST /api/v1/escrow/fund | pending → funded. Debits the source wallet. |
POST /api/v1/escrow/release | funded → released. Credits the destination wallet. |
POST /api/v1/escrow/dispute | funded → disputed. No funds move — this only marks the escrow disputed. Resolving a dispute (arbitration, refund) is not built yet. |
All four require authentication (a real API key via x-api-key, or a logged-in dashboard session) and check that the caller is a party to the wallet(s) involved — create/fund require you to own the source wallet; release/dispute accept either party (funder or recipient).
curl https://valta.co/api/v1/escrow/create \
-H 'x-api-key: vlt_live_...' \
-H 'Content-Type: application/json' \
-d '{"fromWalletId": 42, "toWalletId": 91, "amount": 500}'
curl https://valta.co/api/v1/escrow/fund \
-H 'x-api-key: vlt_live_...' \
-H 'Content-Type: application/json' \
-d '{"escrowId": 7}'
Worked example: release racing a dispute
A funded escrow (id: 7, $500) receives both calls within milliseconds of each other — the funder disputes a delivery problem right as the recipient marks it complete and triggers a release.
Whichever call's transaction commits first wins. Say release wins:
// POST /api/v1/escrow/release response
{
"success": true,
"escrowId": 7,
"status": "released",
"intentId": "int_a1b2..."
}
The dispute call, having lost the race, gets a real denial — not a generic 500, and not a silent no-op:
// POST /api/v1/escrow/dispute response (409)
{
"error": "This escrow was already released before the dispute could complete"
}
Pulling the export for the dispute's intent_id shows the same story from the audit side — state: "blocked", a blocked_reason naming exactly what it lost to, and an xor_siblings entry pointing at the release intent that won. See Payout Intents for the intent state machine underneath escrow, and Intents API Reference for the export endpoint.
Idempotency
Every escrow call accepts the same Idempotency-Key header (or an intent_id body field) as spend calls — a retried fund/release/dispute with the same key replays the original outcome instead of acting twice. See Payout Intents for the full retry contract.
Not yet built
- Dispute resolution.
disputedis a real, exclusive terminal state — what happens after (arbitration, refund-to-funder) is not built. - Cross-account escrow. Both wallets in an escrow currently need to resolve to real
bot_walletrows the API can check ownership on; there's no first-class "counterparty on a different Valta account" concept yet. - SDK support. No
valta.escrow.*resource yet — use the REST calls above directly.