Skip to content

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
400BAD_REQUESTA required query parameter is missing, such as biller on a product catalogue call.
401UNAUTHORIZEDMissing, malformed, invalid or suspended Bearer token.
402INSUFFICIENT_BALANCEYour wallet cannot cover this purchase. Nothing was charged. Fund the wallet and retry.
403GATEWAY_ERRORUnregistered client_id, or a Public Key used on a Secret-Key-only endpoint.
403ACCOUNT_RESTRICTEDYour account is not currently permitted to transact. Contact support.
405METHOD_NOT_ALLOWEDWrong HTTP method for this endpoint. The Allow header names the accepted one.
409ALREADY_PROCESSINGA request with this idempotency_key is still in flight. Retry shortly to receive the original result.
422VALIDATION_FAILEDOne or more fields are missing or fail validation. errors names each field.
422AMOUNT_MISMATCHAn amount was sent for a catalogue-priced product and does not match its listed price. Omit amount, or send the listed price.
422VERIFICATION_FAILEDA validate call could not confirm the recipient. Check the smartcard, meter or profile number.
422GATEWAY_ERRORA field is well-formed but not resolvable, such as an unknown product_code or a biller that is not configured.
429RATE_LIMITEDRate limited. Includes retry_after_seconds and a Retry-After header. See Rate Limits.
500GATEWAY_ERRORUnexpected server-side failure. Safe to retry with the same idempotency_key.
502GATEWAY_ERRORA product catalogue could not be loaded right now. Retry shortly.
503PROVIDER_UNAVAILABLEThis biller cannot be served right now. Your wallet is not charged; retry the same request shortly.
503QUEUE_UNAVAILABLEThe 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

POST /airtime/purchase
FieldDescription
client_idrequiredYour client identifier.
billerrequiredMTN, AIRTEL, GLO or 9MOBILE.
recipientrequiredRecipient phone number.
amountrequiredThe airtime face value to deliver, in NGN. Numeric, minimum 0.01.
idempotency_keyrequiredA 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

GET /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

POST /data/purchase
FieldDescription
client_idrequiredYour client identifier.
billerrequiredMTN, AIRTEL, GLO or 9MOBILE.
recipientrequiredRecipient phone number.
product_coderequiredA product_code from that biller's own /data/products list.
amountoptionalThe 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_keyrequiredA 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

POST /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

GET /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

POST /tv/purchase
FieldDescription
client_idrequiredYour client identifier.
billerrequiredDSTV, GOTV or STARTIMES.
recipientrequiredThe customer's smartcard/IUC number.
product_coderequiredA product_code from this biller's own /tv/products list.
amountoptionalThe 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_phonerecommendedThe customer's phone number, used for delivery notifications about this purchase.
idempotency_keyrequiredA 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

POST /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

POST /electricity/purchase
FieldDescription
client_idrequiredYour client identifier.
billerrequiredThe disco: IKEDC, EKEDC or AEDC.
recipientrequiredThe customer's meter number.
meter_typerequiredprepaid or postpaid.
amountrequiredNumeric, minimum 0.01.
customer_phonerecommendedThe customer's phone number, used for delivery notifications about this purchase.
idempotency_keyrequiredA 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

POST /edu/validate

JSON body: biller, recipient.

Response

{
  "status": "success",
  "customer_name": "WAEC Candidate"
}

List PIN products for a board

GET /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)

POST /edu/purchase
FieldDescription
client_idrequiredYour client identifier.
billerrequiredWAEC, NECO, NABTEB or JAMB.
recipientrequiredThe candidate's profile ID. For WAEC, pass the recipient phone number here instead. WAEC PINs are not tied to a profile.
product_coderequiredA product_code from this biller's own /edu/products list.
quantityoptionalNumber of PINs to buy, default 1. The amount charged is the product price multiplied by this quantity.
customer_phonerecommendedThe customer's phone number, used for delivery notifications about this purchase.
idempotency_keyrequiredA 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.

SituationWhat happens
Sufficient balanceThe wallet is debited and the request is accepted with 202.
Insufficient balanceThe request is rejected with 402 INSUFFICIENT_BALANCE. Nothing is charged.
Accepted but not completedThe 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.

GET /transactions/status
Query parameterDescription
client_idrequiredYour client identifier.
referencerequiredThe 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"
  }
}
FieldDescription
transaction_statuspending, success or failed. See the lifecycle below.
amount_chargedWhat actually left your wallet. amount is an alias of this value.
service_amountThe product price before any commission you earned was applied.
commissionYour commission on this transaction. commission_received is an alias of this value.
tokenThe delivered token or PIN, for products that issue one (electricity, education). null otherwise, and until the transaction succeeds.
refundedtrue once a failed transaction has been credited back to your wallet.
completed_atPresent 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:

StatusMeaning
pendingAccepted and in progress. Your wallet has already been debited. Not a final state — keep waiting.
successCompleted. completed_at is set, token is populated where the product delivers one, and a transaction.completed webhook fires. Final.
failedCould 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

POST /webhooks/subscribe
FieldDescription
client_idrequiredYour client identifier.
urlrequiredYour HTTPS endpoint. Max 2048 characters.
enabledoptionalDefaults 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}.

EventFires when
transaction.completedA transaction reaches success. For electricity, this is also when token is populated.
transaction.failedA transaction reaches failed. Your wallet has already been refunded by the time this fires.
HeaderWhat it carries
X-Ureh-SignatureThe HMAC-SHA256 digest, lowercase hex. Verify this before trusting the body.
X-Ureh-TimestampUnix seconds. The signed string is this value, a literal ., then the raw body.
X-Ureh-EventThe event name, identical to event in the body — lets you route without parsing first.
X-Ureh-Delivery-IDA 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.