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_.
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
123457to 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.
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.
- Check the card number. Is it 4084 0840 8408 4081? Not 4242 4242 4242 4242? Not your actual debit card?
- Check the CVV. Must be 408 for the Visa test card, 123 for the Verve test card.
- Check the expiry. Must be any date in the future. A past date will fail.
- Check your key prefix.
sk_test_on the server,pk_test_on the client. Both from the same Paystack account. - 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.
- Check the PIN and OTP. If using the Charge API, did you submit PIN 408408 and OTP 123456? Both steps are required.
- 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.
- Initialize a transaction from your server. Confirm you get an
authorization_urloraccess_codeback. - Complete the payment using test card 4084 0840 8408 4081, PIN 408408, OTP 123456.
- Check your Paystack dashboard (Test mode toggle). The transaction should appear as "Success."
- Verify the transaction via the API. The response should show
status: "success"and the correct amount. - If you have webhooks configured, check your webhook endpoint logs. You should receive a
charge.successevent.
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.
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