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

# Airtime, Gift Cards & Bills

> Sell mobile top-ups, gift cards and utility bill payments from one API, on a prepaid balance you fund with stablecoins.

Sell **mobile airtime and data**, **gift cards**, and **utility bill payments** to your
customers through one API. You fund a prepaid balance with stablecoins, set your own
margin, and keep everything you charge on top.

<Info>
  Base URL: `https://api.rach.finance/api/v1/` · Auth: `X-API-Key`
</Info>

<Note>
  **One service, three products.** Airtime, gift cards and bills share one order model, one
  status vocabulary, one webhook family and one balance. Look any order up at
  `GET /vas/orders/{reference}` regardless of what it was.
</Note>

## How it works

<Steps>
  <Step title="Fund your balance">
    Send USDT or USDC on Polygon or BSC to your funding address. A flat **\$1** is deducted
    and the rest becomes spendable.
  </Step>

  <Step title="Set your margin">
    Decide what you charge your customers on top. Rach never takes any part of it.
  </Step>

  <Step title="Quote, then sell">
    Price an order first to see exactly what you earn, then buy. Your balance is charged;
    your customer pays you whatever you decided.
  </Step>
</Steps>

## 1. Fund your balance

```bash theme={null}
curl https://api.rach.finance/api/v1/vas/funding-addresses \
  -H "X-API-Key: $RACH_KEY"
```

```json Response theme={null}
{
  "addresses": [
    { "network": "POL", "address": "0xAbC123...", "currencies": ["USDT", "USDC"] },
    { "network": "BSC", "address": "0xAbC123...", "currencies": ["USDT", "USDC"] }
  ],
  "funding_fee_usd": "1"
}
```

<Warning>
  **Only USDT or USDC on Polygon or BSC will credit your balance.** Any other asset, or any
  other chain, will not — those funds are not recoverable through this flow.

  A flat \*\*$1** is deducted per deposit. A deposit that cannot cover the fee is not credited
    at all, so send more than $1.
</Warning>

Stablecoins credit **1:1 with USD**, so there is no exchange-rate gap between what you send
and what you can spend.

The address is yours permanently — reuse it for every top-up. Nothing needs to be
pre-registered, and no memo or tag is required.

### When the money becomes spendable

Your balance credits once the deposit reaches its **confirmation threshold**, not the
moment it appears on-chain. We wait so that a chain reorganisation cannot take back funds
you have already spent.

| Chain       | Confirmations | Typical wait  |
| ----------- | ------------- | ------------- |
| **BSC**     | 12            | \~40 seconds  |
| **Polygon** | 128           | \~4–5 minutes |

<Tip>
  **Fund on BSC if you want the balance quickly.** Polygon's threshold is deliberately deep
  because it has had deep reorganisations; BSC settles in well under a minute.
</Tip>

You also receive the usual `wallet.deposit.confirmed` webhook, so you can credit your own
system the moment the balance is live rather than polling.

Check the balance any time:

```bash theme={null}
curl https://api.rach.finance/api/v1/vas/balance -H "X-API-Key: $RACH_KEY"
```

```json Response theme={null}
{ "balance": "499.000000", "currency": "USD", "lifetime_funded": "499.000000", "lifetime_spent": "0" }
```

## 2. Set your margin

This is what **you** earn on every order. Rach bills you a price; you charge your customer
that price plus your markup, and keep the difference.

```bash theme={null}
curl -X POST https://api.rach.finance/api/v1/vas/markup \
  -H "X-API-Key: $RACH_KEY" \
  -H "Content-Type: application/json" \
  -d '{ "is_enabled": true, "fee_mode": "percentage", "fee_percent": "0.03", "fee_cap_usd": "5" }'
```

<Warning>
  `fee_percent` is a **fraction, not a percent** — `"0.03"` means 3%. Sending `"3"` would
  mean 300%; values above `1` are rejected outright rather than silently overcharging your
  customers.
</Warning>

Use `"fee_mode": "flat"` with `flat_fee_usd` for a fixed charge instead, and `applies_to`
(`airtime`, `giftcard`, `bill`) to set different margins per product.

## 3. Quote before you sell

A quote shows the whole economics of an order — including what you make — without buying
anything or touching your balance.

```bash theme={null}
curl -X POST https://api.rach.finance/api/v1/airtime/quote \
  -H "X-API-Key: $RACH_KEY" -H "Content-Type: application/json" \
  -d '{ "phone": "+2348031234567", "country_code": "NG", "amount": "1000", "use_local_amount": true }'
```

