Bonaventure OgetoBy Bonaventure Ogeto|

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.

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.

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.

Ready to build real-world apps?

Join the McTaba Labs full-stack marathon (4 months full-time · 6 months part-time). Learn M-Pesa, USSD, and WhatsApp engineering while shipping 8 production apps.

Apply to the McTaba Marathon