# 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](./kenya-payins.md).

<div class="endpoint"><span class="method post">POST</span><code>https://api.payd.money/api/v2/payments/payin</code></div>

## How it works

1. You look up the mobile money networks for the customer's country with [payment options by country](./network-discovery.md).
2. The customer picks their network, and you send the payin with that network's ids.
3. The customer approves the payment on their phone, by prompt, USSD code or in their mobile money app, depending on the network.
4. Payd posts the result to your `callback_url`, and the status endpoint shows `success` or `failed`.

## 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](./network-discovery.md) when you build your checkout, and cache the result for a few minutes. See [limits and settlement](./limits-and-settlement.md).

## 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

::: code-group

```bash [cURL]
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 [Node.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 [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](./authentication.md#find-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](./webhooks.md). |
| `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 `+256771234567` or `+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](./pan-african-payouts.md).

## Response

The response has the same shape as an [M-Pesa collection](./kenya-payins.md#response). Store `gateway_response.merchant_reference`, then wait for the [webhook](./webhooks.md) and confirm with the [status endpoint](./transaction-status.md).

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](./network-discovery.md) for the country. |

See [errors](./errors.md) for every code.
