API Reference
Every endpoint, in one place
Authentication, the five bill payment products, transaction status and webhook delivery. Plain HTTP and JSON throughout, no client library required.
Base URL
Ureh runs two environments. They expose the same endpoints, the same request and response shapes and the same error codes, so an integration built against Sandbox needs only its base URL and credentials changed to go live.
| Environment | Base URL |
|---|---|
| Live | https://api.ureh.io/v1 |
| Sandbox | https://sandbox.ureh.io/v1 |
Live carries production traffic: real money moves and real value is delivered. Sandbox is for development and testing, where nothing is charged and nothing is delivered.
Credentials are issued per environment and are not interchangeable: a Sandbox key will not authenticate against Live. Keep the base URL in configuration rather than in code, so promoting an integration is a configuration change.
Every endpoint on this page is written relative to that prefix. Examples use the Sandbox host so they can be run as written.
An older /v1/... prefix still resolves for the endpoints that
existed before the current structure, purely so existing integrations keep
working. It is not a complete mirror, the product catalogue endpoints
(/data/products, /tv/products,
/edu/products) exist only under /api/v1, and the
legacy prefix instead carries their superseded /variations
predecessors. Treat /v1 as deprecated.
Every request below requires an Authorization: Bearer header.
See Authentication for the difference between
your Secret Key and Public Key.
Authentication
Every request carries your API key as a bearer token, plus your
client_id in the request body (or as a query parameter for
GET requests). See
Authentication
and API Keys
in the documentation for key rotation and revocation, or
Rate Limits
and IP
Whitelisting for the other controls on your credentials.
Authorization: Bearer sk_live_xxxxxxxxxxxxxxxx
Two key types, enforced server side:
| Key | Prefix | Can call |
|---|---|---|
| Secret Key | sk_ |
Every endpoint, including purchases and wallet debits. |
| Public Key | pk_ |
Read-only lookups only: product catalogues and validate calls. Rejected on purchase and webhook-subscribe endpoints. |
A Public Key on a purchase endpoint is rejected with 403
GATEWAY_ERROR, never silently downgraded. Keep your Secret Key
server side only.
Errors
Every error response shares one envelope: status of
"error", a machine-readable code, and a readable
message. Validation failures add an errors object
keyed by field name.
| HTTP | Code | Meaning |
|---|---|---|
| 400 | BAD_REQUEST | A required query parameter is missing, such as biller on a product catalogue call. |
| 401 | UNAUTHORIZED | Missing, malformed, invalid or suspended Bearer token. |
| 402 | INSUFFICIENT_BALANCE | Your wallet cannot cover this purchase. Nothing was charged. Fund the wallet and retry. |
| 403 | GATEWAY_ERROR | Unregistered client_id, or a Public Key used on a Secret-Key-only endpoint. |
| 403 | ACCOUNT_RESTRICTED | Your account is not currently permitted to transact. Contact support. |
| 405 | METHOD_NOT_ALLOWED | Wrong HTTP method for this endpoint. The Allow header names the accepted one. |
| 409 | ALREADY_PROCESSING | A request with this idempotency_key is still in flight. Retry shortly to receive the original result. |
| 422 | VALIDATION_FAILED | One or more fields are missing or fail validation. errors names each field. |
| 422 | AMOUNT_MISMATCH | An amount was sent for a catalogue-priced product and does not match its listed price. Omit amount, or send the listed price. |
| 422 | VERIFICATION_FAILED | A validate call could not confirm the recipient. Check the smartcard, meter or profile number. |
| 422 | GATEWAY_ERROR | A field is well-formed but not resolvable, such as an unknown product_code or a biller that is not configured. |
| 429 | RATE_LIMITED | Rate limited. Includes retry_after_seconds and a Retry-After header. See Rate Limits. |
| 500 | GATEWAY_ERROR | Unexpected server-side failure. Safe to retry with the same idempotency_key. |
| 502 | GATEWAY_ERROR | A product catalogue could not be loaded right now. Retry shortly. |
| 503 | PROVIDER_UNAVAILABLE | This biller cannot be served right now. Your wallet is not charged; retry the same request shortly. |
| 503 | QUEUE_UNAVAILABLE | The purchase could not be accepted. Your wallet is not charged; retry the same request shortly. |
{
"status": "error",
"code": "VALIDATION_FAILED",
"message": "Validation criteria failed.",
"errors": {
"amount": "The amount field is required."
}
}
Branch on code, never on message, which may be
reworded. Broadly: 4xx other than
409 and 429 means fix the request before retrying;
409, 429, 500, 502 and
503 are safe to retry unchanged, reusing the same
idempotency_key. See
Handling failures and retries.
Airtime
Airtime top-ups across the major Nigerian networks.
Purchase airtime
/airtime/purchase
| Field | Description | |
|---|---|---|
| client_id | required | Your client identifier. |
| biller | required | MTN, AIRTEL, GLO or 9MOBILE. |
| recipient | required | Recipient phone number. |
| amount | required | The airtime face value to deliver, in NGN. Numeric, minimum 0.01. |
| idempotency_key | required | A unique value per attempt. Reusing one returns the original transaction instead of double-charging. Ureh generates a separate reference for the transaction itself — see Transactions. |
Request
{
"client_id": "URH-8F2C91A4D6B0E37F1A",
"biller": "MTN",
"recipient": "08012345678",
"amount": 500,
"idempotency_key": "5c2f9e2a-6b7a-4b31-9d3a-df2b6c9a4e10"
}
Response 202 Accepted
{
"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"
}
}
Data
Data bundle purchases. Each bundle has a product_code. List a
biller's products, then pass the product_code you want to the
purchase call.
List data plans
/data/products?biller=MTN
Response
{
"status": "success",
"biller": "MTN",
"products": [
{
"product_code": "MTN_DATA_1GB_30DAYS",
"name": "1GB (30 Days)",
"amount": 300
},
{
"product_code": "MTN_DATA_2GB_30DAYS",
"name": "2GB (30 Days)",
"amount": 580
}
]
}
Purchase a data bundle
/data/purchase
| Field | Description | |
|---|---|---|
| client_id | required | Your client identifier. |
| biller | required | MTN, AIRTEL, GLO or 9MOBILE. |
| recipient | required | Recipient phone number. |
| product_code | required | A product_code from that biller's own /data/products list. |
| amount | optional | The price comes from the catalogue, never from the request. If you do send it, it must match the product's listed price or the call is rejected with AMOUNT_MISMATCH. |
| idempotency_key | required | A unique value per attempt. |
Request
{
"client_id": "URH-8F2C91A4D6B0E37F1A",
"biller": "MTN",
"recipient": "08012345678",
"product_code": "MTN_DATA_1GB_30DAYS",
"idempotency_key": "7a1e4b3c-2f9d-4c6a-8e1b-1a2b3c4d5e6f"
}
Response 202 Accepted
{
"status": "pending",
"code": "PROCESSING",
"message": "Transaction accepted.",
"data": {
"reference": "20241043321819600133890838",
"recipient": "08012345678",
"biller": "MTN",
"amount": 300,
"channel": "API_GATEWAY",
"currency": "NGN",
"created_at": "2024-08-25 14:32:07"
}
}
Cable TV
Cable subscription payments and renewals for DStv, GOtv and Startimes.
Validate a smartcard
/tv/validate
Confirms a smartcard number resolves to a real customer before you charge
anyone. Send a JSON body with biller and recipient.
A Public Key is accepted, as this is a read-only lookup.
Response
{
"status": "success",
"customer_name": "ADAEZE O."
}
List bouquets for an operator
/tv/products?biller=DSTV
The query parameter is biller, the same DSTV/GOTV/STARTIMES value the
purchase call below takes. Each bouquet is returned with the
product_code you pass to the purchase call and the
amount it will cost you.
Response
{
"status": "success",
"biller": "DSTV",
"products": [
{
"product_code": "DSTV_TV_PADI",
"name": "DStv Padi",
"amount": 3600
},
{
"product_code": "DSTV_TV_YANGA",
"name": "DStv Yanga",
"amount": 5100
}
]
}
Purchase a subscription
/tv/purchase
| Field | Description | |
|---|---|---|
| client_id | required | Your client identifier. |
| biller | required | DSTV, GOTV or STARTIMES. |
| recipient | required | The customer's smartcard/IUC number. |
| product_code | required | A product_code from this biller's own /tv/products list. |
| amount | optional | The price comes from the catalogue. If you send it, it must match the product's listed price or the call is rejected with AMOUNT_MISMATCH. |
| customer_phone | recommended | The customer's phone number, used for delivery notifications about this purchase. |
| idempotency_key | required | A unique value per attempt. |
Request
{
"client_id": "URH-8F2C91A4D6B0E37F1A",
"biller": "DSTV",
"recipient": "1234567890",
"product_code": "DSTV_TV_PADI",
"customer_phone": "08012345678",
"idempotency_key": "9b3d6f1a-4c2e-4a7b-8f01-2c3d4e5f6a7b"
}
Response 202 Accepted
{
"status": "pending",
"code": "PROCESSING",
"message": "Transaction accepted.",
"data": {
"reference": "20246379402654235116155940",
"recipient": "1234567890",
"biller": "DSTV",
"amount": 3600,
"channel": "API_GATEWAY",
"currency": "NGN",
"created_at": "2024-08-25 14:32:07"
}
}
Electricity
Prepaid and postpaid electricity bill payments.
Validate a meter
/electricity/validate
JSON body: biller, recipient and
meter_type, where meter_type is prepaid
or postpaid.
Response
{
"status": "success",
"customer_name": "ADAEZE O.",
"address": "14 ADENIYI JONES AVE, IKEJA"
}
Purchase a token
/electricity/purchase
| Field | Description | |
|---|---|---|
| client_id | required | Your client identifier. |
| biller | required | The disco: IKEDC, EKEDC or AEDC. |
| recipient | required | The customer's meter number. |
| meter_type | required | prepaid or postpaid. |
| amount | required | Numeric, minimum 0.01. |
| customer_phone | recommended | The customer's phone number, used for delivery notifications about this purchase. |
| idempotency_key | required | A unique value per attempt. |
Acceptance returns the same 202 envelope as every other product. The meter token
itself is not available yet at that point: it arrives once the transaction
reaches success, in the token field of both the
transaction.completed webhook (see Webhooks)
and the transaction status response.
Request
{
"client_id": "URH-8F2C91A4D6B0E37F1A",
"biller": "IKEDC",
"recipient": "04123456789",
"meter_type": "prepaid",
"amount": 5000,
"customer_phone": "08012345678",
"idempotency_key": "e1f2a3b4-5c6d-4e7f-8a9b-0c1d2e3f4a5b"
}
Response 202 Accepted
{
"status": "pending",
"code": "PROCESSING",
"message": "Transaction accepted.",
"data": {
"reference": "20247816184959310341316475",
"recipient": "04123456789",
"biller": "IKEDC",
"amount": 5000,
"channel": "API_GATEWAY",
"currency": "NGN",
"created_at": "2024-08-25 14:32:07"
}
}
Education
Result-checker PIN purchases for WAEC, NECO, NABTEB and JAMB.
Validate a profile
/edu/validate
JSON body: biller, recipient.
Response
{
"status": "success",
"customer_name": "WAEC Candidate"
}
List PIN products for a board
/edu/products?biller=WAEC
The query parameter is biller, the same examination board value the
purchase call below takes.
Response
{
"status": "success",
"biller": "WAEC",
"products": [
{
"product_code": "WAEC_EDU_RESULT_CHECKER",
"name": "WAEC Result Checker PIN",
"amount": 900
}
]
}
Purchase PIN(s)
/edu/purchase
| Field | Description | |
|---|---|---|
| client_id | required | Your client identifier. |
| biller | required | WAEC, NECO, NABTEB or JAMB. |
| recipient | required | The candidate's profile ID. For WAEC, pass the recipient phone number here instead. WAEC PINs are not tied to a profile. |
| product_code | required | A product_code from this biller's own /edu/products list. |
| quantity | optional | Number of PINs to buy, default 1. The amount charged is the product price multiplied by this quantity. |
| customer_phone | recommended | The customer's phone number, used for delivery notifications about this purchase. |
| idempotency_key | required | A unique value per attempt. |
Request
{
"client_id": "URH-8F2C91A4D6B0E37F1A",
"biller": "WAEC",
"recipient": "08012345678",
"product_code": "WAEC_EDU_RESULT_CHECKER",
"quantity": 5,
"customer_phone": "08012345678",
"idempotency_key": "c4d5e6f7-8a9b-4c0d-9e1f-2a3b4c5d6e7f"
}
Response 202 Accepted
{
"status": "pending",
"code": "PROCESSING",
"message": "Transaction accepted.",
"data": {
"reference": "20242553419283276483503056",
"recipient": "08012345678",
"biller": "WAEC",
"amount": 4500,
"channel": "API_GATEWAY",
"currency": "NGN",
"created_at": "2024-08-25 14:32:07"
}
}
Wallet
Your wallet funds every purchase. There is no separate endpoint to debit it: each purchase call above determines the authoritative price and debits your wallet as part of accepting the request. Funding, balance and ledger history are managed from the Developer Console, not through this API.
| Situation | What happens |
|---|---|
| Sufficient balance | The wallet is debited and the request is accepted with 202. |
| Insufficient balance | The request is rejected with 402 INSUFFICIENT_BALANCE. Nothing is charged. |
| Accepted but not completed | The debit is credited back automatically. refunded on the transaction reflects this. |
Fund your wallet and view your balance from the Developer Console's Wallet page.
Transactions
Every purchase returns a reference immediately. Use it to look up
the transaction at any time. Treat this endpoint as a fallback for a transaction
you suspect is stuck; webhooks are the recommended way to
track settlement in production.
The reference is a separate identifier Ureh generates for the
transaction — it is not your idempotency_key. Read it from the
purchase response's data.reference (or from the webhook payload)
and store it if you want to look the transaction up later.
/transactions/status
| Query parameter | Description | |
|---|---|---|
| client_id | required | Your client identifier. |
| reference | required | The reference returned when the purchase was accepted. |
Requires your Secret Key. A reference belonging to another client returns
404, never another client's data.
Request
curl "https://api.ureh.io/v1/transactions/status?client_id=URH-8F2C91A4D6B0E37F1A&reference=20246063015832257676576215" \
-H "Authorization: Bearer sk_live_xxxxxxxxxxxxxxxx"
Response 200 OK
{
"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"
}
}
| Field | Description |
|---|---|
| transaction_status | pending, success or failed. See the lifecycle below. |
| amount_charged | What actually left your wallet. amount is an alias of this value. |
| service_amount | The product price before any commission you earned was applied. |
| commission | Your commission on this transaction. commission_received is an alias of this value. |
| token | The delivered token or PIN, for products that issue one (electricity, education). null otherwise, and until the transaction succeeds. |
| refunded | true once a failed transaction has been credited back to your wallet. |
| completed_at | Present only once the transaction has settled. |
Every eligible purchase earns you commission automatically — 2% to ₦150 per
transaction, depending on the product. commission and
service_amount above show exactly what was earned and what it was
earned on. See Developer
Earnings for the full model.
The response also carries network, a deprecated alias of
biller kept for older integrations. Read biller in new
code; network may be removed in a future version.
transaction_status moves through a fixed lifecycle:
| Status | Meaning |
|---|---|
| pending | Accepted and in progress. Your wallet has already been debited. Not a final state — keep waiting. |
| success | Completed. completed_at is set, token is populated where the product delivers one, and a transaction.completed webhook fires. Final. |
| failed | Could not be completed. Your wallet is credited back automatically, refunded is true, and a transaction.failed webhook fires. Final. |
Webhooks
Subscribe once and Ureh calls your endpoint the moment a transaction reaches a final state, instead of polling Transactions on a timer.
Subscribe
/webhooks/subscribe
| Field | Description | |
|---|---|---|
| client_id | required | Your client identifier. |
| url | required | Your HTTPS endpoint. Max 2048 characters. |
| enabled | optional | Defaults to true. |
Response
{
"status": "success",
"data": {
"url": "https://your-app.com/webhooks/ureh",
"secret": "whsec_xxxxxxxxxxxxxxxxxxxxxxxxxxxx",
"enabled": true,
"signature_version": "v1"
}
}
Store secret immediately. It's returned once, at subscription
time, and used to verify every delivery's signature.
Delivery
Every delivery is a POST of compact JSON, signed with
HMAC-SHA256 over {timestamp}.{raw_body}.
| Event | Fires when |
|---|---|
| transaction.completed | A transaction reaches success. For electricity, this is also when token is populated. |
| transaction.failed | A transaction reaches failed. Your wallet has already been refunded by the time this fires. |
| Header | What it carries |
|---|---|
| X-Ureh-Signature | The HMAC-SHA256 digest, lowercase hex. Verify this before trusting the body. |
| X-Ureh-Timestamp | Unix seconds. The signed string is this value, a literal ., then the raw body. |
| X-Ureh-Event | The event name, identical to event in the body — lets you route without parsing first. |
| X-Ureh-Delivery-ID | A UUID, unique per delivery record. Every retry of the same event reuses it, so it identifies the delivery rather than the attempt. |
Headers
Content-Type: application/json
X-Ureh-Signature: b254effa849fb3bf232dcaeaef5518da806a456f533982923c32ed7e31841ada
X-Ureh-Timestamp: 1785373930
X-Ureh-Event: transaction.completed
X-Ureh-Delivery-ID: 3f7c1b92-5a4e-4d61-b8f0-6c2a9e1d3b47
Body
{
"event": "transaction.completed",
"timestamp": "2024-08-25T14:32:10Z",
"signature_version": "v1",
"data": {
"reference": "20246063015832257676576215",
"status": "success",
"service_type": "VTU_AIRTIME",
"amount": 500,
"token": null,
"completed_at": "2024-08-25T14:32:10Z",
"metadata": {}
}
}
Failed deliveries retry automatically after 1m, 5m, 15m, 30m, 1h, 6h and 24h — seven attempts in total. See Verifying webhook signatures for the exact verification code in PHP and Node.js.