Skip to content
MojoPay Omni Docs

MojoPay Omni API

v1.0

Start with the integration quick start, then the full Omni API guide. Use the OpenAPI spec, YAML, sample payloads, or API tester when you need the contract.

Integration quick start

Hosted checkout

Redirect customers to MojoPay; they enter mobile money on your pay page.

  1. POST /v1/oauth/token
  2. POST /v1/checkout/sessions + Idempotency-Key
  3. Redirect to returned url
  4. Fulfill on checkout_session.succeeded webhook

Server-side collection

You already have the customer wallet and network.

  1. POST /v1/collections with mobile + provider
  2. Fulfill on collection.succeeded webhook
  3. Confirm with GET /v1/transactions/verify/{merchant_reference} if needed

Webhooks (required)

At-least-once delivery — verify signatures and dedupe by event id.

  1. Verify MojoPay-Signature on raw body
  2. Reject timestamps outside ±5 minutes
  3. Return 200 quickly; process async

Omni API guide

MojoPay Omni API

Version 1.0

Overview

HTTPS JSON API for collections (wallet + hosted checkout), payouts (mobile money + bank), verification, mandates/debits, metadata, and balance.

There is no sandbox: only the production API is published. Treat integration as final against live rails — that is the most reliable way to see real latency, provider behavior, and end-to-end outcomes. Use small amounts, controlled test data, and idempotent retries while you validate. See Testing mode for the amount cap and open-checkout rules that apply until Go Live.

New to the API? Start with Integration quick start below, then use the OpenAPI spec or sample payloads when wiring your client. In the merchant portal, Mojo Ai can also help with Omni API integration — paste a sample request, response, or error and ask for a fix.

Environment Base URL
API (this host) https://omni.mojo-pay.com/v1

All curl examples below use this base URL automatically when you read the guide on this site.

TLS 1.2+ is required. Send Content-Type: application/json and Accept: application/json on API requests unless noted otherwise.

Integration quick start

1. Portal setup (one time)

Sign in at /portal/login. Only users with the Portal admin role can change integration settings; Team member users can view activity but cannot edit developers, team, or financial settings.

In the merchant portal (/portal/developers — Portal admin only):

  1. API clients — create a client; store client_id and client_secret securely (secret shown once).
  2. API credentials — create an API client, then set that client's HTTPS webhook URL; copy the webhook secret (shown once). Optional API IP allowlist is on the API security tab.
  3. Confirm country and API product flags on /portal/settings (read-only profile; Portal admin only; country drives mobile networks, currency, balance defaults, and your Testing amount limit).
  4. Check the portal shell badge: Testing or Live. While Testing, keep collection and fixed checkout amounts at or below the country limit; do not use open (flexible) amount checkout.

Never commit secrets to source control or embed them in mobile apps.

Testing mode

New merchants start in Testing mode (visible as a badge in the merchant portal). You still call the production API and live processors; MojoPay limits exposure until staff switch you to Go Live.

Rule Detail
Max amount (per country) Caps money-in only: POST /v1/collections, fixed-amount POST /v1/checkout/sessions, and the amount a customer enters on hosted /pay/{id}. Limit is configured per country by MojoPay.
No open / flexible checkout amount_mode: open is rejected while Testing. Use amount_mode: fixed with an amount at or below the limit. Portal pay-link create hides the open option.
Errors Over-limit or open-amount attempts return 400 with error.code invalid_parameter (field amount or amount_mode).
Not capped Payouts, verifications, mandates/debits, and balance/metadata reads are not subject to the testing amount cap.
API tester /api-tester/ calls the same live API — Testing rules apply there too.
Portal help Mojo Ai in /portal can explain Testing/Go Live and help debug Omni API payloads against this guide.

After Go Live, the amount cap and open-amount restriction no longer apply.

Portal team roles

Role Can do
Portal admin (owner) API clients, webhooks, IP allowlist, team members, pay links, payouts, settlement changes
Team member (member) View dashboard, collections, transactions, payouts, pay links, settlement — no settings or write actions

The portal UI hides actions your role cannot perform; the server returns 403 if a write is attempted without permission.

Collection fees

When platform collection fees apply to your merchant account:

  • Send amount as what you want to receive (merchant principal).
  • Responses include fee_amount and amount_payable (what the customer pays at checkout or on the wallet rail).
  • Fee rules are configured by MojoPay staff (platform defaults + optional per-merchant overrides), not in your API request body.
  • Hosted checkout sessions expire expires_at (currently 1 hour after create); create a new session if the link expires.

