Skip to main content

Providers & rails

What this covers / who it's for. Which capabilities Stables executes itself and which it offloads to the underlying finance rail; what the platform owns vs what the provider owns; how provider truth reaches our records (webhooks + reconciliation); and why our view of a status can briefly lag the provider's. Read this to reason about latency, trust boundaries, and "who do we call when X is wrong".

Multiple providers, one neutral seam

Stables integrates external finance providers behind a single neutral capability seam — domain code asks for a capability (onboard a customer, create a wallet, execute a payout), never for a named vendor (rails module README; registry: app-modules/rails/config/rails.php). Which provider actually serves a capability is chosen at runtime by the tenant's jurisdiction, so different providers can serve the same capability in different regions (ADR D94, superseding D60's "replaceable, not concurrent"). The engineering provider-integration page carries the machinery.

Five providers exist in the codebase today, at several ship-states — this is a genuinely multi-provider platform now, not a single-rail one:

ProviderServesShip-state
ConduitThe money rail — accounts, deposits, payouts, orders, virtual accounts — for every jurisdiction; plus onboarding everywhere Sumsub is not primaryLive — the incumbent, still authoritative for all financial state except custody (now routed to Utila, pre-product)
SumsubOnboarding (KYB/KYC + the beneficial-owner ownership graph) — primary for GB + EULive but pre-product — code-live, production-gated (below)
UtilaCrypto custody (MPC wallets) — primary for all six jurisdictionsLive-in-code, pre-product — routed as the custody primary (D98), deploy-gated; not serving real production custody yet
FingerprintFraud / device intelligence — device-risk screening on login, sign-up, and payout — primary for all six jurisdictionsLive-in-code, deploy-gated — the screening feature ships default-disabled + central-context screening config-gated off (below); pre-product
SEONFraud / device intelligence — same screening capability — permitted (secondary) in all six jurisdictionsLive-in-code, deploy-gated — as above; pre-product

Which provider serves a capability (the routing rule)

Availability follows the same regulatory-first logic as plans & entitlements: a tenant's jurisdiction must license a provider for a capability before that provider can serve it. Routing is per (capability group × jurisdiction) — capabilities are grouped (onboarding, custody, fiat rails, …), and each group resolves to one provider for a given jurisdiction. If no licensed provider can serve a group for a tenant's jurisdiction — an unlicensed region, a data-residency mismatch, or a suspended jurisdiction — the action is refused outright, never quietly redirected to some default provider. An operator can pin a specific tenant to one of its jurisdiction's permitted providers, but can never pin one the jurisdiction forbids (compliance beats commerce).

Onboarding — live multi-provider (Sumsub for GB/EU)

Onboarding is the first capability where two providers serve concurrently. For GB and EU tenants, KYB/KYC and the beneficial-owner ownership graph route to Sumsub (its primary); ZA/NG/KE/US stay on Conduit, which also remains a permitted fallback everywhere (ADR D95).

