Developers

Partner API Product guide

Bills & utilities

ECG electricity, Ghana Water, DStv, GOtv, StarTimes and Telecel Broadband — look up the account, quote, then pay.

Base URL http://fex.mojo-pay.com/v1
Open API tester

Overview

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.

Integration flow

  1. Get a Bearer token with POST /oauth/token.
  2. List billers with GET /catalog/products?category=bills and read each input_schema.
  3. Look up the customer’s account or meter with POST /catalog/products/{id}/lookup and show the account name to the customer.
  4. Create a quote with the bill fields and amount.
  5. Create an order from the quote and send the customer to payment.url.
  6. Wait for order.completed or order.failed — biller confirmation is asynchronous.

Products

Product Name Required fields Lookup
PRD_HB_DSTV
hb-dstv
DSTV account_number amount Yes
PRD_HB_ECG
hb-ecg-bill
ECG Prepaid & Postpaid phone meter_number amount Yes
PRD_HB_GWCL
hb-gwcl-bill
Ghana Water Bill meter_number phone email amount Yes
PRD_HB_GOTV
hb-gotv
GOtv account_number amount Yes
PRD_HB_STARTIMES
hb-startimes-bill
StarTimes TV account_number amount Yes
PRD_HB_TELECEL_BROADBAND
hb-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.

Authentication

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.

Global headers

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.
POST /oauth/token No auth

Issue a short-lived Bearer access token.

Send Authorization: Bearer <access_token> on all other routes.

Body parameters

client_id required
string
From Partner Portal → API credentials.
client_secret required
string
Shown once when you generate or rotate credentials in the Partner Portal.
grant_type optional
string
Optional. Must be client_credentials when sent.
POST http://fex.mojo-pay.com/v1/oauth/token
Request
{
  "grant_type": "client_credentials",
  "client_id": "fex_…",
  "client_secret": "…"
}
Response 200
{
  "access_token": "…",
  "token_type": "Bearer",
  "expires_in": 1200
}

Billers

Billers live under the bills category. Products that return a lookup_url support account lookup before quoting.

Fields per biller

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 /catalog/products Bearer

List billers available to your partner.

Query parameters

country optional
string
ISO country code.
category optional
string
Category code from list categories.
GET http://fex.mojo-pay.com/v1/catalog/products
Request
GET /v1/catalog/products?category=bills
Response 200
{
  "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} Bearer

Retrieve a single product by public id (PRD_…).

Path parameters

id required
string
Product public id.
GET http://fex.mojo-pay.com/v1/catalog/products/{id}
Request
GET /catalog/products/{id}
Authorization: Bearer <token>
Response 200
{
  "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"
}

Account lookup

Confirm who you are paying before taking the customer’s money. FEX also runs the lookup during quote and returns it as lookup.

POST /catalog/products/{id}/lookup Bearer

Resolve account details or purchasable options before quoting — data bundles for a phone, the account name / amount due for DSTV, GOtv, StarTimes, Ghana Water and Telecel Broadband, or the meters linked to a phone for ECG. Only products that return a lookup_url support this; others answer 422 lookup_unsupported.

ECG lookups return type: meters with one item per meter linked to the phone. A 404 not_found means the biller does not recognise the account.

Path parameters

id required
string
Product public id.

Body parameters

fields required
object
Lookup destination: phone (data bundles, ECG), account_number (DSTV, GOtv, StarTimes), landline_number (Telecel Broadband), or meter_number + phone (Ghana Water).
POST http://fex.mojo-pay.com/v1/catalog/products/{id}/lookup
Request (DStv account)
{
  "fields": {
    "account_number": "7029140992"
  }
}
Response 200
{
  "object": "lookup",
  "product_id": "PRD_HB_DSTV",
  "type": "account",
  "label": "Successful",
  "items": [],
  "account": {
    "name": "KWAME MENSAH",
    "amount_due": "0.00"
  }
}

Quotes

A quote locks the price for the fields you send. Every field must satisfy the product input_schema.

POST /quotes Bearer

Price a product for the submitted fields. Quotes expire — create the order before expires_at.

Body parameters

product_id required
string
Product public id from the catalog.
customer_id optional
string
Optional FEX customer id — provider-specific fields are merged from linked profiles. For SecondsTax, api_user defaults to the customer’s email or phone (same identity used on provider-funds / provider-portfolios).
fields required
object
Must satisfy the product input_schema. Explicit fields override merged customer/profile values (e.g. fields.api_user).
POST http://fex.mojo-pay.com/v1/quotes
Request (ECG bill)
{
  "product_id": "PRD_HB_ECG",
  "fields": {
    "phone": "0541312238",
    "meter_number": "P09137104",
    "amount": 12.5
  }
}
Response 201
{
  "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} Bearer

Retrieve a quote owned by the authenticated partner.

Path parameters

id required
string
Quote public id.
GET http://fex.mojo-pay.com/v1/quotes/{id}
Request
GET /quotes/{id}
Authorization: Bearer <token>

Orders

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.

POST /orders Bearer

Create an order from a quote and start payment.

Open payment.url. Sandbox-tier partners use FEX hosted sandbox pay; live-tier partners use MojoPay Omni when configured. Then poll or wait for webhooks. Configure your webhook URL under Partner Portal → Webhooks.

Headers

Idempotency-Key required
UUID recommended. Same key returns the same order.

Body parameters

quote_id required
string
From POST /quotes.
partner_reference required
string
Your reconciliation key.
payment_method optional
string
checkout (default) or collection.
success_url optional
url
Return URL after payment UX.
cancel_url optional
url
Cancel / abandon URL.
customer optional
object
Optional customer details.
metadata optional
object
Arbitrary partner metadata.
POST http://fex.mojo-pay.com/v1/orders
Request
{
  "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" }
}
Response 201
{
  "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} Bearer

Retrieve an order by public id.

Path parameters

id required
string
Order public id.
GET http://fex.mojo-pay.com/v1/orders/{id}
Request
GET /orders/{id}
Authorization: Bearer <token>
GET /orders Bearer

List up to 50 most recent orders for the partner.

Query parameters

status optional
string
Filter by order status.
GET http://fex.mojo-pay.com/v1/orders
Request
GET /orders
Authorization: Bearer <token>
Response 200
{
  "object": "list",
  "data": [
    { "id": "ORD_…", "object": "order", "status": "completed" }
  ]
}
POST /orders/{id}/cancel Bearer

Cancel an order when still cancellable (typically before payment completes).

Path parameters

id required
string
Order public id.
POST http://fex.mojo-pay.com/v1/orders/{id}/cancel
Request
POST /orders/{id}/cancel
Authorization: Bearer <token>

Webhooks

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.

Verify signature

Header format:

FEX-Signature: t=<unix_timestamp>,v1=<hmac_sha256_hex>

Signed payload: {timestamp}.{raw_body} using your webhook_secret.

Event envelope
{
  "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"
    }
  }
}

Event types

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.

Errors

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 envelope

{
  "error": {
    "code": "validation_failed",
    "message": "Product fields failed validation.",
    "details": { "errors": { "amount": ["required"] } },
    "request_id": "req_…"
  }
}

Common codes

unauthorized not_found product_unavailable validation_failed quote_unavailable quote_expired invalid_state lookup_unsupported provider_not_found provider_rejected provider_timeout