---
title: First Purchase in 5 Minutes
description: Complete a sandbox purchase end to end with no real money involved
---


Make your first purchase against the sandbox: authenticate, check your balance, buy airtime, and verify it completed. Nothing is charged and no real airtime is delivered.

All requests in this guide use the sandbox base URL:

```
https://my.rizpay.app/api/partners/sandbox/v1
```

## Prerequisites

- A RizPay business account
- API access enabled in your settings

That's it. No funding required: the sandbox returns a mock balance.

## Step 1: Create a Sandbox API Key

1. Log in at [my.rizpay.app](https://my.rizpay.app)
2. Go to **Settings > API Keys**
3. Click **Create New Key**
4. Select the **Sandbox** environment
5. Select these scopes: `read_balance`, `view_products`, `purchase_airtime`, `read_transactions`
6. Copy your secret key (shown only once!)

Your key looks like `sk_test_xxxxxxxxxxxx`. Sandbox keys only work on sandbox endpoints, so there is no risk of touching production.

> **Warning:** Never share your secret key or commit it to version control. Store it in an environment variable:
>
> ```bash
> export RIZPAY_API_KEY="sk_test_your_test_key"
> ```

## Step 2: Confirm the API Is Up

The health endpoint needs no authentication:

```bash
curl -X GET \
  https://my.rizpay.app/api/partners/sandbox/v1/health
```

Response:

```json
{
  "status": { "code": 200, "message": "OK" },
  "data": {
    "service": "partners-api-sandbox",
    "version": "1.0.0",
    "environment": "sandbox",
    "timestamp": "2026-05-18T11:39:50+01:00"
  }
}
```

## Step 3: Check Your Balance

Verify your key works. The sandbox returns a mock balance:

```bash
curl -X GET \
  -H "Authorization: Bearer $RIZPAY_API_KEY" \
  https://my.rizpay.app/api/partners/sandbox/v1/account/balance
```

Response:

```json
{
  "status": { "code": 200, "message": "Balance retrieved" },
  "data": {
    "balance": "100000.00",
    "currency": "NGN",
    "updated_at": "2026-05-18T11:40:02+01:00"
  }
}
```

If you get an `ENVIRONMENT_MISMATCH` error, you are using a `sk_live_` key. Create a sandbox key (`sk_test_`) and try again.

## Step 4: Pick a Product

List airtime products. The sandbox returns real product data:

```bash
curl -X GET \
  -H "Authorization: Bearer $RIZPAY_API_KEY" \
  "https://my.rizpay.app/api/partners/sandbox/v1/products/airtimes?network=MTN"
```

Response:

```json
{
  "status": { "code": 200, "message": "Airtime products retrieved" },
  "data": [
    {
      "id": "prd_42",
      "type": "airtime",
      "attributes": {
        "display_name": "MTN Airtime",
        "network": "MTN",
        "min_amount": "50.0",
        "max_amount": "50000.0",
        "price": {
          "currency": "NGN",
          "basis": "face_value",
          "min_amount": "50.00",
          "max_amount": "50000.00"
        }
      }
    }
  ],
  "pagination": {
    "page": 1,
    "per_page": 20,
    "total_pages": 1,
    "total_count": 1
  }
}
```

Note the product `id`: you need it for the purchase. Product ids are numeric with a `prd_` prefix (e.g. `prd_42`) and differ per environment, so always use the `id` from your own response, not the one in this example.

## Step 5: Create the Purchase

The sandbox decides the outcome of a purchase from the phone number you send. Only these test numbers are recognized for airtime:

| Phone number     | Outcome                                                       |
| ---------------- | ------------------------------------------------------------- |
| `08011111111`    | `successful` immediately                                      |
| `08022222222`    | `pending`, auto-transitions to `successful` after ~30 seconds |
| `08033333333`    | `pending`, auto-transitions to `failed` after ~30 seconds     |
| `08044444444`    | `failed` with `INSUFFICIENT_BALANCE`                          |
| `08055555555`    | `failed` with `VALIDATION_ERROR`                              |
| `08066666666`    | `reversed`                                                    |
| Any other number | `failed` with `PURCHASE_FAILED`                               |

Use `08011111111` for an instant success. Replace `prd_42` with the product `id` from Step 4:

```bash
curl -X POST \
  -H "Authorization: Bearer $RIZPAY_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "product_id": "prd_42",
    "phone_number": "08011111111",
    "amount": "100.00",
    "external_reference": "1736234400T3S4T5"
  }' \
  https://my.rizpay.app/api/partners/sandbox/v1/purchases
```

Response:

```json
{
  "status": { "code": 201, "message": "Purchase created successfully" },
  "data": {
    "id": "txn_5f1c3a9e-2b4d-4c6f-8a1e-9d7b5c3a2f4e",
    "type": "transaction",
    "attributes": {
      "amount": "100.0",
      "currency": "NGN",
      "status": "successful",
      "category": "purchase",
      "description": "Successful airtime purchase",
      "reference": "sandbox_1a2b3c4d5e6f7a8b9c0d",
      "external_reference": "1736234400T3S4T5",
      "product_type": "airtime",
      "phone_number": "08011111111",
      "meter_number": null,
      "price": {
        "product_amount": "100.00",
        "fee_amount": "0.00",
        "total_debit": "100.00",
        "currency": "NGN",
        "basis": "face_value"
      },
      "created_at": "2026-05-18T11:41:10+01:00",
      "updated_at": "2026-05-18T11:41:10+01:00"
    }
  }
}
```

> **Tip:** The `external_reference` must be exactly 16 characters: a 10-digit Unix timestamp plus 6 alphanumeric characters, and unique per transaction. See [Duplicate Prevention](/developers/docs/core-concepts/duplicate-prevention) for code that generates one.

If you send a phone number that is not in the table above, the sandbox rejects the purchase:

```json
{
  "status": { "code": 400, "message": "Bad Request" },
  "error": {
    "code": "PURCHASE_FAILED",
    "message": "Purchase failed. Please try again."
  }
}
```

## Step 6: Walk the Pending Lifecycle

Production purchases usually start as `pending` and complete on the provider side. To rehearse that flow, create a second purchase with the pending test number `08022222222` (and a fresh `external_reference`):

```bash
curl -X POST \
  -H "Authorization: Bearer $RIZPAY_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "product_id": "prd_42",
    "phone_number": "08022222222",
    "amount": "100.00",
    "external_reference": "1736234460P3N6D1"
  }' \
  https://my.rizpay.app/api/partners/sandbox/v1/purchases
```

The response has `"status": "pending"`. It transitions to `successful` on its own after about 30 seconds, or you can query it to complete it immediately (replace the id with the `id` from your response):

```bash
curl -X POST \
  -H "Authorization: Bearer $RIZPAY_API_KEY" \
  https://my.rizpay.app/api/partners/sandbox/v1/purchases/txn_7e2d4b1c-9a3f-4e5d-b6c8-1f0a9e8d7c6b/query
```

Response:

```json
{
  "status": { "code": 200, "message": "Purchase details retrieved" },
  "data": {
    "id": "txn_7e2d4b1c-9a3f-4e5d-b6c8-1f0a9e8d7c6b",
    "type": "transaction",
    "attributes": {
      "amount": "100.0",
      "currency": "NGN",
      "status": "successful",
      "category": "purchase",
      "description": "Pending transaction - transitions to successful after delay",
      "reference": "sandbox_0d9c8b7a6f5e4d3c2b1a",
      "external_reference": "1736234460P3N6D1",
      "product_type": "airtime",
      "phone_number": "08022222222",
      "meter_number": null,
      "price": {
        "product_amount": "100.00",
        "fee_amount": "0.00",
        "total_debit": "100.00",
        "currency": "NGN",
        "basis": "face_value"
      },
      "created_at": "2026-05-18T11:42:30+01:00",
      "updated_at": "2026-05-18T11:42:35+01:00"
    }
  }
}
```

You can fetch the transaction at any time with `GET /purchases/:id` and get the same shape back.

> **Note:** `POST /purchases/:id/query` only works while a transaction is `pending`. Querying a transaction that already completed returns an `INVALID_STATUS` error, so there is nothing to query on the Step 5 purchase: it was `successful` from the start.

That's the complete purchase lifecycle: created, pending, successful. In production the transition happens on the provider side and you can receive it as a [webhook event](/developers/docs/webhooks/overview) instead of polling.

## What You Just Did

| Step          | Endpoint                                              | What it proved                  |
| ------------- | ----------------------------------------------------- | ------------------------------- |
| Health check  | `GET /health`                                         | The API is reachable            |
| Balance       | `GET /account/balance`                                | Your key authenticates          |
| List products | `GET /products/airtimes`                              | You can discover products       |
| Purchase      | `POST /purchases`                                     | You can create transactions     |
| Lifecycle     | `POST /purchases/:id/query` then `GET /purchases/:id` | You can handle pending statuses |

## Going Live

When your integration works end to end in the sandbox:

1. Create a production API key (`sk_live_`)
2. Change the base URL to `https://my.rizpay.app/api/partners/v1`
3. Fund your account and test with a small amount first

The endpoints and payloads are identical between sandbox and production. The test phone numbers are sandbox-only: in production, every valid number is a real purchase.

## Next Steps

- [Sandbox Testing](/developers/docs/core-concepts/sandbox) - Full sandbox behavior and testing checklist
- [Authentication](/developers/docs/getting-started/authentication) - Scopes and environment separation
- [Error Handling](/developers/docs/getting-started/errors) - Handle every error code gracefully
- [Webhooks](/developers/docs/webhooks/overview) - Get notified instead of polling
