McTaba Labs logo
Software EngineeringThe WhiteboardProjectsEventsMasterclassesWebinarsLearn
Book Call

500+ developers already in the community

Get answers, find accountability partners, and stay ahead

Join Discord →
McTaba Labs logo

Educating 1 million students in tech by 2030.

Programs

  • HighSchool Graduates Launchpad
  • AI & Business Automation
  • Corporate Training
  • McTaba Story & Mission
  • Full-Stack Curriculum
  • Course Pricing & Plans
  • Frequently Asked Questions

Resources

  • Join Discord Community
  • Tech Events in Nairobi
  • Masterclasses
  • Free Resources
  • Learn
  • Campus
  • Africa Tech Talent Report
  • Partner With Us
  • Start a Project
  • Our Work

Contact Us

  • 124 Manyani East Road, Lavington, Nairobi
  • +254727795603
  • Book a Discovery Call

Office Hours:

Mon-Fri: 9am - 2pm EAT

Sat: 10am - 12pm EAT

© 2027 McTaba Labs - A product of Edtech Industries. All rights reserved.

Privacy PolicyTerms of ServiceRefund Policy
  1. Learn
  2. Paystack for African Developers
  3. Paystack USSD Payment Not Completing
Bonaventure OgetoBy Bonaventure Ogeto|Updated July 20, 2026

Paystack USSD Payment Not Completing

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.

In this article

  1. 1. How Paystack USSD Payments Work
  2. 2. The USSD Timeout Problem
  3. 3. Bank USSD Downtime: A Frequent Reality in Nigeria
  4. 4. Initiating USSD Payments Correctly
  5. 5. Customer Messaging That Reduces USSD Failures
  6. 6. Handling USSD Payment Status on the Backend
  7. 7. Diagnostic Steps When USSD Payments Keep Failing
  8. 8. Offering Alternative Payment Channels When USSD Fails
  9. 9. How to Verify Your USSD Payment Flow Is Working