Settlement and wallets (portal)

Successful collections credit your collection wallet. MojoPay uses T+1 settlement:

  • Actual collection wallet — total succeeded funds on record.
  • Available collection wallet — funds released after the next-day settlement window (default 08:00 in the platform settlement timezone). These can be moved without an instant settlement fee.
  • Held funds — still within the T+1 window; moving them early via Move funds on /portal/settlement incurs an instant settlement fee on the held portion (percentage capped; shown in the portal before you confirm).

Auto-settlement (when approved by MojoPay) moves released collection funds daily at the scheduled time to your saved destination (payout wallet, bank, or mobile money) without an instant fee.

Payouts (API or portal) draw from the payout wallet. Move funds from collection → payout first, or rely on auto-settlement to fund the payout wallet.

GET /v1/balance returns collection_wallet (available), collection_wallet_actual, collection_wallet_held, and payout_wallet — see Metadata and balance.

2. Choose your integration path

Your goal API Confirm success
Customer pays on a hosted page (redirect) POST /v1/checkout/sessions → redirect to url Webhook checkout_session.succeeded (+ optional GET /v1/transactions/verify/{merchant_reference})
Server-initiated mobile-money collection (you already have wallet + network) POST /v1/collections Webhook collection.succeeded
Payout to mobile money or bank POST /v1/payouts Webhook payout.succeeded
Name match before payout POST /v1/verifications/msisdn or bank_accounts 200 with status: verified

Recommendation: integrate hosted checkout first — fewest fields at create time, customer enters wallet details on MojoPay, and you reconcile with one merchant_reference.

3. Authenticate (every ~20 minutes)

curl
curl -sS -X POST "https://omni.mojo-pay.com/v1/oauth/token" \
  -H "Content-Type: application/json" \
  -d '{"client_id":"YOUR_CLIENT_ID","client_secret":"YOUR_CLIENT_SECRET"}'

Response includes access_token (opaque, 64 characters) and expires_in (default 1200 seconds). Cache the token server-side and refresh before expiry.

4. Hosted checkout (minimal happy path)

Generate a fresh UUID for each Idempotency-Key.

curl
# 1) Token
TOKEN=$(curl -sS -X POST "https://omni.mojo-pay.com/v1/oauth/token" \
  -H "Content-Type: application/json" \
  -d '{"client_id":"YOUR_CLIENT_ID","client_secret":"YOUR_CLIENT_SECRET"}' \
  | jq -r .access_token)

# 2) Create session
curl -sS -X POST "https://omni.mojo-pay.com/v1/checkout/sessions" \
  -H "Authorization: Bearer $TOKEN" \
  -H "Content-Type: application/json" \
  -H "Idempotency-Key: $(uuidgen)" \
  -d '{
    "amount_mode": "fixed",
    "currency": "GHS",
    "amount": "1.00",
    "merchant_reference": "ORD-2026-001",
    "description": "Test order",
    "success_url": "https://merchant.example.com/paid",
    "cancel_url": "https://merchant.example.com/cancelled"
  }'

Redirect the customer to the returned url. Do not mark the order paid on redirect alone — wait for a checkout_session.succeeded webhook (or poll GET /v1/transactions/verify/ORD-2026-001 from your server).

5. Webhook handler checklist

  1. Read the raw request body (do not re-serialize JSON before verify).
  2. Verify MojoPay-Signature (see below).
  3. Deduplicate on event id (EVT_…) — delivery is at-least-once.
  4. Update your order only on terminal events (succeeded / failed).
  5. Respond 200 (or 204) quickly; do heavy work asynchronously.

6. Verify webhook signatures

Header format: MojoPay-Signature: t=<unix_seconds>,v1=<hex>

Signed payload: {t}.{raw_body} with HMAC-SHA256 and your webhook secret.

Reject timestamps outside ±300 seconds (5 minutes) of your server clock.

Simulates what MojoPay sends to your webhook URL. Replace the URL, secret, and body as needed.

WEBHOOK_URL="https://merchant.example.com/webhooks/mojopay"
WEBHOOK_SECRET="whsec_from_portal"
TIMESTAMP=$(date +%s)

