Capay Global

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

ParameterTypeDescription
countryrequiredstringISO 3166-1 alpha-2 of the destination (CM, NG, ...).
methodrequiredstringmobile_money or bank.
currencyoptionalstringISO 4217. When given for a bank destination it must match the bank’s currency.
msisdnoptionalstringmobile_money only. International format, 8 to 15 digits, optional +. Must carry the dial code of country.
account_numberoptionalstringbank only. 6 to 20 digits.
bank_codeoptionalstringbank only. A code from GET /v1/banks?country=... for the same country.

Example request

curl
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

200 found
{
  "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"
}
200 not found
{
  "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"
}
200 unsupported
{
  "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"
}
bank destination
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.

FieldTypeDescription
objectstringAlways resolution.
country / method / currencystringThe normalised request (method is mobile_money or bank).
destinationobjectmsisdn, account_number, bank_code and bank_name (from the bank list) as resolved.
is_supportedbooleanfalse when no source can answer for this corridor (see reason_code).
foundbooleantrue only when the provider returned an account-holder name.
account_namestring | nullThe provider’s name, upper-cased and whitespace-collapsed. Never invented.
display_namestring | nullMasked form for screens: first initial plus last name (J. DOE).
activeboolean | nullWhether the account exists at the operator, when the source can say.
sourcestring | nullProvider code that answered (paystack, mtn_momo, simulator).
source_kindstring | nullname_enquiry, active_check or simulator.
cachedbooleantrue when served from the merchant’s cache; resolved_at is then the original lookup time.
resolved_at / expires_atstring | nullISO timestamps. After expires_at the next call asks the provider again.
reason_codestring | nullresolve_unsupported or invalid_customer; null when found or active only.
payout_availablebooleanWhether an open, funded payout corridor serves this country and method right now (the same rule GET /v1/providers publishes).
livemodebooleantrue 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

ParameterTypeDescription
countryrequiredstringISO 3166-1 alpha-2, e.g. NG.
typeoptionalstringbank, mobile_money or microfinance to filter the list.

Example request

curl
curl "https://api.capayglobal.com/v1/banks?country=NG" \
  -H "Authorization: Bearer sk_test_8fK2q0Zr41Xa"

Example response

200 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"
}
200 empty list
{
  "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.

POST /v1/payouts body
{
  "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"
}
payout object
"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 digitsOutcome
0001found, JANE DOE
0002found, JOHN DOE
0003not found (reason_code invalid_customer)
0004source down (503 provider_unavailable)
0009active only (found false, active true)
otherfound, 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.

CodeHTTPWhen
validation_error422Missing or malformed country, method, msisdn, account_number, bank_code or currency; an unknown bank code for a country with a loaded list.
insufficient_scope403The key holds neither payouts:read nor payouts:write.
card_data_rejected422account_number looks like a card number (16+ Luhn-valid digits).
rate_limited429The per-key resolve budget or the app quota is exhausted (Retry-After tells you when).
provider_unavailable503Every name source for the corridor is down or timed out.
internal_error500The limiter could not meter the call (fail closed) or an unexpected failure.