```json Response theme={null}
{
  "quote": {
    "type": "airtime",
    "product_name": "MTN Nigeria",
    "face_value": "1000",
    "face_currency": "NGN",
    "merchant_price": "0.796020",
    "merchant_markup": "0.024881",
    "customer_price": "0.820901",
    "merchant_earns": "0.024881",
    "currency": "USD"
  }
}
```

| Field             | Meaning                                                       |
| ----------------- | ------------------------------------------------------------- |
| `face_value`      | What the recipient actually receives                          |
| `merchant_price`  | What Rach charges **you** — deducted from your balance        |
| `merchant_markup` | Your margin, from the configuration above                     |
| `customer_price`  | What to charge your customer (`merchant_price` + your markup) |
| `merchant_earns`  | Your profit on this order                                     |

## 4. Sell

### Airtime, data and bundles

```bash theme={null}
curl -X POST https://api.rach.finance/api/v1/airtime/topup \
  -H "X-API-Key: $RACH_KEY" -H "Content-Type: application/json" \
  -d '{
    "reference": "order-2026-0001",
    "customer_id": "cust_001",
    "phone": "+2348031234567",
    "country_code": "NG",
    "amount": "1000",
    "use_local_amount": true
  }'
```

Omit `operator_id` and we detect the network from the number, so your customer never has to
pick it (or pick it wrong). Set `use_local_amount: true` to send the amount in the
recipient's own currency — usually what they expect to type.

**Data plans and bundles use the same endpoint.** Pick the data product from the operator
list and pass its `operator_id`:

```bash theme={null}
# Data plans only — render these as their own tab
curl "https://api.rach.finance/api/v1/airtime/operators?country=NG&type=data" \
  -H "X-API-Key: $RACH_KEY"
```

`type` accepts `airtime`, `data` or `bundle`; omit it for everything. Each entry also
carries `data` and `bundle` booleans if you would rather group them yourself.

<Tip>
  **Check for promotions before you sell.** Operators run bonuses — "recharge 1974 XAF or
  more and the receiver gets 100% extra" — which is free upside for your customer on the
  same spend.

  ```bash theme={null}
  curl "https://api.rach.finance/api/v1/airtime/promotions?country=NG" \
    -H "X-API-Key: $RACH_KEY"
  ```
</Tip>

### Gift cards

```bash theme={null}
curl -X POST https://api.rach.finance/api/v1/giftcards/orders \
  -H "X-API-Key: $RACH_KEY" -H "Content-Type: application/json" \
  -d '{
    "reference": "gc-2026-0001",
    "product_id": 2,
    "unit_price": "25",
    "recipient_email": "customer@example.com"
  }'
```

The **redeem code** is attached to the order on success and returned here and at
`GET /vas/orders/{reference}`.

<Warning>
  Redeem codes are **never sent in webhooks**. A gift card code is a bearer secret — anyone
  holding it can spend it — so it is only ever returned over an authenticated request.
</Warning>

### Utility bills

Browse billers, optionally filtered by category:

```bash theme={null}
# Categories available in a country, for a filter UI
curl "https://api.rach.finance/api/v1/bills/types?country=NG" -H "X-API-Key: $RACH_KEY"

# Billers in one category
curl "https://api.rach.finance/api/v1/bills/billers?country=NG&type=ELECTRICITY_BILL_PAYMENT" \
  -H "X-API-Key: $RACH_KEY"
```

Then pay — quote first with `POST /bills/quote` if you want to show a price:

```bash theme={null}
curl -X POST https://api.rach.finance/api/v1/bills/pay \
  -H "X-API-Key: $RACH_KEY" -H "Content-Type: application/json" \
  -d '{
    "reference": "bill-2026-0001",
    "biller_id": 1,
    "subscriber_account": "04220312345",
    "amount": "5000",
    "use_local_amount": true
  }'
```

<Warning>
  **Check the meter or account number with your customer before paying.** The provider does
  not validate subscriber accounts, so a payment to a mistyped meter number is delivered to
  whoever owns that meter and cannot be recalled.
</Warning>

## `reference` is your idempotency key

<Warning>
  **Always send a unique `reference`, and reuse it on retries.** Repeating a POST with the
  same reference returns the **original order** instead of buying a second time.

  This is enforced by a database constraint, not a best-effort check — so retrying after a
  timeout is always safe. Without it, a network blip means your customer gets charged once
  and topped up twice.
</Warning>

## What happens when something fails

Orders run in a fixed sequence, so a failure is never ambiguous:

1. The amount is validated against the operator's real rules (fixed denominations, min/max).
2. **Your balance is debited** — if it is too low the order stops here and nothing is spent.
3. The provider is called.
4. **If the provider fails, your balance is refunded automatically.**

