# nTZS Partner API — full documentation > Programmable Tanzanian shilling payments on Base. This file is generated from the > same capability registry, error catalogue and OpenAPI spec the site renders, so it > cannot drift from the live API. Machine-readable schemas: https://www.ntzs.co.tz/openapi.json ## Start here nTZS is a Tanzanian shilling stablecoin on Base. The partner API moves real money: it collects from mobile money, card and bank, issues wallets, transfers value, pays out to phones and bank accounts, pays bills and tills, and settles against USDC. - Base URL: `https://www.ntzs.co.tz` - Auth: `Authorization: Bearer ` on every request. `ntzs_live_…` is production, `ntzs_test_…` is the sandbox. - Amounts are **whole Tanzanian shillings**, always integers. Never send a decimal TZS amount. - Machine-readable contract: [https://www.ntzs.co.tz/openapi.json](https://www.ntzs.co.tz/openapi.json) — OpenAPI 3.1, every endpoint, every schema. Generate a client from it rather than writing fetch calls by hand. **Build against test mode first.** A test key simulates the whole lifecycle with no money and no chain, and `POST /api/v1/testmode/advance` settles every pending transaction on demand — so you can write code, run it, advance the state machine, read the webhook and correct, in a loop, without waiting on a rail. Most payment APIs cannot do this. Your test key mirrors your live capabilities, so what works in the sandbox works in production. ## Choosing what to build Products like "WaaS" or "Ramp" are not boxes to pick — they are bundles of capabilities. Your account is granted a set, and any endpoint outside it answers 403 no matter how valid your key is. Work out which capabilities your business needs, then read only those endpoints. - `wallets` — Create and manage end-user wallets (HD) for your customers. (granted per partner, not on by default) - `collections` — Collect funds (T+0) from all mobile-money networks and banks. (KYB required) - `disbursements` — Pay out to mobile money or banks — single payouts or bulk runs. (KYB required) - `transfers` — Move value between your users and external addresses on-chain. - `treasury` — Hold and manage balances and sub-wallets for your business. - `swap` — Convert between USDC and nTZS at a live rate. - `ramp` — Wallet-less settlement: USDC ⇄ mobile money, no per-user wallets. (KYB required) - `biashara` — Embed a full merchant product — payment links, sales, settlement and working capital — in your app. (KYB required; granted per partner, not on by default) - `wakala` — Give each of your agents one digital float that pays any mobile wallet, bank or biller. (KYB required; granted per partner, not on by default) Common compositions: | If you are building | You need | | --- | --- | | Insurance / recurring collection | `collections` + `treasury` | | Payroll or bulk payouts | `disbursements` + `treasury` | | Cross-border settlement | `ramp` | | A neobank or consumer wallet | `wallets` + `collections` + `disbursements` + `transfers` + `swap` | | A super-app with payments inside | `wallets` + `collections` + spend | | A merchant product embedded in your app | `biashara` | | Agent / float network | `wakala` | **A note on `wallets`.** Issuing wallets to your own end users is granted per partner, because every user wallet is an identity the platform carries in its KYC and regulatory reporting perimeter. Every account gets a **treasury wallet** at signup, and sub-wallets, with no grant needed — so not holding `wallets` never means "no wallet at all". If you only need to collect, hold and pay out as a business, you do not need `wallets`. ## Rules that decide whether an integration works These are the things that decide whether an integration works, and none of them are guessable from an endpoint list. ### Quotes are mandatory where they exist Withdrawals, spend and ramp price themselves through a quote endpoint. Call it, show the user the `recipientName` and total it returns, then pass the `quoteId` back to the execute call. **Never recompute a fee yourself.** Quotes expire; an expired one returns 409 `quote_stale` and the answer is a new quote, not a retry. Terms that do not match the quote return `quote_mismatch`. Executing spend with no quote at all returns `quote_required`. ### Confirm the destination before paying `POST /api/v1/lookup/recipient-name` resolves the registered name behind a phone number; `POST /api/v1/lookup/merchant-name` does the same for a till or bill account. Show it before the user confirms. These are **fail-soft**: `name: null` means no confirmation was available, not that the payment will fail. Show the raw number and continue — never block on null. A malformed request is a different thing and still returns 400. ### Retries and idempotency Every money-moving endpoint accepts an `Idempotency-Key` header. Send one. A 502 can mean the request was taken but not confirmed, and retrying without a key can pay twice. - `no` — Deterministic — fix the request and resend; retrying unchanged returns the same answer. - `backoff` — Transient — retry with exponential backoff. - `verify` — The outcome is genuinely unknown. Query the resource before retrying, or you risk paying twice. An idempotency key makes the retry safe. ### Errors come in three shapes 1. **Machine code in `error`** — The common shape: `error` is the code, `message` the human sentence. ```json { "error": "insufficient_balance", "message": "Sender has insufficient nTZS balance" } ``` 2. **Machine code in `code`** — Identity and KYC paths invert it: `error` is prose and `code` is the machine value. ```json { "error": "A NIDA number is required to create a wallet.", "code": "kyc_required" } ``` 3. **No machine code** — Authentication failures carry a sentence only. Match on the HTTP status. ```json { "error": "Missing or invalid Authorization header. Expected: Bearer " } ``` Branch on `code ?? error` and always keep the HTTP status. Reading `error` alone silently mis-handles about a third of the catalogue. ### Money never disappears quietly A payment from the wrong number or for the wrong amount is held for review rather than guessed. A 202 is success-in-flight, not a failure. A user with no wallet address is not broken — no wallet is ever issued without a verified identity, so `kyc_pending_review` is a normal state to render, not an error to retry. ## Recipes Complete sequences. Adapt these rather than composing endpoints from scratch. ### Collect money from a customer 1. `POST /api/v1/users` — once per customer, idempotent on `externalId`. Skip if you collect to treasury. 2. `POST /api/v1/deposits` with `amountTzs` (min 500) and `phoneNumber`. The payer gets a prompt. 3. Wait for the webhook, or poll `GET /api/v1/deposits/{id}` until the status is terminal. 4. On 502 `initiation_uncertain`, **read the deposit back before retrying** — the collection may still have been taken. Omit `userId` to collect into your treasury instead of a user wallet — that is the path if you have no `wallets` capability. ### Pay someone out 1. `POST /api/v1/lookup/recipient-name` — show who is being paid. 2. `POST /api/v1/withdrawals/quote` with `amountTzs` (net to recipient, min 5,000) and either `phoneNumber` or `bankCode` + `accountNumber`. 3. Show `recipientName` and `totalFeeTzs`. If `quoteId` is null the balance is insufficient — stop. 4. `POST /api/v1/withdrawals` with the same terms plus `quoteId`, and an `Idempotency-Key`. 5. Track with `GET /api/v1/withdrawals/{id}` or the webhook. ### Onboard a user and give them a wallet Needs the `wallets` capability. 1. `POST /api/v1/users` with `externalId`, `email`, and for Tanzania `nidaNumber` + the user's own `phone`. 2. A **201** means verified — `walletAddress` is populated and you are done. 3. A **202** means verification is open. `kyc_pending_review` → wait for the webhook. `kyc_attestation_required` → you verify and report via `POST /api/v1/users/{id}/kyc/attestation`. 4. Never poll hard for a wallet. Render the pending state. ### Pay a bill or a till 1. Scan or collect the destination. `POST /api/v1/lookup/qr` resolves a TANQR payload; `POST /api/v1/lookup/merchant-name` resolves the trading name. 2. `POST /api/v1/spend/quote` — returns the name, the fee and the total. A fee applies to every payment; never render a biller as free. 3. `POST /api/v1/spend` with the `quoteId` and an `Idempotency-Key`. 4. **Persist the returned reference** — it is what the biller recognises when the customer queries the payment. 5. On 502 `till_network_required` the burn was reverted and the balance restored: retry with the `network` field. ### Test the whole thing without money 1. Get a test key from the developer dashboard. 2. Run any recipe above against it — same endpoints, same host. 3. `POST /api/v1/testmode/advance` to settle everything pending immediately. 4. A NIDA ending `0000` forces a manual review; clear it with `POST /api/v1/testmode/users/{id}/approve` to exercise that branch. 5. `POST /api/v1/testmode/reset` to wipe and start again. ## Agent credentials — delegated keys An agent credential is a delegated API key: your authority, with a smaller envelope. Issue one to an autonomous agent, an internal service, or a contractor, instead of handing over the master key. It cannot exceed your own grant, and it adds three limits the master key has no way to express: - **A capability subset** — omit it to inherit yours. Always intersected with your live capabilities, so revoking a capability from your account removes it from every credential you issued, with no second step. - **A per-transaction ceiling** — required. - **A rolling 24-hour ceiling** — required. Rolling, not a calendar day: a calendar day hands an agent two full budgets either side of midnight, and an agent has no reason to know it is midnight. Both ceilings are mandatory at issue time. An uncapped delegated key is the master key with extra steps. ### Issue one Requires approved KYB and a dashboard session — never the API key. A key that can mint keys cannot be meaningfully scoped, because it could issue itself a wider one. ``` POST /api/v1/partners/agent-credentials { "label": "billing-agent", // required — you will need it to decide what to revoke "capabilities": ["disbursements"], // optional; omit to inherit yours "perTxnCapTzs": 50000, // required "dailyCapTzs": 500000, // required "expiresInDays": 90 // optional, default 90, max 365 } 201 → { "apiKey": "ntzs_agent_…", "expiresAt": "…" } ``` **The key is shown once.** Only its hash is stored, exactly as with your master key. `GET` the same path to list credentials (never key material) and `DELETE ?id=…` to revoke. Revocation is immediate and not reversible — issue a new credential instead. ### Using one Send it as a bearer token like any key. Everything downstream sees the partner it acts for, so ownership, webhooks and ledgers behave identically. What differs is the envelope: - Outside its capability subset → 403 with the usual capability error. - Above the per-transaction ceiling → 403 `agent_per_txn_cap_exceeded`. - Above what is left of the rolling window → 403 `agent_daily_cap_exceeded`, with `remainingTzs` so you can decide whether to wait or split. - Revoked or expired → 403 `agent_credential_revoked` / `_expired`, which are distinguishable from a bad key on purpose: they are things you can act on. Caps apply to outbound movements — withdrawals, spend, merchant payments and nTZS transfers. Ramp settlement and USDC transfers are not capped yet, because the ceilings are TZS-denominated and converting would hold you to a rate you never agreed. ### What it is not This delegates the **partner's** authority. Authorising an agent to spend an **end user's** balance is a different thing, with its own consent and evidence requirements, and is not what this is. ## Endpoint reference Full schemas for every operation are in the OpenAPI spec. This is the map. ### Users - `POST /api/v1/users` — Create a user and provision a wallet Requires the `wallets` capability — end-user wallet issuance is granted per partner, and without it this answers 403. Idempotent on `externalId`: calling again returns the existing user rather than creating a second one. For a Tanzanian user, `nidaNumber` and `phone` are required — no wallet is ever issued without a verified identity, so a 202 with `kyc_pending_review` is a normal outcome, not a failure. - `GET /api/v1/users/{id}` — Read a user, their wallet and balances ### Identity - `POST /api/v1/users/{id}/kyc` — Submit or re-submit identity details for a user - `POST /api/v1/users/{id}/kyc/attestation` — Attest a KYC outcome you performed yourself For partners operating under a reliance agreement: you verified the identity, you report the outcome, and the wallet is issued on the strength of it. Granted per partner — without the grant this answers 403 `kyc_reliance_not_granted`. An API key alone must never be able to manufacture a verified identity. ### Collections - `POST /api/v1/deposits` — Collect funds from a payer Minimum 500 TZS. The payer receives a prompt on their phone for `mobile_money`. A 502 `initiation_uncertain` means we could not confirm whether the prompt was delivered and the collection may still be taken — read the deposit back before retrying. - `GET /api/v1/deposits/{id}` — Read a deposit and its current status Poll this for the status change, or take the webhook. While the deposit is open it echoes the payment instructions so you can re-show them. ### Transfers - `POST /api/v1/transfers` — Move value between users or to an external address Provide exactly one of `toUserId` or `toAddress`, never both. ### Disbursements - `POST /api/v1/withdrawals/quote` — Price a payout before executing it Give exactly one destination: a phone, or a bank (`bankCode` + `accountNumber`). `amountTzs` is what the recipient receives, net. Show the returned `recipientName` and `totalFeeTzs` to the user, then pass `quoteId` to POST /v1/withdrawals. Never recompute the fee yourself. - `POST /api/v1/withdrawals` — Execute a payout against a quote `amountTzs` and the destination must match the quote exactly, or you get `quote_mismatch`. An expired quote returns 409 `quote_stale` — request a new one rather than retrying. - `GET /api/v1/withdrawals/{id}` — Read a payout and its current status ### Spend - `POST /api/v1/spend/quote` — Price a bill or till payment - `POST /api/v1/spend` — Pay a bill or merchant till against a quote A quote is mandatory: without one you get `quote_required`. Persist the returned reference — it is what the biller recognises. - `GET /api/v1/spend/{id}` — Read a spend and its current status - `GET /api/v1/spend/billers` — The biller catalogue Each entry carries its reference label and validation rules. A fee applies to every payment — never render a biller as free. - `POST /api/v1/merchant/pay` — Pay a Biashara merchant from a wallet balance The balance tender at a Biashara checkout: your signed-in user pays the merchant from their own nTZS balance. Settlement is synchronous — the `201` is final. The merchant receives the full amount. A sale is a transfer between two holders: nothing is minted and no deposit is created. ### Lookup - `POST /api/v1/lookup/recipient-name` — Resolve the registered name behind a mobile number For the "Sending to: JOHN DOE" line before a user confirms. Fail-soft: `name: null` means no confirmation was available — show the raw number and continue, never block. Rate limited to 30/min per partner and audited per call. - `POST /api/v1/lookup/merchant-name` — Resolve the trading name behind a till or bill account Fail-soft like the recipient lookup. Bill validation is amount-aware — send `amountTzs` when known. Upstream can take ~25s, so debounce and never put this on a keystroke. Rate limited to 60/min per partner. - `POST /api/v1/lookup/qr` — Resolve a scanned TANQR payload to a destination ### Swap - `POST /api/v1/swap` — Convert between USDC and nTZS Responds as a Server-Sent Events stream so you can show progress through each leg. - `GET /api/v1/swap/rate` _(no auth)_ — Current USDC/nTZS rate Public — no API key required. Valid for roughly 30 seconds. Show this to the user before they confirm a swap; do not cache it past its validity. Throttled per source address (60/min by default): cache a quote for its `expiresAt` window rather than re-fetching per keystroke. `liquidityUnknown: true` means the solver balance behind `lowLiquidity` could not be read within the time budget of the quote; the price is unaffected. ### Ramp - `POST /api/v1/ramp/quote` — Lock a settlement rate for 60 seconds - `POST /api/v1/ramp/offramp` — USDC to mobile money A 202 is success-in-flight, not a failure. Track it with GET /v1/ramp/{id} or the settlement webhooks. - `POST /api/v1/ramp/onramp` — Mobile money to USDC - `GET /api/v1/ramp/{id}` — Read one settlement - `GET /api/v1/ramp/settlements` — List settlements - `GET /api/v1/ramp/balance` — Your USDC settlement float ### Test mode - `GET /api/v1/testmode` — Inspect the sandbox state for this key - `POST /api/v1/testmode/advance` — Settle every pending simulated transaction now The reason the sandbox is worth building against: no waiting on a rail. Write code, run it, advance, observe the webhook, correct. Test keys only — a live key gets `test_mode_only`. - `POST /api/v1/testmode/reset` — Wipe every simulated user and transaction on this key - `POST /api/v1/testmode/users/{id}/approve` — Clear a simulated manual KYC review Use a NIDA ending 0000 to force a review, then clear it here to exercise the pending-review branch. ### Platform - `GET /api/v1/reconcile` — Platform-wide supply integrity check NOT your account statement — this compares on-chain totalSupply against every tracked wallet on the platform. It reads each wallet from chain, so latency grows with `walletsChecked`: call it on a schedule, never in a request path. - `GET /api/v1/supply` — Total nTZS in circulation ## Error reference Every code the API returns. `field` says which JSON key carries it. ### Request validation | Code | Status | Field | Retry | Meaning | | --- | --- | --- | --- | --- | | `missing_required_fields` | 400 | error | no | A required body field is absent | | `invalid_amount` | 400 | error | no | Amount is zero, negative, or below the minimum | | `amount_too_large` | 400 | error | no | Amount is above the per-transaction ceiling for this rail | | `invalid_address` | 400 | error | no | toAddress is not a valid Ethereum address | | `invalid_token` | 400 | error | no | Unsupported token symbol for this transfer | | `invalid_transfer` | 400 | error | no | Provide either toUserId or toAddress, not both | | `invalid_phone` | 400 | both | no | Not a valid Tanzanian mobile number — note this code appears in both shapes | | `invalid_external_reference` | 400 | error | no | externalReference is not a string, is over 128 characters, or contains control characters — send your order id as a plain string | | `phone_required` | 400 | code | no | A phone number is required for this operation | | `phone_invalid` | 400 | code | no | Phone failed validation on the create-user path | | `invalid_country` | 400 | code | no | country must be an ISO 3166-1 alpha-2 code | | `payload_required` | 400 | code | no | QR lookup called without a payload | | `invalid_entry` | 400 | code | no | IP allowlist entry is not a valid address or CIDR | | `invalid_utility_ref` | 400 | error | no | Utility reference is not valid for this biller | | `unknown_biller` | 400 | error | no | No biller matches the supplied code | | `till_network_required` | 502 | error | verify | Till accepted but its mobile network is unknown — the burn was reverted and the balance restored. Retry with the network field | ### Authentication and access | Code | Status | Field | Retry | Meaning | | --- | --- | --- | --- | --- | | `(no code)` | 401 | prose | no | Missing, empty, or invalid API key — match on the status, there is no machine code | | `ip_not_allowed` | 403 | code | no | Request source IP is not on your allowlist | | `capability_required` | 403 | error | no | Your account does not have the capability this endpoint needs — request it in the dashboard | | `kyb_required` | 403 | code | no | The capability is granted but your KYB is not yet approved | | `capability_not_held` | 403 | code | no | You tried to delegate a capability your own account does not hold — an agent credential can never out-scope its issuer | | `agent_credential_revoked` | 403 | code | no | This agent credential was revoked. Issue a new one; revocation is not reversible | | `agent_credential_expired` | 403 | code | no | This agent credential passed its expiry. Issue a new one | | `agent_credential_partner_inactive` | 403 | code | no | The partner account behind this agent credential is deactivated | | `agent_per_txn_cap_exceeded` | 403 | code | no | Above this agent credential's per-transaction ceiling. Split the movement or use a credential with a higher limit | | `agent_daily_cap_exceeded` | 403 | code | no | Above what is left of this agent credential's rolling 24h ceiling. The window is rolling, so allowance returns gradually rather than at midnight | | `merchant_unavailable` | 403 | error | no | This merchant cannot accept payments right now | | `wallet_frozen` | 403 | error | no | The wallet is frozen and cannot pay | | `endpoint_retired` | 410 | code | no | This endpoint has been withdrawn — see the migration note in its response | | `not_available_in_test_mode` | 501 | error | no | This product is not simulated in the sandbox; use a live key | | `test_mode_only` | 400 | error | no | The inverse — a test-mode-only endpoint called with a live key | | `already_test` | 400 | error | no | This account is already a sandbox account | ### Identity and KYC | Code | Status | Field | Retry | Meaning | | --- | --- | --- | --- | --- | | `kyc_required` | 400 | code | no | nidaNumber (and phone) missing on a TZ create-user — non-TZ signups send country instead | | `kyc_pending_review` | 202 | code | no | Not an error — verification is open. Offer a capture session and wait for the webhook | | `kyc_attestation_required` | 202 | code | no | The platform does not verify for you — report the outcome via the attestation endpoint | | `kyc_failed` | 400 | code | no | Verification returned a negative result | | `kyc_already_decided` | 409 | code | no | This case already has a terminal outcome and cannot be re-attested | | `kyc_reliance_not_granted` | 403 | code | no | Your account may not attest KYC outcomes — this is granted per partner | | `identity_already_registered` | 409 | code | no | This identity already backs another wallet on your platform | | `nida_already_registered` | 409 | code | no | This NIDA already backs another wallet on your platform | | `identity_mismatch` | 409 | code | no | The attested identity does not match the one on the open case | | `kyc_unavailable` | 503 | code | backoff | Verification is temporarily unavailable — retry shortly | ### Quotes | Code | Status | Field | Retry | Meaning | | --- | --- | --- | --- | --- | | `quote_required` | 400 | error | no | This endpoint will not price itself: call the quote endpoint, show the user the name and fees, then send the quote | | `quote_mismatch` | 400 | error | no | Quote was issued for different terms (user, destination, or amount) | | `invalid_quote` | 400 | error | no | Quote is malformed or already expired — request a new one | | `quote_stale` | 409 | error | no | Pricing moved since the quote was issued — request a new one. Never recompute fees yourself | ### Balance and wallet state | Code | Status | Field | Retry | Meaning | | --- | --- | --- | --- | --- | | `insufficient_balance` | 400 / 402 | error | no | Not enough nTZS — details carries available, requested and shortfall | | `wallet_not_provisioned` | 400 | error | backoff | Wallet address is still being derived — retry shortly | | `treasury_not_provisioned` | 400 | error | no | Provision your partner treasury before funding disbursements from a sub-wallet | | `funding_source_required` | 400 | error | no | Say which source funds this disbursement | | `not_provisioned` | 503 | code | no | The feature is not set up on this account yet | | `user_not_found` | 404 | both | no | userId not found under your partner account — appears in both shapes | | `token_paused` | 503 | error | backoff | nTZS transfers are paused by the issuer — withdrawals and spends are refused before anything is debited; retry later | ### Conflicts and duplicates | Code | Status | Field | Retry | Meaning | | --- | --- | --- | --- | --- | | `duplicate_withdrawal` | 409 | error | no | A withdrawal with this idempotency key already exists — read it back rather than resending | ### Self-custody makers (market-maker API) | Code | Status | Field | Retry | Meaning | | --- | --- | --- | --- | --- | | `pooled_capital_active` | 409 | code | no | Self-custody can only be enabled after deactivating: your capital is still in the shared pool | | `NOT_SELF_CUSTODY` | 409 | code | no | Submit orders only after enabling self-custody (PUT /api/v1/mm/custody) | | `BAD_ORDER` | 400 | code | no | The RfqOrder struct is malformed — `error` names the field | | `BAD_SIGNATURE` | 400 | code | no | The signature is malformed, does not recover to your registered maker wallet, or the 0x contract rejects it | | `UNSUPPORTED_CHAIN` | 400 | code | no | RFQ orders are accepted on Base only | | `UNKNOWN_TOKEN` | 400 | code | no | makerToken and takerToken must be registry tokens on Base | | `SAME_TOKEN` | 400 | code | no | makerToken and takerToken must differ | | `NOT_NTZS_PAIR` | 400 | code | no | One side of every order must be nTZS | | `WRONG_MAKER` | 400 | code | no | order.maker must be your registered maker wallet | | `WRONG_TAKER` | 400 | code | no | order.taker must be the SimpleFX solver wallet — GET /api/v1/mm/rfq/info | | `WRONG_TX_ORIGIN` | 400 | code | no | order.txOrigin must be the SimpleFX solver wallet | | `EXPIRES_TOO_SOON` | 400 | code | no | expiry must be at least 60 seconds ahead | | `EXPIRES_TOO_LATE` | 400 | code | no | expiry must be within 24 hours | | `ORDER_NOT_FILLABLE` | 409 | code | no | The 0x contract reports the order as filled, cancelled, expired or invalid — `orderStatus` says which | | `CHAIN_UNAVAILABLE` | 503 | code | backoff | The order could not be read on-chain from any endpoint — retry shortly | ### Compliance screening | Code | Status | Field | Retry | Meaning | | --- | --- | --- | --- | --- | | `compliance_screening_blocked` | 403 | error | no | The destination address cannot be paid from the platform | | `compliance_screening_unavailable` | 503 | error | backoff | Screening is down, so nothing was sent. Nothing moved — retry shortly | ### Rails and availability | Code | Status | Field | Retry | Meaning | | --- | --- | --- | --- | --- | | `rate_limited` | 429 | error | backoff | Too many requests — back off exponentially and respect any Retry-After | | `relayer_unavailable` | 503 | error | backoff | Gas relay is temporarily offline — retry shortly | | `network_error` | 500 | error | backoff | RPC connection timed out | | `database_error` | 500 | error | backoff | The transfer record could not be written — nothing moved | | `configuration_error` | 500 | error | no | Server-side configuration is missing — contact support, retrying will not help | | `initiation_failed` | 502 | code | verify | The provider rejected the collection outright | | `initiation_uncertain` | 502 | code | verify | We could not confirm whether the prompt was delivered and the collection may still be taken — query the deposit before retrying | | `bank_rail_unavailable` | 503 | error | backoff | Bank payouts are not enabled on this environment yet | | `bank_amount_unsupported` | 400 | error | no | This amount cannot be sent over the bank rail | | `spend_disabled` | 503 | error | backoff | Spend rails are not enabled on this environment yet | | `spend_kind_disabled` | 503 | error | no | This particular spend destination is not enabled | | `ramp_unavailable` | 502 / 503 | error | backoff | The ramp service errored pricing this quote — nothing was charged | | `ramp_not_provisioned` | 503 | error | no | Ramp is not set up on this account | | `ramp_spend_disabled` | 503 | error | no | Lipa/bill off-ramp destinations are pending regulatory approval | | `wakala_float_disabled` | 503 | error | no | Sub-wallet funded disbursements are not enabled on this environment | | `signup_disabled` | 503 | error | no | Self-serve sandbox signup is closed on this deployment | | `migration_pending` | 503 | error | backoff | The deployment is ahead of its database migration — retry shortly |