Developers

Reference

Partner API

Integrate once. Offer bills, investments, insurance, and more through a stable /v1 surface.

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

Introduction

The FEX Partner API lets banks and fintechs distribute financial products through one integration. You never call product providers or MojoPay directly — catalog, quotes, orders, payments, and webhooks all go through FEX.

Quickstart

  1. Sign in to the Partner Portal and open API credentials.
  2. Generate a client_id / client_secret pair (the secret is shown once — store it securely).
  3. Try live calls in the API tester, or exchange credentials for a Bearer token via POST /oauth/token.
  4. List catalog products and read each product’s input_schema.
  5. Create a quote, then create an order from that quote.
  6. Send the customer to payment.url (MojoPay Omni hosted checkout when configured).
  7. Handle signed webhooks and/or poll GET /orders/{id}.

Base URL

All paths below are relative to http://fex.mojo-pay.com/v1. API version in webhooks: 2026-06-01.

API credentials (self-service)

Create up to two named clients under Partner Portal → API credentials. Never share your client_secret; if it leaks, rotate the secret (same client_id). Secrets cannot be revoked. The same /v1 base URL is used for sandbox and live — your partner tier controls catalog visibility and payment rails.

Sandbox vs live

Partners start on the sandbox tier to integrate against the same Partner API. Sandbox-tier partners see products (and providers) with status sandbox or active, and complete payment on FEX’s hosted sandbox pay page. When MojoPay promotes you to a live tier (standard / enterprise), the catalog returns active products only and payment.url uses MojoPay Omni. Product codes and request shapes stay the same — only eligibility and payment change.

Authentication

OAuth 2.0 client credentials. Tokens are opaque (not JWT) and expire after about 20 minutes.

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
}

Errors

Failed responses use a consistent envelope. Use error.code for programmatic handling and request_id when contacting support.

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

Catalog

Catalog responses are FEX products. Each includes an input_schema that drives quote and order field shapes. Provider-native endpoints are never exposed.

Available products

The catalog is partner-scoped — do not hardcode product codes from this page. Always call GET /catalog/products (and read each input_schema). MojoPay enables products per partner in Admin; your credentials only see those products (still filtered by tier, country, and status). These are FEX products used on quotes/orders (hb-*, st-*, bs-*, ic-*, hi-*, xo-*, ml-*) — not SecondsTax mutual-fund names or Xoxoday / MojoLife voucher SKUs. For investable SecondsTax funds, use GET /customers/{id}/provider-funds. For Xoxoday gift vouchers, use GET /providers/{id}/vouchers then quote xo-gift-voucher; for MojoLife gift cards and vouchers, list PRV_MOJOLIFE the same way and quote ml-gift-card / ml-voucher. New products appear without a Partner API version change once enabled for you.

Product families

Provider Example codes Categories Products Guide
Black Star (black-star) bs-fund-deposit bs-sell-order investments 2 Investments
Hollard Insurance (hollard) hi-home-policy hi-motor-policy hi-travel-policy insurance_general 3 Insurance
FEX hb-at-airtime hb-mtn-airtime hb-telecel-airtime +9 more airtime bills data 12 Airtime & data, Bills & utilities
IC Securities (ic-securities) ic-fund-investment ic-equity-market-order ic-fixed-income-order investments stocks treasury 3 Investments
MojoLife (mojolife) ml-deal ml-gift-card ml-voucher deals gift_cards 3 Rewards
FEX ppn-topup ppn-gift-card airtime gift_cards 2 Airtime & data, Rewards
SecondsTax (secondstax) st-fund-redemption st-fund-subscription investments 2 Investments
Xoxoday Canvas (xoxoday) xo-gift-voucher xo-experience xo-physical-voucher gift_cards 3 Rewards

Orientation only — this is the FEX product catalog (what you pass as product_id on quotes), not provider fund pickers. SecondsTax exposes two FEX products (st-fund-subscription, st-fund-redemption); the many mutual funds (Slytherin, etc.) come from GET /customers/{id}/provider-funds. Top-ups use the same subscription product + same fund_id again. Your API credentials return a partner-scoped subset via GET /v1/catalog/products.

GET /catalog/categories Bearer

List category codes available to your partner (e.g. airtime, data, bills, investments).

Query parameters

country optional
string
Override partner default country. Also accepts X-FEX-Country.
GET http://fex.mojo-pay.com/v1/catalog/categories
Request
GET /catalog/categories
Authorization: Bearer <token>
Response 200
{
  "object": "list",
  "data": [
    { "code": "bills", "object": "category" },
    { "code": "investments", "object": "category" },
    { "code": "insurance_general", "object": "category" },
    { "code": "stocks", "object": "category" }
  ]
}
GET /catalog/products Bearer

List products, optionally filtered by category or country. Use ?category=bills for utility / pay-TV billers, ?category=airtime or ?category=data for top-ups. Products with a lookup_url support product lookup.

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 /catalog/products
Authorization: Bearer <token>
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"
}
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.

For data bundles, quote with fields.bundle = the item value and fields.amount = its amount — the pair must match (some values such as flexi_data_bundle repeat with different amounts). Account lookups return type: account with an account map (e.g. name, amount_due). Bundle lists change — always look up before quoting.

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 (MTN data bundles)
{
  "fields": {
    "phone": "0246912184"
  }
}
Response 200
{
  "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
}

Quotes

Price a product for the submitted fields. For data bundles and billers that support lookup, FEX runs the lookup during quote and returns it as lookup (e.g. the account name) so you can confirm with the customer before paying. Quotes expire — create the order before expires_at. Bill, airtime and data purchases are asynchronous: after payment the order stays fulfilling until the biller or network confirms, then you receive order.completed or order.failed.

POST /quotes Bearer

Create a priced quote for a catalog product (example: ECG bill).

Fields differ per product — always read the product input_schema. Each product guide has a quote example for its product line.

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).

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

Each API client under Partner Portal → API & webhooks can have its own webhook_url and signing secret. FEX fans out order events to every active client with a URL configured, signing each delivery with that client’s secret (HMAC-SHA256, FEX-Signature).

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.

Rate limits

Limits

Route group Limit
POST /oauth/token 30 requests / minute
Authenticated /v1/* 120 requests / minute

What partners never see

  • Provider base URLs, OAuth, or API keys
  • Direct IC Securities / Black Star / SecondsTax / Hollard (or other provider) calls
  • MojoPay merchant credentials (FEX owns the payment rail)