Banking Service Interface
The provider-neutral BaaS and CaaS API surface — accounts, payments, cards, crypto, FX, and membership plans under /v1/bank.
LP-3040: Banking Service Interface
Abstract
This LP standardizes the banking service interface served by bankd (luxfi/bank): one authenticated REST surface under /v1/bank covering accounts, balances, transfers, outbound payments, beneficiaries, cards, crypto movement, exchange, FX, and membership plans. The interface is provider-neutral by construction — the banking counterparty (issuer), FX venue, and crypto backend are deployment choices behind stable route contracts, never API differences. A single deployment switch (BANK_SANDBOX) selects the seeded testnet sandbox or live rails; the surface is identical in both. Clients integrate once — dashboard, white-label tenant, SDK, or agent — and inherit every backend.
Motivation
Banking APIs rot when upstream vendors leak into the client contract. Every issuer, card processor, and exchange has its own lifecycle, error dialect, and identifier scheme; a surface that exposes them forces every client to re-integrate on every counterparty change. The interface in this LP absorbs that variance at exactly one seam per concern and publishes one shape.
Specification
Authentication and environments
Every route below requires Authorization: Bearer <JWT> issued by Lux IAM (lux.id), except those marked public. Record ownership is enforced server-side by matching the authenticated principal against the owner of every account-scoped record.
| Environment | App | API |
|---|---|---|
| Production | app host | https://api.<domain>/v1 |
| Sandbox | sandbox host | https://api.sandbox.<domain>/v1 |
Sandbox and production run the same binary; BANK_SANDBOX (default on) selects deterministic settlement, seeded fixtures, testnet crypto, and the simulated issuer.
Route catalog
| Method | Path | Purpose | Auth |
|---|---|---|---|
| GET | /v1/bank/health | Health + sandbox flag | public |
| GET | /v1/bank/config | Currencies, network, partner disclosure | public |
| GET | /v1/bank/plans | Membership ladder (see below) | public |
| POST | /v1/bank/onboard | Create account from KYC profile | JWT |
| GET | /v1/bank/overview | Dashboard aggregate | JWT |
| GET | /v1/bank/transactions | Transaction stream | JWT |
| GET | /v1/bank/account/summary | Account list | JWT |
| POST | /v1/bank/transfers | Book transfer between own accounts | JWT |
| POST | /v1/bank/payments/outbound | Payment to a beneficiary; rail selected downstream | JWT |
| GET | /v1/bank/accounts/{id}/balances | Per-currency balances (available + held, minor units) | JWT |
| GET | /v1/bank/accounts/{id}/wallets | On-chain wallets | JWT |
| GET | /v1/bank/accounts/{id}/transactions | Account history | JWT |
| GET/POST/DELETE | /v1/bank/beneficiaries[/{id}] | Counterparty registry | JWT |
| GET/POST | /v1/bank/cards | Card ledger; issue | JWT |
| POST | /v1/bank/cards/{id}/freeze · /unfreeze | Card state | JWT |
| POST | /v1/bank/cards/account | Open issuer card account | JWT |
| GET | /v1/bank/cards/account | Issuer account state | JWT |
| GET | /v1/bank/cards/kyc | Issuer KYC status | JWT |
| POST | /v1/bank/cards/virtual | Create virtual-card account | JWT |
| GET | /v1/bank/cards/virtual/kyc-url | Hosted KYC URL (sensitive; never log) | JWT |
| GET | /v1/bank/cards/virtual/consent-url | Consent URL (sensitive; never log) | JWT |
| POST | /v1/bank/cards/virtual/order | Order the card once approved | JWT |
| POST | /v1/bank/crypto/send | On-chain send from the wallet | JWT |
| POST | /v1/bank/crypto/deposit | Testnet faucet (sandbox only) | JWT |
| GET | /v1/bank/crypto/prices | Reference prices | JWT |
| GET | /v1/bank/wallet | Wallet + crypto holdings | JWT |
| POST | /v1/bank/exchange/quote · /execute | Fiat FX + crypto buy/sell/convert | JWT |
| POST | /v1/bank/fx/quote · /execute | Dealt FX (forexd) | JWT |
| POST | /v1/bank/webhooks/* | Inbound provider events, HMAC-SHA256 signed | HMAC |
Amounts are integers in minor units; decimals in responses tells the client how to render (2 for most fiat, 0 for JPY, 6 for crypto).
The issuer seam (CaaS)
Card issuance runs the issuer's own account, KYC, and consent lifecycle. Clients branch only on the normalized status + nextAction pair:
status | nextAction | Client action |
|---|---|---|
not_started | register | POST /v1/bank/cards/virtual |
pending | complete_kyc | fetch KYC URL, open in browser, poll |
pending | accept_agreement | fetch consent URL, open, poll |
pending | none | wait, poll |
rejected | retry_kyc | fresh KYC URL, retry, poll |
approved | order_card | POST /v1/bank/cards/virtual/order |
error | retry_kyc | retry; support if persistent |
Platform KYC and issuer KYC never substitute for each other. The issuer implementation (Issuer in luxfi/bank issuer.go) is selected by BANK_ISSUER; the sandbox simulates it deterministically. Hosted URLs pass straight to the user's browser and are never logged or persisted.
The FX and crypto seams
Dealt FX executes through forexd provider adapters (FXProvider in luxfi/forex); crypto conversion deals on a spot venue adapter and settles plain fiat over banking rails. The crypto movement routes (send, sandbox deposit) operate on the account wallet; in sandbox the ledger is the testnet backend and every send returns a txHash on lux-testnet.
Membership plans
GET /v1/bank/plans publishes the plan ladder — id, name, monthly price, included card and IBAN, rail allowances, FX and deposit rates, transaction limits, holders. A plan on the account overrides entity-type transaction limits. The published ladder is the single pricing source for every surface that renders plans.
Status transitions
pending -> processing -> completed
pending -> failed | cancelled
processing -> failed | cancelled
All other transitions are rejected. Debits hold funds atomically at creation; completion releases the hold; failure and cancellation reverse it.
Rationale
One seam per concern (Issuer, FXProvider, crypto backend) keeps verification logic and policy enforcement unbraided: the route contract states what the product does; the adapter states how one counterparty does it. The status/nextAction pair is the smallest interface that survives every issuer's KYC workflow without leaking it. Minor-unit integers plus a decimals hint eliminate float drift across 2/0/6-dp currencies.
Backwards Compatibility
New interface; no prior standard. Additive response fields (e.g. an issuer's virtualAccount object) MUST be tolerated when absent.
Security Considerations
IAM is the only authorizer; there is no API-key path on customer routes. Hosted KYC/consent URLs are sensitive and never logged, persisted, or sent to analytics. Webhooks validate X-Signature = hex(HMAC-SHA256(secret, body)). The audit log is immutable (update/delete blocked at the hook layer). Balance debits are transactional; the floor check rejects any write that would take available below zero.
Copyright
Copyright and related rights waived via CC0.