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

# Hosted checkout

> Send customers to a PrimePay payment page for one order, then confirm the result on your server.

A checkout session is a PrimePay-hosted payment page for one order. Your server creates the session, you redirect the customer to its `url`, and the customer chooses how to pay and approves the payment on their phone. PrimePay then sends the customer back to your site and tells your server the result by [webhook](https://docs.primepay.mw/api-reference/webhooks.md).

Use it when you don't want to build your own payment form.

| If you want to… | Use |
| - | - |
| Send the customer to a PrimePay page from your website or app | **Hosted checkout** (this page) |
| Collect from a number you already have, with your own form | [Payments](https://docs.primepay.mw/api-reference/payments-overview.md) |
| Send a link by WhatsApp, SMS or QR code, with no website | [Payment links](https://docs.primepay.mw/api-reference/payment-links-overview.md) |

> [!NOTE]
>
> Checkout sessions use `snake_case` field names (`return_url`, `line_items`) and their errors carry `type` and `param`. The other endpoints use `camelCase`.

## How it works

**1. Create a session from your server**

`POST /v1/checkout/sessions` with the amount, your order `reference`, a `return_url` and a `cancel_url`. Send an `Idempotency-Key` so a retry returns the same session.

**2. Redirect the customer to url**

The response has a `url` on `checkout.primepay.mw`. The session can be paid for 30 minutes.

**3. The customer pays**

They choose a mobile money operator, enter their number and approve the payment on their phone.

**4. The customer comes back**

PrimePay sends them to your `return_url` with `session_id`, `reference` and `status` added to the address.

**5. Confirm before you fulfil**

Call `GET /v1/checkout/sessions/{id}` from your server, or wait for the `checkout.session.completed` webhook. Hand over the goods only when `status` is `complete`.

> [!WARNING]
>
> Don't fulfil an order because the customer arrived at your `return_url`. Anyone can type that address. Always confirm with the API or the webhook.

## Create a session

```json theme={null}
{
  "amount": 42500.00,
  "currency": "MWK",
  "reference": "ORDER-5531",
  "description": "2 x School shoes",
  "return_url": "https://shop.example.com/orders/5531/done",
  "cancel_url": "https://shop.example.com/cart",
  "customer": {
    "first_name": "Chikondi",
    "last_name": "Banda",
    "email": "customer@example.com",
    "phone": "0991234567"
  },
  "line_items": [
    { "name": "School shoes", "quantity": 2, "unit_amount": 20000.00 },
    { "name": "Delivery", "quantity": 1, "unit_amount": 2500.00 }
  ],
  "metadata": { "cart": "88412" }
}
```

| Field | Required | Notes |
| - | - | - |
| `amount` | Yes | 1 to 10,000,000, at most 2 decimal places. |
| `currency` | No | `MWK` (the default). Another currency is accepted only once PrimePay takes payments in that country; today that is `MWK` only. |
| `reference` | Yes | Your order or invoice number, up to 100 characters. |
| `description` | No | Shown to the customer. Up to 255 characters. |
| `return_url` | Yes | Where the customer goes after paying. A public `https://` address. Sandbox also allows `http://localhost`. |
| `cancel_url` | Yes | Where the customer goes if they go back without paying. Same rules as `return_url`. |
| `customer` | No | `first_name`, `last_name`, `email`, `phone` (a Malawi mobile number). Fills in the page for the customer. |
| `line_items` | No | Up to 50 items shown on the page: `name`, `quantity`, `unit_amount`, optional `description`. They must add up to `amount`. |
| `metadata` | No | A JSON object of your own values, up to 50 keys and 8 KB. Returned with the session and in webhooks. |
| `sub_merchant_account_code` | No | Aggregator accounts only: collect for this linked sub-merchant. The agreed fee always applies; don't send `aggregator_fee`. |

The key must be a secret key; a payments-only key also works. The `Idempotency-Key` header is required, 8 to 255 characters. On a retry with the same key, `description` and `metadata` may differ.

## One session per order

A `reference` can have only one session that is open or paid.

* Creating a new session for an order that still has an **open** session cancels the old one, so the customer can't pay twice from an old tab.
* If the order is **already paid**, you get `409 order_already_paid`.
* If the customer is **approving a payment** right now, you get `409 payment_in_progress`. Wait for the webhook.

## When the customer comes back

```text theme={null}
https://shop.example.com/orders/5531/done?session_id=3c0f6f4e-…&reference=ORDER-5531&status=success
```

| Value | Meaning |
| - | - |
| `session_id` | The session's `id`. Use it to fetch the session. |
| `reference` | Your order number. |
| `status` | `success` when the payment is complete, `pending` when the customer came back before the result was known. |

If the customer goes back without paying, they are sent to your `cancel_url` unchanged.

## Session status

| Status | Meaning |
| - | - |
| `open` | Waiting for the customer. Can be paid at `url`. |
| `processing` | The customer is approving a payment on their phone. |
| `complete` | Paid. Final. `payment` has the transaction, fee and net amount. |
| `expired` | Not paid within 30 minutes. Final. |
| `cancelled` | Cancelled by you, or replaced by a newer session for the same order. Final. |

`url` is `null` once a session can no longer be paid.

## Webhooks

| Event | When |
| - | - |
| `checkout.session.completed` | The session was paid. `payment.succeeded` is sent as well. |
| `checkout.session.expired` | The session was never paid. |

See [Webhooks](https://docs.primepay.mw/api-reference/webhooks.md) for the body and how to check the signature.

## Cancel a session

`POST /v1/checkout/sessions/{id}/expire` cancels an unpaid session, for example when the customer empties their cart. A paid session can't be cancelled (`409 session_complete`).

## Checkout errors

```json theme={null}
{
  "error": {
    "type": "invalid_request_error",
    "code": "line_items_total_mismatch",
    "param": "line_items",
    "message": "line_items add up to 40000.00 but amount is 42500.00."
  }
}
```

| Code | HTTP | Meaning |
| - | - | - |
| `parameter_missing` | 400 | A required field is missing. `param` names it. |
| `parameter_too_long` | 400 | A field is longer than allowed. |
| `amount_out_of_range` / `amount_too_precise` | 400 | `amount` is outside 1 to 10,000,000, or has more than 2 decimal places. |
| `currency_not_supported` | 400 | PrimePay doesn't take payments in this currency. `message` lists the ones accepted now (today `MWK`). |
| `invalid_url` / `url_not_https` / `url_not_public` | 400 | `return_url` or `cancel_url` isn't a full web address, isn't HTTPS, or isn't public (live). |
| `invalid_email` / `invalid_phone` | 400 | `customer.email` or `customer.phone` isn't valid. |
| `too_many_line_items` / `invalid_quantity` / `invalid_unit_amount` | 400 | A problem with `line_items`. |
| `line_items_total_mismatch` | 400 | The items don't add up to `amount`. |
| `invalid_metadata` | 400 | `metadata` isn't an object, or is too large. |
| `aggregator_fee_not_allowed` | 400 | You sent `aggregator_fee`. The agreed fee always applies. |
| `not_an_aggregator` / `sub_merchant_not_found` | 400 | `sub_merchant_account_code` was sent by an account that isn't an aggregator, or doesn't match an active sub-merchant. |
| `resource_missing` | 404 | No session with that id on this account. |
| `order_already_paid` | 409 | This `reference` was already paid through another session. |
| `payment_in_progress` | 409 | The customer is approving a payment on a session for this order. |
| `session_complete` | 409 | A paid session can't be cancelled. |
| `duplicate_reference` / `session_changed` | 409 | Another request for the same order ran at the same moment. Retry. |


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