Cash in

Charging a customer's MTN MoMo or Airtel Money wallet. They get a PIN prompt; you get a webhook.

The endpoint

POST/v1/paymentsApp key
curl https://api.xendlypay.com/v1/payments \
  -H "Authorization: Bearer $XENDLY_KEY" \
  -H "Content-Type: application/json" \
  -H "Idempotency-Key: order-1043" \
  -d '{
    "amount": 5000,
    "currency": "RWF",
    "msisdn": "0788924941",
    "description": "Order #1043",
    "reference": "1043",
    "metadata": { "order_id": "1043" }
  }'

The body

FieldTypeRequiredNotes
amountintegeryesMinor units. RWF has no subdivision, so 20000 is twenty thousand francs. A float here is a rounding argument with a customer later.
msisdnstringyes0788924941, 250788924941 or +250 788 924 941. We normalise all of them.
currencystringnoRWF. It is the only value, and it is the default.
descriptionstringnoWhat the payment is for. Shown in your dashboard and on exports; not shown to the customer.
referencestringnoYour own id for this. Echoed on every webhook, and the cheapest way to join our record to your order table.
metadataobjectnoArbitrary JSON, stored and returned untouched. We never read it.

Send an Idempotency-Key

Not required, and you should send one anyway. It is the difference between a timeout you can safely retry and a customer charged twice. How it works.

What comes back

201 Created

{
  "success": true,
  "payload": {
    "id": "0f8c2b7e-1a45-4c31-9d02-6e8f1b2c3d44",
    "status": "pending",
    "direction": "collect",
    "amount": 20000,
    "fee": 620,
    "currency": "RWF",
    "msisdn": "250788924941",
    "network": "mtn",
    "reference": "1043",
    "created_at": "2026-09-12T09:14:02Z",
    "completed_at": null
  },
  "timestamp": "2026-09-12T09:14:02.000Z"
}

pending is not paid

A 201 means the network was asked, not that anybody paid. Fulfil on the payment.succeeded webhook and nothing else. The whole argument is here.

Which network it goes over

You do not choose, and there is no provider field to get wrong. We read the prefix and route accordingly:

PrefixNetworkField value
078, 079MTN MoMo"mtn"
072, 073Airtel Money"airtel"
anything elseNot a mobile money number we can reach"unknown"

A number we cannot route is refused at the door with a 400, before a payment row exists, so a typo costs you a rejected request, not a transaction to explain.

What it costs

3.1% of the amount for your first 3 months and 2.3% after that, taken from what we owe you. The customer pays the amount you asked for and not a franc more.

Customer paysOur feeYou keep
20,000 RWF620 RWF19,380 RWF

A failed or cancelled payment costs nothing. The fee is recorded on the transaction at the rate that applied when it happened, so a price change never restates yesterday. See fees and settlement.

When it does not work

StatusMeansDo
400The amount is not a positive integer, or the number is not MTN/Airtel.Fix the request. Retrying will not help.
401The key is wrong, revoked, or edited.Check which key the environment is holding.
403A live key on an unverified business, or a disabled app.Finish verification, or re-enable the app.
402 / 409The network refused: declined, no wallet, or a limit exceeded.Show the customer the message and offer to retry.
429Over your per-minute budget.Wait for Retry-After and retry. Nothing was recorded.

Every failure body carries a human-readable message. Errors and limits has the shape and the full list.

Limits

Minimum100 RWF
MaximumWhatever the customer's wallet and the network allow. MTN and Airtel each cap per transaction and per day, and those caps are theirs, not ours.
Settlement24 hours after the payment succeeds