{
"grant_type": "client_credentials",
"client_id": "fex_…",
"client_secret": "…"
}
{
"access_token": "…",
"token_type": "Bearer",
"expires_in": 1200
}
Partner API Product guide
ECG electricity, Ghana Water, DStv, GOtv, StarTimes and Telecel Broadband — look up the account, quote, then pay.
http://fex.mojo-pay.com/v1
Everything you need to sell bills & utilities 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=bills and read each input_schema.POST /catalog/products/{id}/lookup and show the account name to the customer.payment.url.order.completed or order.failed — biller confirmation is asynchronous.| Product | Name | Required fields | Lookup |
|---|---|---|---|
PRD_HB_DSTVhb-dstv |
DSTV | account_number amount |
Yes |
PRD_HB_ECGhb-ecg-bill |
ECG Prepaid & Postpaid | phone meter_number amount |
Yes |
PRD_HB_GWCLhb-gwcl-bill |
Ghana Water Bill | meter_number phone email amount |
Yes |
PRD_HB_GOTVhb-gotv |
GOtv | account_number amount |
Yes |
PRD_HB_STARTIMEShb-startimes-bill |
StarTimes TV | account_number amount |
Yes |
PRD_HB_TELECEL_BROADBANDhb-telecel-broadband |
Telecel Broadband | landline_number broadband_account 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
}
Billers live under the bills category. Products that return a lookup_url support account lookup before quoting.
| Biller | Quote fields | Lookup with |
|---|---|---|
ECG prepaid & postpaid (hb-ecg-bill) |
phone meter_number amount |
phone — returns the meters linked to it |
Ghana Water (hb-gwcl-bill) |
meter_number phone email amount |
meter_number + phone — returns account name and amount due |
DStv / GOtv (hb-dstv, hb-gotv) |
account_number amount |
account_number — returns account name and amount due |
StarTimes (hb-startimes-bill) |
account_number amount |
account_number |
Telecel Broadband (hb-telecel-broadband) |
landline_number broadband_account amount |
landline_number |
Amounts are GHS with at most 2 decimals. Paying ECG links the phone to the meter if it is not linked yet.
GET /v1/catalog/products?category=bills
{
"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"
}
Confirm who you are paying before taking the customer’s money. FEX also runs the lookup during quote and returns it as lookup.
{
"fields": {
"account_number": "7029140992"
}
}
{
"object": "lookup",
"product_id": "PRD_HB_DSTV",
"type": "account",
"label": "Successful",
"items": [],
"account": {
"name": "KWAME MENSAH",
"amount_due": "0.00"
}
}
A quote locks the price for the fields you send. Every field must satisfy the product input_schema.
{
"product_id": "PRD_HB_ECG",
"fields": {
"phone": "0541312238",
"meter_number": "P09137104",
"amount": 12.5
}
}
{
"id": "QTE_…",
"object": "quote",
"product_id": "PRD_HB_ECG",
"currency": "GHS",
"amount": "12.50",
"fex_fee_amount": "0.00",
"mojopay_fee_amount": "0.00",
"amount_payable": "12.50",
"input_fields": {
"phone": "0541312238",
"meter_number": "P09137104",
"amount": 12.5
},
"expires_at": "2026-08-30T15:00:00+00:00",
"lookup": {
"type": "meters",
"label": "Successful",
"items": [
{ "display": "THOMAS ANANE (P09137104)", "value": "P09137104", "amount": "-1.14" }
],
"account": null
}
}
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