openapi: 3.0.3 info: title: MojoPay Omni API version: "1.0" description: | HTTPS JSON API for collections, hosted checkout, payouts, bank and mobile verification, mandates, reference metadata, and wallet balance. Errors return an `ApiError` object: `error.code`, `error.message`, `error.request_id`, and optional `error.doc_url` for more detail. `request_id` values look like `REQ_` + 12 uppercase alphanumeric characters. On POST requests that create or move money (or create checkout sessions or mandates), send an `Idempotency-Key` header. Reuse the same key only when safely retrying the **same** JSON body. **Integration quick start:** See `/doc` for credentials setup, hosted checkout curl examples, webhook signature verification (PHP/Node), and an integration checklist. Sample JSON bodies: `/doc/samples.json`. Portal Mojo Ai can help debug Omni API payloads against the same guide. **Testing mode:** New merchants start in Testing mode on live rails. Money-in (`POST /v1/collections` and fixed-amount `POST /v1/checkout/sessions`, plus amounts entered on `/pay/{id}`) is capped to a per-country max until Go Live. `amount_mode: open` is not allowed while Testing. Details: `/doc#testing-mode`. **Required headers on authenticated requests:** `Authorization: Bearer {token}`, `Content-Type: application/json`, `Accept: application/json`. **Sub-accounts (when enabled for your merchant):** also send `X-Mojo-Account-Ref` with the target account ref on every request except `GET /v1/metadata/*` and `GET /v1/balance`. Use `GET /v1/account` to verify the resolved account. **Money-moving POSTs also require:** `Idempotency-Key` (UUID recommended; reuse only when retrying the same body). **Webhooks** (configured per API client in the merchant portal): at-least-once delivery with `MojoPay-Signature` to the client that created the resource. Event types (v1.0): `collection.processing|succeeded|failed`, `checkout_session.processing|succeeded|failed`, `payout.processing|succeeded|failed`, `mandate.processing|succeeded|cancelled`, `mandate_debit.processing|succeeded|failed`, and subscription lifecycle events (`subscription.created`, `subscription.active`, `subscription.cancelled`, `subscription.failed`, `subscription.charge_failed`) when linked to plans. Event `id` values look like `EVT_` + 24 uppercase alphanumeric characters. Verify HMAC on `{timestamp}.{raw_body}`; reject timestamps outside ±300 seconds. Portal-created pay links do not emit outbound webhooks. **Security:** Bearer tokens are merchant-scoped; resource lookups never expose another tenant’s data (404). Optional API IP allowlist is configured in the portal. Portal **Team member** users can view activity but cannot change API clients, webhooks, or financial settings. **Wallet balance (`GET /v1/balance`):** `collection_wallet` is the **available** (T+1-released) collection balance; `collection_wallet_actual` is total succeeded credits; `collection_wallet_held` is still within the T+1 hold. Payouts draw from `payout_wallet`. Merchants move funds via the portal settlement page or approved auto-settlement. Rate limits apply to keep the API reliable. When exceeded, responses use HTTP 429 and error code `rate_limited`. OAuth token requests are limited per client; authenticated calls are limited per merchant account. servers: - url: https://omni.mojo-pay.com description: Current application (APP_URL) - url: https://api.mojopay.com description: Production tags: - name: OAuth - name: Collections - name: Checkout - name: Transactions - name: Payouts - name: Verifications - name: Mandates - name: Subscriptions - name: Metadata - name: Balance - name: Accounts paths: /v1/oauth/token: post: tags: [OAuth] summary: Issue access token (client_credentials) operationId: oauthToken requestBody: required: true content: application/json: schema: $ref: "#/components/schemas/OAuthTokenRequest" responses: "200": description: Bearer token issued content: application/json: schema: $ref: "#/components/schemas/OAuthTokenResponse" "400": description: invalid_parameter content: application/json: schema: $ref: "#/components/schemas/ApiError" "401": description: invalid_client content: application/json: schema: $ref: "#/components/schemas/ApiError" "403": description: Forbidden (e.g. invalid credentials or product not enabled for your account) content: application/json: schema: $ref: "#/components/schemas/ApiError" "429": description: rate_limited content: application/json: schema: $ref: "#/components/schemas/ApiError" /v1/collections: post: tags: [Collections] summary: Create collection (wallet collection) operationId: createCollection security: [{ bearerAuth: [] }] parameters: - $ref: "#/components/parameters/IdempotencyKey" requestBody: required: true content: application/json: schema: $ref: "#/components/schemas/CreateCollectionRequest" responses: "201": description: Created content: application/json: schema: $ref: "#/components/schemas/Collection" "400": content: application/json: schema: $ref: "#/components/schemas/ApiError" "401": content: application/json: schema: $ref: "#/components/schemas/ApiError" "403": content: application/json: schema: $ref: "#/components/schemas/ApiError" "409": description: idempotency_key_mismatch content: application/json: schema: $ref: "#/components/schemas/ApiError" "429": content: application/json: schema: $ref: "#/components/schemas/ApiError" get: tags: [Collections] summary: Get collection by merchant_reference operationId: getCollectionByMerchantReference security: [{ bearerAuth: [] }] parameters: - name: merchant_reference in: query required: true schema: type: string responses: "200": content: application/json: schema: $ref: "#/components/schemas/Collection" "400": content: application/json: schema: $ref: "#/components/schemas/ApiError" "401": content: application/json: schema: $ref: "#/components/schemas/ApiError" "404": content: application/json: schema: $ref: "#/components/schemas/ApiError" /v1/collections/{id}: get: tags: [Collections] summary: Get collection by id operationId: getCollectionById security: [{ bearerAuth: [] }] parameters: - name: id in: path required: true schema: type: string responses: "200": content: application/json: schema: $ref: "#/components/schemas/Collection" "401": content: application/json: schema: $ref: "#/components/schemas/ApiError" "404": content: application/json: schema: $ref: "#/components/schemas/ApiError" /v1/checkout/sessions: post: tags: [Checkout] summary: Create hosted checkout session operationId: createCheckoutSession security: [{ bearerAuth: [] }] parameters: - $ref: "#/components/parameters/IdempotencyKey" requestBody: required: true content: application/json: schema: $ref: "#/components/schemas/CreateCheckoutSessionRequest" responses: "201": description: Created (slim create payload) content: application/json: schema: $ref: "#/components/schemas/CheckoutSessionCreate" "400": content: application/json: schema: $ref: "#/components/schemas/ApiError" "409": content: application/json: schema: $ref: "#/components/schemas/ApiError" /v1/checkout/sessions/{id}: get: tags: [Checkout] summary: Get checkout session operationId: getCheckoutSession security: [{ bearerAuth: [] }] parameters: - name: id in: path required: true schema: type: string responses: "200": content: application/json: schema: $ref: "#/components/schemas/CheckoutSession" "404": content: application/json: schema: $ref: "#/components/schemas/ApiError" /v1/transactions/verify/{merchant_reference}: get: tags: [Transactions] summary: Unified verify by merchant_reference operationId: verifyTransaction security: [{ bearerAuth: [] }] parameters: - name: merchant_reference in: path required: true schema: type: string responses: "200": description: Collection, payout, or checkout snapshot content: application/json: schema: type: object additionalProperties: true "404": content: application/json: schema: $ref: "#/components/schemas/ApiError" /v1/payouts: post: tags: [Payouts] summary: Create payout operationId: createPayout security: [{ bearerAuth: [] }] parameters: - $ref: "#/components/parameters/IdempotencyKey" requestBody: required: true content: application/json: schema: $ref: "#/components/schemas/CreatePayoutRequest" responses: "201": content: application/json: schema: $ref: "#/components/schemas/Payout" "400": content: application/json: schema: $ref: "#/components/schemas/ApiError" "409": content: application/json: schema: $ref: "#/components/schemas/ApiError" /v1/payouts/{merchant_reference}: get: tags: [Payouts] summary: Get payout by merchant_reference operationId: getPayout security: [{ bearerAuth: [] }] parameters: - name: merchant_reference in: path required: true schema: type: string responses: "200": content: application/json: schema: $ref: "#/components/schemas/Payout" "404": content: application/json: schema: $ref: "#/components/schemas/ApiError" /v1/verifications/msisdn: post: tags: [Verifications] summary: MSISDN / mobile name check operationId: verifyMsisdn security: [{ bearerAuth: [] }] requestBody: required: true content: application/json: schema: type: object required: [mobile] properties: mobile: type: string pattern: "^\\d{10,15}$" provider: type: string maxLength: 64 responses: "200": description: Verification outcome content: application/json: schema: $ref: "#/components/schemas/VerificationResult" "502": description: gateway_error (upstream KYC provider failure) content: application/json: schema: $ref: "#/components/schemas/ApiError" /v1/verifications/bank_accounts: post: tags: [Verifications] summary: Bank account name check operationId: verifyBankAccount security: [{ bearerAuth: [] }] requestBody: required: true content: application/json: schema: type: object required: [bank_code, account_number] properties: bank_code: type: string maxLength: 32 description: Bank code from metadata banks. account_number: type: string maxLength: 64 responses: "200": content: application/json: schema: $ref: "#/components/schemas/VerificationResult" "502": description: gateway_error content: application/json: schema: $ref: "#/components/schemas/ApiError" /v1/mandates: post: tags: [Mandates] summary: Create mandate operationId: createMandate security: [{ bearerAuth: [] }] parameters: - $ref: "#/components/parameters/IdempotencyKey" requestBody: required: true content: application/json: schema: $ref: "#/components/schemas/CreateMandateRequest" responses: "201": content: application/json: schema: type: object additionalProperties: true get: tags: [Mandates] summary: List / get mandate by merchant_reference operationId: listMandates security: [{ bearerAuth: [] }] parameters: - name: merchant_reference in: query required: true schema: type: string responses: "200": content: application/json: schema: type: object additionalProperties: true /v1/mandates/{mandate_id}/cancel: post: tags: [Mandates] summary: Cancel mandate operationId: cancelMandate security: [{ bearerAuth: [] }] parameters: - $ref: "#/components/parameters/IdempotencyKey" - name: mandate_id in: path required: true schema: type: string requestBody: required: true content: application/json: schema: type: object required: [mobile, reason] properties: mobile: type: string reason: type: string responses: "200": content: application/json: schema: type: object additionalProperties: true /v1/mandates/{mandate_id}/debits: post: tags: [Mandates] summary: Create mandate debit operationId: createMandateDebit security: [{ bearerAuth: [] }] parameters: - $ref: "#/components/parameters/IdempotencyKey" - name: mandate_id in: path required: true schema: type: string requestBody: required: true content: application/json: schema: type: object required: [amount, currency, merchant_reference, provider] properties: amount: type: string currency: type: string minLength: 3 maxLength: 3 merchant_reference: type: string maxLength: 255 provider: type: string maxLength: 64 responses: "201": content: application/json: schema: type: object additionalProperties: true /v1/mandate_debits/status: get: tags: [Mandates] summary: Mandate debit status operationId: mandateDebitStatus security: [{ bearerAuth: [] }] parameters: - name: mandate_id in: query required: true schema: type: string - name: merchant_reference in: query required: true schema: type: string responses: "200": content: application/json: schema: type: object additionalProperties: true /v1/subscription_plans: get: tags: [Subscriptions] summary: List active subscription plans description: Requires the subscriptions product to be enabled for your merchant. operationId: listSubscriptionPlans security: [{ bearerAuth: [] }] parameters: - $ref: "#/components/parameters/MerchantAccountRef" responses: "200": content: application/json: schema: type: object required: [object, data] properties: object: type: string example: list data: type: array items: $ref: "#/components/schemas/SubscriptionPlan" "403": description: forbidden (subscriptions not enabled) content: application/json: schema: $ref: "#/components/schemas/ApiError" /v1/subscriptions: post: tags: [Subscriptions] summary: Enroll a subscription description: Creates a subscriber and starts mandate enrollment for a subscription plan. operationId: createSubscription security: [{ bearerAuth: [] }] parameters: - $ref: "#/components/parameters/IdempotencyKey" - $ref: "#/components/parameters/MerchantAccountRef" requestBody: required: true content: application/json: schema: $ref: "#/components/schemas/CreateSubscriptionRequest" responses: "201": content: application/json: schema: $ref: "#/components/schemas/Subscription" "403": description: forbidden (subscriptions not enabled) content: application/json: schema: $ref: "#/components/schemas/ApiError" "422": description: validation_error (e.g. invalid or inactive plan_id) content: application/json: schema: $ref: "#/components/schemas/ApiError" get: tags: [Subscriptions] summary: Get subscription by merchant_reference operationId: getSubscription security: [{ bearerAuth: [] }] parameters: - name: merchant_reference in: query required: true schema: type: string - $ref: "#/components/parameters/MerchantAccountRef" responses: "200": content: application/json: schema: $ref: "#/components/schemas/Subscription" "404": description: not_found content: application/json: schema: $ref: "#/components/schemas/ApiError" /v1/metadata/banks: get: tags: [Metadata] summary: Bank reference list operationId: metadataBanks security: [{ bearerAuth: [] }] responses: "200": content: application/json: schema: type: object additionalProperties: true /v1/metadata/mobile_providers: get: tags: [Metadata] summary: Mobile money providers operationId: metadataMobileProviders security: [{ bearerAuth: [] }] responses: "200": content: application/json: schema: type: object additionalProperties: true /v1/balance: get: tags: [Balance] summary: Wallet balance operationId: getBalance security: [{ bearerAuth: [] }] parameters: - name: currency in: query required: false schema: type: string minLength: 3 maxLength: 3 responses: "200": content: application/json: schema: $ref: "#/components/schemas/BalanceSnapshot" /v1/account: get: tags: [Accounts] summary: Resolved merchant account context description: Returns the authenticated merchant and the sub-account resolved from `X-Mojo-Account-Ref` when sub-accounts are enabled. operationId: getAccountContext security: [{ bearerAuth: [] }] parameters: - $ref: "#/components/parameters/MerchantAccountRef" responses: "200": content: application/json: schema: $ref: "#/components/schemas/AccountContext" "400": description: invalid_parameter (missing account header when required) content: application/json: schema: $ref: "#/components/schemas/ApiError" "404": description: not_found (unknown account ref) content: application/json: schema: $ref: "#/components/schemas/ApiError" components: securitySchemes: bearerAuth: type: http scheme: bearer bearerFormat: opaque description: Short-lived access token from POST /v1/oauth/token parameters: IdempotencyKey: name: Idempotency-Key in: header required: true schema: type: string maxLength: 255 example: 7c9e6679-7425-40de-944b-e07fc1f90ae7 description: Required for money-moving and session/mandate creates; replays must use the same body. MerchantAccountRef: name: X-Mojo-Account-Ref in: header required: false schema: type: string pattern: '^[a-z0-9][a-z0-9_-]{2,63}$' example: accra-branch description: Required when sub-accounts are enabled for the merchant (except metadata and balance routes). Targets the sub-account for this request. schemas: AccountContext: type: object required: [object, merchant] properties: object: type: string example: account_context merchant: type: object properties: id: type: integer name: type: string sub_accounts_enabled: type: boolean account: type: object nullable: true properties: id: type: string example: MAC_ABCDEF123456 object: type: string example: merchant_account account_ref: type: string example: accra-branch name: type: string status: type: string example: active ApiError: type: object required: [error] properties: error: type: object required: [code, message, request_id] properties: code: type: string description: Stable machine-readable error code. message: type: string doc_url: type: string format: uri description: Optional URL with more detail for this error. request_id: type: string description: Correlation id, e.g. REQ_8F3A2C1B9D0E example: REQ_8F3A2C1B9D0E OAuthTokenRequest: type: object required: [client_id, client_secret] properties: grant_type: type: string enum: [client_credentials] client_id: type: string client_secret: type: string OAuthTokenResponse: type: object required: [access_token, token_type, expires_in] properties: access_token: type: string description: Opaque 64-character bearer token (not a JWT). example: a1b2c3d4e5f6g7h8i9j0k1l2m3n4o5p6q7r8s9t0u1v2w3x4y5z6a7b8c9d0e1f2 token_type: type: string example: Bearer expires_in: type: integer example: 1200 example: access_token: a1b2c3d4e5f6g7h8i9j0k1l2m3n4o5p6q7r8s9t0u1v2w3x4y5z6a7b8c9d0e1f2 token_type: Bearer expires_in: 1200 CreateCollectionRequest: type: object required: [amount, currency, merchant_reference, mobile, provider] properties: amount: type: string description: > Merchant principal to receive. While the merchant is in Testing mode, must be at or below the country testing max amount (see `/doc#testing-mode`). currency: type: string minLength: 3 maxLength: 3 merchant_reference: type: string mobile: type: string provider: type: string description: type: string Collection: type: object description: > Includes id, status, amounts, and references; fields may vary by payment rail and lifecycle. `amount` is credited to the merchant on success. When fees apply, `fee_amount` is charged to the payer on top, and `amount_payable` is the gross collected (`amount` + `fee_amount`). additionalProperties: true properties: fee_amount: type: string description: Platform fee added on top of `amount` (same currency). amount_payable: type: string description: Gross amount the payer pays (`amount` + `fee_amount`). CreateCheckoutSessionRequest: type: object required: [currency, merchant_reference, success_url, cancel_url] properties: amount_mode: type: string enum: [fixed, open] default: fixed description: > `fixed` (default) requires `amount` at create time. `open` lets the customer enter the amount on the hosted pay page. `open` is not available while the merchant is in Testing mode (use `fixed` at or below the country testing max, or Go Live). See `/doc#testing-mode`. amount: type: string description: > Required when `amount_mode` is `fixed` (or omitted). Omit for `open`. In Testing mode, fixed amounts must be at or below the country testing max. currency: type: string merchant_reference: type: string payment_rail: type: string nullable: true enum: [mobile_money, card] description: > `mobile_money` locks the session to mobile money; `mobile` is then required at create unless pre-filled later on the hosted page. When the key is omitted, the session stores `null` and the customer chooses mobile money on `/pay/{id}` (portal pay links send explicit JSON `null`). Card is not accepted on hosted checkout in v1.0. mobile: type: string description: > Required when `payment_rail` is `mobile_money` at create time. Omit when the customer will enter wallet details on the hosted page. provider: type: string description: Mobile money provider code when required for the chosen rail (metadata mobile_providers). success_url: type: string format: uri cancel_url: type: string format: uri description: type: string CheckoutSessionCreate: type: object required: [id, object, status, url, expires_at] description: > Fixed-amount creates include `currency`, `amount_mode`, `amount`, `fee_amount`, and `amount_payable` when fees are computed at create time. Open-amount creates omit amount fields until the customer submits an amount on the hosted pay page. `expires_at` is typically one hour after create. properties: id: type: string example: CS_4NQP81ABCDEF object: type: string enum: [checkout_session] status: type: string enum: [processing] currency: type: string example: GHS amount_mode: type: string enum: [fixed, open] amount: type: string description: Merchant principal; present for fixed amount at create time. fee_amount: type: string description: Platform fee added on top of `amount` when fees apply. amount_payable: type: string description: Gross amount the payer pays (`amount` + `fee_amount`). url: type: string format: uri expires_at: type: string format: date-time example: 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" CheckoutSession: type: object additionalProperties: true properties: id: type: string object: type: string enum: [checkout_session] status: type: string currency: type: string amount_mode: type: string enum: [fixed, open] amount: type: string nullable: true fee_amount: type: string amount_payable: type: string nullable: true merchant_reference: type: string mobile: type: string nullable: true payment_rail: type: string nullable: true provider: type: string nullable: true card_scheme: type: string nullable: true VerificationResult: type: object required: [status] properties: status: type: string enum: [verified, invalid] profile: type: object nullable: true properties: first_name: type: string last_name: type: string BalanceSnapshot: type: object required: [currency, available, collection_wallet, collection_wallet_actual, collection_wallet_held, payout_wallet, pending] properties: currency: type: string available: type: string description: Total across available collection_wallet and payout_wallet. collection_wallet: type: string description: Available collection balance (released after T+1; movable without instant settlement fee). collection_wallet_actual: type: string description: Total succeeded collection credits on record (actual collection wallet). collection_wallet_held: type: string description: Collection funds still held until the next T+1 release window. payout_wallet: type: string pending: type: string provider_float: type: string description: Present when upstream float query succeeds. CreatePayoutRequest: type: object required: [type, currency, amount, merchant_reference] properties: type: type: string enum: [mobile_money, bank] currency: type: string amount: type: string merchant_reference: type: string mobile: type: string provider: type: string account_number: type: string bank_code: type: string narration: type: string transaction_type: type: string transaction_date: type: string expires_at: type: string Payout: type: object additionalProperties: true CreateMandateRequest: type: object required: [mobile, provider, currency, amount, frequency, starts_at, ends_at, merchant_reference] properties: mobile: type: string provider: type: string currency: type: string amount: type: string frequency: type: string enum: [DAILY, WEEKLY, MONTHLY] starts_at: type: string format: date-time ends_at: type: string format: date-time merchant_reference: type: string description: type: string plan_id: type: string description: When set, links the mandate to a subscription plan and creates a subscriber. customer_name: type: string description: Required when plan_id is set. customer_email: type: string format: email description: Required when plan_id is set. notes: type: string SubscriptionPlan: type: object required: [object, plan_id, name, currency, amount, frequency, status] properties: object: type: string example: subscription_plan plan_id: type: string example: PLN_ABCDEF123456 name: type: string description: type: string currency: type: string minLength: 3 maxLength: 3 amount: type: string frequency: type: string enum: [DAILY, WEEKLY, MONTHLY] duration_type: type: string duration_months: type: integer nullable: true status: type: string example: active CreateSubscriptionRequest: type: object required: [plan_id, merchant_reference, customer_name, customer_email, mobile, provider] properties: plan_id: type: string example: PLN_ABCDEF123456 merchant_reference: type: string customer_name: type: string customer_email: type: string format: email mobile: type: string pattern: "^\\d{10,15}$" provider: type: string notes: type: string Subscription: type: object additionalProperties: true properties: object: type: string example: subscription subscription_id: type: string example: SUB_ABCDEF123456 status: type: string merchant_reference: type: string plan_id: type: string mandate_id: type: string