Mobile Money Payments (USSD Push)
Accept EcoCash, InnBucks, and OneMoney without redirects — the customer approves payment on their handset.
How USSD Push Payments Work
When a customer chooses to pay with EcoCash, InnBucks, or OneMoney, the payment flow is fundamentally different from a card payment. There is no card number to collect and no redirect to a payment page. Instead, the customer's phone receives a USSD prompt — a text-based menu that appears on any phone, even basic feature phones that cannot access the internet.
Here is what happens step by step:
Customer selects EcoCash on checkout
Your server calls POST /v1/checkout/create
walletType: 'ecocash' and customerPhone
Watenga calls EcoCash API
EcoCash pushes USSD prompt to customer's phone
*151# Pay $25.00 to MyShop? Enter PIN:
Customer enters their EcoCash PIN on their handset
EcoCash sends result to Watenga webhook
AND Watenga polls EcoCash for status
Watenga fires payment.capture.completed webhook to your server
Your server fulfills the order
The key difference from card payments: you never collect any payment credentials from the customer. You only need their phone number. EcoCash, InnBucks, and OneMoney handle all authentication on their own network.
Implementation
Step 1: Collect the customer's phone number
Phone must be in E.164 format (+2637XXXXXXXX). Watenga normalises 0-prefixed numbers automatically, but E.164 is recommended.
<form id="mobile-money-checkout">
<label for="phone">EcoCash number</label>
<input
type="tel"
id="phone"
name="phone"
value="+263"
placeholder="771234567"
autocomplete="tel"
required
/>
<button type="submit">Pay with EcoCash</button>
</form>Step 2: Create the checkout session
curl -X POST https://api.watenga.africa/v1/checkout/create \
-H "Authorization: Bearer sk_test_YOUR_SECRET_KEY" \
-H "Content-Type: application/json" \
-d '{
"amount": "25.00",
"currency": "USD",
"merchantTransactionId": "INV-001",
"walletType": "ecocash",
"customerPhone": "+263771234567",
"returnUrl": "https://yoursite.com/thank-you",
"notificationUrl": "https://yoursite.com/webhooks/watenga"
}'Step 3: Show a waiting screen and poll for confirmation
Because the customer must interact with their phone, your checkout page should show a waiting screen. Do not redirect the customer. Poll the status endpoint every 3 seconds until the payment is confirmed or rejected.
// Poll every 3 seconds, max 2 minutes
const transactionId = checkout.checkoutId;
const maxAttempts = 40; // 40 × 3s = 120s
for (let i = 0; i < maxAttempts; i++) {
await new Promise((r) => setTimeout(r, 3000));
const status = await watenga.transactions.getMobileStatus(transactionId);
if (status.isSuccess) {
// Payment confirmed — fulfill the order
break;
}
if (!status.isPending) {
// Payment failed or was cancelled
break;
}
}Show a spinner with the message: “Check your phone — approve the [EcoCash/InnBucks/OneMoney] payment prompt.” Include the amount prominently so the customer knows what they are approving. Include a “Cancel” button that calls DELETE /v1/checkout/{id} to abort.
<div class="watenga-waiting" role="status" aria-live="polite">
<div class="spinner" aria-hidden="true"></div>
<p class="message">
Check your phone — approve the <strong>EcoCash</strong> payment prompt.
</p>
<p class="amount">USD 25.00</p>
<button type="button" id="cancel-payment">Cancel</button>
</div>
<!-- Cancel calls DELETE /v1/checkout/{checkoutId} -->Step 4: Handle the webhook (always)
Polling is a backup. The primary confirmation mechanism is the webhook. Your notificationUrl will receive a payment.capture.completed or payment.capture.declined event when EcoCash processes the response. Always verify payment via webhook before fulfilling high-value orders.
Example mobile money webhook payload:
{
"id": "evt_550e8400-e29b-41d4-a716-446655440000",
"object": "event",
"type": "payment.capture.completed",
"created": 1717406400,
"data": {
"id": "txn_01HQXYZ",
"amount": "25.0000",
"net": "24.1300",
"currency": "USD",
"status": "completed"
}
}const crypto = require('crypto');
app.post(
'/webhooks/watenga',
express.raw({ type: 'application/json' }),
async (req, res) => {
// Header: Watenga-Signature: t=<unix>,v1=<hmac-sha256-hex>
const header = req.headers['watenga-signature'] || '';
const parts = Object.fromEntries(
header.split(',').map((kv) => kv.split('=').map((s) => s.trim())),
);
const timestamp = Number(parts.t);
const expected = crypto
.createHmac('sha256', process.env.WATENGA_WEBHOOK_SECRET)
.update(`${timestamp}.${req.body}`, 'utf8')
.digest('hex');
const a = Buffer.from(parts.v1 || '');
const b = Buffer.from(expected);
if (a.length !== b.length || !crypto.timingSafeEqual(a, b)) {
return res.status(401).send('Invalid signature');
}
const event = JSON.parse(req.body.toString());
if (event.type === 'payment.capture.completed') {
await fulfillOrder(event.data.id);
}
if (event.type === 'payment.capture.declined') {
await markOrderFailed(event.data.id);
}
res.sendStatus(200);
},
);Supported Wallets
| Wallet | Currencies | Status | Phone Prefix |
|---|---|---|---|
| EcoCash | USD, ZWG | Live | 077x, 078x |
| InnBucks | USD | Coming soon | 088x |
| OneMoney | USD, ZWG | Coming soon | 071x |
InnBucks and OneMoney are coming soon.
Get notified when InnBucks and OneMoney go live.
Common Issues
Customer did not receive the prompt
EcoCash USSD prompts can occasionally be delayed by the network. If the customer does not see a prompt within 30 seconds, they can manually trigger payment by dialling *151# on their handset and navigating to pending transactions. Your polling will catch the confirmation regardless of how it was approved.
Wrong phone number
If the phone number does not belong to an active EcoCash account, the initiation will fail with error code ECOCASH_INIT_FAILED. Always validate that the number format is correct (+2637XXXXXXXX) before calling the API. Let customers correct their number and retry — you can create a new checkout session for the same order.
Customer cancelled or entered wrong PIN
If the customer presses “Cancel” or enters an incorrect PIN three times, EcoCash rejects the payment. Your webhook will receive payment.capture.declined with a descriptive reason. Show the customer a clear message and give them the option to try again or switch to a different payment method.
Timeout — customer never responded
If your polling loop completes without a confirmed payment, the session has expired. The customer may have ignored the prompt. Do not assume payment failed — check your webhook events log in the Developer Dashboard to confirm the final status before marking the order as failed.
Get notified when InnBucks and OneMoney go live.
