Async by design
What this covers / who it's for. The four deliberate not-instant patterns in the platform — submit-then-listen, request-then-poll, fleet/background work, and after-the-action forwarding — what each status means to a user, how polling should behave, and how a lost session recovers. Read this before designing any UI or support flow around "why isn't it done yet".
There is no push transport in V1 — realtime is polling, by decision (ADR D67: the FE polls; broadcast/websockets are V2 — see the ADR index). Everything below describes what the client sees while polling.
Pattern 1 — Submit-then-listen (provider actions)
Anything the finance rail executes returns immediately as accepted, and the truth arrives later by webhook: KYB progression, wallet creation, payout and order status, co-sign confirmation. The user sees a pending state that resolves when the provider speaks — normally seconds, longer if webhooks stall (Providers & rails). The UI's job is to poll the record, not to wait on the request.
Pattern 2 — Request-then-poll (exports, statements, computed answers)
Long-running client requests — transaction exports and monthly statements today — return HTTP 202 plus a pollable request id instead of the result (async-requests README, payments README).
The lifecycle is fixed and small: queued → processing → succeeded | failed. Terminal states
are immutable — a succeeded or failed request never changes again.
| Status | What it means to the user |
|---|---|
queued | Accepted; work has not started. Nothing is wrong — keep polling |
processing | A worker picked it up. Keep polling |
succeeded | Done. The response carries a typed result: a link (short-lived download URL), a confirmation, or an answer (inline data) |
failed | Terminal, with a safe reason (never internal error detail). Retry = submit a new request |
Poll etiquette (the platform's own guidance): poll every 2–5 seconds with backoff while non-terminal, stop on a terminal status. Result links are short-lived, but an expired link is never a dead end — re-fetch the request and follow the link again; the artifact download itself is separately permission-gated, so being able to see a poll result does not grant the bytes.
Recovery surface: a reload or a returning session re-discovers its in-flight and recent requests through the async-request list endpoint (filterable, newest-first) and resumes polling by id. The list is retention-bounded (terminal requests are pruned after 30 days — Data lifecycle guarantees), so it is recent history, not an archive.
Pattern 3 — Fleet & background work (the task runner)
Scheduled multi-unit work — reconciliation sweeps, monthly statement generation, document forward re-drives — runs as batched task runs: one unit per tenant (or per item), tracked on a durable, operator-visible ledger (task-runner README). Product-relevant guarantees:
- One unit's failure never aborts the fleet — one tenant's error can't stop everyone else's statement.
- Failed units are retryable by operators; the ledger (also exposed as a backoffice API) shows exactly which tenant/unit failed and why.
- Stuck detection: a unit still "running" after 120 minutes is flagged by an hourly sweep — detection and alerting only, never silent auto-healing.
Pattern 4 — After-the-action forwarding (documents)
Uploading a KYB/KYC document succeeds when it lands in our vault; forwarding it to the rail happens afterwards, on a queue. A forward failure never fails the upload — failed forwards are re-driven by an hourly sweep (documents README). Consequence: "uploaded here" and "received by the provider" are two separate moments, usually seconds apart.
The product-relevant clock (what runs when)
The full schedule lives in Observability & ops; the handful a product person should know:
| Cadence | What |
|---|---|
| Every 15 seconds | FX rate cache warming (rates served ≤ 20s stale) |
| Every minute | Compliance SLA breach scan |
| Hourly | Stale-mirror reconciliation; document forward re-drive; stuck-task detection |
| Daily | Retention prunes; document integrity sweep |
| Monthly (1st, 02:00) | Statement generation for every tenant |