Skip to Content
Pesa VoucherDeveloper Documentation

PesaVoucher Mobile Money Integration Guide

Pan-African mobile money collections and payouts


PesaVoucher Mobile Money Integration Guide

Pan-African mobile money collections and payouts — one API, one callback contract, twenty countries.

This document explains how to initiate mobile money deposits and payouts through PesaVoucher, and what payload PesaVoucher sends to your callback_url once a transaction resolves. It follows the same conventions as PesaVoucher's other automated payment channels (STK Push, Paybill) — if you've integrated those, this will look familiar.

1. Overview

Coverage20 countries, 16 currencies, every major mobile money wallet (MTN, Airtel, Orange, Vodacom, M-Pesa, Moov, Wave, Tigo, Halotel, TNM, Zamtel, and more)
FlowsDeposits (collect from a customer), payouts (disburse to a customer), refunds
ModelAsynchronous — every request returns an acceptance, the outcome arrives later by callback or status check
AuthX-Api-Key + X-Api-Secret on every endpoint — the same credentials used for your other PesaVoucher integrations

Unlike a bank card charge, a mobile money transaction is never resolved synchronously — it always requires the customer to authorise on their handset (or, for a small number of providers, via a redirect). Your integration should be built around that: initiate, then wait for the callback or poll status. Never assume a request that returned "accepted" has been paid.

2. Authentication

Every endpoint below requires:

X-Api-Key: <your key>
X-Api-Secret: <your secret>

There is no lower-friction alternative (no merchant-id-only path) — payouts move money outward, and a guessable identifier alone is not enough to authorise a disbursement.

3. Collecting a Deposit

POST /api/v1/momo/deposit
X-Api-Key: <key>
X-Api-Secret: <secret>
Content-Type: application/json
{
"amount": "150",
"currency": "ZMW",
"phone_number": "+260 763 456789",
"country": "ZMB",
"account_reference": "ORDER-123",
"customer_message": "PesaVoucher topup"
}
{
"success": true,
"transaction_id": "afb57b93-7849-49aa-babb-4c3ccbfe3d79",
"status": "ACCEPTED",
"provider": "MTN_MOMO_ZMB",
"provider_name": "MTN",
"provider_predicted": true,
"country": "ZMB",
"currency": "ZMW",
"amount": "150.00",
"message": "Deposit accepted. The customer must authorise it on their handset."
}

ACCEPTED means the PIN prompt is on its way — not that you've been paid. The outcome arrives by callback (Section 7) or by polling GET /api/v1/momo/status (Section 6).

Send the phone number in full international format

A national-format number (0763456789) is valid in almost every market this API covers, so it cannot be safely expanded without knowing the country — guessing would risk addressing the request to the wrong country's network. Always send phone_number in full international format (+260...) together with the ISO country code.

Provider selection

Omit provider and PesaVoucher will detect it automatically from the phone number. Supply it explicitly if your checkout UI already asked the customer which wallet they're paying from — this skips a lookup and is marginally faster.

GET /api/v1/momo/providers returns the live list of providers, per-provider transaction limits, and two flags described below, so your checkout UI can branch without hardcoding exceptions.

Two provider types need special handling

TypeWhat changes
requires_redirect: trueNo PIN prompt. The response carries a redirect_url — you must send the customer there or the deposit expires unpaid.
requires_preauth: trueThe customer must complete a pre-authorisation step in their wallet app before you call deposit, and you pass the resulting preauth_code. Without it, the deposit is always rejected.

4. Disbursing a Payout

Same request shape as a deposit, with phone_number naming the recipient.

POST /api/v1/momo/payout
X-Api-Key: <key>
X-Api-Secret: <secret>
Content-Type: application/json
{
"amount": "150",
"currency": "ZMW",
"phone_number": "+260 763 456789",
"country": "ZMB",
"account_reference": "PAYOUT-456"
}

Two things are different about payouts:

They draw down a float balance. A payout is funded from a balance you've already topped up with PesaVoucher for that country/currency. If your float is insufficient, the request fails at that check — before anything is sent to a network.

ENQUEUED is a real, non-final outcome. Some networks can't process disbursements in the moment; rather than rejecting the payout, it's accepted and queued for later processing:

{
"status": "ENQUEUED",
"cancellable": true,
"message": "This network is not processing disbursements right now, the payout has been queued..."
}

The funds are committed but have not moved yet. Treat this as pending, not success and not failure — crediting a recipient's ledger at this point risks paying them twice if the payout later fails elsewhere in your reconciliation, and marking it failed risks the reverse. You can cancel a still-ENQUEUED payout with POST /api/v1/momo/cancel_payout; once it starts processing, it can no longer be cancelled.

5. Refunds

POST /api/v1/momo/refund
X-Api-Key: <key>
X-Api-Secret: <secret>
Content-Type: application/json
{ "transaction_id": "<the deposit's transaction_id>", "amount": "50" }

