Payment Intents API
The canonical Brain HTTP surface for proposing, approving, and executing financial actions is the PaymentIntent family. The agent_id-keyed proposal route from earlier drafts (POST /v1/agents/{agent_id}/propose) and the /v1/actions/* paths are not implemented. Both are documented as deprecated stubs in the spec and return 404. Use the routes below.
Create (propose)
POST /v1/payment-intents
Get
GET /v1/payment-intents/{id}
Approve (confirm-mode)
POST /v1/payment-intents/{id}/approve
Reject
POST /v1/payment-intents/{id}/reject
Execute (gated)
POST /v1/payment-intents/{id}/execute
Pause / Resume
POST /v1/payment-intents/{id}/{pause,resume}
Replay-investigation
GET /v1/payment-intents/{id}/replay-investigation
Agent-driven full run
POST /v1/agents/run (see Agents API)
Propose a Payment
POST /v1/payment-intents
Authorization: Bearer <token>
Content-Type: application/json
{
"action_type": "ach_outbound",
"source_account_id": "acct_ops",
"destination_counterparty_id": "cp_aws",
"amount": "7800.00",
"currency": "USD",
"invoice_id": "inv_8231",
"evidence_ids": ["rp_001"]
}action_type is one of ach_outbound | ach_inbound | wire | onchain_transfer | erp_writeback | card_payment | x402_settle | escrow_release. amount is a decimal string. The valid currency depends on action_type: the two on-chain settlement actions (x402_settle, escrow_release) require USDC and reject three-letter codes, while every other action requires a three-letter code matching ^[A-Z]{3}$ and rejects USDC.
For the special invoice shortcut (resolves amount / currency / counterparty / source / evidence from a Ledger invoice):
Response (201 Created) is a full PaymentIntent with a PolicyDecision already attached:
Errors: 400, 403, 404 (invoice not found / not accessible), 409 (invoice already paid / agent_proposal_duplicate), 422.
Get a PaymentIntent
Returns the same PaymentIntent shape as above. 404 if unknown or tenant-isolated.
Status Lifecycle
proposed
Created; Policy is evaluating
pending_approval
Policy returned confirm; awaiting approver signatures
awaiting_second_approval
First approval recorded; a distinct second approver must sign
approved
All required approvals collected (or Policy returned allow)
paused
Kill-switch hold on an approved intent; resume re-runs the gate
dispatching
Gate passed; execution enqueued to the outbox, settling async
rejected
Policy returned reject, or an approver rejected
executed
Rail dispatch succeeded
failed
§6 gate failed or rail dispatch errored
cancelled
Cancelled before approval, or from paused → cancelled
dispatching is a full PaymentIntent state, not an outbox-only one: execute transitions the intent approved → dispatching and it stays there until the outbox worker settles it to executed or failed. The execution row the worker drives has its own separate ExecutionState values (dispatched, in_flight, completed, failed).
SDK status aliases. The SDK's higher-level action.status collapses these HTTP states onto the policy-decision triple: proposed / approved → auto, pending_approval → needs_approval, rejected → rejected; executed, failed, and cancelled pass through unchanged. So SDK code branching on "auto" is matching the same state HTTP code sees as approved. See Policy → decision vocabulary across surfaces.
Approve a pending_approval Intent
pending_approval IntentNo request body. Returns 200 with the updated PaymentIntent. Approvers are determined by Policy (the confirm rule's required_approvers / quorum); each approver hits this endpoint independently and the intent flips to approved once the quorum is met.
Reject
reason is optional (≤ 500 chars). Returns 200 with the rejected PaymentIntent.
Execute an Approved Intent
No request body. Runs the deterministic §6 pre-execution gate against live Ledger state, then atomically transitions the intent approved → dispatching and enqueues a pending outbox row. The outbox worker dispatches the rail and settles asynchronously.
202 Accepted:
execution_id is null on this immediate response and populated when the worker picks the row up. Settlement notifications arrive via the rail-specific webhook (e.g. Plaid TRANSFER_EVENTS_UPDATE).
A gate failure returns 409 with payment_intent_gate_failed and details naming the failing check (see Errors → Pre-execution gate failures).
Rails
The rail returned on execute is not the same vocabulary as the create-time action_type. The mapping:
rail
Implementation
bank_ach
Plaid Transfer (authorize → create; settled async via webhook)
onchain_base
BrainSmartAccount.executeViaSessionKey (Base)
erp_writeback
NetSuite SuiteTalk (fail-closed stub)
x402_base
USDC-on-Base settlement (mapped from x402_settle; unregistered at boot, fail-closed)
escrow_base
BrainEscrow lock release (mapped from escrow_release; unregistered at boot, fail-closed)
notification
Surface-to-human (no money path)
The x402_base and escrow_base rails are shadow-first: they throw rather than fake-settle until promoted.
Pause / Resume (Kill-Switch)
An approved intent can be held without a terminal transition, then released:
No request body for either. Resume re-evaluates the §6 gate against the current Ledger state. Defending against drift while paused. And returns 409 if any check now fails.
A halted agent (POST /v1/agents/{agent_id}/halt) pauses every one of its in-flight intents at once.
Replay Investigation
Typed forensic record. The intent, each execution (with its typed rail receipt), and the linking ids you'd join to reconstruct exactly what happened:
The policy decision and the audit chain are referenced by id and joined via their owning service APIs (Policy + Audit).
Agent-Driven Runs
Most agent activity goes through the higher-level run endpoint, which routes → resolves an action → dry-runs the §6 gate → persists an agent_runs row → proposes through this same gated path:
See the Agents API for the full run / routing / kill-switch surface.
What's Next
Last updated