Individuals and businesses are peer onboarding subjects (ADR D110). The client DECLARES which — the vocabulary is individual (KYC) or business (KYB) — on both the requirements read and the application submit; there is no silent default (previously every applicant was silently created as an individual, so KYB was impossible). Sumsub serves both subjects (each with its own dashboard verification level); the Conduit adapter is reference-only and serves only the business subject (it fails closed for an individual). An individual's application is approved on their own verification without a beneficial-owner gate; a business still requires its ≥25% ownership graph complete. A GB/EU tenant genuinely onboards through Sumsub today — but this is code-live and pre-product: it is not serving real production traffic, held behind three go-live gates a product person should know about (10-onboarding-identity.md):

  • The frontend must branch on the verification mode. For a Sumsub route, verification hands back an embedded-WebSDK token rather than a hosted redirect URL; the SPA (a separate frontend repo) must mount the WebSDK accordingly, or the live KYC flow breaks.
  • EU/UK data residency + a DPA must be bound before real EU/GB personal data routes to Sumsub (the grant shipped with residency enforcement deliberately off, matching Conduit's incumbent posture — an accepted pre-product deferral).
  • A stuck-application runbook must land: the KYB approval gate never rejects or re-opens, so a ≥25% owner who never verifies leaves an application pending indefinitely with no SLA or operator override today. (The other half of this gap — the KYB approval gate trusting a locally mirrored owner set that a missed owner update could leave stale — is now closed: an hourly sweep re-pulls the authoritative ownership graph for every open application; see the onboarding module README.)

Custody — now routed to Utila (all six jurisdictions)

Crypto custody now routes to Utila, an MPC custodian with no human co-sign ceremony — approvals are API votes evaluated by a policy engine, not a URL a person visits. The full path shipped — the adapter, the webhooks, the in-app approval quorum, and the real forward — and the go-live grant (ADR D98) routes Custody to Utila across all six jurisdictions (ZA/NG/KE/GB/EU/US). Conduit is retained only as an override-only escape hatch (an explicit per-tenant pin for legacy Conduit-modeled flows) — not an automatic fallback: Conduit cannot serve custody transactions, so a custody move under a Conduit override fails loud rather than mis-routing money. This is live-in-code + deploy-gated (a deploy provisions the Utila vaults + per-vault secrets and runs the grant) and pre-product — not serving real production custody money yet. An EU data-residency + DPA binding and an accepted travel-rule field drop remain go-live carry-overs (20-custody.md §5a; ADR D98) — noted, not restated here. Introducing Utila also reshaped the custody contract away from Conduit's hosted-URL signer/quorum ceremony toward neutral API-vote approvals (ADR D96) — likewise code-live but pre-product (no live tenant relies on it).

The inbound half is now built too — Utila signs its webhooks with a different (asymmetric) scheme, and the platform accepts, verifies, and stores those deliveries. When a Utila custody transaction changes, its webhook is a lightweight "something changed" nudge, so the platform reads the authoritative transaction back from Utila and records our own copy from that — never trusting amounts off the raw notification (keeping money exact). A transaction's AML-screening outcome opens a compliance case when it comes back deny or alert — and, by deliberate design, opening a case never freezes the account or blocks the transfer; stopping money stays a separate human decision (Compliance & trust). Like the outbound adapter, this feed is live-in-code + deploy-gated: custody now routes to Utila (D98), but no real custody webhook is processed until the deploy-time write-path steps (vault + secrets) land — pre-product.

Fraud / device intelligence — a segregated, off-umbrella capability

Fraud / device-intelligence screening is a genuinely new capability, served by two engines — Fingerprint (primary) and SEON (permitted) — across all six jurisdictions (ADR D102; fraud module README). It is deliberately off the money-rail umbrella: a device-risk verdict never touches funds, so it is routed on its own seam, kept separate from the capabilities that move money. The verdict shields the sensitive moments — login, sign-up, and payout — with two deliberately different postures: a screening outage on login / sign-up fails open (proceed, never brick a login), while a payout fails closed to step-up and records the skip rather than sliding money through silently. A deny blocks the action and opens a compliance case (Compliance & trust); the full failure behaviour is on Dependencies & failure modes. Like the other new providers this is live-in-code + deploy-gated: it stays inert until ops enables the screening feature — noted, not restated here.

Conduit configuration facts

Two ship-state facts about the money rail a product person should know:

  • The base URL defaults to Conduit's sandbox. A deployment must explicitly point at production (packages/stables/conduit-sdk/config/conduit.php) — the default is sandbox precisely so a misconfigured environment can never accidentally touch production money. Sandbox and production are fully isolated (separate keys, separate data).
  • Outbound calls time out after 30 seconds by default (same config) — the ceiling on how long a synchronous provider-backed action can hang before failing.

What is offloaded (the capability surface)

The rails module defines 13 capability contracts — the complete, enumerable list of what the provider does for us (app-modules/rails/src/Contracts/):

CapabilityWhat the provider executes
Customer onboardingCreating/updating the business customer record on the rail (KYB)
Identity verificationKYC checks on persons, via the provider's KYC vendor
Customer managementReading/maintaining the provider-side customer
WalletsCustodial wallet creation and lifecycle
Wallet signersRegistering signing parties on a wallet
Signing quorumCo-sign / quorum rules for wallet actions
Virtual accountsFiat virtual account issuance
OrdersFX / conversion order execution
PayoutsOutbound payment execution
TransactionsThe provider-side transaction ledger (deposits included)
Whitelist recipientsApproved-recipient (whitelist) management
Registered addressesRegistered crypto address management
DocumentsReceiving the KYB/KYC documents we forward

Fraud / device-risk screening (AssessesFraudRisk, served by Fingerprint/SEON) is not one of these 13 — it is a separate, off-umbrella capability, segregated from the money-rail contracts precisely because a device-risk verdict never moves money (fraud module README; ADR D102).

The ownership split (handoff model, V1)

The platform ownsThe provider owns
The durable record: a local mirror of every provider entity, plus everything the provider never sees (teams, roles, preferences, cases, notifications)Custody of funds and wallets
The audit trail — every action, denial, and failure, tamper-evident (Compliance & trust)Actual money movement (payouts, orders, deposits settle on the rail)
Authorization and dual control — RBAC, maker/checker, co-sign approval flows run here before anything reaches the railThe KYC vendor relationship and verification decisioning
The encrypted document vault (we store first, then forward — Where data lives)SAR/CTR filing with regulators in the V1 handoff model — we prepare and record filings, the provider files (Compliance & trust)

Every outbound call is logged

Every request to the provider is recorded per attempt in the append-only central vendor request log: bodies are encrypted at rest, known-sensitive fields (credentials, tax IDs, account numbers, …) are redacted before persisting, and oversized bodies are truncated (16 KB cap). The log is transport telemetry, not the compliance record — it is pruned on a 90-day window (the authoritative KYB/KYC record lives in the document vault + onboarding mirror). Source: app-modules/rails/config/rails.php, rails README.

How provider truth comes back (and why our view can lag)

Provider-backed actions are submit-then-listen: we submit, the provider answers "accepted", and the real outcome (KYB progression, wallet created, payout settled, order filled) arrives later as a webhook (Async by design).

Inbound webhook mechanics (webhooks README):

  • Signed, with a replay window. Deliveries are signature-verified, fail-closed — each provider brings its own scheme (Conduit and Sumsub sign differently), and Conduit deliveries older than 300 seconds (or dated in the future) are rejected (app-modules/webhooks/config/webhook-client.php).
  • Exactly-once. Duplicate deliveries (same source + event id) are detected and skipped, so a provider retry can never double-apply a status change.
  • Processed on a dedicated queue (webhooks), separate from all other background work.

Consequence: our view is eventually consistent with the provider's. Normally the lag is seconds (webhook delivery + queue processing). If the webhooks queue is backed up or its worker is down, statuses freeze at their last-known value while the provider moves on — see Dependencies & failure modes.

The healing layers (when a webhook never arrives)

  1. The provider retries. Conduit redelivers failed webhooks on a retry ladder (30s → 2m → 15m → 1h → 4h), covering transient drops (docs/tracking/conduit-funding-deferrals.md).
  2. The hourly reconciliation sweep. Every hour, mirrors that have gone stale (no update within their expected window and not in a terminal state) are re-read directly from the provider API and healed (provider-mirror README, ADR D59). Limit: the sweep heals coarse status only. Some webhook-only sub-states (e.g. a deposit held for sender information) are not exposed by the provider's read API, so a permanently lost webhook for those specific transitions is not sweep-recoverable — the retry ladder is the cover for them (funding README).
  3. Drift records. When reconciliation finds the provider and our mirror disagree, the divergence is recorded as a drift record for operator review — field names and coarse change markers only, never raw PII values. Unresolved drift is never auto-deleted; it is live evidence until an operator resolves it (provider-mirror README).

What this means in practice

  • A status shown in Stables is the last thing the provider told us — trust it to seconds under normal operation, to about an hour under webhook loss (coarse states), with the named sub-state exceptions above.
  • "The provider says X but we show Y" is expected transiently; if it persists past the hourly sweep, look for a drift record — that is the designed operator surface for divergence.
  • Anything involving custody, settlement, or KYC decisioning is a provider conversation; anything involving who was allowed to do what, when, and what we recorded is ours.

← Product wiki index