Developers

Partner API Product guide

Investments

Mutual funds, equities and fixed income through SecondsTax, IC Securities and Black Star — customers, KYC, funds and holdings.

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

Overview

Everything you need to sell investments 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. Create the customer once with POST /customers.
  3. SecondsTax: submit KYC with a fund manager, then poll POST /customers/{id}/kyc/refresh until approved. IC Securities / Black Star: link the existing provider account with POST /customers/{id}/provider-profiles.
  4. Browse investable funds with GET /customers/{id}/provider-funds.
  5. Create a quote with customer_id so FEX merges the provider identity, then create an order and send the customer to payment.url.
  6. Show holdings with GET /customers/{id}/provider-portfolios.

Products

Product Name Provider Required fields Lookup
PRD_BS_DEPOSIT
bs-fund-deposit
Black Star Fund Deposit Black Star client_id portfolio_id security_id amount —
PRD_BS_SELL
bs-sell-order
Black Star Sell Order Black Star client_id portfolio_id security_uuid quantity —
PRD_IC_FUND
ic-fund-investment
IC Securities Fund Investment IC Securities account_id amount —
PRD_ST_REDEEM
st-fund-redemption
SecondsTax Fund Redemption SecondsTax api_user subscription_id amount —
PRD_ST_SUBSCRIBE
st-fund-subscription
SecondsTax Fund Subscription SecondsTax api_user fund_id amount —
PRD_IC_EQUITY
ic-equity-market-order
IC Securities Equity Market Order IC Securities account_id isin transaction_type volume —
PRD_IC_FIXED_INCOME
ic-fixed-income-order
IC Securities Fixed Income Order IC Securities account_id isin transaction_type volume —

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
}

Customers

Register end customers once, submit provider KYC (SecondsTax), link pre-provisioned provider ids (IC Securities, Black Star), then pass customer_id on quotes so FEX merges the right fields. For SecondsTax (st-*), the investor must select a fund manager before KYC — send fund_manager_code as BLACK (BlackStar) or INVES (InvestCorp). After submit, poll POST /customers/{id}/kyc/refresh until kyc_status is approved (or rejected). The investor identity is the customer’s email or phone — FEX sends it as X-Api-User (also stored on the provider profile after KYC). Use GET /customers/{id}/provider-funds for the investable fund catalog and GET /customers/{id}/provider-portfolios for that investor’s holdings. Existing SecondsTax investors can skip KYC if the customer already has the same email/phone SecondsTax knows.

POST /customers Bearer

Create a customer record for your partner organization.

POST http://fex.mojo-pay.com/v1/customers
Request
POST /customers
Authorization: Bearer <token>
GET /customers/{id} Bearer

Retrieve a customer by FEX id (CUS_…) or your partner_reference.

GET http://fex.mojo-pay.com/v1/customers/{id}
Request
GET /customers/{id}
Authorization: Bearer <token>
POST /customers/{id}/kyc Bearer

Submit KYC to a provider that supports onboarding (SecondsTax). Requires a fund manager selection (BLACK or INVES).

SecondsTax requires fund_manager_code (BLACK or INVES), plus image.front and image.back (base64 or public URL); image.selfie is recommended. FEX flattens this JSON to their multipart /auth/kyc-form and injects fundDetails + fundManagerNames / fundManagerEmails from the LOV (fund-manager email included). Contact email (else phone) is stored on the provider profile. After submit, kyc_status is submitted until you poll SecondSTAX via POST /customers/{id}/kyc/refresh. Quotes and fund/portfolio calls use the customer’s email or phone as X-Api-User. Existing investors can skip KYC when the FEX customer already has that same email/phone; optional fields.api_user on a quote still overrides.

Path parameters

id required
string
Customer public id (CUS_…).

Body parameters

