Capay Global

Collections

A collection pulls money in from a payer: a mobile money or bank debit that credits your merchant balance. You create a collection intent, we route it to a provider, and the transaction moves through its lifecycle with a webhook at every step. Amounts are decimal strings with an explicit currency; zero-decimal currencies such as XAF and XOF carry no fraction.

The collection object

Every collections endpoint returns the same object inside the response envelope. The id is prefixed col_, amounts are decimal strings in the currency's own units, and fees are always broken out. These are the fields you interpret most often:

statusstringrequired
Where the collection is in its lifecycle. Always branch on this; never assume success.
amountstringrequired
The requested amount, as a decimal string in the collection's currency.
feesobjectrequired
The fee breakdown: platform_fee, provider_fee, total_fee (all decimal strings) and bearer (merchant or customer).
net_amountstring | nullrequired
The amount credited to your balance after fees, as a decimal string. Null until the collection is captured.
order_amountstring | nullrequired
Customer-pays collections only: the order you asked for. amount is what the payer was charged and net_amount equals this exactly. Null when your business pays the fee.
auth_modelstring | nullrequired
Which continuation the payer needs: STK_PROMPT, OTP, REDIRECT or NONE. Tells your client what to show next.
hosted_urlstring | nullrequired
When auth_model is REDIRECT, the provider's hosted payment page. Send the payer's browser there to complete the payment; null on prompt-based (STK_PROMPT) collections.
reason_codestring | nullrequired
A machine-readable reason from the closed reason-code vocabulary, set on non-success outcomes. Null while the collection is healthy.
requires_requerybooleanrequired
True when dispatch timed out and the final status will arrive from the reconciler by webhook rather than in this response.
operatorstring | nullrequired
The specific operator the transaction was routed to (for example MTN), when known.
livemodebooleanrequired
False for sandbox collections, true for production. Mirrors the environment of the key that created it.

Create a collection

Creates the collection intent and dispatches it. This call moves money, so it requires the collections:write scope and an Idempotency-Key: retrying with the same key and body replays the stored response instead of charging the payer twice. Card is not served on this surface (no card data ever enters the API); use mobile_money or bank. On success you get 201 with the collection in its post-dispatch state (pending for a mobile money prompt).

POST/v1/collections

Headers

Authorizationheaderrequired
Your secret key as a bearer token: Bearer sk_test_... in sandbox, Bearer sk_live_... in production.
Idempotency-Keyheaderrequired
A unique key (1-255 chars) that makes the call safe to retry. Same key and body replays the stored response; same key with a different body returns idempotency_key_reused.
X-Scenario-Keyheadersandbox only
Forces a named simulator scenario (for example success.late or failed.insufficient_funds). Ignored in production; an unknown key returns validation_error.
X-Correlation-Idheaderoptional
An optional client-supplied id, echoed back on the response and stored in your request logs.

Request body

amountstringrequired
The amount to collect, as a decimal string in the currency's major unit. Zero-decimal currencies (XAF, XOF, UGX, RWF and others) take a whole number such as "5000"; two-decimal currencies take up to two places such as "12.50". Must be greater than zero.
currencystringrequired
The 3-letter ISO 4217 currency code, uppercase (for example XAF).
countrystringrequired
The 2-letter ISO 3166-1 country code, uppercase (for example CM).
methodstringrequired
How the payer pays: mobile_money or bank. card is rejected with method_not_available; card data is never accepted by this API.
msisdnstringrequired
The payer's phone number, 8 to 15 digits with an optional leading +. Required when method is mobile_money.
merchant_referencestringrequired
Your own reference for this collection, up to 128 characters, unique per app. Reusing a value returns validation_error, which makes it a second line of defense against duplicates.
customerobjectoptional
Free-form details about the payer that you attach and we echo back unchanged.
metadataobjectoptional
Free-form key/value pairs you attach and we echo back unchanged.
fee_bearerstringoptional
Who pays the collection fee: merchant or customer. Omit it to use your business setting (default merchant). With customer, amount is your order and the payer is charged the order plus the fee.

Request

