> ## Documentation Index
> Fetch the complete documentation index at: https://docs.kemypay.com/llms.txt
> Use this file to discover all available pages before exploring further.

# Collections

> Request a mobile money payment from your customer

> [!NOTE]
>
> **Beta.** Collections are being rolled out in production. You can already prepare your integration with this guide.

A **collection** asks your customer to pay from their mobile money account. They receive a prompt on their phone and confirm with their PIN. You get the result by webhook.

## Request a payment

```bash cURL theme={null}
curl -X POST https://api.kemypay.com/v1/mobile-money/collections \
  -H "Authorization: Bearer sk_live_YOUR_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "service": "019a8f2e-4c1b-7d3a-9e55-2b6f0c1d8a47",
    "amount": "1000",
    "currency": "XAF",
    "operator": "MTN_CMR",
    "phone_number": "237670000000",
    "reference": "order-2026-10-29-0001",
    "description": "Afroyamo Premium",
    "metadata": { "user_id": "42" }
  }'
```

```javascript Node.js theme={null}
const res = await fetch('https://api.kemypay.com/v1/mobile-money/collections', {
  method: 'POST',
  headers: {
    Authorization: `Bearer ${process.env.KEMYPAY_SECRET_KEY}`,
    'Content-Type': 'application/json',
  },
  body: JSON.stringify({
    service: process.env.KEMYPAY_SERVICE_CODE,
    amount: '1000',
    currency: 'XAF',
    operator: 'MTN_CMR',
    phone_number: '237670000000',
    reference: `order-${order.id}`, // your own unique reference
    description: 'Afroyamo Premium',
    metadata: { user_id: String(user.id) },
  }),
});
const { data: payment } = await res.json(); // status: "processing"
```

```php PHP theme={null}
$ch = curl_init('https://api.kemypay.com/v1/mobile-money/collections');
curl_setopt_array($ch, [
    CURLOPT_POST => true,
    CURLOPT_RETURNTRANSFER => true,
    CURLOPT_HTTPHEADER => [
        'Authorization: Bearer ' . getenv('KEMYPAY_SECRET_KEY'),
        'Content-Type: application/json',
    ],
    CURLOPT_POSTFIELDS => json_encode([
        'service' => getenv('KEMYPAY_SERVICE_CODE'),
        'amount' => '1000',
        'currency' => 'XAF',
        'operator' => 'MTN_CMR',
        'phone_number' => '237670000000',
        'reference' => 'order-' . $order->id,
        'description' => 'Afroyamo Premium',
        'metadata' => ['user_id' => (string) $user->id],
    ]),
]);
$payment = json_decode(curl_exec($ch), true)['data'];
```

```python Python theme={null}
import os, requests

res = requests.post(
    "https://api.kemypay.com/v1/mobile-money/collections",
    headers={"Authorization": f"Bearer {os.environ['KEMYPAY_SECRET_KEY']}"},
    json={
        "service": os.environ["KEMYPAY_SERVICE_CODE"],
        "amount": "1000",
        "currency": "XAF",
        "operator": "MTN_CMR",
        "phone_number": "237670000000",
        "reference": f"order-{order.id}",
        "description": "Afroyamo Premium",
        "metadata": {"user_id": str(user.id)},
    },
)
payment = res.json()["data"]
```

```json Response (201) theme={null}
{
  "success": true,
  "data": {
    "code": "019a9c41-7e2d-4b8f-a1c3-5d6e7f8a9b0c",
    "reference": "order-2026-10-29-0001",
    "service_code": "019a8f2e-4c1b-7d3a-9e55-2b6f0c1d8a47",
    "status": "processing",
    "country": "CMR",
    "operator": "MTN_CMR",
    "phone_number": "237670000000",
    "currency": "XAF",
    "value": "1000.00",
    "amount": "1000.00",
    "fee": "33.00",
    "fee_bearer": "merchant",
    "net_amount": "967.00",
    "description": "Afroyamo Premium",
    "metadata": { "user_id": "42" },
    "failure_code": null,
    "failure_message": null,
    "operator_transaction_id": null,
    "callback_url": null,
    "created_at": "2026-10-29T10:00:00+00:00",
    "completed_at": null
  },
  "meta": { "request_id": "a4f8e2c1b3d97056", "timestamp": "2026-10-29T10:00:00+00:00", "mode": "live" }
}
```

