Credentials
API key only; session credentials are rejected.What is checked, and when
A fiat account is a local balance until its details are issued, so the checks are split across three points: opening the account, issuing its details, and moving money through it. Each check has its own status and its own failure shape, and knowing which one you hit is the difference between a configuration change and a support conversation.Opening an account
POST /v1/accounts runs three checks in order.
1
Is the currency open for issuance?
Sakawave can switch account issuance off per currency. Closed answers
409 currency_capability_unavailable. Nothing you configure changes this.2
Is this a crypto account below tier 2?
Crypto requires tier 2 for the deposit feature. A denial answers
403 carrying reason, currentTier, and requiredTier — and no code.3
Is this a customer wallet in a non-NGN currency?
Refused outright with
403 international_customer_wallet_not_supported, regardless of your own approvals.Issuing details
POST /v1/accounts/{accountId}/details is the first call to a provider on an account’s behalf. For your own non-NGN fiat account, it checks your verification first and your international access second.
All three answer
403 and carry the account’s currency. International access is one approval for your business, not one per currency. Both checks run on every request, so an approval withdrawn after an account opened stops new details being issued for it.
NGN and crypto accounts skip both checks.
Moving money
Sending money from an account, and crediting a deposit that arrives through its details, are checked against the holder’s tier for the account’s currency. NGN moves at tier 1; every other fiat currency moves at tier 3. Below that, a transfer is refused with403 carrying reason: currency_not_available, currentTier, and requiredTier, and a deposit through the account’s details is not credited.
What this means in practice
Rate limits
Per 60-second window, counted twice — once against your business, once against the key.
Plus 300 per minute per IP and 1,200 per minute per credential across all routes. See rate limits.
Webhooks
Both
account.* payloads carry the same object the REST endpoints return; account.detail.* carry the detail. Match a detail to your account by account.id: after a 202 you have never seen the detail’s own details.id.
Deduplicate on the Sakawave-Webhook-Id header — the body has only event and data. See verify signatures.
Internal platform balances, such as card settlement accounts, emit nothing and never appear in these APIs. If you see an
account.* event, it is for an account you can also read over REST.Provider dependence
Opening an account is local and always fast. Issuing a detail is not — it talks to a payment provider, and that call can fail (503 provider_unavailable) or not finish in time (202).
Design for both:
- Treat
503as retryable. Issuance walks three steps at the provider and skips any already taken, so a retry resumes rather than duplicating. - Treat
202as normal, not exceptional. Do not block a signup flow on a destination that has not arrived.
Configuration checklist
- Error handling reads
code ?? reason, and logscurrentTierandrequiredTierwhen present -
transferReadyis checked before advertising an account as payable -
balanceused for arithmetic;balanceFormattedonly for display -
202handled as a normal outcome of issuing a detail -
account.updatedandaccount.detail.createdsubscribed, rather than polling - Non-NGN plans confirmed against your verification and international access before you write the code
- Offboarding flow accounts for closure being dashboard-only