curl
curl https://api.capayglobal.com/v1/collections \
  -H "Authorization: Bearer sk_test_8fK2q0Zr41Xa" \
  -H "Idempotency-Key: col-order-10482-a1b2c3" \
  -H "Content-Type: application/json" \
  -d '{
    "amount": "5000",
    "currency": "XAF",
    "country": "CM",
    "method": "mobile_money",
    "msisdn": "+237650000001",
    "merchant_reference": "order-10482",
    "customer": { "name": "Ada Nkemelu", "email": "[email protected]" },
    "metadata": { "cart_id": "cart_88213" }
  }'

Response

201 response
{
  "status": "success",
  "data": {
    "id": "col_7Qm3k1Xa9Zr4",
    "object": "collection",
    "status": "pending",
    "amount": "5000",
    "currency": "XAF",
    "country": "CM",
    "method": "mobile_money",
    "msisdn": "+237650000001",
    "merchant_reference": "order-10482",
    "customer": { "name": "Ada Nkemelu", "email": "[email protected]" },
    "metadata": { "cart_id": "cart_88213" },
    "fees": {
      "platform_fee": "100",
      "provider_fee": "0",
      "total_fee": "100",
      "bearer": "merchant"
    },
    "net_amount": null,
    "auth_model": "STK_PROMPT",
    "hosted_url": null,
    "reason_code": null,
    "requires_requery": false,
    "provider": "simulator",
    "operator": "MTN",
    "livemode": false,
    "created_at": "2026-09-01T10:15:00Z",
    "completed_at": null,
    "expires_at": "2026-09-01T10:45:00Z"
  },
  "request_id": "3fa85f64-5717-4562-b3fc-2c963f66afa6"
}

Retrieve a collection

Fetches one collection by its id (col_...) or raw UUID. Always scoped to your merchant and the key's environment: an id from the other environment returns 404. Needs the collections:read scope.

GET/v1/collections/{id}

curl
curl https://api.capayglobal.com/v1/collections/col_7Qm3k1Xa9Zr4 \
  -H "Authorization: Bearer sk_test_8fK2q0Zr41Xa"
200 response
{
  "status": "success",
  "data": {
    "id": "col_7Qm3k1Xa9Zr4",
    "object": "collection",
    "status": "completed",
    "amount": "5000",
    "currency": "XAF",
    "country": "CM",
    "method": "mobile_money",
    "msisdn": "+237650000001",
    "merchant_reference": "order-10482",
    "customer": { "name": "Ada Nkemelu", "email": "[email protected]" },
    "metadata": { "cart_id": "cart_88213" },
    "fees": {
      "platform_fee": "100",
      "provider_fee": "0",
      "total_fee": "100",
      "bearer": "merchant"
    },
    "net_amount": "4900",
    "auth_model": "STK_PROMPT",
    "hosted_url": null,
    "reason_code": null,
    "requires_requery": false,
    "provider": "simulator",
    "operator": "MTN",
    "livemode": false,
    "created_at": "2026-09-01T10:15:00Z",
    "completed_at": "2026-09-01T10:15:08Z",
    "expires_at": "2026-09-01T10:45:00Z"
  },
  "request_id": "5c7d9e10-2b34-4a56-9f78-e01a2b3c4d5e"
}

List collections

Returns your collections newest first, as a keyset-paginated list. Needs the collections:read scope. has_more tells you whether another page exists; pass the last id as starting_after to fetch it.

GET/v1/collections

Query parameters

limitintegeroptional
How many collections to return, 1 to 100. Defaults to 10.
statusstringoptional
Return only collections in this status (for example completed or pending).
starting_afterstringoptional
A collection id (col_...) to page after. Use the id of the last row from the previous page.
curl
curl "https://api.capayglobal.com/v1/collections?limit=1&status=completed" \
  -H "Authorization: Bearer sk_test_8fK2q0Zr41Xa"
