Appearance
Collect with mobile money in Africa
Ask a customer to pay from their mobile money wallet: MTN MoMo, Airtel Money, Orange Money, Wave and others. For M-Pesa in Kenya, see collect with M-Pesa.
POST
https://api.payd.money/api/v2/payments/payinHow it works
- You look up the mobile money networks for the customer's country with payment options by country.
- The customer picks their network, and you send the payin with that network's ids.
- The customer approves the payment on their phone, by prompt, USSD code or in their mobile money app, depending on the network.
- Payd posts the result to your
callback_url, and the status endpoint showssuccessorfailed.
Where you can collect
| Country | country | currency | Mobile money networks today |
|---|---|---|---|
| Uganda | UG | UGX | Airtel Money, MTN MoMo |
| Tanzania | TZ | TZS | Airtel Money, Halopesa, M-Pesa, Tigo Pesa |
| Rwanda | RW | RWF | MTN MoMo |
| Zambia | ZM | ZMW | Airtel Money, MTN MoMo |
| Côte d'Ivoire | CI | XOF | MTN MoMo, Moov Money, Orange Money, Wave |
| Senegal | SN | XOF | Free Money, Orange Money, Wave |
| Cameroon | CM | XAF | MTN MoMo, Orange Money |
| Benin | BJ | XOF | MTN MoMo, Moov Money |
| Togo | TG | XOF | Moov Money, TogoCell |
| Botswana | BW | BWP | Orange Money |
Networks, limits and settlement times change, and the API is the source of truth for all three. Read them from payment options by country when you build your checkout, and cache the result for a few minutes. See limits and settlement.
1. Pick the network
Fetch the options for the customer's country and keep the networks for collections:
bash
curl https://api.payd.money/api/v2/payments/discovery/UG \
-u "$PAYD_API_USERNAME:$PAYD_API_PASSWORD"Keep networks where channel_type is momo, ramp_type is deposit and status is active. Show network_name to the customer. When they choose one, keep its network_id, channel_id, min_amount and max_amount:
json
{
"id": "04a3083a-567c-4a2b-aa39-fa8f19e64341-deposit",
"network_id": "04a3083a-567c-4a2b-aa39-fa8f19e64341",
"network_name": "Airtel Mobile Money",
"channel_type": "momo",
"ramp_type": "deposit",
"channel_id": "e167def0-c4f0-46e2-aaa9-50046f13b0a7",
"min_amount": 15001,
"max_amount": 3000000,
"status": "active"
}Check the amount against min_amount and max_amount before you send the payin. A 0 means there is no published limit for that side.
2. Send the payin
bash
curl https://api.payd.money/api/v2/payments/payin \
-u "$PAYD_API_USERNAME:$PAYD_API_PASSWORD" \
-H "Content-Type: application/json" \
-d '{
"amount": 20000,
"currency": "UGX",
"country": "UG",
"transaction_channel": "mobile",
"customer": {
"first_name": "Grace",
"last_name": "Namubiru",
"phone_number": "+256771234567",
"email": "grace@example.com",
"country": "UG",
"document_type": "national_id",
"document_number": "CM94012345ABCD",
"dob": "1994-03-12T00:00:00Z",
"address": { "line_1": "Plot 12 Kampala Road", "city": "Kampala", "country": "UG" }
},
"vendor_config": { "account_id": "4f28836c-2f1e-4d9b-9a51-6f3c2b1d8e70" },
"provider_config": {
"network_id": "04a3083a-567c-4a2b-aa39-fa8f19e64341",
"channel_id": "e167def0-c4f0-46e2-aaa9-50046f13b0a7"
},
"payment_config": {
"account_number": "+256771234567",
"remark": "Order 8842",
"reference": "order-8842",
"callback_url": "https://example.com/webhooks/payd"
}
}'js
const auth = Buffer.from(
`${process.env.PAYD_API_USERNAME}:${process.env.PAYD_API_PASSWORD}`,
).toString('base64')
const headers = { Authorization: `Basic ${auth}`, 'Content-Type': 'application/json' }
// 1. Networks the customer can pay from in Uganda
const options = await fetch('https://api.payd.money/api/v2/payments/discovery/UG', { headers })
.then((r) => r.json())
const networks = Object.values(options.provider_networks ?? {})
.flatMap((group) => group.networks)
.filter((n) => n.channel_type === 'momo' && n.ramp_type === 'deposit' && n.status === 'active')
// 2. The customer picked one
const network = networks.find((n) => n.network_name === 'Airtel Mobile Money')
const res = await fetch('https://api.payd.money/api/v2/payments/payin', {
method: 'POST',
headers,
body: JSON.stringify({
amount: 20000,
currency: 'UGX',
country: 'UG',
transaction_channel: 'mobile',
customer: {
first_name: 'Grace',
last_name: 'Namubiru',
phone_number: '+256771234567',
email: 'grace@example.com',
country: 'UG',
document_type: 'national_id',
document_number: 'CM94012345ABCD',
dob: '1994-03-12T00:00:00Z',
address: { line_1: 'Plot 12 Kampala Road', city: 'Kampala', country: 'UG' },
},
vendor_config: { account_id: process.env.PAYD_ACCOUNT_ID },
provider_config: { network_id: network.network_id, channel_id: network.channel_id },
payment_config: {
account_number: '+256771234567',
reference: 'order-8842',
callback_url: 'https://example.com/webhooks/payd',
},
}),
signal: AbortSignal.timeout(35_000),
})
const body = await res.json()
if (!res.ok) throw new Error(`${body.error_code}: ${body.error_message}`)
console.log(body.gateway_response.merchant_reference)python
import os
import requests
AUTH = (os.environ["PAYD_API_USERNAME"], os.environ["PAYD_API_PASSWORD"])
# 1. Networks the customer can pay from in Uganda
options = requests.get("https://api.payd.money/api/v2/payments/discovery/UG", auth=AUTH, timeout=15).json()
networks = [
n
for group in (options.get("provider_networks") or {}).values()
for n in group["networks"]
if n["channel_type"] == "momo" and n["ramp_type"] == "deposit" and n["status"] == "active"
]
# 2. The customer picked one
network = next(n for n in networks if n["network_name"] == "Airtel Mobile Money")
res = requests.post(
"https://api.payd.money/api/v2/payments/payin",
auth=AUTH,
json={
"amount": 20000,
"currency": "UGX",
"country": "UG",
"transaction_channel": "mobile",
"customer": {
"first_name": "Grace",
"last_name": "Namubiru",
"phone_number": "+256771234567",
"email": "grace@example.com",
"country": "UG",
"document_type": "national_id",
"document_number": "CM94012345ABCD",
"dob": "1994-03-12T00:00:00Z",
"address": {"line_1": "Plot 12 Kampala Road", "city": "Kampala", "country": "UG"},
},
"vendor_config": {"account_id": os.environ["PAYD_ACCOUNT_ID"]},
"provider_config": {"network_id": network["network_id"], "channel_id": network["channel_id"]},
"payment_config": {
"account_number": "+256771234567",
"reference": "order-8842",
"callback_url": "https://example.com/webhooks/payd",
},
},
timeout=35,
)
res.raise_for_status()
print(res.json()["gateway_response"]["merchant_reference"])Fields
| Field | Type | Required | Description |
|---|---|---|---|
amount | number | Yes | Amount in the local currency, as a whole number, within the network's limits. |
currency | string | Yes | The country's currency from the table above. |
country | string | Yes | The two-letter country code. Required for XOF and XAF, where several countries share a currency. |
transaction_channel | string | Yes | mobile. |
customer.first_name, customer.last_name | string | Yes | The name on the mobile money wallet, if you have it. |
customer.phone_number | string | Yes | The wallet number in international form with a +, such as +256771234567. |
customer.email | string | Yes | The customer's email. |
customer.country | string | No | The same country code. |
customer.document_type | string | Yes | The customer's ID: national_id, passport, drivers_license or voters_card. |
customer.document_number | string | Yes | The number on that ID. |
customer.dob | string | Yes | The customer's date of birth, as a timestamp such as 1994-03-12T00:00:00Z. |
customer.address | object | Yes | line_1, city and country, plus state and postal_code where they apply. |
vendor_config.account_id | string | Yes | Your account id. |
provider_config.network_id | string | Yes | network_id of the network the customer chose. |
provider_config.channel_id | string | Yes | channel_id of that network, from its deposit entry. |
payment_config.account_number | string | Yes | The wallet number again, in the same form. |
payment_config.reference | string | No | Your own id for the payment. |
payment_config.callback_url | string | No | Where Payd posts the result. See webhooks. |
payment_config.remark | string | No | A short note kept with the payment. |
Who is paying
These networks check who sends the money, so every payin carries the customer's ID, date of birth, email and address, as in the example. Ask for them on your checkout before you send the payin. M-Pesa in Kenya needs only the name and phone number.
Phone numbers
- Send the full international number with a
+, such as+256771234567or+221771234567. - In Côte d'Ivoire and Benin the national number keeps its leading 0, such as
+2250712345678. - Mobile money payouts use the same number without the
+. See payouts across Africa.
Response
The response has the same shape as an M-Pesa collection. Store gateway_response.merchant_reference, then wait for the webhook and confirm with the status endpoint.
Some networks take a few minutes to confirm. Keep your "waiting for payment" screen up, and tell MTN customers to look for a pending approval in their MoMo app if no prompt appears.
Common errors
error_code | Message | Fix |
|---|---|---|
invalid_argument | names a missing field | Send the field. provider_config.network_id and channel_id are needed outside Kenya. |
provider_error | the network's reason | Usually a number that is not registered on that network, or an amount outside its limits. |
resource_not_found | no pricing configured... | Payd does not offer this country, currency and channel together. Check the options for the country. |
See errors for every code.

