{
"grant_type": "client_credentials",
"client_id": "fex_…",
"client_secret": "…"
}
{
"access_token": "…",
"token_type": "Bearer",
"expires_in": 1200
}
Partner API Product guide
MTN, Telecel and AT airtime top-ups and data bundles, with live bundle lookup per phone number.
http://fex.mojo-pay.com/v1
Everything you need to sell airtime & data through the FEX Partner API. All paths are relative to the /v1 base URL above. Shared concepts (sandbox vs live, rate limits) are on the Partner API overview.
POST /oauth/token.GET /catalog/products?category=airtime or ?category=data.POST /catalog/products/{id}/lookup.phone + amount; data also needs the bundle from the lookup.payment.url.order.completed or order.failed.| Product | Name | Required fields | Lookup |
|---|---|---|---|
PRD_HB_AT_AIRTIMEhb-at-airtime |
AirtelTigo Airtime | phone amount |
— |
PRD_PPN_TOPUPppn-topup |
International Airtime Top-up | sku_id mobile amount |
Yes |
PRD_HB_MTN_AIRTIMEhb-mtn-airtime |
MTN Airtime | phone amount |
— |
PRD_HB_TELECEL_AIRTIMEhb-telecel-airtime |
Telecel Airtime | phone amount |
— |
PRD_HB_AT_DATAhb-at-data |
AirtelTigo Data Bundle | phone bundle amount |
Yes |
PRD_HB_MTN_DATAhb-mtn-data |
MTN Data Bundle | phone bundle amount |
Yes |
PRD_HB_TELECEL_DATAhb-telecel-data |
Telecel Data Bundle | phone bundle amount |
Yes |
Orientation only — your credentials see a partner-scoped subset. Always call GET /catalog/products and read each input_schema rather than hardcoding this table.
Create a client_id / client_secret under Partner Portal → API credentials, then exchange them for a Bearer token (about 20 minutes). Send Authorization: Bearer <token> on every other call.
| Header | Required | Description |
|---|---|---|
Authorization |
Yes* | Bearer <token> (*except token endpoint) |
Content-Type |
POST | application/json |
Accept |
Recommended | application/json |
X-FEX-Country |
No | ISO country (e.g. GH). Defaults to partner profile. |
Idempotency-Key |
Recommended | On POST /orders — replay-safe create. |
{
"grant_type": "client_credentials",
"client_id": "fex_…",
"client_secret": "…"
}
{
"access_token": "…",
"token_type": "Bearer",
"expires_in": 1200
}
Airtime is under the airtime category and data bundles under data, one product per network (MTN, Telecel, AT).
hb-mtn-airtime, hb-telecel-airtime, hb-at-airtime): phone + amount, from GHS 0.50 up to GHS 100 per order.hb-mtn-data, hb-telecel-data, hb-at-data): phone + bundle + amount, where the bundle and amount come from the lookup.0246912184) or international (233246912184).GET /v1/catalog/products?category=data
{
"object": "list",
"data": [
{
"id": "PRD_HB_ECG",
"object": "product",
"code": "hb-ecg-bill",
"name": "ECG Prepaid & Postpaid",
"category": "bills",
"country": "GH",
"currency": "GHS",
"status": "sandbox",
"input_schema": {
"type": "object",
"required": ["phone", "meter_number", "amount"],
"properties": {
"phone": { "type": "string" },
"meter_number": { "type": "string" },
"amount": { "type": "number" }
}
},
"min_amount": "1.00",
"max_amount": null,
"provider": { "id": "PRV_…", "code": "…", "name": "…" },
"lookup_url": "https://…/v1/catalog/products/PRD_HB_ECG/lookup"
}
]
}
GET /catalog/products/{id}
Authorization: Bearer <token>
{
"id": "PRD_HB_ECG",
"object": "product",
"code": "hb-ecg-bill",
"name": "ECG Prepaid & Postpaid",
"category": "bills",
"input_schema": {
"type": "object",
"required": ["phone", "meter_number", "amount"],
"properties": {
"phone": { "type": "string", "title": "Phone linked to the meter" },
"meter_number": { "type": "string", "title": "Meter / account number" },
"amount": { "type": "number", "title": "Amount (GHS)" }
}
},
"provider": { "id": "PRV_…", "code": "…", "name": "…" },
"lookup_url": "https://…/v1/catalog/products/PRD_HB_ECG/lookup"
}
Data bundles differ per phone number and change often — look them up right before quoting.
{
"fields": {
"phone": "0246912184"
}
}
{
"object": "lookup",
"product_id": "PRD_HB_MTN_DATA",
"type": "bundles",
"label": "Successful",
"items": [
{ "display": "349.24MB", "value": "data_bundle_3", "amount": "3.00" },
{ "display": "1.19GB", "value": "flexi_data_bundle", "amount": "20.00" }
],
"account": null
}
A quote locks the price for the fields you send. Every field must satisfy the product input_schema.
{
"product_id": "PRD_HB_MTN_DATA",
"fields": {
"phone": "0246912184",
"bundle": "data_bundle_3",
"amount": 3
}
}
{
"id": "QTE_…",
"object": "quote",
"product_id": "PRD_HB_MTN_DATA",
"currency": "GHS",
"amount": "3.00",
"amount_payable": "3.00",
"input_fields": {
"phone": "0246912184",
"bundle": "data_bundle_3",
"amount": 3
},
"expires_at": "2026-08-30T15:00:00+00:00"
}
GET /quotes/{id}
Authorization: Bearer <token>
Lifecycle: pending_payment → paid → fulfilling → completed (or failed / cancelled / expired). After payment the order moves to fulfilling while the biller or network confirms. Most complete within seconds; some take a few minutes. FEX retries status checks automatically, so do not re-order — wait for the webhook or poll the order.
{
"quote_id": "QTE_…",
"partner_reference": "INV-1001",
"payment_method": "checkout",
"success_url": "https://partner.example/success",
"cancel_url": "https://partner.example/cancel",
"customer": { "email": "customer@example.com" },
"metadata": { "channel": "mobile_app" }
}
{
"id": "ORD_…",
"object": "order",
"status": "pending_payment",
"partner_reference": "INV-1001",
"currency": "GHS",
"amount": "50.00",
"amount_payable": "50.00",
"product_id": "PRD_…",
"payment": {
"id": "CS_…",
"method": "checkout",
"status": "processing",
"url": "https://omni.mojo-pay.com/pay/CS_…",
"merchant_reference": "FEX-ORD_…-1"
},
"failure": null,
"created_at": "…",
"completed_at": null
}
GET /orders/{id}
Authorization: Bearer <token>
GET /orders
Authorization: Bearer <token>
{
"object": "list",
"data": [
{ "id": "ORD_…", "object": "order", "status": "completed" }
]
}
POST /orders/{id}/cancel
Authorization: Bearer <token>
Configure a webhook URL per API client under Partner Portal → API & webhooks. FEX signs every delivery with FEX-Signature (HMAC-SHA256). Treat order.completed / order.failed as the source of truth — not the payment redirect.
Header format:
FEX-Signature: t=<unix_timestamp>,v1=<hmac_sha256_hex>Signed payload: {timestamp}.{raw_body} using your webhook_secret.
{
"id": "EVT_…",
"type": "order.completed",
"api_version": "2026-06-01",
"created": "2026-08-25T14:30:00+00:00",
"data": {
"object": {
"id": "ORD_…",
"object": "order",
"status": "completed"
}
}
}
| Type | When |
|---|---|
order.pending_payment |
Order created; customer should pay |
order.paid |
Payment confirmed |
order.fulfilling |
Provider fulfillment started |
order.completed |
Terminal success |
order.failed |
Terminal failure |
order.cancelled |
Cancelled by partner or system |
Respond with 2xx quickly. Delivery is at-least-once — dedupe on event id.
Failed responses use the standard envelope. Use error.code for handling and request_id when contacting support. Provider codes (provider_*) can also appear on a failed order as failure.code.
{
"error": {
"code": "validation_failed",
"message": "Product fields failed validation.",
"details": { "errors": { "amount": ["required"] } },
"request_id": "req_…"
}
}
unauthorized
not_found
product_unavailable
validation_failed
quote_unavailable
quote_expired
invalid_state
lookup_unsupported
provider_not_found
provider_rejected
provider_timeout