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

# Wallet payments

Pesaswap supports three mobile money wallets in Kenya: M-Pesa Express, Airtel Money, and MTN MoMo. All three use the same request shape — the only thing that changes is `payment_method_type`. In every case, the customer approves the payment on their own handset; you never collect a PIN or OTP yourself.

<Note>
  You need an API key, a `profile_id`, and a `customer_id` before making a wallet payment.
</Note>

## Request

Send a `POST` request to `/payments` with `payment_method: "wallet"` and `confirm: true`. This creates the payment and immediately confirms it, which triggers the prompt on the customer's phone.

The example below uses M-Pesa Express (`m_pesa_express`).

<CodeGroup>
  ```bash curl theme={"system"}
  curl --request POST \
    --url https://api.sandbox.pesaswap.io/payments \
    --header "api-key: {{api_key}}" \
    --header "Content-Type: application/json" \
    --data '{
      "amount": 100,
      "currency": "KES",
      "confirm": true,
      "customer_id": "{{customer_id}}",
      "profile_id": "{{profile_id}}",
      "payment_method": "wallet",
      "payment_method_type": "m_pesa_express",
      "phone": "{{phone}}",
      "phone_country_code": "254"
    }'
  ```

  ```javascript Node.js theme={"system"}
  const response = await fetch("https://api.sandbox.pesaswap.io/payments", {
    method: "POST",
    headers: {
      "api-key": "{{api_key}}",
      "Content-Type": "application/json",
    },
    body: JSON.stringify({
      amount: 100,
      currency: "KES",
      confirm: true,
      customer_id: "{{customer_id}}",
      profile_id: "{{profile_id}}",
      payment_method: "wallet",
      payment_method_type: "m_pesa_express",
      phone: "{{phone}}",
      phone_country_code: "254",
    }),
  });

  const payment = await response.json();
  console.log(payment);
  ```

  ```python Python theme={"system"}
  import requests

  response = requests.post(
      "https://api.sandbox.pesaswap.io/payments",
      headers={
          "api-key": "{{api_key}}",
          "Content-Type": "application/json",
      },
      json={
          "amount": 100,
          "currency": "KES",
          "confirm": True,
          "customer_id": "{{customer_id}}",
          "profile_id": "{{profile_id}}",
          "payment_method": "wallet",
          "payment_method_type": "m_pesa_express",
          "phone": "{{phone}}",
          "phone_country_code": "254",
      },
  )

  payment = response.json()
  print(payment)
  ```

  ```php PHP theme={"system"}
  <?php
  $curl = curl_init();

  curl_setopt_array($curl, [
      CURLOPT_URL => "https://api.sandbox.pesaswap.io/payments",
      CURLOPT_RETURNTRANSFER => true,
      CURLOPT_CUSTOMREQUEST => "POST",
      CURLOPT_HTTPHEADER => [
          "api-key: {{api_key}}",
          "Content-Type: application/json",
      ],
      CURLOPT_POSTFIELDS => json_encode([
          "amount" => 100,
          "currency" => "KES",
          "confirm" => true,
          "customer_id" => "{{customer_id}}",
          "profile_id" => "{{profile_id}}",
          "payment_method" => "wallet",
          "payment_method_type" => "m_pesa_express",
          "phone" => "{{phone}}",
          "phone_country_code" => "254",
      ]),
  ]);

  $response = curl_exec($curl);
  curl_close($curl);

  echo $response;
  ```
</CodeGroup>

To use a different wallet, change `payment_method_type` and `currency` — everything else in the request stays the same.

| Wallet         | `payment_method_type` | Currency | Notes                                  |
| -------------- | --------------------- | -------- | -------------------------------------- |
| M-Pesa Express | `m_pesa_express`      | `KES`    | STK push; customer approves on handset |
| Airtel Money   | `airtel_money`        | `KES`    | Customer approves on handset           |
| MTN MoMo       | `momo_mtn`            | `EUR`    | Customer approves on handset           |

## Response

A successful call returns a `200` with a payment object. The fields that matter right now:

```json theme={"system"}
{
  "payment_id": "pay_...",
  "status": "requires_customer_action",
  "amount": 100,
  "currency": "KES"
}
```

**The money has not moved yet.** `status: requires_customer_action` means the prompt is now on the customer's phone, waiting for them to approve or decline. Nothing settles until they respond.

## What to do next

Poll `GET /payments/{payment_id}?force_sync=true` until the status settles. See the [quickstart](/essentials/payments-guide/quickstart) for the full polling loop.

<Warning>
  `force_sync` defaults to `false`. Without it, this endpoint returns the last **stored** status — it does not contact the connector. If you omit `force_sync=true`, your polling loop will keep reading a stale `requires_customer_action` status and will never see the payment settle.
</Warning>

## Status reference

| Status                     | What it means                                                                       | What to do      |
| -------------------------- | ----------------------------------------------------------------------------------- | --------------- |
| `requires_customer_action` | The prompt is on the customer's handset.                                            | Keep polling.   |
| `processing`               | The customer has responded and the connector is finalizing the result.              | Keep polling.   |
| `succeeded`                | The payment settled. The money has moved.                                           | Stop — settled. |
| `failed`                   | The customer declined, the prompt timed out, or the connector rejected the payment. | Stop — failed.  |

## Common errors

<AccordionGroup>
  <Accordion title="Wrong phone_country_code">
    It is a separate field from `phone` — do not prefix the number with the country code yourself. Sending `254` in both `phone` and `phone_country_code` will send the prompt to the wrong number or fail validation.
  </Accordion>

  <Accordion title="Wallet not enabled on the profile">
    Each wallet (M-Pesa Express, Airtel Money, MTN MoMo) has to be connected on the `profile_id` you're using. A request for a wallet that isn't configured fails before any prompt is sent.
  </Accordion>

  <Accordion title="Customer doesn't respond">
    If the customer ignores the prompt, the payment stays in `requires_customer_action` until the prompt expires, then moves to `failed`. There's nothing to retry on your side except creating a new payment.
  </Accordion>
</AccordionGroup>

## Next steps

<CardGroup cols={2}>
  <Card title="Quickstart" icon="rocket" href="/essentials/payments-guide/quickstart">
    The full create-and-poll payment loop.
  </Card>

  <Card title="Card payments and 3DS" icon="credit-card" href="/essentials/payments-guide/card-payments">
    Accept cards with 3DS redirects.
  </Card>

  <Card title="Mandates and recurring" icon="repeat" href="/essentials/payments-guide/mandates-and-recurring">
    Save a wallet for repeat charges.
  </Card>

  <Card title="Refunds" icon="rotate-ccw" href="/essentials/payments-guide/refunds">
    Refund a settled wallet payment.
  </Card>
</CardGroup>
