Resolve & banks
Confirm who owns a mobile money number or bank account before you pay it, and list the bank codes a country accepts. Read-only: nothing here moves money.
Why resolve first
A payout to the wrong account is the most expensive support ticket there is. POST /v1/resolve asks the provider on the corridor who holds the destination and answers with the account-holder name exactly as the provider returned it. Names are never guessed: a corridor with no name source answers is_supported false and a closed reason_code instead of an invented name.
Results are cached per merchant and environment (found names for 7 days, not-found answers for 60 minutes by default) and served with cached true. The check is advisory: a payout can still be created when the name is missing, and the provider confirms the account again at dispatch.
Resolve a destination
POST/v1/resolve
Send the destination the way you would send it on POST /v1/payouts. mobile_money needs msisdn; bank needs account_number and bank_code (codes from GET /v1/banks). bank_transfer is accepted as an alias of bank and normalised, never echoed. Not a money endpoint: no Idempotency-Key.
Headers
- AuthorizationBearer sk_test_... or sk_live_.... The key needs the payouts:read or payouts:write scope (a collections-only key is refused with insufficient_scope).
- Content-Typeapplication/json
Request body
| Parameter | Type | Description |
|---|---|---|
| countryrequired | string | ISO 3166-1 alpha-2 of the destination (CM, NG, ...). |
| methodrequired | string | mobile_money or bank. |
| currencyoptional | string | ISO 4217. When given for a bank destination it must match the bank’s currency. |
| msisdnoptional | string | mobile_money only. International format, 8 to 15 digits, optional +. Must carry the dial code of country. |
| account_numberoptional | string | bank only. 6 to 20 digits. |
| bank_codeoptional | string | bank only. A code from GET /v1/banks?country=... for the same country. |
Example request
curl -X POST https://api.capayglobal.com/v1/resolve \
-H "Authorization: Bearer sk_test_8fK2q0Zr41Xa" \
-H "Content-Type: application/json" \
-d '{
"country": "CM",
"method": "mobile_money",
"currency": "XAF",
"msisdn": "+237650000001"
}'Example response
{
"status": "success",
"data": {
"object": "resolution",
"country": "CM",
"method": "mobile_money",
"currency": "XAF",
"destination": {
"msisdn": "+237650000001",
"account_number": null,
"bank_code": null,
"bank_name": null
},
"is_supported": true,
"found": true,
"account_name": "JANE DOE",
"display_name": "J. DOE",
"active": true,
"source": "simulator",
"source_kind": "simulator",
"cached": false,
"resolved_at": "2026-09-04T10:00:00.000Z",
"expires_at": "2026-09-11T10:00:00.000Z",
"reason_code": null,
"payout_available": true,
"livemode": false
},
"request_id": "b1e2c3d4-5f60-4a71-8b92-c0d1e2f3a4b5"
}{
"status": "success",
"data": {
"object": "resolution",
"country": "CM",
"method": "mobile_money",
"currency": "XAF",
"destination": { "msisdn": "+237650000003", "account_number": null, "bank_code": null, "bank_name": null },
"is_supported": true,
"found": false,
"account_name": null,
"display_name": null,
"active": false,
"source": "simulator",
"source_kind": "simulator",
"cached": false,
"resolved_at": "2026-09-04T10:00:00.000Z",
"expires_at": "2026-09-04T11:00:00.000Z",
"reason_code": "invalid_customer",
"payout_available": true,
"livemode": false
},
"request_id": "c2d3e4f5-6a71-4b82-9c03-d1e2f3a4b5c6"
}{
"status": "success",
"data": {
"object": "resolution",
"country": "KE",
"method": "bank",
"currency": "KES",
"destination": { "msisdn": null, "account_number": "0123456789", "bank_code": "KE-SIM-001", "bank_name": null },
"is_supported": false,
"found": false,
"account_name": null,
"display_name": null,
"active": null,
"source": null,
"source_kind": null,
"cached": false,
"resolved_at": "2026-09-04T10:00:00.000Z",
"expires_at": null,
"reason_code": "resolve_unsupported",
"payout_available": false,
"livemode": true
},
"request_id": "d3e4f5a6-7b82-4c93-8d04-e1f2a3b4c5d6"
}curl -X POST https://api.capayglobal.com/v1/resolve \
-H "Authorization: Bearer sk_test_8fK2q0Zr41Xa" \
-H "Content-Type: application/json" \
-d '{
"country": "NG",
"method": "bank",
"currency": "NGN",
"account_number": "0000000002",
"bank_code": "058"
}'The five answers
Every state below is a 200 with the same object; read is_supported, found, active and reason_code together.
- foundis_supported true, found true, account_name set, display_name masked (J. DOE), reason_code null.
- not foundis_supported true, found false, reason_code invalid_customer: the destination is not registered with the operator. Cached for a short time so a typo is cheap to retry.
- active onlyis_supported true, found false, active true, source_kind active_check: the account exists but the operator shares no name (MTN without the KYC entitlement).
- unsupportedis_supported false, source null, reason_code resolve_unsupported: no provider on this corridor confirms names yet, or no bank list is loaded for the country.
- source downHTTP 503 provider_unavailable: every name source for the corridor is temporarily unavailable. Retry with backoff; the payout itself can still be created.
The resolution object
Returned by POST /v1/resolve. The provider payload behind it is never exposed.
| Field | Type | Description |
|---|---|---|
| object | string | Always resolution. |
| country / method / currency | string | The normalised request (method is mobile_money or bank). |
| destination | object | msisdn, account_number, bank_code and bank_name (from the bank list) as resolved. |
| is_supported | boolean | false when no source can answer for this corridor (see reason_code). |
| found | boolean | true only when the provider returned an account-holder name. |
| account_name | string | null | The provider’s name, upper-cased and whitespace-collapsed. Never invented. |
| display_name | string | null | Masked form for screens: first initial plus last name (J. DOE). |
| active | boolean | null | Whether the account exists at the operator, when the source can say. |
| source | string | null | Provider code that answered (paystack, mtn_momo, simulator). |
| source_kind | string | null | name_enquiry, active_check or simulator. |
| cached | boolean | true when served from the merchant’s cache; resolved_at is then the original lookup time. |
| resolved_at / expires_at | string | null | ISO timestamps. After expires_at the next call asks the provider again. |
| reason_code | string | null | resolve_unsupported or invalid_customer; null when found or active only. |
| payout_available | boolean | Whether an open, funded payout corridor serves this country and method right now (the same rule GET /v1/providers publishes). |
| livemode | boolean | true for sk_live_ keys. |
Limits and caching
On top of the app quota, every key has its own resolve budget: 30 calls per minute and 2,000 per day by default. A denied call answers 429 rate_limited with Retry-After and X-RateLimit-Resolve-Remaining: 0; successful calls carry the remaining per-minute budget in X-RateLimit-Resolve-Remaining. Cache resolved names on your side: the answer is stable for the whole cache window.
The limiter fails closed. If the platform cannot meter a call it answers 500 internal_error rather than serve a name unmetered; retry shortly.
List bank codes
GET/v1/banks
GET /v1/banks?country=NG returns the codes to send as bank_code. Reference data: no scope beyond a valid key. Production keys only see lists a provider synced; sandbox keys also see the simulator’s seed banks. An empty list is honest and carries a note.
Query parameters
| Parameter | Type | Description |
|---|---|---|
| countryrequired | string | ISO 3166-1 alpha-2, e.g. NG. |
| typeoptional | string | bank, mobile_money or microfinance to filter the list. |
Example request
curl "https://api.capayglobal.com/v1/banks?country=NG" \ -H "Authorization: Bearer sk_test_8fK2q0Zr41Xa"
Example response
{
"status": "success",
"data": {
"object": "list",
"country": "NG",
"currency": "NGN",
"count": 2,
"source": "seed",
"as_of": null,
"banks": [
{ "code": "044", "name": "Access Bank", "slug": null, "type": "bank", "swift_code": null },
{ "code": "058", "name": "Guaranty Trust Bank", "slug": null, "type": "bank", "swift_code": null }
],
"note": null,
"livemode": false
},
"request_id": "e4f5a6b7-8c93-4da4-9e15-f2a3b4c5d6e7"
}{
"status": "success",
"data": {
"object": "list",
"country": "KE",
"currency": null,
"count": 0,
"source": null,
"as_of": null,
"banks": [],
"note": "No bank list is loaded for KE yet. Resolution and bank payouts for KE open once a provider list is synced; mobile money is unaffected.",
"livemode": true
},
"request_id": "f5a6b7c8-9da4-4eb5-af26-a3b4c5d6e7f8"
}Bank payouts and the name check on a payout
POST /v1/payouts with method bank now takes a beneficiary object: bank_code (from GET /v1/banks), account_number (6 to 20 digits) and an optional account_name, your own label for the recipient. The bank code must exist in the list for that country and environment.
When a fresh resolution exists for the destination at creation time, the payout carries it: beneficiary.verified_name and name_check.status verified. The money path itself never calls a provider for a name; without a fresh cache row name_check.status is unchecked and the provider confirms the account at dispatch.
{
"amount": "25000",
"currency": "NGN",
"country": "NG",
"method": "bank",
"beneficiary": {
"bank_code": "058",
"account_number": "0000000002",
"account_name": "John Doe"
},
"merchant_reference": "salary-2026-09-0042"
}"beneficiary": {
"bank_code": "058",
"bank_name": "Guaranty Trust Bank",
"account_number_last4": "0002",
"account_name": "John Doe",
"verified_name": "JOHN DOE"
},
"name_check": { "status": "verified", "account_name": "JOHN DOE" }Sandbox fixtures
sk_test_ keys never reach a provider. The last four digits of the msisdn (mobile money) or account number (bank) pick the answer; any other suffix resolves found as SANDBOX ACCOUNT nnnn. Sandbox bank codes for NG are 058, 044, 057, 033 and 011; GH has GH-SIM-001 plus MTN, VOD and ATL; KE has KE-SIM-001.
| Last 4 digits | Outcome |
|---|---|
| 0001 | found, JANE DOE |
| 0002 | found, JOHN DOE |
| 0003 | not found (reason_code invalid_customer) |
| 0004 | source down (503 provider_unavailable) |
| 0009 | active only (found false, active true) |
| other | found, SANDBOX ACCOUNT nnnn |
Which corridors resolve today
As of 2026-09-04. Sources are governed by the platform team without a deploy; a source whose credentials are not yet stored answers is_supported false until they are.
- CM mobile moneyMTN account-holder check: active true; the name follows once the KYC entitlement is confirmed on the API user.
- NG, GH, KE bankPaystack account resolution, live once the Paystack key is stored. Until then: is_supported false.
- GH mobile moneyPaystack wallet resolution (MTN, VOD, ATL), same condition.
- Sandbox, every corridorThe simulator answers every mobile money destination and every bank code from the seed list.
Errors
Both endpoints use the standard error envelope. reason_code inside a 200 is not an error.
| Code | HTTP | When |
|---|---|---|
| validation_error | 422 | Missing or malformed country, method, msisdn, account_number, bank_code or currency; an unknown bank code for a country with a loaded list. |
| insufficient_scope | 403 | The key holds neither payouts:read nor payouts:write. |
| card_data_rejected | 422 | account_number looks like a card number (16+ Luhn-valid digits). |
| rate_limited | 429 | The per-key resolve budget or the app quota is exhausted (Retry-After tells you when). |
| provider_unavailable | 503 | Every name source for the corridor is down or timed out. |
| internal_error | 500 | The limiter could not meter the call (fail closed) or an unexpected failure. |