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.changedContracts:
contract.signed,contract.change_requestPayments:
payment.intent.created,payment.succeeded,payment.failedEscrow:
escrow.releasedDisputes:
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=...withnextCursorin 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
429with backoff; resumes fromnextCursor.SSE stream reconnects and resumes without message loss (at-least-once semantics).