## Fields

| Field | Required | Notes |
| - | - | - |
| `service` | Yes | The code of the service that collects. It must be `active` (or `pending_review` in test mode) |
| `amount` | Yes | The value of the goods, as a string. Respect the `decimals` of the operator |
| `currency` | Yes | `XAF`, `XOF`, `KES`… Congo - Kinshasa accepts `CDF` and `USD` |
| `phone_number` | Yes | With the country code. Spaces, dashes and `+` are removed |
| `reference` | Yes | **Your** unique reference. See [Safe retries](#safe-retries) |
| `operator` | No | `MTN_CMR`… Guessed from the phone number when missing |
| `country` | No | A check: refused if the operator is in another country |
| `fee_bearer` | No | Overrides the setting of the service |
| `description` | No | Shown to your customer on the prompt: 4 to 22 letters, digits or spaces |
| `metadata` | No | Up to 10 keys with text values, returned in webhooks |
| `webhook_url` | No | https. Takes priority over the service for this payment |
| `callback_url` | No | Takes priority over the service for this payment |

## Statuses

```
pending ──▶ processing ──▶ completed
                      └──▶ failed
```

| Status | Meaning |
| - | - |
| `pending` | Saved, the operator has not answered yet. We check it for you |
| `processing` | Your customer has a prompt on their phone |
| `completed` | Paid. `net_amount` is on the balance of your service |
| `failed` | Not paid. `failure_code` says why. No money was taken |

**Wait for the webhook**, not for the response: your customer can take a minute to type their PIN.

## Failure codes

| `failure_code` | What happened | What to tell your customer |
| - | - | - |
| `payer_not_found` | The number is not a mobile money account of this operator | Check the number or the operator |
| `payment_not_approved` | Wrong PIN, declined, or no answer in time | Try again and approve the prompt |
| `insufficient_balance` | Not enough money on the account | Top up the mobile money account |
| `payer_limit_reached` | A limit of the account was reached | Try a smaller amount or later |
| `invalid_phone_number` | The number is not valid for this operator | Check the number |
| `invalid_amount` | Amount refused by the operator (limits, decimals) | Check the amount |
| `operator_unavailable` | The operator is down | Use another operator or try later |
| `payment_in_progress` | A payment is already waiting on the phone | Approve or cancel it first |
| `expired` | The payment never started | Try again |
| `unknown_error` | Unknown failure | Try again |

In every failure case, **no money was taken**.

## Safe retries

`reference` makes the request **idempotent**. If your server times out, send the exact same request again:

* same `reference`, same service, amount and number → `200` with the **same payment**. Your customer is never charged twice;
* same `reference` with another service, amount or number → `422 VALIDATION_ERROR`.

Use a reference that identifies the order in your system, like `order-1234`.

## Common pitfalls

> **Decimals**
>
> Whole amounts only for most XAF and XOF operators. `1000.50` is refused when `decimals` is `0`.

> **Limits apply to what the customer pays**
>
> With `fee_bearer: "customer"`, the customer pays more than the value: check `within_limits` with [`POST /quote`](https://docs.kemypay.com/api-reference/collections/quote).

> **Operator closed**
>
> `503 OPERATION_FAILED` with `failure_code: operator_unavailable`: offer another operator of the country.

> **The PIN prompt shows our payment processor**
>
> The prompt is sent by the operator and shows the name of our payment processor, not your brand. Tell your customers to expect it.


This documentation is built and hosted on [Mintlify](https://mintlify.com), a developer documentation platform.