API reference
One base URL, one authentication scheme, one response envelope. Every /v1 endpoint shares the conventions on this page: bearer keys, the success/error envelope with its request_id, idempotency on money-mutating calls, decimal-string money, keyset pagination and rate limits. The read-only Core resources below round out the overview; Collections, Payouts, Refunds, Events and Webhook endpoints each have their own page.
Base URL
All requests go to the versioned base URL below over HTTPS. Every response is JSON, and the API version is pinned in the path.
https://api.capayglobal.com/v1
A method and path that no route matches returns 404 route_not_found. Trailing slashes are ignored.
Authentication and scopes
Send your secret key as a bearer token on every request. Sandbox keys start with sk_test_, production keys with sk_live_; the environment is derived from the key alone. A missing, malformed, revoked, expired or wrong-environment key always returns the same opaque 401 invalid_api_key.
Authorization: Bearer sk_test_XXXXXXXXXXXX # sandbox Authorization: Bearer sk_live_XXXXXXXXXXXX # production
Keys carry scopes (fail-closed: a key with no scopes can call nothing but the unscoped reads). A call to an endpoint your key is not scoped for returns 403 insufficient_scope. The scope vocabulary is:
collections:read collections:write payouts:read payouts:write refunds:read refunds:write balances:read events:read events:write webhooks:read webhooks:write fx:read fx:write subscriptions:read subscriptions:write * (grants every scope)
whoami, providers, pricing and fx/rates need no scope: any valid key can call them. balances needs balances:read; FX quotes need fx:read to read and fx:write to create (agent keys cannot quote).
The response envelope
Every response, success or error, is wrapped in an envelope. Success carries status "success", a data object and a request_id. Error carries status "error" and an error object with a machine-readable type and code, a human message, a next_step, and the request_id. Validation failures add a validation_errors array naming the offending fields.
{
"status": "success",
"data": {
"merchant": {
"id": "0f9a2c34-6b1d-4e8a-9c21-7d2f5a1b3c4d",
"public_id": "mch_8fK2q0Zr41Xa",
"trading_name": "Sunrise Electronics"
},
"app": {
"id": "b7e1d2c3-4f5a-6789-abcd-ef0123456789",
"name": "Default app",
"environment": "SANDBOX"
},
"key": { "prefix": "sk_test_8fK2", "scopes": ["read", "write"] },
"livemode": false
},
"request_id": "3fa85f64-5717-4562-b3fc-2c963f66afa6"
}{
"status": "error",
"error": {
"type": "validation_error",
"code": "validation_error",
"message": "One or more request parameters are invalid.",
"next_step": "Fix the fields listed in validation_errors and retry the request.",
"validation_errors": [
{ "field": "country", "message": "country must be a 2-letter ISO code, e.g. CM" },
{ "field": "amount", "message": "amount is required as a decimal string, e.g. \"5000\"" }
],
"request_id": "c4a1f8e2-9d30-47b6-8e5f-1a2b3c4d5e6f"
}
}The request_id is also returned as the X-Request-Id response header on every call, including errors. Quote it to support and we can find the exact invocation in the logs.
X-Request-Id: 3fa85f64-5717-4562-b3fc-2c963f66afa6
Idempotency
Every money-mutating POST (creating a collection, payout or refund, and the cancel/replay actions) requires an Idempotency-Key header: a unique string of 1 to 255 characters that you generate per logical request. Retrying with the same key and the same body replays the original stored response instead of doing the work twice. A missing key on these endpoints is a 422 validation_error.
curl -X POST https://api.capayglobal.com/v1/collections \
-H "Authorization: Bearer sk_test_XXXXXXXXXXXX" \
-H "Idempotency-Key: col_req_9f2c7a10b8" \
-H "Content-Type: application/json" \
-d '{
"amount": "5000",
"currency": "XAF",
"country": "CM",
"method": "mobile_money",
"msisdn": "+237650000000",
"merchant_reference": "order_10482"
}'A replayed response is byte-for-byte the original and carries an Idempotency-Replayed: true header:
HTTP/1.1 201 Created X-Request-Id: 3fa85f64-5717-4562-b3fc-2c963f66afa6 Idempotency-Replayed: true
Reusing a key with a different body returns 422 idempotency_key_reused. Reusing a key while the first request is still in flight returns 409 request_in_flight: wait and retry with the same key to receive its stored response. On a 5xx the reservation is released, so retrying with the same key is always safe.
Money format
Every monetary amount is a decimal string paired with an explicit ISO 4217 currency: never a float, never a bare integer of subunits. The number of decimal places is fixed by the currency. Zero-decimal currencies carry no fraction; all others carry exactly two.
XAF XOF UGX RWF TZS GNF BIF DJF KMF zero-decimal: "5000" means 5000 NGN GHS USD EUR ... (all others) two-decimal: "12.50" means 12.50
Zero-decimal currencies (the minor unit is the unit, never multiplied by 100): XAF, XOF, UGX, RWF, TZS, GNF, BIF, DJF, KMF. For these, "5000" means 5000 francs, and a fractional amount is rejected.
Pagination and filtering
List endpoints return an object of type "list": a data array of resources plus a has_more boolean. Pages are keyset-based and ordered newest first. To fetch the next page, pass the id of the last item you received as starting_after.
curl "https://api.capayglobal.com/v1/collections?limit=2&starting_after=col_2n4Kq8Zr41Xa&status=completed" \ -H "Authorization: Bearer sk_test_XXXXXXXXXXXX"
{
"status": "success",
"data": {
"object": "list",
"data": [
{ "id": "col_2n4Kq8Zr41Xa", "object": "collection", "status": "completed" },
{ "id": "col_1m3Jp7Yq30Wz", "object": "collection", "status": "completed" }
],
"has_more": true
},
"request_id": "b2c3d4e5-6f7a-4b8c-9d0e-1f2a3b4c5d6e"
}Common list query parameters
| Field | Type | Description |
|---|---|---|
| limit | integer | Page size, 1 to 100. Collections, payouts and payout batches default to 10; balance_transactions, events and the items page of a payout batch default to 25. |
| starting_after | string | A resource id (e.g. col_...). Returns the page of results after that item. An unknown id returns 404. |
| status | string | Collections and payouts (and the items page of a payout batch). Filters to one status: initiated, pending, processing, enqueued, in_reconciliation, completed, failed, cancelled, expired or reversed. Payout batches use their own vocabulary: created, processing, completed, partially_completed, failed, cancelled. |
Each element of data is the full resource object, identical to what you get when you fetch it by id. has_more is true when more pages remain.
Rate limits
Requests are metered per app. Every response carries X-RateLimit-Limit and X-RateLimit-Remaining. Unauthenticated requests (no key, or an invalid key) are metered per source IP instead.
X-RateLimit-Limit: 120 X-RateLimit-Remaining: 118 X-Request-Id: 3fa85f64-5717-4562-b3fc-2c963f66afa6
Exceeding the app limit returns 429 rate_limited with Retry-After and X-RateLimit-Reset (both in seconds). Exceeding the unauthenticated IP limit returns 429 ip_rate_limited. Back off for the stated window, then retry.
{
"status": "error",
"error": {
"type": "rate_limit_error",
"code": "rate_limited",
"message": "Too many requests: the rate limit for this app has been exceeded.",
"next_step": "Back off and retry after the window resets; see the X-RateLimit-* response headers.",
"request_id": "c3d4e5f6-7a8b-4c9d-0e1f-2a3b4c5d6e7f"
}
}Core resources
These read-only endpoints need no request body and (except balances) no scope. They are the fastest way to confirm a key works, discover what you can transact, and price a payment before you create it.
GET /v1/whoami
Method and path: GET/v1/whoami
Returns the merchant, app, environment and key behind the credential you presented. The fastest check that a key is valid and points where you expect.
Scope: None (any valid key).
No parameters and no request body.
curl https://api.capayglobal.com/v1/whoami \ -H "Authorization: Bearer sk_test_XXXXXXXXXXXX"
{
"status": "success",
"data": {
"merchant": {
"id": "0f9a2c34-6b1d-4e8a-9c21-7d2f5a1b3c4d",
"public_id": "mch_8fK2q0Zr41Xa",
"trading_name": "Sunrise Electronics"
},
"app": {
"id": "b7e1d2c3-4f5a-6789-abcd-ef0123456789",
"name": "Default app",
"environment": "SANDBOX"
},
"key": { "prefix": "sk_test_8fK2", "scopes": ["read", "write"] },
"livemode": false
},
"request_id": "3fa85f64-5717-4562-b3fc-2c963f66afa6"
}Response data fields
| Field | Type | Description |
|---|---|---|
| merchant | object | id, public_id (mch_...) and trading_name of the merchant. |
| app | object | id, name and environment (SANDBOX or PRODUCTION) of the app the key belongs to. |
| key | object | prefix (the visible sk_test_/sk_live_ prefix), scopes, kind (server or agent), label, purpose, expires_at, paused, live_money_enabled, allowed_countries, allowed_methods, beneficiary_policy and caps[] (per-currency caps with what was used today and this month, what remains and when each window resets). Server keys state the effective truth: live_money_enabled true, null allowlists, caps []. See Keys for AI agents. |
| livemode | boolean | true in production, false in sandbox. |
Errors: 401 invalid_api_key, 429 rate_limited, 500 internal_error.
GET /v1/providers
Method and path: GET/v1/providers
Lists every provider your keys can reach that is enabled and configured, with its open corridors: countries, currencies, methods, per-corridor amount limits as decimal strings, and live health status. Use it to build your own payment method selector.
Scope: None (any valid key).
No parameters and no request body.
curl https://api.capayglobal.com/v1/providers \ -H "Authorization: Bearer sk_test_XXXXXXXXXXXX"
{
"status": "success",
"data": {
"providers": [
{
"code": "mtn_momo",
"name": "MTN Mobile Money",
"status": "operational",
"countries": ["CM"],
"currencies": ["XAF"],
"methods": ["mobile_money"],
"operators": [
{
"country": "CM",
"currency": "XAF",
"operator": "MTN",
"method": "mobile_money",
"direction": "collection",
"status": "OPEN",
"min_amount": "100",
"max_amount": "1000000"
}
]
}
]
},
"request_id": "9b2c1a08-3e44-4f6d-8a17-c05e9f21d3b6"
}Each provider in data.providers
| Field | Type | Description |
|---|---|---|
| code | string | Stable provider code, e.g. mtn_momo. |
| name | string | Human display name. |
| status | string | Live health, e.g. operational. |
| countries / currencies / methods | string[] | The union across the provider open corridors. |
| operators | object[] | Per corridor: country, currency, operator, method, status, and min_amount / max_amount as decimal strings in that currency. |
Errors: 401 invalid_api_key, 429 rate_limited, 500 internal_error.
GET /v1/pricing
Method and path: GET/v1/pricing
Returns the exact fee breakdown for a payment before you create it: provider fee, platform fee, total, and who bears them. Delegates to the same fee engine the money path uses, so the quote and the charge can never disagree. All amounts are decimal strings in the requested currency.
Scope: None (any valid key).
Query parameters (all required)
| Field | Type | Description |
|---|---|---|
| direction | string | "collection" or "payout". |
| country | string | 2-letter ISO country code, e.g. CM. |
| currency | string | 3-letter ISO currency code, e.g. XAF. |
| method | string | mobile_money, bank or card. |
| amount | string | Decimal string in the currency, e.g. "5000" for XAF. |
curl "https://api.capayglobal.com/v1/pricing?direction=collection&country=CM¤cy=XAF&method=mobile_money&amount=5000" \ -H "Authorization: Bearer sk_test_XXXXXXXXXXXX"
{
"status": "success",
"data": {
"direction": "collection",
"country": "CM",
"currency": "XAF",
"method": "mobile_money",
"amount": "5000",
"fees": {
"provider_fee": "0",
"platform_fee": "100",
"total_fee": "100",
"bearer": "merchant"
}
},
"request_id": "5c7d9e10-2b34-4a56-9f78-e01a2b3c4d5e"
}Errors: 401 invalid_api_key, 422 validation_error, 429 rate_limited, 500 internal_error.
GET /v1/balances
Method and path: GET/v1/balances
Returns your balances per currency for the environment the key targets: available, pending and locked, each a decimal string. Returned as a single list; there is no pagination.
Scope: balances:read.
No parameters and no request body.
curl https://api.capayglobal.com/v1/balances \ -H "Authorization: Bearer sk_test_XXXXXXXXXXXX"
{
"status": "success",
"data": {
"object": "list",
"data": [
{
"currency": "XAF",
"available": "125000",
"pending": "5000",
"locked": "0",
"livemode": false
},
{
"currency": "NGN",
"available": "48250.00",
"pending": "0.00",
"locked": "1500.00",
"livemode": false
}
],
"has_more": false
},
"request_id": "a1b2c3d4-5e6f-4a7b-8c9d-0e1f2a3b4c5d"
}Each balance in data.data
| Field | Type | Description |
|---|---|---|
| currency | string | 3-letter ISO currency code. |
| available | string | Spendable balance, decimal string. |
| pending | string | Funds settling in, not yet spendable. |
| locked | string | Funds reserved against in-flight payouts or refunds. |
| livemode | boolean | true in production, false in sandbox. |
Errors: 401 invalid_api_key, 403 insufficient_scope, 429 rate_limited, 500 internal_error.