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

# M-PESA and Airtel Money

> Send a payment prompt to a customer's phone and confirm the result.

Use a mobile money charge when you already have the customer's phone number. Tanda sends a payment prompt to their phone (an STK push on M-PESA); they enter their M-PESA or Airtel Money PIN to approve, and the money lands in your KES wallet.

## 1. Send the prompt

Call [Mobile money charge](https://api.tandapay.io/api-reference/endpoint/mobile-money-charge.md).

```bash cURL theme={null}
curl -X POST https://api.tandapay.io/api/b2b/fiat/deposit/momo \
  -H "Authorization: Bearer $TOKEN" \
  -H "Content-Type: application/json" \
  -d '{
    "amount": 1500,
    "currency": "KES",
    "phoneNumber": "254712345678",
    "externalReference": "order_10045"
  }'
```

```javascript Node.js theme={null}
const charge = await fetch("https://api.tandapay.io/api/b2b/fiat/deposit/momo", {
  method: "POST",
  headers: { Authorization: `Bearer ${await getToken()}`, "Content-Type": "application/json" },
  body: JSON.stringify({
    amount: 1500,
    currency: "KES",
    phoneNumber: "254712345678",
    externalReference: "order_10045",
  }),
}).then((r) => r.json());
```

```python Python theme={null}
charge = requests.post(
    "https://api.tandapay.io/api/b2b/fiat/deposit/momo",
    headers={"Authorization": f"Bearer {get_token()}"},
    json={
        "amount": 1500,
        "currency": "KES",
        "phoneNumber": "254712345678",
        "externalReference": "order_10045",
    },
).json()
```

```json Response theme={null}
{
  "success": true,
  "message": "Initiated request.",
  "transactionId": "aB3dE5fG7hJ9kL1mN2pQ"
}
```

| Field | Required | Notes |
| :- | :-: | :- |
| `amount` | Yes | Whole shillings, e.g. `1500`. |
| `currency` | Yes | `KES`. |
| `phoneNumber` | Yes | `2547XXXXXXXX` or `2541XXXXXXXX`, no `+`. |
| `externalReference` | Yes | Your unique ID for this attempt. |
| `momoOperatorId` | No | `mpesa` or `airtel`. If omitted, Tanda detects the network from the number. |

> [!TIP]
>
> Tell the customer to check their phone as soon as you get the response. Prompts time out after a short while; if the customer misses it, start a new charge with a new `externalReference`.

## 2. Wait for the outcome

The customer approves (or ignores) the prompt on their phone, so the result arrives asynchronously as a `transaction_updated` [webhook](https://api.tandapay.io/guides/webhooks/overview.md).

```json Successful theme={null}
{
  "event": "transaction_updated",
  "data": {
    "transactionId": "aB3dE5fG7hJ9kL1mN2pQ",
    "status": "successful",
    "type": "deposit",
    "externalReference": "order_10045",
    "method": "momo",
    "thirdPartyReference": "TJS4K2LM9P"
  },
  "timestamp": "2026-10-28T09:14:22.100Z"
}
```

```json Failed theme={null}
{
  "event": "transaction_updated",
  "data": {
    "transactionId": "aB3dE5fG7hJ9kL1mN2pQ",
    "status": "failed",
    "type": "deposit",
    "externalReference": "order_10045",
    "method": "momo",
    "note": "The user cancelled the request."
  },
  "timestamp": "2026-10-28T09:14:22.100Z"
}
```

`thirdPartyReference` is the M-PESA or Airtel receipt number. Store it: customers quote it when they contact you.

If you haven't received a webhook, check with [Get transaction](https://api.tandapay.io/api-reference/endpoint/get-transaction.md) using the `transactionId` or your `externalReference`. Fulfil only when `chargeStatus` is `successful`.

## 3. Handle an OTP step

Occasionally a charge needs a one-time code. The webhook arrives with `status: "pending"` and `stepRequired: "otp"`:

```json theme={null}
{
  "event": "transaction_updated",
  "data": {
    "transactionId": "aB3dE5fG7hJ9kL1mN2pQ",
    "status": "pending",
    "type": "deposit",
    "externalReference": "order_10045",
    "stepRequired": "otp",
    "method": "momo"
  },
  "timestamp": "2026-10-28T09:14:05.000Z"
}
```

Ask the customer for the code and submit it with [Validate OTP](https://api.tandapay.io/api-reference/endpoint/validate-otp.md):

```bash theme={null}
curl -X POST https://api.tandapay.io/api/b2b/fiat/deposit/aB3dE5fG7hJ9kL1mN2pQ/validate-otp \
  -H "Authorization: Bearer $TOKEN" \
  -H "Content-Type: application/json" \
  -d '{ "otp": "111111" }'
```

A final `successful` or `failed` webhook follows.

## Why prompts fail

The `note` field explains a failure. The common ones:

| What happened | What to do |
| :- | :- |
| Customer cancelled or didn't enter their PIN in time | Offer to try again with a new charge. |
| Phone off or out of coverage | Ask the customer to check their phone, then retry. |
| Insufficient balance | Ask the customer to top up, or offer another method. |
| Number not registered for M-PESA or Airtel Money | Check the number or offer another method. |
| Network busy | Retry after a short delay. Check [Service status](https://api.tandapay.io/api-reference/endpoint/service-status.md) before retrying many at once. |

The full list is in [Transaction errors](https://api.tandapay.io/guides/errors/transaction-errors.md). Don't branch your code on the wording of `note`; use `status`.

## Refunds

You can refund a successful mobile money payment back to the customer's wallet. See [Refunds](https://api.tandapay.io/guides/collections/refunds.md).


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