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

# Payments

> Collect money from a customer's mobile money account into your PrimePay account.

A payment collects money from a customer's mobile money account into your account. The customer approves it on their phone, so the result arrives later, by [webhook](https://docs.primepay.mw/api-reference/webhooks.md): to your endpoints under **Settings › Webhooks**, or to the payment's own `callbackUrl`. You can also check a payment at any time with `GET /v1/payments/{reference}`.

## Operators

| Operator | `shortCode` | How the customer approves |
| - | - | - |
| Airtel Money | `airtel` | A PIN prompt on their phone |
| TNM Mpamba | `tnm` | A PIN prompt on their phone |
| PrimePay Wallet | `primepay_wallet` | In the PrimePay app. In sandbox, use a [test wallet number](https://docs.primepay.mw/api-reference/sandbox.md#test-primepay-wallet-numbers). |

Get the operators with `GET /mobile-money/operators?country=MW` and send an operator's `refId` as `provider` when you create a payment. A publishable key is enough for this call.

## How a payment runs

**1. You create the payment**

`POST /v1/payments/mobile-money` with the amount, the customer's number, the operator and your `merchantOrderId`. The payment collects into the account that owns the API key.

**2. PrimePay answers 202 Accepted**

The response has a `reference` and `"status": "processing"`. If the operator refuses the request straight away, the answer is `402` with `"status": "failed"` and a `failureCode`.

**3. The customer approves on their phone**

This can take a few seconds or a couple of minutes.

**4. You get the result**

A webhook tells you the payment is `success` or `failed`. Hand over the goods only on `success`.

> [!WARNING]
>
> Never treat a timeout or an error from PrimePay as a failed payment. Check its status first.

## Fields

| Field | Required | Notes |
| - | - | - |
| `amount` | Yes | What your customer owes. Depending on your fee setting, the customer may pay PrimePay's fee on top. |
| `currency` | Yes | `MWK`. |
| `customerPhone` | Yes | International format, `+265` and 9 digits. Must belong to the operator. |
| `provider` | Yes | An operator `refId` from `GET /mobile-money/operators`. |
| `merchantOrderId` | Yes | Your order or invoice number, up to 64 characters (letters, digits, `- _ . /`). One active or paid payment per order. |
| `description` | No | Up to 140 characters. |
| `email`, `firstName`, `lastName` | No | The customer, for receipts and your records. |
| `callbackUrl` | No | Sends this payment's webhooks here as well as to your endpoints. Must be HTTPS, on one of your webhook or allowed callback hosts. Required if you have no webhook endpoint. |
| `metadata` | No | Your own key/value pairs, echoed back in webhooks. |

## Payment status

| Status | Meaning |
| - | - |
| `pending`, `processing` | Waiting for the customer or the operator. |
| `success` | Paid. Final. |
| `failed` | Declined, or not approved in time. `failureCode` says why. Final. |
| `expired`, `cancelled` | Never paid. Final. |
| `reversed` | Paid, then returned. |

Check a payment with `GET /v1/payments/{reference}`, or find the newest payment for an order with `GET /v1/payments?merchantOrderId=ORDER-5531`.

## Collecting for a sub-merchant (aggregators)

Aggregator accounts collect on behalf of sub-merchants linked to them, with `POST /v1/sub-merchants/{subMerchantCode}/payments/mobile-money`. The body is the same as an ordinary payment, and `subMerchantCode` is the sub-merchant's 8-digit account code.

The sub-merchant is credited and you keep the fee the sub-merchant agreed to when they accepted your link. That fee can't be set or changed per payment. Don't send `subMerchantAccountCode` or `aggregatorFee` in the body.

## Refunds

`POST /v1/payments/{reference}/refunds` sends money back to the number that paid, in full or in part. Leave out `amount` to refund everything still refundable.

* Refunds need a full secret key and an `Idempotency-Key` header. Payments-only keys are refused.
* A refund comes out of the payment's money not yet settled first, then your available balance. PrimePay's fee isn't returned.
* It is `processing` until the network answers, then `success` or `failed`. A failed refund's money goes back to your balance.
* Several partial refunds are fine until they add up to the payment's amount.

`GET /v1/payments/{reference}/refunds` lists a payment's refunds with `refundable` (what is left). `GET /v1/refunds/{reference}` returns one refund.

## Payment errors

| Code | HTTP | Meaning |
| - | - | - |
| `idempotency_key_required` / `idempotency_key_invalid` | 400 | The `Idempotency-Key` header is missing, or isn't 8 to 255 characters. |
| `idempotency_key_reused` | 422 | That key was already used for a different payment. |
| `request_in_progress` | 409 | The first request with that key is still running. Try again shortly. |
| `invalid_provider` | 400 | `provider` isn't an operator id. |
| `provider_not_supported` | 400 | That operator isn't available right now. |
| `invalid_currency` | 400 | The operator doesn't support that currency. |
| `invalid_phone` | 400 | The number isn't in the operator's country. |
| `invalid_amount` | 400 | Zero, negative or more than 2 decimal places. |
| `amount_too_low` / `amount_too_high` | 400 | Outside the operator's limits, counting any fee the customer pays. For sub-merchant payments, also when nothing would be left for the sub-merchant after the fees. |
| `wallet_not_found` / `wallet_inactive` | 400 | PrimePay Wallet: no active wallet on that number. |
| `use_sub_merchant_endpoint` | 400 | You sent `subMerchantAccountCode` in the body. Use the sub-merchant endpoint instead. |
| `aggregator_fee_not_allowed` | 400 | You sent `aggregatorFee`. The agreed fee always applies. |
| `not_an_aggregator` | 403 | Only aggregator accounts can collect for a sub-merchant. |
| `sub_merchant_not_found` | 404 | No active sub-merchant with that code is linked to your account. |
| `invalid_callback_url` / `callback_host_not_allowed` | 400 | `callbackUrl` isn't HTTPS, or isn't on your hosts. |
| `webhook_url_required` | 400 | Add a webhook endpoint, or send `callbackUrl`. |
| `duplicate_order` | 409 | This `merchantOrderId` already has a payment in progress or paid. The response includes its `reference`. |

## Refund errors

| Code | HTTP | Meaning |
| - | - | - |
| `payment_not_found` | 404 | No payment with that reference on this account. |
| `not_refundable` | 400 | The payment wasn't successful, or nothing is left to refund. |
| `amount_exceeds_refundable` | 400 | More than what's left; the response includes `refundable`. |
| `insufficient_funds` | 400 | Your balance can't cover the refund yet. |
| `idempotency_key_required` / `idempotency_key_reused` | 400 / 422 | The key is missing, or was already used for a different refund. |


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