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 Sandbox Card Numbers Not Working
Bonaventure OgetoBy Bonaventure Ogeto|Updated July 20, 2026

Paystack Sandbox Card Numbers Not Working

Paystack has its own test card numbers. Stripe cards (4242 4242 4242 4242) will not work. Use 4084 0840 8408 4081 for a successful test charge, enter 408408 as PIN, and 123456 as OTP. Make sure your secret key starts with sk_test_ and you are calling https://api.paystack.co, not a live endpoint. If you are using the Popup or Inline JS, confirm your public key also starts with pk_test_.

In this article

  1. 1. Why Your Test Card Is Being Declined
  2. 2. The Correct Paystack Test Card Numbers
  3. 3. Test Mode vs Live Mode: The Key Mismatch Problem
  4. 4. Handling PIN and OTP Challenges on the Charge API
  5. 5. Testing with Popup and Inline JS
  6. 6. Verifying the Complete Server-Side Flow in Test Mode
  7. 7. Five-Minute Debugging Checklist
  8. 8. Simulating Different Failure Scenarios in Sandbox
  9. 9. How to Verify Everything Is Working

Why Your Test Card Is Being Declined

You copy a test card number from a tutorial, paste it into the Paystack checkout, and get "Transaction Failed" or "Card number is invalid." No useful error message. Just a dead end.

This happens more often than you would expect, and the root cause is almost always one of three things: wrong card numbers, wrong key type, or incomplete charge flow. The good news is that each one takes less than a minute to fix once you know what to look for.

The most common mistake is copying Stripe's test card number (4242 4242 4242 4242) and using it on Paystack. These are completely different payment processors with completely different test environments. Stripe's numbers mean nothing to Paystack's sandbox. Paystack has its own set of test cards with specific PINs and OTPs that must be used together.

The Correct Paystack Test Card Numbers

Paystack provides several test cards that simulate different outcomes. Here are the ones you actually need.

Successful charge (Visa):

  • Card number: 4084 0840 8408 4081
  • Expiry: any future date (e.g., 12/30)
  • CVV: 408
  • PIN (when prompted): 408408
  • OTP (when prompted): 123456

Successful charge (Mastercard):

  • Card number: 5060 6611 1111 1111 111
  • Expiry: any future date
  • CVV: 123
  • PIN: 408408
  • OTP: 123456

Failed charge (insufficient funds):

  • Card number: 4084 0840 8408 4081
  • Same details as above, but use OTP 123457 to simulate failure

3D Secure / Bank Authentication:

  • Card number: 4000 0000 0000 0002
  • Simulates 3DS redirect flow

All test cards use the same PIN (408408) and success OTP (123456). These are documented on Paystack's website, but they are easy to miss if you jump straight to building.

Get weekly developer tips

Join 25,000+ developers. Practical guides, job tips, and new content — straight to your inbox.

No spam. Unsubscribe anytime.

Test Mode vs Live Mode: The Key Mismatch Problem

Paystack issues two pairs of keys: test keys and live keys. Each pair has a public key and a secret key. The naming convention is straightforward:

  • Test public key: pk_test_xxxxxxxxxxxxxxxx
  • Test secret key: sk_test_xxxxxxxxxxxxxxxx
  • Live public key: pk_live_xxxxxxxxxxxxxxxx
  • Live secret key: sk_live_xxxxxxxxxxxxxxxx

If you use a live secret key to initialize a transaction, Paystack expects a real card. Test cards will fail. If you use a test public key on the frontend but a live secret key on the backend, the transaction will break in confusing ways. Both keys must be from the same mode.

Here is how to verify which keys you are using:

// Quick key check - add this temporarily during debugging
const secretKey = process.env.PAYSTACK_SECRET_KEY;
console.log('Key mode:', secretKey.startsWith('sk_test_') ? 'TEST' : 'LIVE');
console.log('Key prefix:', secretKey.substring(0, 8) + '...');

// Never log the full key. Just enough to confirm the mode.

A surprisingly common scenario: you deploy to staging and copy your live keys from the Paystack dashboard instead of your test keys. Everything looks right until you try a test card and it fails. Check the prefix.

Handling PIN and OTP Challenges on the Charge API