provider_id required
string
Provider public id — use PRV_SECONDSTAX for SecondsTax.
fund_manager_code required
string
SecondsTax fund manager the investor selects before KYC: BLACK (BlackStar) or INVES (InvestCorp). May also be sent as kyc.fund_manager_code. FEX maps this to SecondsTax fundDetails + fundManagerNames / fundManagerEmails using the LOV email (partners do not send the fund-manager email).
kyc required
object
SecondsTax KYC form fields as nested JSON. FEX posts multipart to SecondsTax /auth/kyc-form.
POST http://fex.mojo-pay.com/v1/customers/{id}/kyc
Request (SecondsTax)
{
  "provider_id": "PRV_SECONDSTAX",
  "fund_manager_code": "BLACK",
  "kyc": {
    "generalDetails": {
      "title": "Mr",
      "firstName": "John",
      "middleName": "Michael",
      "lastName": "Doe",
      "dateOfBirth": "1996-01-03",
      "gender": "Male",
      "maritalStatus": "Single",
      "nationality": "Ghanaian",
      "countryOfBirth": "Ghana",
      "placeOfBirth": "Accra",
      "mothersMaidenName": "Smith",
      "tin": "1234567890"
    },
    "contactDetails": {
      "phone": { "countryCode": "+233", "number": "0255361949" },
      "email": "john@example.com",
      "digitalAddress": "GA-123-4567",
      "residentialAddress": "123 Main Street, Accra",
      "emergencyContactName": "Jane Doe",
      "emergencyContactPhone": "+233209876543",
      "emergencyContactAddress": "456 Secondary Street, Accra",
      "emergencyEmail": "jane.doe@example.com"
    },
    "employmentDetails": {
      "employmentStatus": "Employed",
      "profession": "Software Engineer",
      "employerName": "Tech Corp Ltd",
      "employerAddress": "789 Business Rd, Accra",
      "employerPhone": "+233302123456",
      "employerDigitalAddress": "GA-987-6543"
    },
    "identityDetails": {
      "idName": "Ghana Card",
      "idNumber": "GHA-1234567890",
      "issueDate": "2015-05-01",
      "expiryDate": "2030-05-01",
      "placeOfIssue": "Accra"
    },
    "financialDetails": {
      "riskTolerance": "Medium",
      "sourceOfFunds": "Salary",
      "csdNumber": "CSD123456"
    },
    "nextOfKin": {
      "title": "Mrs",
      "firstName": "Mary",
      "middleName": "Ann",
      "lastName": "Doe",
      "relationship": "Sister",
      "phone": { "countryCode": "+233", "number": "209112233" },
      "residentialAddress": "321 Family Street, Kumasi",
      "digitalAddress": "AK-321-7654",
      "email": "mary.doe@example.com"
    },
    "referralCode": "DOLIPS39",
    "image": {
      "front": "data:image/jpeg;base64,…",
      "back": "data:image/jpeg;base64,…",
      "selfie": "data:image/jpeg;base64,…"
    }
  }
}
POST /customers/{id}/kyc/refresh Bearer

Refresh SecondsTax KYC status from their application-data API and update the provider profile (submitted → approved / rejected / etc.).

Looks up the investor in SecondSTAX GET /application-data/investors?search=… (email/phone). User-level approved/rejected wins even if the fund-manager org row is still submitted. SecondSTAX does not push KYC webhooks to FEX — partners should call this after submit (or on a schedule) until status is terminal.

Path parameters

id required
string
Customer public id (CUS_…) or your partner_reference.

Body parameters

provider_id required
string
Provider public id or code — PRV_SECONDSTAX / secondstax.
POST http://fex.mojo-pay.com/v1/customers/{id}/kyc/refresh
Request
{
  "provider_id": "PRV_SECONDSTAX"
}
Response 200
{
  "customer": { "id": "CUS_…", "provider_profiles": [{ "provider_code": "secondstax", "kyc_status": "approved" }] },
  "provider_profile": {
    "provider_id": "PRV_SECONDSTAX",
    "provider_code": "secondstax",
    "kyc_status": "approved",
    "profile": {
      "api_user": "john@example.com",
      "fund_manager_code": "BLACK",
      "provider_user_id": "ec8d9686-…",
      "provider_kyc_status": "approved",
      "organisation_kyc_status": "verified",
      "kyc_refreshed_at": "2026-09-17T11:00:00+00:00"
    }
  }
}
GET /customers/{id}/provider-funds Bearer

List SecondsTax funds available to invest in (id, name, description, min investment, risk). Maps to their GET /fund catalog — not the investor’s holdings.

Show name (and description when present) in the UI; pass id as fields.fund_id on subscription quotes. Response api_user is the resolved customer email/phone FEX sent to SecondsTax. Empty descriptions are null. Inactive funds are omitted. Holdings are on GET /customers/{id}/provider-portfolios.

Path parameters

