Audience: PMs, Finance, Devs, Admins
Outcomes: Deposit verified; order moves to InProgress
; milestone releases logged
Request deposit — Cards (Stripe)
curl -X POST $API_BASE/api/payments/intents -b cookies.txt \ -H 'Content-Type: application/json' \ -d '{"orderId":"ord_123","amount":150000,"currency":"USD"}'
Request deposit — Bank (open banking)
curl -X POST $API_BASE/api/payments/bank/link -b cookies.txt \ -H 'Content-Type: application/json' \ -d '{"orderId":"ord_123","amount":150000,"currency":"EUR","returnUrl":"https://app.example.com/return"}'
Rule of motion
Status moves to
DepositPaid
only after a verified webhookFrom there, PM sets
InProgress
(or auto-move per policy)
Watch with SSE
const es = new EventSource('/api/events/stream', { withCredentials: true }); es.addEventListener('payment.succeeded', e => console.log('deposit ok', JSON.parse(e.data)));
Release funds (per milestone)
curl -X POST $API_BASE/api/escrow/release -b cookies.txt \ -H 'Content-Type: application/json' \ -d '{"orderId":"ord_123","milestoneId":"m2","amount":100000}'
Events
milestone.released
, payment.succeeded
(if 3rd-party), order.status.changed
Invoices
Releases generate/update invoices; download from the order or
GET /api/invoices/:orderId.pdf
Edge cases
Release fails → check escrow balance, crypto gas, provider outage; retry (idempotent)
Partial releases allowed; never exceed remaining milestone balance
QA checklist
Webhook with bad signature rejected; no state change
Duplicate release request deduped by reference/event ID
Runbook: “Deliverable accepted but no payout”
Verify a release was created; trigger manually or via policy and re-check webhook logs