# Vend by product code

> Vend any airtime or data product by its code, for callers that already speak in plan codes.

Source: https://useroutegate.com/docs/api/public/vend

`POST /api/v1/vend`

Send the product code (`serviceid`, `service_id` or `product_code`) and the phone number (`receiver_phone_number`, `destination` or `phone_number`). Airtime products also need `amount_kobo`. The destination must be a phone number, so bills go through their own endpoints.

Authenticate with a bearer API key: `rg_test_` in test mode, `rg_live_` in live.

## Parameters

| Name | In | Type | Required | Description |
| --- | --- | --- | --- | --- |
| `Idempotency-Key` | header | string | yes | Created once per order and resent unchanged on every retry of it. See [idempotency](/docs/concepts/idempotency). At most 128 characters. |
| `wait` | query | integer | no | Long-poll up to this many seconds for a final state; capped by the server. The answer is still `202`, so read `status`. The server stops waiting in time to answer before the request's own deadline, whatever you ask for. |

## Body

| Field | Type | Description |
| --- | --- | --- |
| `receiver_phone_number` | string | Receiver phone number (beneficiary) |
| `destination` | string | Alias for receiver_phone_number. |
| `phone_number` | string | Alias for receiver_phone_number. |
| `network` | string | Network name such as mtn, airtel, glo, 9mobile. |
| `network_id` | string | Optional network UUID. |
| `serviceid` | string | Service ID or plan code (e.g. MTN-1GB) |
| `service_id` | string | Alias for serviceid. |
| `product_code` | string | Alias for serviceid. |
| `amount` | integer | Amount (optional for fixed data bundles) |
| `amount_kobo` | integer | Amount in kobo. |
| `client_reference` | string | At most 128 characters. |
| `metadata` | object | Up to 20 string values of your own, returned on the transaction and its webhooks. `route`, `plan_name`, `product_id`, `delivered_mb`, `requested_mb` and `partial` are Routegate's and are replaced. |

A field that is not in this table is refused with `400 VALIDATION_FAILED`.

## Request

```bash
curl -X POST https://api.useroutegate.com/api/v1/vend \
  -H "Authorization: Bearer $ROUTEGATE_API_KEY" \
  -H "Idempotency-Key: ord-00047" \
  -H "Content-Type: application/json" \
  -d '{
    "product_code": "MTN-VTU",
    "destination": "+2348012345678",
    "amount_kobo": 10000,
    "client_reference": "ord-00047"
  }'
```

## Response

`202`

```json
{
  "success": true,
  "code": "OK",
  "message": "Transaction accepted",
  "data": {
    "id": "01a0d8df-a6b8-7c3e-9f21-4b6d8e0a2c57",
    "reference": "txn_01a0d8dfa6b87d10a3e54c2b9f86d1e4",
    "client_reference": "ord-00047",
    "mode": "test",
    "status": "queued",
    "product_code": "MTN-VTU",
    "network": "mtn",
    "service": "airtime",
    "destination": "+234********78",
    "amount_kobo": 10000,
    "price_kobo": 10000,
    "metadata": {
      "data_type": "airtime",
      "plan_name": "MTN Airtime",
      "product_id": "00000000-0000-7000-8000-000000001301",
      "route": "provider"
    },
    "created_at": "2026-09-25T14:02:11Z"
  }
}
```

`status` is `queued` in this answer. It moves through `processing`, and `pending` when a channel has not confirmed, to `successful` or `failed`; `reversed` and `refunded` are in the enum but nothing sets them. The outcome arrives as a [webhook](/docs/concepts/webhooks) naming this `reference`.

## Error

`402` `WALLET_INSUFFICIENT_FUNDS`: The wallet's available balance does not cover the price. Nothing was created: fund the wallet, then send the order with a new `Idempotency-Key`. Every code is in [errors](/docs/concepts/errors).

```json
{
  "success": false,
  "data": null,
  "message": "Insufficient wallet balance: available balance (0.00 NGN) is less than required amount (100.00 NGN)",
  "code": "WALLET_INSUFFICIENT_FUNDS"
}
```

## Give this to your coding agent

Read `https://useroutegate.com/docs/api/public/vend.md` and implement this call. Create the `Idempotency-Key` once per order and resend the same key and the same body on every retry of it. Treat `202` as accepted, with `status` `queued`, and take the final state from the webhook. Amounts are integers in kobo.
