Skip to content

Payments API

Endpoint details

8.1 Collection (wallet collection)

POST /v1/collections (Idempotency-Key required)

  • Default rail: mobile money when payment_rail is omitted.
  • Required fields (always): amount, currency, merchant_reference.
  • Mobile money (payment_rail=mobile_money or omitted): also require mobile, provider. Optional: description.
  • Bank transfer / dynamic VA (payment_rail=bank_transfer): requires bank transfer enabled for your merchant. Optional: account_name, description. Do not send mobile / provider. Response includes a nested virtual_account (temporary account number, bank name, expires_at). Show those details to the payer; wait for collection.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: amount must 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, or 404 if you have not issued one yet.

Customer dedicated accounts (one reusable account per payer):

  • POST /v1/customers — body: name, optional email / mobile. merchant_reference is assigned by MojoPay (CUST-…); do not send it. Returns object: 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 receive collection.succeeded. Collection payloads include customer_id when attributed to a dedicated customer VA.
  • GET /v1/customers/{id}/collections lists those collections (object: list, customer_id, paginated data).
  • POST .../virtual-account/deactivate sets status INACTIVE so 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:

  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, or card when enabled for your merchant).
  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_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.
  • 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 the rail on the hosted page (portal pay links send explicit JSON null). card is accepted only when MojoPay has enabled card checkout for your merchant; otherwise it returns 400 invalid_parameter.
  • 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 (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_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_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_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

Direct debit flow: POST /v1/mandates, then GET /v1/mandates until succeeded, then POST /v1/mandates/{mandate_id}/debits.

  1. Create — POST /v1/mandates (Idempotency-Key required). Body: mobile, provider, currency, amount, frequency, starts_at, merchant_reference, optional description. Ghana direct debit is MTN only. starts_at sets 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 from starts_at 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.
    • Create returns processing (HTTP 201). Poll GET or wait for mandate.succeeded before 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 first merchant_reference.
    • POST /v1/mandates without plan_id does not auto-debit. Subscription enrollments (POST /v1/subscriptions, hosted subscribe, and POST /v1/mandates with plan_id) charge the plan amount in the same request when a mandate is already approved.
  2. Get — GET /v1/mandates?merchant_reference= (your mandate reference, not the MD_ id). While processing, 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 after status is succeeded.
  3. Debit — POST /v1/mandates/{mandate_id}/debits (Idempotency-Key required; body: amount, currency, merchant_reference, provider). Debit while pending returns HTTP 409 (invalid_parameter). Collection fees apply using the merchant's configured fee mode (on_top or absorb) — same semantics as POST /v1/collections. Responses include fee_amount, fee_mode, amount_payable, and merchant_settlement_amount; the customer is debited amount_payable upstream.
    • 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 for POST /v1/mandates or POST /v1/subscriptions. Hosted subscribe (/subscribe/{merchant-slug}) is the exception — Omni charges those plans on the plan frequency.
  4. Debit status (optional) — GET /v1/mandate_debits/status?mandate_id=&merchant_reference= or wait for mandate_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/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 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 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. When the mobile number already has an approved mandate, status is active immediately and Omni charges the plan amount once in the same request (no second MoMo approval). Confirm that first debit via GET /v1/mandate_debits/status ({merchant_reference}-INIT) or mandate_debit.succeeded. Later cycles are your responsibility: schedule POST /v1/mandates/{mandate_id}/debits in your app using the plan frequency. 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; use POST /v1/subscriptions when you will collect on your own schedule.
  • 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.