Valta Docs
Payout Intents
Available now, via the REST API described below (
POST /api/v1/spend,POST /api/v1/escrow/*). Not yet wrapped by the SDK — see SDK support.
An intent is the record of one financial decision: an agent spend, an escrow fund/release/dispute, or a transfer. It has one ID, and it moves through exactly one state machine:
authorized → submitted → settled
→ failed
→ blocked
- authorized — claimed, not yet acted on.
- submitted — in flight (used internally by XOR-guarded actions; see Escrow).
- settled — the terminal success state. Money moved, or the record was created. Never revisited.
- failed — an unexpected error during execution (a DB error, a rail failure). Safe to retry with a new intent.
- blocked — a real denial: a policy check failed, a limit was hit, or the intent lost a mutual-exclusion race (see Escrow).
Once an intent reaches settled, failed, or blocked, it never changes again. There is no "paid in the app, pending on the rail" state — an intent is either still open, or it's done.
Why this exists
Without an idempotency key, "did this spend actually happen?" only has an honest answer if the original request's response was received. If a client times out waiting for POST /api/v1/spend and retries, the naive retry either double-spends or the caller has no way to know which outcome is real. An intent ID makes the retry safe: the same key always resolves to the same outcome.
Using an intent ID
Pass an idempotency key on any spend or escrow call — either header works:
curl https://valta.co/api/v1/spend \
-H 'x-api-key: vlt_live_...' \
-H 'Idempotency-Key: your-own-uuid-here' \
-H 'Content-Type: application/json' \
-d '{"agent": "agent_123", "amount": 25, "purpose": "monthly payout"}'
If you omit it, Valta mints one server-side so the spend still gets a real intent record — but a server-minted key only protects a caller who resends the exact same request twice. It does not help the "client crashed before it saw the response, and has no key to resend" case. For that, generate your own key before the first attempt and persist it (a UUID keyed to your own payout record is enough) so a retry after a crash can reuse it.
Worked example: timeout, then retry
- Your server calls
POST /api/v1/spendwithIdempotency-Key: payout_9f2a...for a $40 agent payout. - The request is processed — the wallet is debited, the intent settles — but the response never reaches your server (network drop, your process restarts mid-request).
- Your retry logic fires again, 30 seconds later, with the same
Idempotency-Key. - Valta finds the existing intent already
settled, does not debit the wallet again, and returns the original outcome:
{
"approved": true,
"tx_id": "sdk_7c1e9a2b4f0d3c88",
"amount": 40,
"currency": "USDC",
"wallet": "Payouts",
"new_balance": 460,
"replayed": true
}
replayed: true tells you this was a dedup hit, not a fresh spend — the balance and transaction are exactly what they were after step 2, not doubled.
If step 2 had instead failed (policy denial, insufficient balance), the retry in step 3 would return the same denial and reason, not attempt the spend again — with an HTTP 409 if the original attempt is still genuinely in flight.
What a spend response tells you
| Field | Meaning |
|---|---|
intent_id | The canonical ID for this decision — pass to the export endpoint for a full proof document. |
approved | true only once the intent has settled. |
replayed | true if this response is a dedup hit on a prior attempt, not a new execution. |
reason | Set on any denial — the same string recorded as blocked_reason on the intent. |
requires_approval / request_id | The intent is deliberately left non-terminal, pending a human decision — see Approvals. |
SDK support
packages/valta-sdk does not yet generate or persist idempotency keys automatically, and has no valta.intents.* resource. Until it does, pass Idempotency-Key yourself as shown above. Tracked as a near-term follow-up — the REST contract is stable now, the SDK ergonomics are catching up.