Documentation
Get started with the Ureh API
From a new account to your first successful purchase: what to set up, how authentication works, what every request field means, and a complete worked request. New to Ureh? Start at Build with Ureh for an overview first.
Before you start
Four steps, once, before your first live call. Each links to where you do it.
| Step | Why it's needed | |
|---|---|---|
| 1 | Create an account | Issues your client_id, the identifier every request carries. |
| 2 | Generate your API keys | Gives you a Secret Key for purchases and a Public Key for read-only lookups. |
| 3 | Fund your wallet | Every purchase debits your wallet at request time. An unfunded wallet returns 402 INSUFFICIENT_BALANCE. |
| 4 | Subscribe to webhooks | Optional but recommended: how you learn a transaction settled without polling. |
Your account must be verified and in good standing to transact. If it isn't,
purchase calls return 403 ACCOUNT_RESTRICTED while read-only calls
keep working.
Authentication
Every request carries your API key as a bearer token. There are no sessions or cookies, and each key's permissions are enforced server side.
Authorization: Bearer sk_live_xxxxxxxxxxxxxxxx
Content-Type: application/json
| Key | Prefix | Can call |
|---|---|---|
| Secret Key | sk_ |
Every endpoint, including purchases and wallet debits. Server side only, never in client-side code. |
| Public Key | pk_ |
Read-only lookups only: product catalogues and validate calls. Rejected on a purchase or webhook-subscribe endpoint. |
A Public Key on a purchase endpoint is rejected with 403
GATEWAY_ERROR, never silently downgraded.
API Keys
Generate your key pair from the API Keys page in the Developer Console. If a key is compromised, you have two options:
- Rotate. Issue a new Secret Key from the console. Your
client_idand Public Key are unaffected; only the old secret stops working. - Revoke. Disable a key immediately without issuing a replacement, when you need to stop traffic before reintegrating.
Rate Limits
Every request is checked against four independent dimensions before it is
processed. If any dimension is over its limit, the request is rejected with a
Retry-After telling you how long to wait.
| Dimension | Identified by | Default limit |
|---|---|---|
| ip | Request IP address | 60 requests / 60s |
| api_key | Your bearer token | 100 requests / 60s |
| client | Your client_id | 100 requests / 60s |
| endpoint | The specific route, shared across every caller | Disabled by default |
Purchase endpoints apply a tighter limit of 20 requests per minute on the
client dimension. A rate-limited request returns:
Retry-After: 37
{
"status": "error",
"code": "RATE_LIMITED",
"message": "Too many requests. Please slow down and try again shortly.",
"retry_after_seconds": 37
}
IP Whitelisting
An optional restriction on top of your key. With no entries configured, a key authenticates from any address. Add a trusted address in the console and requests from every other address are rejected exactly as an invalid key is; no distinct error code identifies the whitelist as the cause.
Useful for server-to-server integrations with a fixed egress IP: a leaked key still can't be used from an untrusted address.
Request fields
Every product uses the same field vocabulary. Once you have integrated one, the others differ only in which fields they take; the API Reference lists the exact set per endpoint.
| Field | Used by | What it is |
|---|---|---|
client_id |
Every endpoint | Your account identifier, issued at registration (for example URH-8F2C91A4D6B0E37F1A). Send it in the JSON body on POST calls, or as a query parameter on GET calls. It must match the API key you authenticate with. |
biller |
Every product | Who is being paid: a network (MTN, AIRTEL, GLO, 9MOBILE), a TV operator (DSTV, GOTV, STARTIMES), a disco (IKEDC, EKEDC, AEDC) or an examination board (WAEC, NECO, NABTEB, JAMB). Case-insensitive. |
recipient |
Every product | Who or what receives the purchase: a phone number for airtime and data, a smartcard number for cable TV, a meter number for electricity, a candidate profile ID for education. |
product_code |
Data, Cable TV, Education | Identifies the catalogue product you are buying, taken from that biller's /products list. Its price is fixed by the catalogue, so these calls do not need an amount. |
meter_type |
Electricity | prepaid or postpaid. Electricity has no product catalogue: you choose the meter type and the amount instead. |
amount |
Airtime, Electricity | The value to deliver, in NGN. Required where you choose the amount (airtime, electricity). For catalogue products it is optional and, if sent, must match the listed price — see AMOUNT_MISMATCH in the error reference. |
quantity |
Education | How many PINs to buy. Defaults to 1. The amount charged is the product price times this quantity. |
customer_phone |
Cable TV, Electricity, Education | The end customer's phone number, used for delivery notifications about the purchase. Distinct from recipient, which is the account being credited. Recommended on every call that accepts it. |
idempotency_key |
Every purchase | A value you generate that uniquely identifies one purchase attempt. Reusing it returns the original transaction instead of charging again. Ureh generates a separate reference for the transaction itself. See Idempotent requests. |
Never send a price you calculated yourself. Ureh sets the amount for every purchase and debits exactly that.
Your first request
A single airtime purchase shows the whole flow. Every other product follows the same shape, only the fields change. See the API Reference for each one.
1. Send the request
# Authenticate with your Secret Key
curl -X POST https://api.ureh.io/v1/airtime/purchase \
-H "Authorization: Bearer sk_live_xxxxxxxxxxxxxxxx" \
-H "Content-Type: application/json" \
-d '{
"client_id": "URH-8F2C91A4D6B0E37F1A",
"biller": "MTN",
"recipient": "08012345678",
"amount": 500,
"idempotency_key": "5c2f9e2a-6b7a-4b31-9d3a-df2b6c9a4e10"
}'
2. Read the response
A 202 means the request was accepted and your wallet debited, not
that it has settled. Use data.reference to check on it; it is a
separate identifier Ureh generates, not the idempotency_key you
sent. Validation and authentication failures return the same envelope with
status: "error" and a code. The full list is in the
API Reference.
{
"status": "pending",
"code": "PROCESSING",
"message": "Transaction accepted.",
"data": {
"reference": "20246063015832257676576215",
"recipient": "08012345678",
"biller": "MTN",
"amount": 500,
"channel": "API_GATEWAY",
"currency": "NGN",
"created_at": "2024-08-25 14:32:07"
}
}
3. Know when it settles
A transaction ends at success or failed. There are two
ways to learn which.
Check its status with the reference you were given:
curl "https://api.ureh.io/v1/transactions/status?client_id=URH-8F2C91A4D6B0E37F1A&reference=20246063015832257676576215" \
-H "Authorization: Bearer sk_live_xxxxxxxxxxxxxxxx"
{
"status": "success",
"data": {
"reference": "20246063015832257676576215",
"transaction_status": "success",
"amount": 500,
"service_amount": 500,
"commission": 0,
"amount_charged": 500,
"commission_received": 0,
"service_type": "VTU_AIRTIME",
"biller": "MTN",
"token": null,
"created_at": "2024-08-25 14:32:07",
"updated_at": "2024-08-25 14:32:11",
"refunded": false,
"completed_at": "2024-08-25 14:32:11"
}
}
Or subscribe to webhooks once and be notified the moment it settles, with no polling. This is the recommended approach in production. See Webhooks for the delivery model and Polling vs. webhooks for the tradeoffs.
4. Where to go next
- API Reference — every endpoint, field and error code.
- Idempotent requests — retry safely without double-charging.
- Verifying webhook signatures — before you trust any delivery.
- Handling failures and retries — which errors are safe to retry.