Paystack USSD payments fail to complete for three main reasons: the USSD session timed out before the customer finished (USSD sessions last only 2 to 3 minutes), the customer bank USSD service is down or overloaded, or the customer entered the wrong details during the USSD flow. Show customers the USSD code clearly, warn them the session is time-limited, and always offer an alternative payment channel. On the backend, use webhooks to track completion and run reconciliation for any stuck transactions.
USSD (Unstructured Supplementary Service Data) payments let customers pay by dialing a short code on their phone. No internet connection required on the customer's device, which makes it popular in Nigeria where data can be expensive or unreliable.
The flow:
*737*000*1234#) that the customer must dial.charge.success webhook.The critical weakness: step 4 through 5 happens entirely outside your control. The customer is interacting with their bank's USSD system, which has its own reliability issues and a very short timeout window. If anything goes wrong in that interaction, the payment does not complete.
USSD sessions are not like web sessions. A USSD session lasts only 2 to 3 minutes (some banks even shorter). Within that window, the customer must:
If the customer is slow at any step, or if the bank's USSD system is laggy (which it often is), the session expires. When the session expires, the payment fails. The customer has to start over by dialing the code again, but by then the original Paystack transaction reference may have expired too.
Real-world scenario: A customer dials *737*000*5678#. GTBank's USSD menu takes 10 seconds to respond. The customer reads the options, selects "Pay," waits another 8 seconds. Enters the amount, waits. Enters their PIN. By now 2 minutes have passed. The bank asks for confirmation. The customer reads it, presses 1 to confirm. USSD session expired. Payment did not go through. Customer sees "Transaction Failed" on their phone and assumes the worst.
There is nothing you can do to extend the USSD session timeout. It is controlled by the telco (MTN, Airtel, Glo, 9mobile) and the bank. All you can do is set expectations and provide alternatives.
Get weekly developer tips
Join 25,000+ developers. Practical guides, job tips, and new content — straight to your inbox.
No spam. Unsubscribe anytime.
Nigerian bank USSD services go down more often than most developers expect. This is not a Paystack issue. The USSD infrastructure is operated by the banks and telcos, and it struggles under high load.
When USSD goes down most often:
Which banks are most affected:
All Nigerian banks experience USSD issues, but the frequency varies. The big five (GTBank, Access, UBA, Zenith, First Bank) handle the most volume and therefore experience the most congestion. Smaller banks may have more stable USSD because of lower volume, but their infrastructure is sometimes less robust.
How to detect bank USSD downtime:
Proper initialization reduces failure rates. Here is the correct way to set up a USSD payment through Paystack.
const axios = require('axios');
async function initiateUSSDPayment(email, amountInKobo, bankCode) {
// USSD bank codes for common Nigerian banks:
// GTBank: 058, Zenith: 057, UBA: 033, Access: 044, First Bank: 011
// Sterling: 232, Wema: 035, Stanbic IBTC: 221
try {
const response = await axios.post(
'https://api.paystack.co/charge',
{
email: email,
amount: amountInKobo,
ussd: {
type: bankCode, // Bank code determines which USSD flow
},
currency: 'NGN', // USSD only works with NGN
},
{
headers: {
Authorization: `Bearer ${process.env.PAYSTACK_SECRET_KEY}`,
'Content-Type': 'application/json',
},
}
);
const data = response.data.data;
console.log('USSD charge initiated');
console.log('Status:', data.status); // "pay_offline"
console.log('Display text:', data.display_text); // Instructions for the customer
console.log('USSD code:', data.ussd_code); // The code to dial (if provided)
console.log('Reference:', data.reference);
return {
reference: data.reference,
ussdCode: data.ussd_code,
displayText: data.display_text,
status: data.status,
};
} catch (error) {
const errData = error.response?.data;
console.error('USSD charge failed:', errData?.message || error.message);
// Common errors:
// "Currency not supported" - USSD only works with NGN
// "Channel not available" - The selected bank may not support USSD on Paystack
throw new Error(errData?.message || 'Failed to initiate USSD payment');
}
}
// Example: GTBank USSD payment
initiateUSSDPayment('customer@example.com', 500000, '058')
.then(result => {
console.log('Tell customer to dial:', result.ussdCode || result.displayText);
})
.catch(console.error);
Important notes:
ussd_code or display_text in the response contains the exact code the customer must dial. Display it prominently.The way you present the USSD payment option massively affects the success rate. Most USSD failures are customer errors, not technical failures. Clear instructions prevent the majority of them.
What to show the customer:
// Example UI content (render this in your frontend)
const ussdInstructions = {
title: 'Complete your payment via USSD',
steps: [
'Open your phone dialer (the app you use to make calls)',
'Dial the code shown below exactly as written',
'Follow the prompts from your bank',
'Enter your transaction PIN when asked',
'Confirm the payment',
],
warnings: [
'You have about 2 minutes to complete the USSD session',
'Do not close the USSD menu or switch to another app during the process',
'Make sure you have sufficient balance in your bank account',
],
ussdCode: '*737*000*1234#', // From Paystack response
amount: 'NGN 5,000',
fallbackMessage: 'Having trouble? You can also pay with your debit card or bank transfer.',
};
Design tips:
USSD payments are asynchronous. After showing the customer the USSD code, you wait for them to complete it. Your backend needs to handle the final status via webhooks and provide status checks for the frontend.
const crypto = require('crypto');
const express = require('express');
const app = express();
// Webhook handler for USSD payments
app.post('/webhooks/paystack', express.raw({ type: 'application/json' }), (req, res) => {
res.sendStatus(200);
const hash = crypto
.createHmac('sha512', process.env.PAYSTACK_SECRET_KEY)
.update(req.body)
.digest('hex');
if (hash !== req.headers['x-paystack-signature']) return;
const event = JSON.parse(req.body.toString());
if (event.event === 'charge.success' && event.data.channel === 'ussd') {
console.log('USSD payment completed:', event.data.reference);
console.log('Amount:', event.data.amount / 100, 'NGN');
console.log('Bank:', event.data.authorization.bank);
// Verify server-side before granting value
verifyAndGrantValue(event.data.reference).catch(err => {
console.error('Post-USSD processing error:', err);
});
}
if (event.event === 'charge.failed' && event.data.channel === 'ussd') {
console.log('USSD payment failed:', event.data.reference);
console.log('Reason:', event.data.gateway_response);
// Update your database
markPaymentFailed(event.data.reference, event.data.gateway_response).catch(err => {
console.error('Failed to update USSD payment status:', err);
});
}
});
async function verifyAndGrantValue(reference) {
const response = await axios.get(
`https://api.paystack.co/transaction/verify/${reference}`,
{
headers: {
Authorization: `Bearer ${process.env.PAYSTACK_SECRET_KEY}`,
},
}
);
const data = response.data.data;
if (data.status === 'success' && data.amount === expectedAmount) {
// Grant value to the customer
await db.query(
'UPDATE orders SET payment_status = $1, paid_at = NOW() WHERE reference = $2',
['paid', reference]
);
console.log('Value granted for USSD payment:', reference);
}
}
async function markPaymentFailed(reference, reason) {
await db.query(
'UPDATE orders SET payment_status = $1, failure_reason = $2 WHERE reference = $3',
['failed', reason, reference]
);
}
app.listen(3000);
If USSD payments are consistently not completing, work through this diagnostic checklist.
// Log USSD failure patterns for diagnosis
async function analyzeUSSDFailures() {
const response = await axios.get('https://api.paystack.co/transaction', {
headers: {
Authorization: `Bearer ${process.env.PAYSTACK_SECRET_KEY}`,
},
params: {
channel: 'ussd',
status: 'failed',
from: new Date(Date.now() - 24 * 60 * 60 * 1000).toISOString(),
perPage: 100,
},
});
const failures = response.data.data;
const byBank = {};
const byHour = {};
for (const txn of failures) {
const bank = txn.authorization?.bank || 'unknown';
const hour = new Date(txn.created_at).getHours();
byBank[bank] = (byBank[bank] || 0) + 1;
byHour[hour] = (byHour[hour] || 0) + 1;
}
console.log('USSD failures by bank (last 24h):', byBank);
console.log('USSD failures by hour:', byHour);
// If one bank dominates, their USSD is likely down
// If failures spike at certain hours, it is a congestion issue
}
analyzeUSSDFailures().catch(console.error);
USSD has the highest failure rate of any payment channel on Paystack. You should always offer alternatives. The best approach is to let the customer switch payment methods without losing their order context.
// Initialize transaction via the standard endpoint (supports multiple channels)
async function initializeWithFallback(email, amountInKobo) {
const response = await axios.post(
'https://api.paystack.co/transaction/initialize',
{
email: email,
amount: amountInKobo,
channels: ['card', 'bank', 'ussd', 'bank_transfer'],
// Let the customer choose their preferred channel in the checkout
},
{
headers: {
Authorization: `Bearer ${process.env.PAYSTACK_SECRET_KEY}`,
'Content-Type': 'application/json',
},
}
);
return {
authorizationUrl: response.data.data.authorization_url,
reference: response.data.data.reference,
};
}
Using the Paystack Checkout (popup or redirect) is actually the best approach for USSD because it lets the customer switch channels within the same checkout session. If USSD fails, they can click "try another payment method" and switch to card without you needing to create a new transaction.
If you are building a custom checkout (not using Paystack's popup), build the channel switching yourself. Keep the same order reference but create a new charge with a different channel. Make sure your order system handles the case where the customer tries USSD first, it fails, and then they pay by card. Only one of those should result in a fulfilled order.
Test the complete USSD flow in live mode with a small amount (100 NGN). Sandbox does not fully simulate the bank USSD interaction.
charge.success event within 30 seconds of completing the USSD session.charge.failed event or the transaction status changes to "failed."Monitor your USSD success rate in production. A healthy integration should see 50 to 70% USSD completion rates (meaning 30 to 50% of USSD attempts do not complete, which is normal for this channel). If your completion rate drops below 40%, investigate whether a specific bank is causing the drop.
KES 2,999
The definitive guide to building payment systems with Paystack — from your first transaction to production-grade payment infrastructure across Africa. 9 modules, 47 lessons. Free preview available.
Start Paystack Course