BODY='{"id":"EVT_TEST001ABCDEFGHIJKLMNOPQR","type":"checkout_session.succeeded","api_version":"2026-04-15","created":"2026-04-15T10:05:00Z","data":{"object":{"id":"CS_TEST001ABCDEF","object":"checkout_session","status":"succeeded","currency":"GHS","amount":"1.00","merchant_reference":"ORD-TEST-001","payment_rail":"mobile_money","provider":"MTN"}}}'

SIGNATURE=$(printf '%s' "${TIMESTAMP}.${BODY}" \
  | openssl dgst -sha256 -hmac "${WEBHOOK_SECRET}" \
  | awk '{print $NF}')

curl -sS -X POST "${WEBHOOK_URL}" \
  -H "Content-Type: application/json" \
  -H "MojoPay-Signature: t=${TIMESTAMP},v1=${SIGNATURE}" \
  -d "${BODY}"

Your handler should return 200 when the signature verifies. Use the exact BODY string when computing HMAC — any whitespace change invalidates the signature.

7. Idempotency and safe retries

Situation What to do
Network timeout after POST Retry with the same Idempotency-Key and same JSON body
New payment attempt New Idempotency-Key (e.g. UUID v4)
Same key, different body API returns 409 idempotency_key_mismatch

Use a unique merchant_reference per business transaction (your order id). Max 255 characters; stick to letters, numbers, and - / _ for easiest debugging.

8. Integration checklist

  • OAuth token cached and refreshed before expiry
  • Idempotency-Key on every money-moving POST
  • While Testing: amounts ≤ country limit; no amount_mode: open
  • Webhook signature verified on raw body
  • Event id deduplication store (DB or cache)
  • Order fulfillment only after succeeded webhook or verify
  • GET /v1/metadata/mobile_providers used for network codes (don’t hardcode)
  • Secrets in environment variables, not in app code or URLs

Tools: OpenAPI spec · YAML download · Sample payloads JSON · API tester

Routes

Method Path Notes
POST /v1/oauth/token client_credentials
GET /v1/account resolved account/sub-account context
POST /v1/collections Idempotency-Key
GET /v1/collections/{id} or ?merchant_reference= status
POST /v1/checkout/sessions Idempotency-Key
GET /v1/checkout/sessions/{id} status
GET /v1/transactions/verify/{merchant_reference} unified status lookup
POST /v1/payouts Idempotency-Key
GET /v1/payouts/{merchant_reference} status
POST /v1/verifications/msisdn name check
POST /v1/verifications/bank_accounts name check
POST /v1/mandates Idempotency-Key
GET /v1/mandates?merchant_reference= status
POST /v1/mandates/{mandate_id}/cancel Idempotency-Key; body: mobile + reason
POST /v1/mandates/{mandate_id}/debits Idempotency-Key
GET /v1/mandate_debits/status?mandate_id=&merchant_reference= status
GET /v1/subscription_plans active plans (subscriptions product)
POST /v1/subscriptions Idempotency-Key; enroll subscriber
GET /v1/subscriptions?merchant_reference= subscription status
GET /v1/metadata/banks reference data
GET /v1/metadata/mobile_providers reference data
GET /v1/balance wallet balance (optional currency)