| Status       | Meaning                                                             |
| ------------ | ------------------------------------------------------------------- |
| `successful` | Delivered                                                           |
| `processing` | Accepted, outcome not yet final — Rach settles it in the background |
| `failed`     | Not delivered; your balance was refunded                            |
| `refunded`   | Delivered then reversed by the provider; your balance was refunded  |

A `processing` order resolves on its own within a couple of minutes, and a webhook fires
when it does. If a customer is watching the screen, settle it immediately instead:

```bash theme={null}
curl -X POST https://api.rach.finance/api/v1/vas/orders/order-2026-0001/refresh \
  -H "X-API-Key: $RACH_KEY"
```

<Note>
  You may also see a **503** meaning the service is temporarily unavailable. That is the
  platform topping up its own provider balance — **you were not charged and nothing was
  attempted.** Retry shortly with the same reference.
</Note>

<Note>
  A **502** means the provider failed, not that your request was wrong — your balance was
  already returned. A **400** with an insufficient-balance message means nothing was
  attempted at all; top up and retry with the same reference.
</Note>

<Warning>
  **A client timeout is not a failure.** Purchases complete and are recorded even if your HTTP
  client disconnects mid-flight. Never retry with a *new* reference after a timeout — look the
  order up by your original reference instead.
</Warning>

## Webhooks

Terminal states fire `vas.order.successful`, `vas.order.failed` or `vas.order.refunded` to
your configured webhook URL, carrying the order reference, status, amounts and what you were
charged. Verify them the same way as every other Rach webhook — see [Webhooks](/guides/webhooks).

## Dashboard

One call for a merchant dashboard screen: balance, whether you can currently transact,
order counts, spend versus earnings, and recent orders.

```bash theme={null}
curl "https://api.rach.finance/api/v1/vas/dashboard?days=30" -H "X-API-Key: $RACH_KEY"
```

Money figures count **successful orders only** — failed orders are refunded, so including
them would overstate both your spend and your earnings.

## More endpoints

| Endpoint           | Purpose |
| ------------------ | ------- |
| **Airtime & data** |         |

| Endpoint                                 | Purpose                                                       |
| ---------------------------------------- | ------------------------------------------------------------- |
| `GET /airtime/countries`                 | Countries supported for airtime                               |
| `GET /airtime/operators?country=NG`      | Networks, data plans and bundles — add `&type=data` to filter |
| `GET /airtime/detect?phone=…&country=NG` | Detect the network for a number                               |
| `GET /airtime/promotions?country=NG`     | Operator bonuses you can advertise                            |
| `POST /airtime/quote`                    | Price a top-up and see your margin                            |
| `POST /airtime/topup`                    | Send airtime, data or a bundle                                |

**Gift cards**

| Endpoint                             | Purpose                                       |
| ------------------------------------ | --------------------------------------------- |
| `GET /giftcards/countries`           | Countries with gift cards available           |
| `GET /giftcards/products?country=US` | Gift cards available to buy                   |
| `GET /giftcards/products/{id}`       | One product's full detail, for a product page |
| `GET /giftcards/redeem-instructions` | What a customer does with a code, per brand   |
| `POST /giftcards/quote`              | Price a gift card and see your margin         |
| `POST /giftcards/orders`             | Buy a gift card                               |

**Utility bills**

| Endpoint                        | Purpose                            |
| ------------------------------- | ---------------------------------- |
| `GET /bills/types?country=NG`   | Biller categories, for a filter UI |
| `GET /bills/billers?country=NG` | Utilities available to pay         |
| `POST /bills/quote`             | Price a bill and see your margin   |
| `POST /bills/pay`               | Pay a bill                         |

**Your account**

| Endpoint                               | Purpose                                            |             |
| -------------------------------------- | -------------------------------------------------- | ----------- |
| `GET /vas/funding-addresses`           | Where to send USDT/USDC to top up                  |             |
| `GET /vas/balance`                     | Spendable balance                                  |             |
| `GET /vas/statement`                   | Every credit, debit and fee behind your balance    |             |
| \`GET                                  | POST /vas/markup\`                                 | Your margin |
| `GET /vas/dashboard`                   | Balance, volumes, spend vs earnings, recent orders |             |
| `GET /vas/orders`                      | All your orders, filterable by type and status     |             |
| `GET /vas/orders/{reference}`          | One order (incl. gift card redeem code)            |             |
| `POST /vas/orders/{reference}/refresh` | Settle a processing order now                      |             |

Full schemas are in the [Payments API Reference](/api-reference/introduction) under
**Value-Added Services**.