If you are using the Charge API directly (instead of Popup or Inline JS), you must handle the multi-step flow yourself. The Charge API does not complete in a single request. It returns a status telling you what to do next.

Here is the full flow for a test card charge:

const axios = require('axios');

const PAYSTACK_SECRET = process.env.PAYSTACK_SECRET_KEY; // Must be sk_test_...

async function chargeTestCard() {
  // Step 1: Initiate the charge
  const chargeResponse = await axios.post(
    'https://api.paystack.co/charge',
    {
      email: 'test@example.com',
      amount: 500000, // 5000 NGN in kobo
      card: {
        number: '4084084084084081',
        cvv: '408',
        expiry_month: '12',
        expiry_year: '2030',
      },
    },
    {
      headers: {
        Authorization: `Bearer ${PAYSTACK_SECRET}`,
        'Content-Type': 'application/json',
      },
    }
  );

  console.log('Charge status:', chargeResponse.data.data.status);
  // Expected: "send_pin"

  const reference = chargeResponse.data.data.reference;

  // Step 2: Submit PIN
  const pinResponse = await axios.post(
    'https://api.paystack.co/charge/submit_pin',
    {
      pin: '408408',
      reference: reference,
    },
    {
      headers: {
        Authorization: `Bearer ${PAYSTACK_SECRET}`,
        'Content-Type': 'application/json',
      },
    }
  );

  console.log('After PIN:', pinResponse.data.data.status);
  // Expected: "send_otp"

  // Step 3: Submit OTP
  const otpResponse = await axios.post(
    'https://api.paystack.co/charge/submit_otp',
    {
      otp: '123456',
      reference: reference,
    },
    {
      headers: {
        Authorization: `Bearer ${PAYSTACK_SECRET}`,
        'Content-Type': 'application/json',
      },
    }
  );

  console.log('Final status:', otpResponse.data.data.status);
  // Expected: "success"
  console.log('Gateway response:', otpResponse.data.data.gateway_response);
  // Expected: "Successful"
}

chargeTestCard().catch(console.error);

The mistake people make is sending only the initial charge request and expecting a "success" response. The sandbox simulates the real bank flow: charge, then PIN, then OTP. If you skip the PIN or OTP step, the transaction stays in "pending" forever.

Testing with Popup and Inline JS

If you are using Paystack Popup or Inline JS (which is what most integrations use), the PIN and OTP steps are handled inside the popup window. You do not need to build UI for those steps. But you still need to make sure you are in test mode.

// Frontend: Initialize transaction on your server first
// Make sure your server uses sk_test_ key

// Then open the popup with your pk_test_ key
const handler = PaystackPop.setup({
  key: 'pk_test_xxxxxxxxxxxxxxxx', // Must be pk_test_
  email: 'customer@example.com',
  amount: 500000, // 5000 NGN in kobo
  currency: 'NGN',
  ref: 'test_ref_' + Date.now(),
  callback: function (response) {
    console.log('Payment complete:', response.reference);
    // Verify on your server
  },
  onClose: function () {
    console.log('Popup closed');
  },
});

handler.openIframe();

When the popup opens in test mode, you will see a banner at the top saying "Test Mode." Enter the test card number (4084 0840 8408 4081), CVV 408, any future expiry. The popup will then ask for PIN (408408) and OTP (123456) inside the popup window itself.

If you do not see the "Test Mode" banner, you are using a live public key. Stop and swap it.

Common trap: Some developers initialize the transaction on the server with sk_test_ but use pk_live_ on the frontend (or vice versa). The key pair must match. Both test or both live.

Verifying the Complete Server-Side Flow in Test Mode

The recommended Paystack flow is: initialize on the server, redirect or popup on the client, verify on the server. Here is the full test-mode flow to confirm everything works end to end.

const express = require('express');
const axios = require('axios');
const app = express();

const PAYSTACK_SECRET = process.env.PAYSTACK_SECRET_KEY;