Omit amount for a full refund. A refund reverses a specific deposit and needs no recipient details, since the destination is the original payer and cannot be redirected by the caller. It is only accepted if the deposit belongs to you, has actually completed, and the amount doesn't exceed what's left to refund.

6. Checking Transaction Status

GET /api/v1/momo/status?transaction_id=afb57b93-7849-49aa-babb-4c3ccbfe3d79
X-Api-Key: <key>
X-Api-Secret: <secret>
{
"success": true,
"transaction_id": "afb57b93-7849-49aa-babb-4c3ccbfe3d79",
"type": "deposit",
"status": "COMPLETED",
"is_final": true,
"payment_status": "Success",
"provider": "MTN_MOMO_ZMB",
"country": "ZMB",
"currency": "ZMW",
"requested_amount": "150.00",
"amount": "150.00",
"provider_transaction_id": "...",
"created_at": "2026-07-29T09:14:02Z",
"updated_at": "2026-07-29T09:14:31Z"
}

Use this whenever a callback hasn't arrived within the timing window in Section 9, or as a periodic reconciliation check against your own records. It's safe to call at any time and does not re-trigger anything.

7. Callbacks — How PesaVoucher Notifies You

PesaVoucher sends a callback to your registered callback_url once a transaction reaches a final outcome — server-side, regardless of whether the customer is still on the checkout page.

HTTP POST, Content-Type: application/json, header X-Callback-Type: Mobile-Money:

{
"transaction_id": "afb57b93-7849-49aa-babb-4c3ccbfe3d79",
"type": "deposit",
"status": "Success",
"provider": "MTN_MOMO_ZMB",
"country": "ZMB",
"currency": "ZMW",
"requested_amount": "150.00",
"amount": "150.00",
"phone_number": "+260763456789",
"provider_transaction_id": "8917...",
"account_reference": "ORDER-123",
"failure_code": null,
"failure_message": null,
"timestamp": "2026-07-29T09:14:31Z"
}

Field reference

FieldDescription
transaction_idPesaVoucher's transaction reference — use this for reconciliation/support queries.
typedeposit, payout, or refund.
statusSuccess, Failed, Cancelled, or Processing (see status table below). Only Success means funds have actually moved in your favour.
providerThe network the transaction routed through, e.g. MTN_MOMO_ZMB.
country / currencyWhere and in what currency the transaction settled.
requested_amountWhat was requested at initiation.
amountWhat was actually confirmed — treat this as the source of truth, not requested_amount.
phone_numberThe customer's (deposit) or recipient's (payout) number.
provider_transaction_idThe mobile network operator's own reference for the transaction.
account_referenceThe reference you supplied when initiating.
failure_code / failure_messagePopulated only on Failed.
timestampWhen PesaVoucher sent this callback — not the transaction time.

Status values

statusMeaning
SuccessTransaction completed — funds confirmed. Safe to credit/settle.
ProcessingStill in flight (includes queued payouts). Not final — do not treat as failure.
CancelledCustomer did not authorise, or a payout was cancelled before processing started.
FailedTransaction did not go through — do not credit. Check failure_code/failure_message.

8. What You Should Do on Receipt

  • Only act on status: "Success". Treat Processing as a no-op; Failed/Cancelled just update your own record.
  • Respond 2xx promptly. Non-2xx responses are retried; if delivery keeps failing, reconcile via transaction_id with PesaVoucher support.
  • Be idempotent. The same transaction_id may arrive more than once — don't double-credit.
  • Settle on amount, not requested_amountamount is authoritative.

9. Timing Expectations

  • Most deposits resolve within seconds to low minutes of the customer authorising.
  • Redirect-based providers (no PIN prompt) resolve on the customer's own schedule after being sent to the redirect URL — these can take longer.
  • If no callback has arrived within a few minutes, poll GET /api/v1/momo/status rather than assuming failure or re-submitting the request (re-submitting a deposit risks charging the customer twice).

10. Testing Your Integration

PesaVoucher provides a sandbox environment with test phone numbers that deterministically produce each outcome (success, insufficient balance, wrong PIN, timeout, unregistered wallet). Contact PesaVoucher's integration team for sandbox credentials and the current test-number list before going live.

  1. Confirm your callback endpoint responds 2xx to a manual test POST before running any sandbox transaction.
  2. Run a sandbox deposit and confirm the callback matches the shape in Section 7.
  3. Force a failure case (e.g. the "insufficient balance" test number) and confirm your handler treats Failed correctly.
  4. Re-send your own handler the same payload and confirm it doesn't double-credit.
  5. Repeat for a payout, including the ENQUEUED path if your test account can trigger it.

11. Support / Reconciliation

For any transaction dispute or missing callback, provide PesaVoucher with the transaction_id (preferred) or account_reference — these map directly to the authoritative record on PesaVoucher's side.