How Paystack USSD Payments Work

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:

  1. You initialize a transaction with Paystack, specifying USSD as the payment channel.
  2. Paystack returns a USSD code (like *737*000*1234#) that the customer must dial.
  3. You display this code to the customer on your website or app.
  4. The customer dials the code on their phone. This opens a USSD session with their bank.
  5. The bank's USSD menu walks the customer through authorization: confirm amount, enter PIN, confirm payment.
  6. If successful, the bank notifies Paystack, and Paystack sends you a 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.

The USSD Timeout Problem

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:

  1. Dial the code
  2. Wait for the bank's USSD menu to load
  3. Read and understand the menu options
  4. Select the correct options (sometimes 2 or 3 levels deep)
  5. Enter their transaction PIN
  6. Confirm the payment

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.

Bank USSD Downtime: A Frequent Reality in Nigeria

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:

  • Month-end (25th to 31st): Salary payments flood the banking system
  • Friday evenings and weekends: High consumer spending activity
  • Public holidays: Surge in transactions
  • During bank system maintenance: Usually late night, but sometimes during business hours

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:

  • Monitor your USSD payment success rate. A sudden drop in the success rate for a specific bank indicates that bank's USSD is down.
  • Check social media. Nigerian bank customers are vocal on Twitter/X when USSD is down. Searching for "[bank name] USSD not working" often confirms outages faster than official channels.
  • Check the Paystack status page and Paystack's social channels for any reported payment channel issues.

Initiating USSD Payments Correctly

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 payments only work with NGN. Passing any other currency will fail.
  • Not all banks support USSD payments through Paystack. Check the Paystack documentation for the current list of supported banks.
  • The ussd_code or display_text in the response contains the exact code the customer must dial. Display it prominently.

Customer Messaging That Reduces USSD Failures

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:

  • Make the USSD code huge. The biggest text on the screen. Copyable with one tap on mobile.
  • Number the steps. Do not use a paragraph of text. Use 1, 2, 3, 4, 5.
  • Show the time limit prominently. "You have 2 minutes" makes customers act faster.
  • Add a "try another payment method" link that is always visible. Do not make customers hunt for it if USSD fails.
  • Show a live countdown timer if you want to add urgency, but make it the USSD code display timeout, not the bank session timeout (you do not control the bank's timer).

Handling USSD Payment Status on the Backend

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);

Diagnostic Steps When USSD Payments Keep Failing

If USSD payments are consistently not completing, work through this diagnostic checklist.

  1. Check if it is one bank or all banks. If only GTBank USSD is failing, the problem is GTBank's USSD system. If all banks are failing, the problem might be on your side or Paystack's side.
  2. Check the Paystack status page. Go to status.paystack.com and look for any incidents related to USSD or specific banks.
  3. Verify your initialization is correct. Log the request and response for your USSD charge creation. Make sure the response includes a valid USSD code and a "pay_offline" status.
  4. Check your webhook endpoint. Is it reachable? Is it returning 200? USSD payments that complete but are not reflected in your system point to a broken webhook handler.
  5. Test with a real phone. In live mode, make a small USSD payment (100 NGN) yourself. Dial the code, follow the prompts, and see if it completes. This rules out infrastructure issues on your side.
  6. Check customer phone numbers. USSD payments are tied to the phone number linked to the bank account. If the customer's phone number does not match their bank records, some banks will reject the USSD session.
  7. Check transaction timing. Are failures clustered at specific times? Month-end, Friday evenings, and holiday periods see the highest USSD failure rates due to system congestion.
// 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);

Offering Alternative Payment Channels When USSD Fails

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.

How to Verify Your USSD Payment Flow Is Working

Test the complete USSD flow in live mode with a small amount (100 NGN). Sandbox does not fully simulate the bank USSD interaction.

  1. Initialize a USSD charge for a Nigerian bank you have an account with.
  2. Confirm the response includes a valid USSD code and display text.
  3. Dial the USSD code on your phone. Follow the prompts and complete the payment.
  4. Check your webhook endpoint logs. You should receive a charge.success event within 30 seconds of completing the USSD session.
  5. Verify the transaction via the API. Confirm status is "success" and the channel is "ussd."
  6. Test the failure case: initiate a USSD charge, dial the code, but let the session time out without completing it. Confirm you receive a charge.failed event or the transaction status changes to "failed."
  7. Test your fallback: after a USSD failure, confirm the customer can switch to card or bank transfer and complete the payment.

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.

Key Takeaways

  • ✓USSD sessions are extremely time-limited. Most bank USSD sessions expire in 2 to 3 minutes. Customers must complete the entire flow (dial code, select options, enter PIN) within that window.
  • ✓Bank USSD services go down frequently in Nigeria, especially during peak hours (month-end, salary days). This is outside your control and outside Paystack control.
  • ✓USSD is Nigeria-specific. Paystack USSD payments work only with Nigerian banks and in NGN currency.
  • ✓Always display the USSD code prominently with clear step-by-step instructions. Many customers have never made a USSD payment online before.
  • ✓Offer at least one alternative payment channel alongside USSD. If USSD fails, the customer should be able to switch to card or bank transfer without starting over.
  • ✓USSD transactions are asynchronous like mobile money. Use webhooks as the primary notification mechanism and polling as a fallback for real-time UI updates.

Frequently Asked Questions

Why do USSD payments fail so often compared to card payments?
USSD payments have more points of failure. The customer must manually dial a code, navigate a bank menu, enter a PIN, and confirm payment, all within a 2 to 3 minute window. Card payments happen in seconds with a single click. Additionally, bank USSD infrastructure in Nigeria experiences frequent congestion and downtime, especially during peak periods.
Which Nigerian banks support USSD payments through Paystack?
Paystack supports USSD payments for most major Nigerian banks including GTBank (*737#), Zenith (*966#), UBA (*919#), Access/Diamond (*901#), First Bank (*894#), Sterling (*822#), Wema (*945#), and several others. The available banks may change, so check the current Paystack documentation for the full list.
Can USSD payments work outside Nigeria?
No. Paystack USSD payments are Nigeria-specific. They only work with Nigerian banks and in NGN currency. For other African markets, Paystack supports mobile money (M-Pesa for Kenya, MTN MoMo for Ghana) which serves a similar unbanked/underbanked population but uses a different technical flow.
What does "pay_offline" status mean for a USSD charge?
The "pay_offline" status means Paystack has generated the USSD code and is waiting for the customer to dial it and complete the payment. It is the expected initial status for USSD charges. Once the customer completes (or fails) the USSD session, the status will change to "success" or "failed" and you will receive the corresponding webhook.
How do I handle a customer who says they completed the USSD payment but my system shows nothing?
First, check the Paystack dashboard for the transaction reference. If it shows "success" there but not in your system, your webhook handler missed the event. Process it manually and fix the webhook issue. If the transaction shows "pending" or "failed" on Paystack, the bank may not have confirmed the payment. Ask the customer for their bank confirmation SMS and contact Paystack support with both the Paystack reference and the bank reference.

Learn Paystack Integration

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

Not ready? Create Free Account

Keep reading

Paystack Errors and Troubleshooting: The Complete Reference

Every Paystack error you will hit in production, what causes it, and how to fix it. Authentication failures, webhook signature problems, transaction errors, transfer issues, and frontend bugs. Code-first solutions for each one.

Read more

Paystack Currency Not Supported Error

Paystack returns "Currency not supported" when you try to charge in a currency that is not enabled for your account. This guide explains which currencies each country supports, how to check and enable currencies, and common format mistakes that trigger this error.

Read more

Paystack Transfer Failed: Reasons and Recovery

Paystack transfers fail for specific, diagnosable reasons. This guide covers every failure reason, the exact webhook payload for each, and a production-ready recovery strategy with code.

Read more

Paystack Subaccount Settlement Missing

Your subaccount received transactions but the settlement never arrived in their bank account. Here is how to trace the problem from split to settlement to bank.

Read more

Paystack Webhooks on Cloudflare Workers

A complete guide to handling Paystack webhooks on Cloudflare Workers. Covers the Web Crypto API for HMAC-SHA512 signature verification (since Workers do not have Node.js crypto), request body handling, environment secrets, and a production-ready Worker you can deploy in minutes.

Read more

Integration Testing Paystack Flows End to End

Integration tests for Paystack test the real flow — your backend calls the actual Paystack test API, a webhook fires to your server, and your database is updated. Unlike unit tests with mocks, integration tests catch issues that only appear when the real API is involved.

Read more

Explore related topics

M-Pesa & African Stack

M-Pesa Daraja API tutorials, USSD development, and WhatsApp Business API guides. The African Stack that global bootcamps do not teach.

Learn to Code in Kenya

Everything a Kenyan needs to know before writing their first line of code. Career paths, real costs, hardware requirements, and honest answers about what it takes.

Learning to Code

Roadmaps, language guides, and practical advice for learning to code and launching a programming career.

Breaking Into Tech

Practical, no-nonsense guides for anyone looking to start or change their career in tech. From choosing your first language to acing interviews.

The African Stack: Skills for This Market

The skills that make Kenyan developers hard to replace: M-Pesa, USSD, WhatsApp, fintech, and building for the African market.

Bonaventure Ogeto

Bonaventure Ogeto

Founder, McTaba Labs

Bonaventure is the founder of McTaba Labs, a coding bootcamp in Nairobi, Kenya. He has spent years building products for the African market and mentoring the next generation of developers across the continent.