Hosted checkout
Redirect customers to MojoPay; they enter mobile money on your pay page.
POST /v1/oauth/tokenPOST /v1/checkout/sessions+Idempotency-Key- Redirect to returned
url - Fulfill on
checkout_session.succeededwebhook
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.
Redirect customers to MojoPay; they enter mobile money on your pay page.
POST /v1/oauth/tokenPOST /v1/checkout/sessions + Idempotency-Keyurlcheckout_session.succeeded webhookYou already have the customer wallet and network.
POST /v1/collections with mobile + providercollection.succeeded webhookGET /v1/transactions/verify/{merchant_reference} if neededAt-least-once delivery — verify signatures and dedupe by event id.
MojoPay-Signature on raw body200 quickly; process asyncMojoPay Omni API
Version 1.0
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.
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):
client_id and client_secret securely (secret shown once)./portal/settings (read-only profile; Portal admin only; country drives mobile networks, currency, balance defaults, and your Testing amount limit).Never commit secrets to source control or embed them in mobile apps.
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.
| 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.
When platform collection fees apply to your merchant account:
amount as what you want to receive (merchant principal).fee_amount and amount_payable (what the customer pays at checkout or on the wallet rail).expires_at (currently 1 hour after create); create a new session if the link expires.Successful collections credit your collection wallet. MojoPay uses T+1 settlement:
/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.
| 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.
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.
Generate a fresh UUID for each Idempotency-Key.
# 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).
MojoPay-Signature (see below).id (EVT_…) — delivery is at-least-once.succeeded / failed).200 (or 204) quickly; do heavy work asynchronously.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}"
$payload = file_get_contents('php://input');
$header = $_SERVER['HTTP_MOJOPAY_SIGNATURE'] ?? '';
if (! preg_match('/^t=(\d+),v1=([a-f0-9]{64})$/', $header, $m)) {
http_response_code(400);
exit;
}
[$full, $timestamp, $signature] = $m;
if (abs(time() - (int) $timestamp) > 300) {
http_response_code(400);
exit;
}
$expected = hash_hmac('sha256', $timestamp.'.'.$payload, getenv('MOJOPAY_WEBHOOK_SECRET'));
if (! hash_equals($expected, $signature)) {
http_response_code(401);
exit;
}
$event = json_decode($payload, true, 512, JSON_THROW_ON_ERROR);
// Dedupe on $event['id'], then switch on $event['type'].
http_response_code(200);
import crypto from 'node:crypto';
export function verifyMojoPayWebhook(rawBody, signatureHeader, secret, toleranceSec = 300) {
const match = /^t=(\d+),v1=([a-f0-9]{64})$/.exec(signatureHeader ?? '');
if (!match) return false;
const timestamp = Number(match[1]);
const signature = match[2];
if (Math.abs(Math.floor(Date.now() / 1000) - timestamp) > toleranceSec) {
return false;
}
const expected = crypto
.createHmac('sha256', secret)
.update(`${timestamp}.${rawBody}`)
.digest('hex');
return crypto.timingSafeEqual(
Buffer.from(expected, 'hex'),
Buffer.from(signature, 'hex'),
);
}
Your handler should return 200 when the signature verifies. Use the exact BODY string when computing HMAC — any whitespace change invalidates the signature.
| 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.
Idempotency-Key on every money-moving POSTamount_mode: openid deduplication store (DB or cache)succeeded webhook or verifyGET /v1/metadata/mobile_providers used for network codes (don’t hardcode)Tools: OpenAPI spec · YAML download · Sample payloads JSON · API tester
| 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) |
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).CL_, CS_, PO_, MD_ (lookups are case-insensitive).request_id (errors) — REQ_ + 12 uppercase alphanumeric characters (example: REQ_8F3A2C1B9D0E).id — EVT_ + 24 uppercase alphanumeric characters.mobile and provider for wallet rails instead of nested customer / beneficiary objects. This reduces nesting and copy-paste mistakes.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.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.merchant_reference never return another merchant’s records (missing resources respond 404).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.client_secret / access_token in query strings or redirect URLs.MojoPay-Signature before changing state (see Verify webhook signatures).hash_equals, timingSafeEqual).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).
{
"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"
}
}
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.
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:
MojoPay-Signature containing t=<unix>, v1=<hex> (lowercase hex, 64 characters).{t}.{raw_body} with webhook secret — use the exact raw body bytes received.hash_equals in PHP, timingSafeEqual in Node).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.
POST /v1/collections (Idempotency-Key required)
type may be omitted; default is mobile_money if ever required for forward compatibility).amount, currency, merchant_reference, mobile, provider.description.merchant_reference).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.
Typical flow:
POST /v1/oauth/token.POST /v1/checkout/sessions with Idempotency-Key.url (/pay/{id} on the MojoPay app host).checkout_session.succeeded or checkout_session.failed) — source of truth.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)
currency, merchant_reference, success_url, cancel_url.amount_mode — fixed (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.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.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).merchant_reference).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.
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)mobile, provider, payment_rail when applicableUse this for a single server-side “confirm status” call when you already key your orders by merchant_reference.
POST /v1/payouts with type: mobile_money (Idempotency-Key required)
Required:
type, currency, amount, merchant_reference, mobile, providerOptional: narration, transaction_type (default local), transaction_date, expires_at — omit 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.
POST /v1/payouts with type: bank (Idempotency-Key required)
Required:
type, currency, amount, merchant_reference, account_number, bank_codeOptional: 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.bank_code.Payout status:
GET /v1/payouts/{merchant_reference}.
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.
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=GET /v1/metadata/banksGET /v1/metadata/mobile_providersGET /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.
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.
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.
| 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.
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.
/v1/metadata/*): separate, higher per-minute cap.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
}
}
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.
/portal): account help plus Omni API integration coaching (paste payloads/errors; secrets stay out of chat)doc_url mapping: see Error code catalog above429 with Retry-After when exceededDocumented 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.
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) LTD300303 ABSA BANK (GH) LTD.300304 GCB BANK300305 NATIONAL INVESTMENT BANK LTD300325 UNITED BANK FOR AFRICA (GH) LTD300306 ARP APEX BANK LTD300308 SG GHANA LTD300309 UNIVERSAL MERCHANT BANK300310 REPUBLIC BANK300311 ZENITH BANK (GH) LTD300312 ECOBANK (GH) LTD300313 CAL BANK LTD300316 FIRST ATLANTIC MERCHANT BANK LTD300317 PRUDENTIAL BANK LTD300318 STANBIC BANK GHANA LTD300320 BANK OF AFRICA LTD300323 FIDELITY BANK GHANA LTD300322 GUARANTY TRUST BANK300324 OMNIBSIC GHANA300329 ACCESS BANK (GH) LTD300334 FIRST NATIONAL BANK300331 CONSOLIDATED BANK GHANA300307 AGRICULTURAL DEVELOPMENT BANK300319 FBN BANK LTDCanonical for v1.0. Breaking changes require a new major version.