Merchant Onboarding — Illustrated
How a merchant goes from signup to an operational, production-approved account. Two rails, one gate. Everything Circuit creates in the background (Cybrid, Persona, YellowCard, the platform USDC wallet) is shown per step.
- Cybrid rail — US/CA merchants. Paid via Cybrid ACH/wire to a Plaid-linked bank. KYB via Cybrid-brokered Persona.
- YellowCard rail — African merchants. Paid via YellowCard disbursement to mobile money or bank. KYB via Circuit's own direct Persona integration (Cybrid's Persona is US/CA-only).
The rail is derived from country at signup (US/CA → cybrid, else yellowcard).
1. The two onboarding tracks → one production gate

The gate is structural. authenticateMerchant.ts and authenticatePublishable.ts reject any *_live_* key unless isMerchantProductionReady(merchant) is true. *_test_* keys always work — that is the sandbox. No merchant can integrate with production keys before completing both steps.
5. Endpoint map
| Step | Cybrid rail (US/CA) | YellowCard rail (African) |
|---|---|---|
| Signup | POST /merchants/signup (country) | POST /merchants/signup (country) |
| KYB | POST /merchants/{id}/kyb (Cybrid-Persona) | POST /merchants/{id}/persona-kyb (direct Persona) |
| KYB completion | Cybrid webhook /cybrid/webhook | Persona webhook /persona/webhook (HMAC-verified) |
| Payout destination | POST /merchants/{id}/cybrid/bank-account (Plaid) | GET /merchants/{id}/yc/payout-networks → POST /merchants/{id}/yc/payout-destination |
| Readiness | GET /merchants/{id}/readiness | GET /merchants/{id}/readiness |
| Keys | POST /merchants/{id}/api-keys (test first) | POST /merchants/{id}/api-keys (test first) |
6. Sandbox vs production
- Sandbox is always on.
sk_test_*/pk_test_*keys pass auth at every onboarding state, so a merchant can integrate and test before approval. Sandbox uses the sandbox base URLs/keys for Cybrid, YellowCard, and Persona (per-stage env). - Production requires the gate.
sk_live_*/pk_live_*are rejected (403) untilisMerchantProductionReady— KYB verified and ≥1 validated cashout account for the rail. This protects both rails at one middleware choke point and closes the prior hole where an unverified merchant's live key would authenticate.
7. Cashout accounts (single vs multi-account)
A merchant's cashout account is a payout-destination record (one collection per merchant, one default per rail). The count allowed is gated by the merchant's account level (admin-set):
| Account level | Cashout accounts | Set by |
|---|---|---|
standard (default) | one per rail — a 2nd link is rejected (403) | — |
multi_account | many — one default + per-payment override | admin: POST /admin/merchants/{id}/account-level |
- Linking creates a validated destination row (first = default). A
multi_accountmerchant manages them viaGET/POST/DELETE /merchants/{id}/payout-destinations[...]. - A PaymentIntent may name a
destinationId; otherwise the default is used. The chosen destination is snapshotted onto the Payout at creation for both rails, so a later default change can't redirect an in-flight payout.