200 response
{
  "status": "success",
  "data": {
    "object": "list",
    "data": [
      {
        "id": "col_7Qm3k1Xa9Zr4",
        "object": "collection",
        "status": "completed",
        "amount": "5000",
        "currency": "XAF",
        "country": "CM",
        "method": "mobile_money",
        "msisdn": "+237650000001",
        "merchant_reference": "order-10482",
        "fees": { "platform_fee": "100", "provider_fee": "0", "total_fee": "100", "bearer": "merchant" },
        "net_amount": "4900",
        "auth_model": "STK_PROMPT",
        "hosted_url": null,
        "reason_code": null,
        "requires_requery": false,
        "provider": "simulator",
        "operator": "MTN",
        "livemode": false,
        "created_at": "2026-09-01T10:15:00Z",
        "completed_at": "2026-09-01T10:15:08Z",
        "expires_at": "2026-09-01T10:45:00Z"
      }
    ],
    "has_more": true
  },
  "request_id": "9b2c1a08-3e44-4f6d-8a17-c05e9f21d3b6"
}

Cancel a collection

Cancels a collection that has not yet reached a provider outcome. Only an initiated or pending collection can be cancelled; anything else returns validation_error. This call takes no Idempotency-Key. Needs the collections:write scope.

POST/v1/collections/{id}/cancel

curl
curl -X POST https://api.capayglobal.com/v1/collections/col_7Qm3k1Xa9Zr4/cancel \
  -H "Authorization: Bearer sk_test_8fK2q0Zr41Xa"
200 response
{
  "status": "success",
  "data": {
    "id": "col_7Qm3k1Xa9Zr4",
    "object": "collection",
    "status": "cancelled",
    "amount": "5000",
    "currency": "XAF",
    "reason_code": null,
    "requires_requery": false,
    "livemode": false,
    "created_at": "2026-09-01T10:15:00Z",
    "completed_at": null,
    "expires_at": "2026-09-01T10:45:00Z"
  },
  "request_id": "c4a1f8e2-9d30-47b6-8e5f-1a2b3c4d5e6f"
}

Collection events

Returns the append-only status history for one collection, oldest first: every transition, the actor that made it, and the payload recorded with it. This is the audit trail behind the object's current status. Needs the collections:read scope.

GET/v1/collections/{id}/events

curl
curl https://api.capayglobal.com/v1/collections/col_7Qm3k1Xa9Zr4/events \
  -H "Authorization: Bearer sk_test_8fK2q0Zr41Xa"
200 response
{
  "status": "success",
  "data": {
    "object": "list",
    "collection": "col_7Qm3k1Xa9Zr4",
    "data": [
      {
        "from_status": null,
        "to_status": "initiated",
        "reason_code": null,
        "actor": "api",
        "payload": { "routing": { "provider": "simulator", "corridor_id": "cor_2f1a", "priority": 1 } },
        "created_at": "2026-09-01T10:15:00Z"
      },
      {
        "from_status": "initiated",
        "to_status": "pending",
        "reason_code": null,
        "actor": "api",
        "payload": { "scenario": "success" },
        "created_at": "2026-09-01T10:15:00Z"
      },
      {
        "from_status": "pending",
        "to_status": "processing",
        "reason_code": null,
        "actor": "simulator",
        "payload": {},
        "created_at": "2026-09-01T10:15:04Z"
      },
      {
        "from_status": "processing",
        "to_status": "completed",
        "reason_code": null,
        "actor": "simulator",
        "payload": {},
        "created_at": "2026-09-01T10:15:08Z"
      }
    ],
    "has_more": false
  },
  "request_id": "7e1f0a92-6c53-4b8d-a041-2f9c8e7d6b5a"
}

Hosted checkout and payment links

Collections can also be created without an API key through payment links: the payer opens pay.capayglobal.com/l/<pl_id> and the hosted checkout calls POST /v1/checkout/initiate, which runs this exact create pipeline on the payer's behalf. The payer's page then polls the public GET /v1/checkout/status/{id}, which answers a payer-safe projection (status, reason_code, and on completion receipt_ref: the operator's own transaction id, the number in the payer's SMS). Both endpoints are IP rate-limited and are documented in the Payment links & checkout guide.

POST/v1/checkout/initiate

GET/v1/checkout/status/{id}

Status lifecycle

