Audience: Developers, Support
Outcomes: Real-time UX; robust back-office sync; resilient clients
Events & notifications
SSE (client)
const es = new EventSource('/api/events/stream', { withCredentials: true }); ['order.status.changed','payment.succeeded','contract.message','dispute.opened'].forEach(name => { es.addEventListener(name, e => console.log(name, JSON.parse(e.data))); });
Common events
Orders:
order.created
,order.status.changed
Contracts:
contract.signed
,contract.change_request
Payments:
payment.intent.created
,payment.succeeded
,payment.failed
Escrow:
escrow.released
Disputes:
dispute.opened
,dispute.resolved
Webhooks (providers)
Stripe route uses raw body + signature verification.
Bank route uses HMAC.
Handlers are idempotent; store processed event IDs.
Error model, idempotency & retries
Error format
{ "code":"VALIDATION_ERROR", "message":"Invalid payload", "issues":[{"path":["currency"],"message":"Must be a 3-letter code"}] }
Common codes
UNAUTHORIZED
(401),FORBIDDEN
(403 incl. KYC),NOT_FOUND
(404),
VALIDATION_ERROR
(400),INTERNAL_ERROR
(500).
Idempotency
Provide a stable reference (header or body).
Webhook handlers must dedupe by event ID.
Retry strategy
Network/5xx → exponential backoff.
4xx → fix payload/permissions first.
Rate limits, pagination & filtering
Default rate limit: 100 req/min per org (plan-dependent).
Pagination:
?limit=50&cursor=...
withnextCursor
in responses.Filtering:
?status=InProgress&from=2025-01-01&to=2025-12-31
.Back off on 429; reuse cached reads.
QA checklist
Sending same webhook twice doesn’t duplicate side effects.
Client respects
429
with backoff; resumes fromnextCursor
.SSE stream reconnects and resumes without message loss (at-least-once semantics).