Skip to content

Payments API

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). Self-registration at /register sets the account to Approved with API access on, so you can create API keys immediately and call the production API on live processors. MojoPay still limits exposure until staff switch you to Go Live.

Sandbox API products (on at signup): Hosted checkout, Card on pay links, Payouts, and Metadata. Collections, Subscriptions, and Sub-accounts stay off until MojoPay staff enable them.

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, MojoPay staff also set a fee mode for your account:

Mode What you send as amount Customer / upstream pays You settle (wallet credit)
on_top (default) What you want to receive amount + fee (amount_payable) amount
absorb What the customer pays (invoice) amount (amount_payable equals amount) amount − fee (merchant_settlement_amount)
  • Responses include fee_amount, fee_mode, amount_payable, and merchant_settlement_amount.
  • Fee percentage/cap and fee mode are configured by MojoPay staff (platform defaults + optional per-merchant overrides for %/cap; fee mode is per merchant), not in your API request body.
  • Hosted checkout sessions expire expires_at (currently 24 hours after create); create a new session if the link expires.

Payout fees

Optional per-merchant payout fees may apply when you create a payout:

  • Send amount as what the recipient should receive.
  • When a payout fee is configured, your payout wallet must cover amount + fee. The wallet is debited for both; the recipient still receives the full amount.
  • Responses include fee_amount and amount_debited (amount + fee). When no payout fee applies, fee_amount is "0.00" and amount_debited equals amount.
  • Payout fee rules are set by MojoPay staff per merchant (not in the request body). Both percentage and cap must be greater than zero; otherwise fee_amount is "0.00". There is no platform-wide payout fee default.
  • The same wallet rule applies to portal payouts, CSV import, payout groups, and settlement withdrawals to bank or mobile money. Auto-settlement that empties the payout pot to bank/MoMo reduces the recipient amount so amount + fee still fit. Interval half-hour settlement only moves released collection funds into the payout pot and does not charge an instant settlement fee.

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 on schedule without an instant settlement fee. Daily T+1 and interval auto-settlement may then pay out to bank or mobile money (a payout fee can apply if staff configured one). Merchants on interval release wait a short hold (default 30 minutes) then a half-hour job settles to their approved destination (payout pot, bank, or mobile money) — still no instant settlement 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
Same key while the first request is still running API returns 409 idempotency_key_in_progress — wait briefly and retry
502 gateway_error or 500 on a write Check status first (GET /v1/transactions/verify/{merchant_reference}, GET /v1/payouts/{merchant_reference}, or GET /v1/mandates?merchant_reference=). If the resource exists, follow its status; if it returns 404, retry with the same key and body

Retention: only successful (2xx) responses are stored against an Idempotency-Key; they are kept with no expiry and replayed verbatim for the same key and body. Failed (4xx / 5xx) responses are not stored, so the same key can be retried after you fix the cause. Keys are scoped per merchant, per sub-account, and per endpoint.

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)
  • If using bank VAs: Static VA / bank transfer enabled; show payers account_number + account_name + bank_name; treat dynamic expires_at accounts as one-order only
  • Secrets in environment variables, not in app code or URLs

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