A collection starts at initiated the instant the intent row is written, moves to pending once dispatched (a mobile money prompt is awaiting the payer), then processing, and settles at a terminal status. Terminal statuses are completed, failed, cancelled, expired and reversed; enqueued and in_reconciliation are intermediate states you may also observe. Always handle each status explicitly rather than assuming success. A dispatch that has not resolved within 20 seconds returns 201 pending with requires_requery set true: the reconciler requeries and the truth arrives by webhook.

status lifecycle
initiated -> pending -> processing -> completed
                          \-> failed | expired | cancelled

Webhooks

Each transition emits a signed webhook to your active endpoints subscribed to that type. The body is the event envelope; its data field carries a compact snapshot of the collection (id, status, amount, currency, country, method, reason_code, merchant_reference, livemode). collection.completed is the one that confirms the money landed. Verify the X-Capay-Signature header against the raw body before trusting a delivery: see the Authentication guide.

EventStatusWhen
collection.pendingpendingDispatched and awaiting the payer or the provider.
collection.processingprocessingThe provider accepted it and is settling the transaction.
collection.completedcompletedThe money was collected. This is the confirmation to fulfill the order.
collection.failedfailedThe collection failed; reason_code says why.
collection.expiredexpiredThe payer did not complete it before the intent expired.
collection.cancelledcancelledThe collection was cancelled before a provider outcome.
collection.completed delivery
POST https://your-app.example.com/webhooks/capay
X-Capay-Signature: t=1767225708,v1=6f8b4c2d9e0a1b3c5d7e9f0a2b4c6d8e0f1a3b5c7d9e1f0a2b4c6d8e0f1a3b5c
X-Capay-Event-Id: evt_2n4Kq8Zr41Xa
X-Capay-Event-Type: collection.completed
Content-Type: application/json

{
  "id": "evt_2n4Kq8Zr41Xa",
  "object": "event",
  "type": "collection.completed",
  "created_at": "2026-09-01T10:15:08Z",
  "data": {
    "id": "col_7Qm3k1Xa9Zr4",
    "object": "collection",
    "status": "completed",
    "amount": "5000",
    "currency": "XAF",
    "country": "CM",
    "method": "mobile_money",
    "reason_code": null,
    "merchant_reference": "order-10482",
    "livemode": false
  }
}

Sandbox behavior

In the sandbox the collection never touches a real provider: a simulator drives it through the lifecycle. Send an X-Scenario-Key header to force a named scenario, or let the last four digits of the msisdn pick one. Any unmatched number resolves to success. See the Sandbox guide for the full scenario catalog.

MSISDN endingScenario
…0001success
…0002failed.insufficient_funds
…0003failed.invalid_customer
…0004failed.payer_timeout
…0005expired
…0006enqueued.then_success
…0007in_reconciliation.then_resolved
…0008webhook.duplicate
…0009success.late

Errors

These are the typed error codes the collections endpoints can return, on top of the shared envelope. A 5xx is always safe to retry with the same Idempotency-Key.

CodeHTTPWhen
validation_error422A body field is missing or malformed; validation_errors names each one.
method_not_available422method is card, or no open corridor serves the requested method.
unsupported_country422No open corridor serves this country. Check GET /v1/providers.
unsupported_currency422No open corridor serves this currency. Check GET /v1/providers.
card_data_rejected422The body contained card data (PAN, CVV or expiry), which this API never accepts.
prohibited_corridor403This country and currency pair is blocked by platform policy.
limit_exceeded403A configured merchant, country, provider or platform limit was hit.
risk_rejected403The risk engine declined the transaction. No money moved.
insufficient_scope403The API key lacks the collections:read or collections:write scope for this call.
invalid_api_key401The key is missing, malformed, revoked, expired or for the wrong environment.
resource_not_found404No collection matches this id in the key's environment.
idempotency_key_reused422This Idempotency-Key was already used with a different request body.
request_in_flight409A request with this Idempotency-Key is still being processed.
rate_limited429The app rate limit was exceeded; see the X-RateLimit-* headers.
corridor_closed503The corridor is temporarily closed. Retry later.
service_paused503Collections are temporarily paused on the platform.
provider_unavailable503No provider is currently available for this corridor.
internal_error500Something failed on our side. Retry with the same Idempotency-Key.