// Step 1: Initialize transaction
app.post('/api/pay', async (req, res) => {
  try {
    const response = await axios.post(
      'https://api.paystack.co/transaction/initialize',
      {
        email: req.body.email,
        amount: req.body.amount, // Already in kobo
        callback_url: 'https://yoursite.com/payment/callback',
      },
      {
        headers: {
          Authorization: `Bearer ${PAYSTACK_SECRET}`,
          'Content-Type': 'application/json',
        },
      }
    );

    // Log to confirm test mode
    console.log('Reference:', response.data.data.reference);
    console.log('Auth URL:', response.data.data.authorization_url);

    res.json({
      authorization_url: response.data.data.authorization_url,
      reference: response.data.data.reference,
    });
  } catch (error) {
    console.error('Init failed:', error.response?.data || error.message);
    res.status(500).json({ error: 'Payment initialization failed' });
  }
});

// Step 2: Verify after callback
app.get('/payment/callback', async (req, res) => {
  const reference = req.query.reference;

  try {
    const response = await axios.get(
      `https://api.paystack.co/transaction/verify/${reference}`,
      {
        headers: {
          Authorization: `Bearer ${PAYSTACK_SECRET}`,
        },
      }
    );

    const data = response.data.data;
    console.log('Status:', data.status); // "success" for test cards
    console.log('Amount:', data.amount); // In kobo
    console.log('Channel:', data.channel); // "card"
    console.log('Card type:', data.authorization.card_type);

    if (data.status === 'success') {
      // Grant value to customer
      res.send('Payment verified successfully');
    } else {
      res.send('Payment was not successful: ' + data.gateway_response);
    }
  } catch (error) {
    console.error('Verify failed:', error.response?.data || error.message);
    res.status(500).send('Verification failed');
  }
});

app.listen(3000);

If the initialize call returns a 401, your secret key is wrong or malformed. If it returns a 200 but the test card fails in the checkout page, check that your public key matches the same test account. If verify returns "success" but you expected "failed," remember that OTP 123456 always succeeds. Use 123457 to simulate failure.

Five-Minute Debugging Checklist

Run through this list before opening a support ticket. It catches the problem about 95% of the time.

  1. Check the card number. Is it 4084 0840 8408 4081? Not 4242 4242 4242 4242? Not your actual debit card?
  2. Check the CVV. Must be 408 for the Visa test card, 123 for the Verve test card.
  3. Check the expiry. Must be any date in the future. A past date will fail.
  4. Check your key prefix. sk_test_ on the server, pk_test_ on the client. Both from the same Paystack account.
  5. Check the dashboard toggle. Log into your Paystack dashboard. There is a toggle at the top that switches between Test and Live. Make sure you are looking at Test mode when checking for transactions.
  6. Check the PIN and OTP. If using the Charge API, did you submit PIN 408408 and OTP 123456? Both steps are required.
  7. Check your environment variables. Your .env file might have the right key, but is it actually loaded? Log the first 8 characters of the key at startup to confirm.
// Add to your server startup (remove after debugging)
console.log('=== Paystack Config Check ===');
console.log('Secret key loaded:', !!process.env.PAYSTACK_SECRET_KEY);
console.log('Key mode:', process.env.PAYSTACK_SECRET_KEY?.startsWith('sk_test_')
  ? 'TEST (correct for sandbox)'
  : 'LIVE (test cards will NOT work)');
console.log('============================');

Simulating Different Failure Scenarios in Sandbox

Once your test cards work for the success case, you need to test failure paths. Your code must handle declined cards, insufficient funds, and abandoned checkouts gracefully.

Insufficient funds: Use the success test card but submit OTP 123457 instead of 123456. The transaction will fail with a "declined" gateway response.

Abandoned checkout: Initialize a transaction but never complete it. After some time, it will remain in "abandoned" status. Your webhook handler should handle this (or ignore it, depending on your business logic).

Timeout: Initiate a Charge API call but never submit the PIN. The transaction will eventually time out.

// Test your failure handling
async function testFailureScenario() {
  // Initialize and charge as normal, but use wrong OTP
  const reference = 'fail_test_' + Date.now();

  // ... (charge and PIN steps same as before)

  // Submit wrong OTP to trigger failure
  const otpResponse = await axios.post(
    'https://api.paystack.co/charge/submit_otp',
    {
      otp: '123457', // Wrong OTP triggers failure
      reference: reference,
    },
    {
      headers: {
        Authorization: `Bearer ${process.env.PAYSTACK_SECRET_KEY}`,
        'Content-Type': 'application/json',
      },
    }
  );

  console.log('Status:', otpResponse.data.data.status);
  // Expected: "failed"
  console.log('Gateway response:', otpResponse.data.data.gateway_response);
  // Expected: something like "Declined"
}

