Mobile Money

Accept mobile money payments across Africa with a single integration

Overview

Mobile money is the dominant consumer payment method across most of Africa. Customers hold a wallet balance with a mobile network operator (MTN, M-Pesa, Airtel, Orange, and others) and authorise payments from their handset using a PIN. CrissCross gives you a single integration that covers every supported operator in every supported country.

This page is the umbrella reference for the payment method. For operator-specific copy (logos, marketing notes, and operator-only quirks) see the per-operator pages linked below; for the end-to-end Direct API request/response sequence see the Direct API: Mobile Money guide.


How a mobile money payment works

Unlike a card payment, a mobile money payment is asynchronous: the customer approves the debit on their handset, not in your checkout. The lifecycle looks like this:

  1. The merchant initiates a payment with the customer’s provider and phone number.
  2. The operator sends a push or USSD prompt to the customer’s handset.
  3. The customer enters their PIN to approve (or rejects, or lets it time out).
  4. The operator confirms the outcome back to CrissCross, which notifies you via webhook and exposes the final state on the transaction.

While the customer is approving, the transaction sits in a pending state with authState.type: "pending_approval". There is no POST /payment/authorize step for mobile money — approval happens entirely on the handset, and you either poll or wait for the webhook.

Typical approval takes 10 – 30 seconds. The provider will eventually time the prompt out (usually around 60 – 120 seconds) if the customer takes no action.


Supported countries and operators

CountryOperators
Benin (BEN)Moov, Celtiis, Wave
Burkina Faso (BFA)Moov, Orange, Wave
Cameroon (CMR)MTN, Orange
Côte d’Ivoire (CIV)MTN, Orange, Moov, Wave
Gabon (GAB)Airtel, Moov
Ghana (GHA)AirtelTigo
Kenya (KEN)M-Pesa
Nigeria (NGA)MTN
Rwanda (RWA)MTN, Airtel
Senegal (SEN)Orange, Free, Wave
South Africa (ZAF)MTN
Tanzania (TZA)M-Pesa (Vodacom), Airtel, Tigo, Halotel
Togo (TGO)Moov, Wave
Uganda (UGA)MTN, Airtel
Zambia (ZMB)Airtel, MTN, Zamtel

The set of available operators is determined by the payerDetails.location you submit when creating a checkout session. The exhaustive list returned for a given country is also available from GET /v1/payment/available-methods once a session exists — that endpoint is the source of truth at runtime and will reflect any operator that has been disabled for your account.

Operator slugs (the value you pass in paymentDetails.provider):

airtel, airteltigo, celtiis, free, halotel, moov, mpesa, mtn, orange, tigo, vodacom, wave, zamtel.

For operator-specific marketing copy and supported workflow notes, see:


What you collect from the customer

For any mobile money payment, you need three things:

FieldWhere it goesNotes
OperatorpaymentDetails.providerOne of the operator slugs above. Present a country-filtered list — don’t ask the customer to scroll through operators that don’t exist in their country.
Mobile numberpaymentDetails.payerMobileNumberLocal or E.164 format. CrissCross normalises to E.164 using the payer’s location, so 0701234567 and +254701234567 both work for a Kenyan payer.
Full namepaymentDetails.payerFullNameSome operators require this; we always pass it through. Falls back to payerDetails.fullName from the session if omitted.

The country code itself comes from payerDetails.location on the session — an ISO 3166 alpha-3 code like KEN, NGA, ZAF. You don’t pass the country code on the payment request again.


Integration options

You can accept mobile money through any of the standard CrissCross integration paths:

  • Hosted Checkout — CrissCross renders the operator selector, mobile number field, and pending-approval screen. Lowest engineering cost.
  • CrissCross SDK — drop-in components that handle the input fields and approval polling for you.
  • Direct API: Mobile Money — full control. You collect the inputs, call POST /v1/payment, and either poll or rely on webhooks for completion.

The Direct API guide is the most concrete reference for what fields are sent and what responses look like.


Sandbox testing

Sandbox is on by default for every test merchant. Hit https://api.crisscross.money with sandbox credentials and any mobile money request will be routed to the sandbox connector — no real handset, no real operator, no money moves.

The sandbox decides the outcome based on the last digits of the mobile number you submit. The match is on the normalised E.164 form, so the country code doesn’t matter: 0970000005, +27970000005, and +254970000005 all trigger the same scenario when sent with a payerDetails.location of ZAF, KEN, etc. Any number not listed below succeeds normally.

Authorisation failures

These numbers cause the transaction to move from pending to a final failed state and emit a transaction.failed webhook. The authState.code lets you distinguish reasons.

Mobile numberFailure codeMeaning
0970000005PAYIN_INSUFFICIENT_FUNDSThe payer’s wallet has insufficient funds.
0970000006CUSTOMER_REJECTEDThe customer rejected the prompt on their handset.
0970000007TIMEOUTThe customer didn’t respond before the operator timed out.
0970000008ACCOUNT_NOT_ACTIVEThe mobile money account is not active.
0970000009PAYIN_PAYER_INVALID_ACCOUNTNo mobile money account exists for this number.
0970000010PAYIN_PAYER_LIMIT_EXCEEDEDThe payer’s daily / monthly limit would be exceeded.
0970000014TRANSACTION_NOT_FOUNDThe provider can’t locate the transaction.

System errors

These return an error authState rather than auth_failure. Retry semantics differ — see the Direct API guide for which can be retried.

Mobile numberError codeMeaning
0970000012INTERNAL_ERRORA CrissCross-side error. Not retryable on the same transaction.
0970000013DOWNSTREAM_ERRORThe provider returned an error. Retryable.

Transient errors (test your retry logic)

These numbers misbehave for the first ~2 seconds after the transaction is created, then succeed. They’re useful for verifying that your polling and retry code is resilient.

Mobile numberBehaviour
0970000015POST /v1/payment errors with DOWNSTREAM_ERROR for 2 seconds, then succeeds on the next attempt.
0970000016The transaction is created normally, but GET /v1/payment/{transactionId} errors with DOWNSTREAM_ERROR for 2 seconds before returning a normal authorised state.
0970000017The first execution returns auth_failure; if your account has a fallback processor configured for the same method, the fallback attempt succeeds.

Successful payment

Any number not listed above completes the happy path: the response on POST /v1/payment is pending with authState.type: "pending_approval", and a subsequent GET /v1/payment/{transactionId} returns authorized with a processorReference like PROC-<transactionId>.


Failure and error reference

The full set of authState.code values you can see on a mobile money transaction:

CodeGroupDescription
PAYIN_INSUFFICIENT_FUNDSAuth failureWallet balance too low.
CUSTOMER_REJECTEDAuth failureCustomer declined on handset.
TIMEOUTAuth failureCustomer didn’t respond in time.
ACCOUNT_NOT_ACTIVEAuth failureWallet exists but is inactive / suspended.
PAYIN_PAYER_INVALID_ACCOUNTAuth failureNo wallet for this number on the chosen operator.
PAYIN_PAYER_LIMIT_EXCEEDEDAuth failureDaily or monthly transaction limit would be exceeded.
TRANSACTION_NOT_FOUNDAuth failureProvider can’t locate the transaction (rare).
DECLINEDAuth failureCatch-all decline from the operator.
INTERNAL_ERRORErrorCrissCross-side error.
DOWNSTREAM_ERRORErrorOperator returned an error. May be retryable.

Auth failures are terminal for the transaction — the customer would need to retry with a fresh payment. Errors marked as retryable in the response (authState.canRetry) can be re-executed against the same checkout session.


Next steps