Global conventions

  • JSON request/response; send Content-Type: application/json for JSON bodies.
  • currency: ISO 4217 (GHS, NGN, ...).
  • mobile: digits only, country code first, no + and no spaces (example: 233544338841).
  • amount: non-negative string decimal; up to 4 dp internally, displayed/settled at 2 dp (ceil).
  • merchant_reference: merchant-generated reference; echoed in responses and webhooks.
  • Authorization: Bearer <access_token> on every protected endpoint.
  • Idempotency-Key: required on POST endpoints that move money or create sessions/mandates, and on mandate cancellation (state-changing).
  • Public IDs — uppercase prefixes such as CL_, CS_, PO_, MD_ (lookups are case-insensitive).
  • request_id (errors) — REQ_ + 12 uppercase alphanumeric characters (example: REQ_8F3A2C1B9D0E).
  • Webhook event idEVT_ + 24 uppercase alphanumeric characters.
  • Flat request bodies (collections, checkout, payouts, mandates): use top-level mobile and provider for wallet rails instead of nested customer / beneficiary objects. This reduces nesting and copy-paste mistakes.
  • Sub-accounts (only when enabled for your merchant): send X-Mojo-Account-Ref on every bearer-authenticated request to scope it to a specific sub-account (use main for the primary account). Metadata endpoints (/v1/metadata/*) ignore the header. Idempotency keys and merchant_reference lookups are scoped per resolved account. Call GET /v1/account to confirm the account a request resolves to. When sub-accounts are not enabled, omit the header.

Security baseline

  • HTTPS only for API and webhook URLs. The merchant portal rejects webhook targets that use private/reserved addresses, localhost, or embedded credentials; outbound delivery also skips targets that fail the same checks (including DNS resolution to a non-public IP when enabled).
  • Optional API IP allowlist (portal → Developers → API security, Portal admin only, or admin merchant edit): when configured, POST /v1/oauth/token and all bearer-authenticated routes are rejected unless the client IP matches an allowed IPv4/IPv6 address or CIDR. Empty allowlist means no IP restriction. Behind a reverse proxy, set TRUSTED_PROXIES so the application sees the real client IP.
  • Tenant isolation — Bearer tokens are scoped to one merchant. Lookups by public ID or merchant_reference never return another merchant’s records (missing resources respond 404).
  • OAuth — Invalid or unknown client_id values still run secret verification against a fixed hash to reduce timing leaks; per-IP and per-client rate limits apply to POST /v1/oauth/token.
  • Never send client_secret / access_token in query strings or redirect URLs.
  • Verify every outbound webhook from MojoPay using MojoPay-Signature before changing state (see Verify webhook signatures).
  • Use constant-time signature comparison (hash_equals, timingSafeEqual).
  • Hosted checkout is mobile money only on the pay page (card is not available for hosted sessions in v1.0).

Authentication

POST /v1/oauth/token

Request (minimal — grant_type defaults to client_credentials if omitted):

{
  "client_id": "issued_by_mojopay",
  "client_secret": "issued_by_mojopay"
}

Request (explicit):

{
  "grant_type": "client_credentials",
  "client_id": "issued_by_mojopay",
  "client_secret": "issued_by_mojopay"
}

Response 200:

{
  "access_token": "a1b2c3d4e5f6g7h8i9j0k1l2m3n4o5p6q7r8s9t0u1v2w3x4y5z6a7b8c9d0e1f2",
  "token_type": "Bearer",
  "expires_in": 1200
}

access_token is an opaque 64-character string (not a JWT). Default TTL is 1200 seconds (20 minutes).

Errors

{
  "error": {
    "code": "invalid_parameter",
    "message": "Provider is required for mobile money payouts.",
    "doc_url": "https://docs.mojopay.com/errors/invalid_parameter",
    "request_id": "REQ_8F3A2C1B9D0E"
  }
}

Common HTTP codes: 400, 401, 403, 404, 409, 429, 500.

Idempotency conflict (409) — same Idempotency-Key reused with a different JSON body than the first request:

{
  "error": {
    "code": "idempotency_key_mismatch",
    "message": "Key was already used with a different request body.",
    "doc_url": "https://docs.mojopay.com/errors/idempotency_key_mismatch",
    "request_id": "REQ_9F3A2C1B8E7D"
  }
}

Error code catalog (v1)

doc_url points to https://docs.mojopay.com/errors/{code} when that page is published. Responses may omit doc_url in some environments.

Code HTTP Meaning
invalid_parameter 400 Validation or missing required header/field (e.g. Idempotency-Key).
idempotency_key_mismatch 409 Same idempotency key, different request body.
invalid_client 401 OAuth client id/secret invalid or client revoked.
forbidden 403 Authenticated but not allowed (e.g. merchant API access disabled).
unauthorized 401 Missing/invalid Bearer token or expired token.
not_found 404 Unknown resource for this merchant.
rate_limited 429 Too many requests; use Retry-After when present.
gateway_error 502 Upstream payment/KYC provider error or timeout; safe to retry idempotent reads or a new idempotency key for writes.
internal_error 500 Unexpected server error (reserved; include request_id for support).

Machine-readable contract: docs/openapi/mojopay-v1.openapi.yaml.

Webhooks (outbound to your server)

Configure an HTTPS webhook URL on each API client in the portal (Developers → API credentials). MojoPay POSTs events only for resources that client created to your URL (for example https://your-store.example.com/webhooks/mojopay — not a path on the MojoPay API host). Portal pay links and portal payouts do not send webhooks. Events use this envelope: id, type, api_version, created, data.object.

Fee and tax line-item breakdowns are not included in webhook payloads. When collection fees apply, API responses include fee_amount and amount_payable; the portal pay-link UI shows the same breakdown.

Signature verification:

  • Header: MojoPay-Signature containing t=<unix>, v1=<hex> (lowercase hex, 64 characters).
  • Digest: HMAC-SHA256 over {t}.{raw_body} with webhook secret — use the exact raw body bytes received.
  • Reject timestamps outside ±300 seconds (5 minutes) of your server time.
  • Compare signatures with a constant-time function (hash_equals in PHP, timingSafeEqual in Node).
  • See Integration quick start — Verify webhook signatures for copy-paste examples.

Example event:

{
  "id": "EVT_01JQXYZABCDEFGHIJKLMNOPQR",
  "type": "collection.succeeded",
  "api_version": "2026-04-15",
  "created": "2026-04-15T10:00:00Z",
  "data": {
    "object": {
      "id": "CL_9K2ML0ABCDEF",
      "object": "collection",
      "status": "succeeded",
      "currency": "GHS",
      "amount": "200.00",
      "merchant_reference": "ORD-2026-001",
      "mobile": "233544338841",
      "payment_rail": "mobile_money",
      "provider": "MTN"
    }
  }
}

Event types (v1.0 — emitted today): collection.processing|succeeded|failed, checkout_session.processing|succeeded|failed, payout.processing|succeeded|failed, mandate.processing|succeeded|cancelled, mandate_debit.processing|succeeded|failed.

Subscription lifecycle events (subscription.created, subscription.active, subscription.cancelled, subscription.failed, subscription.charge_failed) are emitted when mandates are linked to subscription plans.

Delivery: treat webhook delivery as at-least-once. The same logical event may be retried; use the event id to deduplicate before updating order state. Always verify MojoPay-Signature first, then process.

Retries: up to 5 delivery attempts with backoff delays of 10s, 60s, 5m, 15m, and 1h between attempts.

Endpoint details

8.1 Collection (wallet collection)

POST /v1/collections (Idempotency-Key required)

  • Implicit rail: mobile money (type may be omitted; default is mobile_money if ever required for forward compatibility).
  • Required fields: amount, currency, merchant_reference, mobile, provider.
  • Optional fields: description.
  • Customer display details (name, email, etc.) are not sent; merchants map those locally (for example using merchant_reference).
  • Testing mode: amount must be at or below the country testing max (see Testing mode).

Body:

{
  "currency": "GHS",
  "amount": "200.00",
  "merchant_reference": "ORD-2026-001",
  "description": "Invoice 42",
  "mobile": "233544338841",
  "provider": "MTN"
}

Response 201: returns resource (id, status, amount, fee_amount, amount_payable, merchant_reference, ...). When fees do not apply, fee_amount is "0.00" and amount_payable equals amount.

8.2 Checkout session (hosted)

Typical flow:

  1. Create token: POST /v1/oauth/token.
  2. Create checkout session: POST /v1/checkout/sessions with Idempotency-Key.
  3. Redirect customer to returned url (/pay/{id} on the MojoPay app host).
  4. Customer completes payment on the hosted page (mobile money only in v1.0).
  5. Receive result via webhook (checkout_session.succeeded or checkout_session.failed) — source of truth.
  6. Optionally confirm with GET /v1/transactions/verify/{merchant_reference} or GET /v1/checkout/sessions/{id}.

Hosted pay page behavior: the customer enters (or confirms) mobile money details, then confirms the number with an SMS one-time code (skipped for ~7 days on the same browser after a successful verify). After OTP, the page shows a pending state while the collection is in flight, then success or failed, and finally redirects to your success_url or cancel_url (typically after a short delay). Your server should not rely on the browser redirect alone — use webhooks or verify.

POST /v1/checkout/sessions (Idempotency-Key required)

  • Required fields: currency, merchant_reference, success_url, cancel_url.
  • amount_modefixed (default) or open.
    • fixed: amount is required; fees are computed at create time. In Testing mode, amount must be at or below the country testing max.
    • open: omit amount; the customer enters the amount on the hosted pay page before paying. Fees are computed when the amount is submitted. Not available in Testing mode (use fixed amounts at or below the country testing limit, or Go Live). See Testing mode.
  • Optional fields: payment_rail, mobile, provider, description.
  • payment_rail — when set to mobile_money, the session is locked to mobile money and mobile is required at create time if you want to pre-fill wallet details. When payment_rail is omitted from the JSON body, the session stores null and the customer chooses mobile money on the hosted page (portal pay links send explicit JSON null). Card is not accepted on hosted checkout in v1.0 even if shown as a disabled option in some UIs.
  • mobile / provider — Required at API create time only when payment_rail is mobile_money. When the customer chooses the rail on the hosted page, they enter mobile and provider on completion; omit those fields from the initial create for that flow.
  • Hosted pay page — Browser GET on the session url (path /pay/{id}). Shows merchant business name, optional checkout logo, amount (or amount entry for open sessions), reference, and mobile-money network selection. Before initiating payment, MojoPay sends an SMS OTP to the wallet number (unless that browser already verified the same number within about 7 days).
  • Customer display details (name, email, etc.) are not sent; merchants map those locally (for example using merchant_reference).
  • Do not include secrets in success_url / cancel_url.

Fixed amount create:

{
  "amount_mode": "fixed",
  "currency": "GHS",
  "amount": "1.00",
  "merchant_reference": "ORD-2026-001",
  "description": "Invoice 42",
  "success_url": "https://merchant.example.com/pay/return",
  "cancel_url": "https://merchant.example.com/pay/cancel"
}

Open amount create (customer enters amount on pay page):

{
  "amount_mode": "open",
  "currency": "GHS",
  "merchant_reference": "DONATION-2026-001",
  "description": "Pay what you want",
  "success_url": "https://merchant.example.com/pay/return",
  "cancel_url": "https://merchant.example.com/pay/cancel"
}

Response 201 (create):

Fixed amount — includes principal and fee fields when computed at create time:

{
  "id": "CS_4NQP81ABCDEF",
  "object": "checkout_session",
  "status": "processing",
  "currency": "GHS",
  "amount_mode": "fixed",
  "amount": "1.00",
  "fee_amount": "0.02",
  "amount_payable": "1.02",
  "url": "https://checkout.example.com/pay/CS_4NQP81ABCDEF",
  "expires_at": "2026-04-15T11:00:00Z"
}

Open amount — slim payload until the customer submits an amount on the hosted page (amount, fee_amount, and amount_payable appear after amount is set):

{
  "id": "CS_4NQP81ABCDEF",
  "object": "checkout_session",
  "status": "processing",
  "currency": "GHS",
  "amount_mode": "open",
  "url": "https://checkout.example.com/pay/CS_4NQP81ABCDEF",
  "expires_at": "2026-04-15T11:00:00Z"
}

Sessions expire at expires_at (currently 1 hour after create). Treat expired sessions as unusable; create a new session for the customer.

Status query: GET /v1/checkout/sessions/{id} returns the full session including amount_mode, amount, fee_amount, amount_payable (null until amount is set for open sessions), and rail fields when known.

8.3 Unified verify by merchant reference

GET /v1/transactions/verify/{merchant_reference}

Returns a normalized snapshot for the object behind your merchant_reference (for example ORDER-123). Lookup order: collection, then payout, then checkout session (most recent match per type). Mandate debits are not included — use GET /v1/mandate_debits/status.

Typical fields:

  • object — resource type (collection, checkout_session, payout)
  • id, status, currency, amount, amount_mode (checkout)
  • Wallet flows: mobile, provider, payment_rail when applicable

Use this for a single server-side “confirm status” call when you already key your orders by merchant_reference.

8.4 Payout (mobile money)

POST /v1/payouts with type: mobile_money (Idempotency-Key required)

Required:

  • type, currency, amount, merchant_reference, mobile, provider

Optional: narration, transaction_type (default local), transaction_date, expires_atomit these unless you need an explicit payout window override; otherwise the platform applies the default processing window from configuration.

Sender or payer context is not sent in API payloads; merchants map that locally (for example against merchant_reference). Beneficiary display names are not accepted on create; use verification or payout status responses if you need matched names.

8.5 Payout (bank)

POST /v1/payouts with type: bank (Idempotency-Key required)

Required:

  • type, currency, amount, merchant_reference, account_number, bank_code

Optional: narration, transaction_type, transaction_date, expires_at — omit window fields unless you need overrides; defaults apply server-side.

Sender or payer context is not sent in API payloads; merchants map that locally (for example against merchant_reference). Account holder names and titles are not accepted on create; resolve via bank rails or read payout status after processing.

Bank-code guidance:

  • bank_code is required and should be sourced from GET /v1/metadata/banks.
  • Do not hardcode bank names for routing; resolve by bank_code.
  • Use the metadata endpoint as canonical if static lists and runtime values differ.

Payout status: GET /v1/payouts/{merchant_reference}.

8.6 Verification

  • POST /v1/verifications/msisdn: mobile required, provider optional.
  • POST /v1/verifications/bank_accounts: bank_code, account_number required.

Invalid account / MSISDN (upstream KYC_INVALID): HTTP 200 with:

{
  "status": "invalid",
  "profile": null
}

Verified: HTTP 200 with status: verified and profile (first_name, last_name).

Upstream/provider errors (timeouts, auth failures, unexpected status codes): HTTP 502 with error code gateway_error and standard error envelope.

8.7 Mandates and mandate debits

  • POST /v1/mandates (Idempotency-Key required): mobile, provider, currency, amount, frequency, starts_at, ends_at, merchant_reference, optional description. Scheduling details such as day-of-month are driven by portal defaults and frequency. Optional plan_id links the mandate to a subscription plan and creates a subscriber; when plan_id is set, customer_name and customer_email are required and notes is optional. Without plan_id, do not send customer name or email; map locally if needed.
  • GET /v1/mandates?merchant_reference=
  • POST /v1/mandates/{mandate_id}/cancel (Idempotency-Key required; body: mobile, reason)
  • POST /v1/mandates/{mandate_id}/debits (Idempotency-Key required; body includes amount, currency, merchant_reference, provider)
  • GET /v1/mandate_debits/status?mandate_id=&merchant_reference=

8.8 Metadata and balance

  • GET /v1/metadata/banks
  • GET /v1/metadata/mobile_providers
  • GET /v1/balance — optional query currency (ISO 4217). Omit currency to use the merchant’s default wallet currency from the portal.

Response fields:

Field Meaning
currency ISO 4217 code
available Total across available collection wallet + payout wallet
collection_wallet Available collection balance (released after T+1; movable without instant fee)
collection_wallet_actual Actual collection balance (all succeeded credits on record)
collection_wallet_held Funds still held until the next T+1 release window
payout_wallet Balance available for payouts
pending In-flight collection amount not yet succeeded
provider_float Optional; present when the upstream float query succeeds

Note: API payouts require funds in payout_wallet. Merchants fund the payout wallet via Move funds on /portal/settlement (instant fee may apply on held collection funds) or via approved auto-settlement after T+1 release.

8.9 Account context (sub-accounts)

GET /v1/account

Returns the account a request resolves to given your Bearer token and optional X-Mojo-Account-Ref header. Use it to confirm sub-account routing before sending money-moving calls.

Response 200:

{
  "object": "account_context",
  "merchant": {
    "id": 1001,
    "name": "Acme Ltd",
    "sub_accounts_enabled": true
  },
  "account": {
    "id": "MA_ABCDEF123456",
    "object": "merchant_account",
    "account_ref": "store-01",
    "name": "Store 01",
    "status": "active"
  }
}

When sub-accounts are not enabled, account is null and the header is ignored.

8.10 Subscriptions

Available only when the subscriptions product is enabled for your merchant (otherwise these endpoints return 403 forbidden).

  • GET /v1/subscription_plans — lists your active plans. Response is { "object": "list", "data": [ ... ] }; each plan includes object (subscription_plan), plan_id, name, description, currency, amount, frequency, duration_type, duration_months, and status.
  • POST /v1/subscriptions (Idempotency-Key required) — enrolls a customer on a plan. Required: plan_id, merchant_reference, customer_name, customer_email, mobile, provider; optional notes. Creates a subscriber and starts mandate enrollment; returns the subscription (object: subscription, subscription_id, status, merchant_reference, plan_id, mandate_id, ...) with 201.
  • GET /v1/subscriptions?merchant_reference= — returns the subscription for your merchant_reference (404 if none).

An invalid or inactive plan_id returns 422 with a validation error.

Status vocabulary

Value Meaning
processing In flight
succeeded Terminal success
failed Terminal error
cancelled Stopped
expired Timed out / invalid

Create endpoints can return processing; deliver value only after webhook confirmation or explicit verify/read returning terminal success.

Rate limits

Limits are per merchant (once authenticated) using the bearer token’s merchant, and per IP for POST /v1/oauth/token. OAuth also applies a per client_id bucket so distributed guessing against one integration cannot bypass the IP cap.

  • Writes (collections, checkout sessions, payouts, mandates, mandate debits, mandate cancel, subscription enrollment): per-minute and per-hour caps.
  • Reads: per-minute cap (status/verify/list endpoints).
  • Metadata (/v1/metadata/*): separate, higher per-minute cap.
  • Verifications: per-minute and per-hour caps.

Tune via MOJOPAY_THROTTLE_* environment variables. Behind a reverse proxy, set TRUSTED_PROXIES (e.g. * or your load balancer CIDRs) so client IP and limits are correct.

Response headers may include X-RateLimit-* and Retry-After. JSON may include retry_after (seconds) when available:

{
  "error": {
    "code": "rate_limited",
    "message": "Too many requests. Retry after a short delay.",
    "request_id": "REQ_9A1B2C3D4E5F",
    "retry_after": 42
  }
}

Appendix A - Changelog

1.0 (2026-04-15): Initial public release.

1.0 (2026-07-11): Documented Testing mode — per-country money-in amount cap; open (flexible) checkout disabled until Go Live; portal badge and API tester follow the same rules.

1.0 (2026-07-11): Renamed product docs to MojoPay Omni API. Portal Mojo Ai includes an API integration specialist grounded in this guide (paste request/response/errors for help).

1.0 (2026-07-12): Application ops changelog for staff/admin features (CEO role, audit logs, SMS settings, transactions ledger coverage, portal accounting fee balances) lives in the repository root CHANGELOG.md. This appendix remains the merchant Omni API narrative history.

1.0 (2026-07-13): Hosted /pay/{id} confirms the customer wallet with an SMS OTP before initiating mobile money (same browser can skip for ~7 days after verify). No Omni API request/response shape change — see checkout session hosted pay notes above and root CHANGELOG.md.

Appendix B - Webhook delivery

  • Webhook retries: up to 5 attempts; backoff 10s, 60s, 5m, 15m, 1h (see Webhooks).
  • Rotate webhook secrets in the portal when your security policy requires it; update your verifier before retiring the old secret.

Appendix C - Integration resources

  • Omni API guide: /doc
  • OpenAPI 3.x viewer: /doc/openapi
  • OpenAPI YAML (Postman/codegen): /doc/openapi.yaml
  • Sample payloads JSON: /doc/samples.json
  • Interactive API tester: /api-tester/ (same live API; Testing mode rules apply)
  • Portal Mojo Ai (/portal): account help plus Omni API integration coaching (paste payloads/errors; secrets stay out of chat)
  • Stable v1 error codes and doc_url mapping: see Error code catalog above
  • Per-route rate limits apply by operation class; expect 429 with Retry-After when exceeded

Documented in this revision: example idempotency mismatch (409) body; webhook deduplication guidance; mandate and subscription webhook events; Testing mode money-in caps; Mojo Ai API integration help.

Appendix D - Ghana bank code reference

The live list is returned by GET /v1/metadata/banks (use that as the canonical source). This appendix is an offline reference only.

Use these as reference values for bank_code in Ghana integrations:

  • 300302 STANDARD CHARTERED BANK (GH) LTD
  • 300303 ABSA BANK (GH) LTD.
  • 300304 GCB BANK
  • 300305 NATIONAL INVESTMENT BANK LTD
  • 300325 UNITED BANK FOR AFRICA (GH) LTD
  • 300306 ARP APEX BANK LTD
  • 300308 SG GHANA LTD
  • 300309 UNIVERSAL MERCHANT BANK
  • 300310 REPUBLIC BANK
  • 300311 ZENITH BANK (GH) LTD
  • 300312 ECOBANK (GH) LTD
  • 300313 CAL BANK LTD
  • 300316 FIRST ATLANTIC MERCHANT BANK LTD
  • 300317 PRUDENTIAL BANK LTD
  • 300318 STANBIC BANK GHANA LTD
  • 300320 BANK OF AFRICA LTD
  • 300323 FIDELITY BANK GHANA LTD
  • 300322 GUARANTY TRUST BANK
  • 300324 OMNIBSIC GHANA
  • 300329 ACCESS BANK (GH) LTD
  • 300334 FIRST NATIONAL BANK
  • 300331 CONSOLIDATED BANK GHANA
  • 300307 AGRICULTURAL DEVELOPMENT BANK
  • 300319 FBN BANK LTD

Canonical for v1.0. Breaking changes require a new major version.