testFailureScenario().catch(console.error);

Build your error handling around these test scenarios. In production, cards fail for many reasons: insufficient funds, expired cards, bank downtime, 3DS authentication failure. If your code only handles the happy path, your first production failure will be ugly.

How to Verify Everything Is Working

After fixing your test card issue, confirm the full loop works.

  1. Initialize a transaction from your server. Confirm you get an authorization_url or access_code back.
  2. Complete the payment using test card 4084 0840 8408 4081, PIN 408408, OTP 123456.
  3. Check your Paystack dashboard (Test mode toggle). The transaction should appear as "Success."
  4. Verify the transaction via the API. The response should show status: "success" and the correct amount.
  5. If you have webhooks configured, check your webhook endpoint logs. You should receive a charge.success event.

If all five steps pass, your sandbox integration is working correctly. When you are ready to go live, swap sk_test_ for sk_live_ and pk_test_ for pk_live_. The API endpoints, payload format, and flow remain identical. The only thing that changes is the keys and the fact that real money moves.

Key Takeaways

  • ✓Paystack test cards are completely different from Stripe test cards. Using 4242 4242 4242 4242 on Paystack will always fail.
  • ✓The primary success test card is 4084 0840 8408 4081 with CVV 408, expiry in any future date, PIN 408408, and OTP 123456.
  • ✓Your secret key must start with sk_test_ and your public key with pk_test_ for sandbox mode. Mixing live and test keys causes silent failures.
  • ✓Paystack sandbox simulates the full card flow including PIN and OTP challenges. Your integration must handle the submit_pin and submit_otp steps on the Charge API.
  • ✓If you are using Paystack Popup or Inline JS, the PIN and OTP steps are handled automatically. You only need to manage them when using the Charge API directly.
  • ✓Test mode transactions never hit real banks. They appear in your dashboard under the Test toggle, not Live.
  • ✓Always verify test transactions server-side just like production. The sandbox verify endpoint returns the same structure as live.

Frequently Asked Questions

Can I use Stripe test card numbers on Paystack?
No. Stripe and Paystack are completely separate payment processors with different test environments. Stripe test card 4242 4242 4242 4242 means nothing to Paystack. Use Paystack test card 4084 0840 8408 4081 with CVV 408, PIN 408408, and OTP 123456.
What PIN and OTP do I use for Paystack test cards?
PIN is 408408 for all Paystack test cards. OTP is 123456 for a successful transaction and 123457 to simulate a failed transaction. These values are fixed and do not change.
Why does my test card work in Popup but fail on the Charge API?
The Charge API requires you to handle PIN and OTP submission manually. After the initial charge call, you must call /charge/submit_pin with PIN 408408, then /charge/submit_otp with OTP 123456. The Popup handles these steps automatically inside the iframe.
My test transactions do not appear in the Paystack dashboard. Where are they?
The Paystack dashboard has a toggle at the top that switches between Test and Live mode. Test transactions only appear when you are viewing Test mode. Click the toggle to switch.
Do sandbox transactions trigger webhooks?
Yes. Paystack sandbox sends webhook events just like live mode. You will receive charge.success, charge.failed, and other events to your configured webhook URL. This is one of the best things about the Paystack sandbox. Use it to test your webhook handlers before going live.

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

Testing Paystack Webhooks Locally with Cloudflare Tunnel

A step-by-step guide to testing Paystack webhooks on your local machine using Cloudflare Tunnel. Covers installation, quick tunnels, named tunnels, signature verification during testing, and common pitfalls.

Read more

Simulating Failed Payments on Paystack

Payment failures fall into several categories — card declines, API timeouts, expired sessions, and bank errors. Each requires different handling in your application. This guide shows how to simulate every major failure type in Paystack test mode and verify your code handles it correctly.

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.