---
title: Hosted checkouts
description: Create a payment link your customer opens to pay you by mobile money.
---

A hosted checkout is a payment page iSmartPay runs for you. You create it, send its link to the customer, and they pay on the page by mobile money.

Hosted checkout belongs to the iSmartPay merchant console. The API passes your requests on to it.

## Before you start

- Your business needs a profile on the merchant console.
- An owner must turn on hosted checkout there. Until then, creating a checkout is refused with `409 checkout_not_enabled`.
- Your business needs verification tier 2.

## Create

```http
POST /v1/checkouts
Idempotency-Key: inv-2210
```

```json
{
  "reference": "inv-2210",
  "amountType": "fixed",
  "amount": "250.00",
  "currency": "GHS",
  "usage": "single",
  "description": "Invoice 2210",
  "successUrl": "https://shop.example.com/paid",
  "cancelUrl": "https://shop.example.com/cart",
  "expiresInMinutes": 60
}
```

- `reference` is yours, and unique for each checkout. A reference already taken is `409 checkout_reference_taken`.
- `currency` is `GHS`. Amounts have two decimal places.
- `description` is at most 140 characters.
- `environment` is optional. Leave it out. If you send one that is not this API's, you get `422 environment_mismatch`.

### Amount

- **`fixed`** (the default). Send `amount`. The customer pays exactly that.
- **`open`**. The customer picks the amount. Leave `amount` out. You can set `minAmount`, `maxAmount` and up to four `suggestedAmounts`.

### Usage and expiry

| `usage`              | Behaviour                                     | `expiresInMinutes`                          |
| -------------------- | --------------------------------------------- | ------------------------------------------- |
| `single` (default)   | Closes after one payment.                     | 5 to 10080. Defaults to 60. `null` is refused. |
| `reusable`           | Stays open and takes many payments.           | 5 to 525600, or leave it out for no expiry. |

A `201` returns the checkout:

```json
{
  "id": "6f0c2d4e-8a1b-4c3d-9e5f-1a2b3c4d5e6f",
  "reference": "inv-2210",
  "amountType": "fixed",
  "amount": "250.00",
  "minAmount": null,
  "maxAmount": null,
  "suggestedAmounts": [],
  "usage": "single",
  "currency": "GHS",
  "description": "Invoice 2210",
  "environment": "test",
  "status": "open",
  "url": "https://pay.example.com/c/...",
  "successUrl": "https://shop.example.com/paid",
  "cancelUrl": "https://shop.example.com/cart",
  "expiresAt": "2026-10-05T11:00:00Z",
  "paidAt": null,
  "collectionReference": null,
  "paymentsCount": 0,
  "totalCollected": "0.00",
  "createdAt": "2026-10-05T10:00:00Z"
}
```

Share the `url` with the customer. It carries a secret, so send it only to the payer.

## Statuses

| Status      | Meaning                                         |
| ----------- | ----------------------------------------------- |
| `open`      | Waiting for payment.                            |
| `paid`      | Paid. Single checkouts only.                    |
| `expired`   | Not paid in time.                               |
| `cancelled` | You cancelled it.                               |

A checkout becomes `paid` only when the payment succeeds. A reusable checkout stays `open` after each payment and is never `paid`. Watch `paymentsCount` and `totalCollected` instead. `totalCollected` counts settled payments only.

## Read and list

`GET /v1/checkouts/{checkoutId}` returns one checkout with its `attempts`: each payment attempt, with the payer's number masked. A `checkoutId` that is not a UUID is `400`. Another business's checkout is `404`.

`GET /v1/checkouts` lists your checkouts in this environment, newest first.

- `status` filters by status.
- `limit` is 1 to 50. It defaults to 20.
- Pass `nextCursor` back unchanged as `cursor` for the next page. Here `nextCursor` is an empty string on the last page.

## Cancel

```http
POST /v1/checkouts/{checkoutId}/cancel
Idempotency-Key: inv-2210-cancel
```

You can cancel an open checkout that has no payment in progress. Otherwise you get `409 checkout_not_open`.

## Knowing when it is paid

There are no checkout webhooks. A paid checkout shows up as a collection: in `collection.*` webhooks, in `GET /v1/events` and in `GET /v1/transactions`. `collectionReference` on the checkout, and on each attempt, names that collection. You can also poll the checkout.

## Permissions

| To                         | The key needs        |
| -------------------------- | -------------------- |
| Create or cancel           | `collections.write`  |
| List or read               | `collections.read`   |

## Errors

The checkout service's `4xx` answers come back unchanged. As well as the [common errors](/guides/errors), you may see:

| Status | Code                       | What to do                                                    |
| ------ | -------------------------- | ------------------------------------------------------------- |
| 409    | `checkout_not_enabled`     | Hosted checkout is off. An owner turns it on in the merchant console. |
| 409    | `checkout_not_open`        | The checkout is not open, or a payment is in progress. Read it to see its status. |
| 409    | `checkout_reference_taken` | Use a new `reference`.                                        |
| 422    | `environment_mismatch`     | Leave `environment` out, or send this API's.                  |
| 502    | `internal`                 | The checkout service failed or could not be reached. Retry with the same `Idempotency-Key` and body. |
| 503    | `checkout_unavailable`     | Hosted checkout is not available right now. Try again later.  |
| 504    | `upstream_timeout`         | No answer within 30 seconds. Retry with the same `Idempotency-Key` and body, or list your checkouts to see if it was made. |
