Payments API
Endpoint details
8.1 Collection (wallet collection)
POST /v1/collections (Idempotency-Key required)
- Default rail: mobile money when
payment_railis omitted. - Required fields (always):
amount,currency,merchant_reference. - Mobile money (
payment_rail=mobile_moneyor omitted): also requiremobile,provider. Optional:description. - Bank transfer / dynamic VA (
payment_rail=bank_transfer): requires bank transfer enabled for your merchant. Optional:account_name,description. Do not sendmobile/provider. Response includes a nestedvirtual_account(temporary account number, bank name,expires_at). Show those details to the payer; wait forcollection.succeeded(or poll GET/verify). - Customer display details (name, email, etc.) are not sent on mobile money; merchants map those locally (for example using
merchant_reference). - Testing mode:
amountmust be at or below the country testing max (see Testing mode).
Mobile money body:
{
"currency": "GHS",
"amount": "200.00",
"merchant_reference": "ORD-2026-001",
"description": "Invoice 42",
"mobile": "233544338841",
"provider": "MTN"
}
Dynamic bank transfer body:
{
"currency": "GHS",
"amount": "25.00",
"merchant_reference": "ORD-BT-001",
"payment_rail": "bank_transfer",
"account_name": "Kofi Mensah",
"description": "Invoice bank transfer"
}
Response 201: returns resource (id, status, amount, fee_amount, fee_mode, amount_payable, merchant_settlement_amount, merchant_reference, ...). When fees do not apply, fee_amount is "0.00" and amount_payable / merchant_settlement_amount equal amount. Bank transfer creates also include payment_rail and virtual_account.
Status query: GET /v1/collections/{id} and GET /v1/collections?merchant_reference= return the same resource shape. While status is processing, these GETs may refresh from the payment processor (throttled to about once every few seconds per collection) before responding — useful if webhooks are delayed. Prefer webhooks as the primary signal; use GET/verify as confirmation.
8.1b Customers and static virtual accounts
Requires MojoPay to enable Static VA on your merchant. This is separate from dynamic bank transfer on collections/checkout (payment_rail=bank_transfer), which issues a temporary account for one order.
Merchant standing account (same details as the portal overview bank block):
POST /v1/virtual-accounts/merchant— issues (or returns) your one ACTIVE merchant static account. Idempotent.GET /v1/virtual-accounts/merchant— returns the active account, or404if you have not issued one yet.
Customer dedicated accounts (one reusable account per payer):
POST /v1/customers— body:name, optionalemail/mobile.merchant_referenceis assigned by MojoPay (CUST-…); do not send it. Returnsobject: customer.POST /v1/customers/{id}/virtual-account— issues (or returns) one ACTIVE dedicated account for that customer. Cap: 1000 active customer accounts per merchant.GET /v1/customers/{id}/virtual-account— active dedicated account.- Inflows to a dedicated account create a succeeded
collection(any amount) and credit your collection wallet; you receivecollection.succeeded. Collection payloads includecustomer_idwhen attributed to a dedicated customer VA. GET /v1/customers/{id}/collectionslists those collections (object: list,customer_id, paginateddata).POST .../virtual-account/deactivatesets statusINACTIVEso further credits are rejected.
Show payers account_number, account_name, and bank_name together. Do not confuse these permanent details with the expiring account returned on dynamic payment_rail=bank_transfer collections.
8.2 Checkout session (hosted)
Typical flow:
- Create token:
POST /v1/oauth/token. - Create checkout session:
POST /v1/checkout/sessionswithIdempotency-Key. - Redirect customer to returned
url(/pay/{id}on the MojoPay app host). - Customer completes payment on the hosted page (mobile money, or card when enabled for your merchant).
- Receive result via webhook (
checkout_session.succeededorcheckout_session.failed) — source of truth. - Optionally confirm with
GET /v1/transactions/verify/{merchant_reference}orGET /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_mode—fixed(default) oropen.fixed:amountis required; fees are computed at create time. In Testing mode,amountmust be at or below the country testing max.open: omitamount; 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 tomobile_money, the session is locked to mobile money andmobileis required at create time if you want to pre-fill wallet details. Whenpayment_railis omitted from the JSON body, the session storesnulland the customer chooses the rail on the hosted page (portal pay links send explicit JSONnull).cardis accepted only when MojoPay has enabled card checkout for your merchant; otherwise it returns400invalid_parameter.mobile/provider— Required at API create time only whenpayment_railismobile_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
GETon the sessionurl(path/pay/{id}). Shows merchant business name, optional checkout logo (uploaded in portal Settings or admin — PNG/JPG), 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 fee fields when computed at create time (on_top example: customer pays principal + fee):
{
"id": "CS_4NQP81ABCDEF",
"object": "checkout_session",
"status": "processing",
"currency": "GHS",
"amount_mode": "fixed",
"amount": "1.00",
"fee_amount": "0.02",
"fee_mode": "on_top",
"amount_payable": "1.02",
"merchant_settlement_amount": "1.00",
"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, fee_mode, amount_payable, and merchant_settlement_amount 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 24 hours 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, fee_mode, amount_payable, merchant_settlement_amount (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.
While the resolved object is still processing, verify may refresh from the payment processor (same short throttle as collection GET) before returning.
Typical fields:
object— resource type (collection,checkout_session,payout)id,status,currency,amount,amount_mode(checkout)- Wallet flows:
mobile,provider,payment_railwhen 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_at — omit these unless you need an explicit payout window override; otherwise the platform applies the default processing window from configuration.
Funding: payouts draw from your payout wallet. If a payout fee is configured for your merchant, the wallet must hold at least amount + fee. Create responses include fee_amount and amount_debited.
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_codeis required and should be sourced fromGET /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:mobilerequired,provideroptional.POST /v1/verifications/bank_accounts:bank_code,account_numberrequired.
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
Direct debit flow: POST /v1/mandates, then GET /v1/mandates until succeeded, then POST /v1/mandates/{mandate_id}/debits.
- Create —
POST /v1/mandates(Idempotency-Keyrequired). Body:mobile,provider,currency,amount,frequency,starts_at,merchant_reference, optionaldescription. Ghana direct debit is MTN only.starts_atsets the upstream mandate start and day-of-debit; Omni does not accept an end date — cancel the mandate when you want it to stop. Scheduling details such as day-of-month are derived fromstarts_atandfrequency. Optionalplan_idlinks the mandate to a subscription plan and creates a subscriber; whenplan_idis set,customer_nameandcustomer_emailare required andnotesis optional. Withoutplan_id, do not send customer name or email; map locally if needed.- Create returns
processing(HTTP201). Poll GET or wait formandate.succeededbefore debiting. A MoMo prompt is expected but not always shown by the network. - If the MSISDN already has an approved mandate, create may return
succeeded(reuse) so you can debit immediately. - A second create while approval is still pending returns HTTP
409— poll the firstmerchant_reference. POST /v1/mandateswithoutplan_iddoes not auto-debit. Subscription enrollments (POST /v1/subscriptions, hosted subscribe, andPOST /v1/mandateswithplan_id) charge the plan amount in the same request when a mandate is already approved.
- Create returns
- Get —
GET /v1/mandates?merchant_reference=(your mandate reference, not theMD_id). Whileprocessing, Omni refreshes from the processor (throttled). If create was accepted but the new reference never appears upstream (because the number already had an approved mandate), GET recovers onto that approved MandateId. Debit only afterstatusissucceeded. - Debit —
POST /v1/mandates/{mandate_id}/debits(Idempotency-Keyrequired; body:amount,currency,merchant_reference,provider). Debit while pending returns HTTP409(invalid_parameter). Collection fees apply using the merchant's configured fee mode (on_toporabsorb) — same semantics asPOST /v1/collections. Responses includefee_amount,fee_mode,amount_payable, andmerchant_settlement_amount; the customer is debitedamount_payableupstream.- Follow-up / recurring charges are not automatic on the API. Call this endpoint on your own schedule (
DAILY/WEEKLY/MONTHLY). Omni does not run that scheduler forPOST /v1/mandatesorPOST /v1/subscriptions. Hosted subscribe (/subscribe/{merchant-slug}) is the exception — Omni charges those plans on the plan frequency.
- Follow-up / recurring charges are not automatic on the API. Call this endpoint on your own schedule (
- Debit status (optional) —
GET /v1/mandate_debits/status?mandate_id=&merchant_reference=or wait formandate_debit.succeeded/mandate_debit.failed.
Cancel (optional): POST /v1/mandates/{mandate_id}/cancel (Idempotency-Key required; body: mobile, reason). The cancel HTTP body may show status: processing (request accepted); the mandate is already cancelled. Cancelling one mandate also cancels other local rows that share the same upstream MandateId. After cancel, start again at step 1 — create returns processing until the customer approves again.
8.8 Metadata and balance
GET /v1/metadata/banksGET /v1/metadata/mobile_providersGET /v1/balance— optional querycurrency(ISO 4217). Omitcurrencyto 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 covering the requested amount plus any configured payout fee. Merchants fund the payout wallet via Move funds on /portal/settlement (instant fee may apply on held collection funds; scheduled interval/T+1 transfer after release does not) or via approved auto-settlement after the hold.
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 includesobject(subscription_plan),plan_id,name,description,currency,amount,frequency,duration_type,duration_months, andstatus.POST /v1/subscriptions(Idempotency-Keyrequired) — enrolls a customer on a plan. Required:plan_id,merchant_reference,customer_name,customer_email,mobile,provider; optionalnotes. Creates a subscriber and starts mandate enrollment; returns the subscription (object: subscription,subscription_id,status,merchant_reference,plan_id,mandate_id, ...) with201. When the mobile number already has an approved mandate, status isactiveimmediately and Omni charges the plan amount once in the same request (no second MoMo approval). Confirm that first debit viaGET /v1/mandate_debits/status({merchant_reference}-INIT) ormandate_debit.succeeded. Later cycles are your responsibility: schedulePOST /v1/mandates/{mandate_id}/debitsin your app using the planfrequency. Omni does not auto-debit API subscriptions after-INIT.- Hosted subscribe (
GET /subscribe/{merchant-slug}, including?plan=SP_…) is different: Omni charges the first plan amount on approval and subsequent amounts on the plan frequency (subscriptions:charge-hosted-recurring). Use hosted when you want Omni to collect; usePOST /v1/subscriptionswhen you will collect on your own schedule. GET /v1/subscriptions?merchant_reference=— returns the subscription for yourmerchant_reference(404 if none).
An invalid or inactive plan_id returns 422 with a validation error.