id required
string
Customer public id (CUS_…) or your partner_reference. FEX uses this customer’s email or phone as SecondsTax X-Api-User.

Query parameters

provider_id required
string
Provider public id or code — PRV_SECONDSTAX / secondstax.
GET http://fex.mojo-pay.com/v1/customers/{id}/provider-funds
Request
GET /customers/{id}/provider-funds
Authorization: Bearer <token>
Response 200
{
  "object": "provider_fund_list",
  "customer_id": "CUS_…",
  "provider_id": "PRV_SECONDSTAX",
  "provider_code": "secondstax",
  "api_user": "0554990633",
  "funds": [
    {
      "id": "4ba4c1db-2f79-4c3f-a232-b32556c48853",
      "name": "Enhanced Equity Beta Fund",
      "description": "An equity-focused fund managed by BlackStar targeting high-growth opportunities with a strategic emphasis on market beta exposure.",
      "currency": "GHS",
      "category": "Equity Funds",
      "minimum_investment": 100,
      "risk_level": "high"
    },
    {
      "id": "3f7275ee-0a35-4875-a811-d494fb1298d3",
      "name": "Slytherin",
      "description": null,
      "currency": "GHS",
      "category": "Specialty",
      "minimum_investment": 50,
      "risk_level": "low"
    },
    {
      "id": "59f41325-c156-4ce2-8bfd-9051039fb884",
      "name": "Gryffindor",
      "description": null,
      "currency": "GHS",
      "category": "Money Market",
      "minimum_investment": 100,
      "risk_level": "medium"
    }
  ]
}
GET /customers/{id}/provider-portfolios Bearer

List the customer’s holdings at a provider (SecondsTax today): fund_name, fund_description, fund_id, subscription_id, amounts, status.

Holdings include fund_name / fund_description for display. Use fund_id on subscription quotes and subscription_id on redemption quotes. To browse funds a customer can buy, call GET /customers/{id}/provider-funds.

Path parameters

id required
string
Customer public id (CUS_…) or your partner_reference. Holdings are for this customer’s email/phone at SecondsTax.

Query parameters

provider_id required
string
Provider public id or code — PRV_SECONDSTAX / secondstax.
GET http://fex.mojo-pay.com/v1/customers/{id}/provider-portfolios
Request
GET /customers/{id}/provider-portfolios
Authorization: Bearer <token>
Response 200
{
  "object": "provider_portfolio",
  "customer_id": "CUS_…",
  "provider_id": "PRV_SECONDSTAX",
  "provider_code": "secondstax",
  "api_user": "0554990633",
  "summary": {
    "total_value": "8180.25",
    "holding_count": 7
  },
  "holdings": [
    {
      "subscription_id": "440b2eb0-…",
      "fund_id": "4ba4c1db-…",
      "fund_name": "Enhanced Equity Beta Fund",
      "fund_description": "An equity-focused fund managed by BlackStar targeting high-growth opportunities with a strategic emphasis on market beta exposure.",
      "fund_category": "Equity Funds",
      "amount": "100.00",
      "status": "active"
    }
  ]
}

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.

SecondsTax quotes include the provider subscription fee in amount_payable. Top up an existing holding by quoting the same fund_id again. Redemptions use st-fund-redemption with the holding’s subscription_id. IC Securities (ic-*) and Black Star (bs-*) products take the linked account_id / client_id + portfolio_id from the customer’s provider profile when you pass customer_id.

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 (SecondsTax fund subscription)
{
  "product_id": "PRD_ST_SUBSCRIBE",
  "customer_id": "CUS_…",
  "fields": {
    "fund_id": "4ba4c1db-2f79-4c3f-a232-b32556c48853",
    "amount": 500
  }
}
Response 201
{
  "id": "QTE_…",
  "object": "quote",
  "product_id": "PRD_ST_SUBSCRIBE",
  "customer_id": "CUS_…",
  "currency": "GHS",
  "amount": "500.00",
  "fex_fee_amount": "…",
  "mojopay_fee_amount": "…",
  "amount_payable": "…",
  "input_fields": {
    "api_user": "john@example.com",
    "fund_id": "4ba4c1db-2f79-4c3f-a232-b32556c48853",
    "amount": 500
  },
  "expires_at": "2026-08-30T15:00:00+00:00"
}
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 FEX places the subscription, redemption or trade with the provider. Provider references appear on the order once available.

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

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 provider_not_found provider_error