Paystack supports QR payments as a payment channel. When you initialize a transaction with the QR channel enabled, Paystack generates a QR code that the customer scans with their banking app. The bank processes the payment and notifies Paystack, which fires a charge.success webhook to your server. QR payments are most useful for in-person sales where the customer has a smartphone with a banking app.
A QR payment on Paystack follows this sequence:
charge.success webhook to your server.From your server's perspective, QR payments look the same as any other payment channel. The initialization and verification steps are identical. The difference is in the customer experience: instead of typing card details or dialing USSD codes, they scan and confirm.
QR payments are faster than bank transfers (no account number to type), more convenient than USSD (no code to remember), and more accessible than card payments (no card needed, just a banking app). For in-person transactions, they provide a smooth scan-and-go experience.
When QR is available as a payment channel on your Paystack account, you can include it in your checkout flow. There are two approaches:
Approach 1: Use Paystack Checkout with QR enabled
Initialize a transaction normally and let the Paystack checkout page show the QR option to the customer. This requires no extra code on your end.
// Initialize transaction (QR will appear as an option on checkout)
const response = await fetch('https://api.paystack.co/transaction/initialize', {
method: 'POST',
headers: {
Authorization: 'Bearer ' + process.env.PAYSTACK_SECRET_KEY,
'Content-Type': 'application/json',
},
body: JSON.stringify({
email: 'customer@example.com',
amount: 350000, // 3,500 NGN
channels: ['qr', 'card', 'bank_transfer'], // Include QR as a channel
metadata: {
order_id: 'pos_order_445',
},
}),
});
const data = await response.json();
// Redirect customer to data.data.authorization_url
// The checkout page will show QR alongside other options
Approach 2: Display QR on your own screen
For POS and kiosk applications, you may want to display the QR code on your own screen instead of redirecting to Paystack's checkout page. You can generate the QR code by encoding the Paystack checkout URL into a QR format using a library.
const QRCode = require('qrcode');
async function generatePaymentQR(amount, email, orderId) {
// Initialize the transaction
const response = await fetch('https://api.paystack.co/transaction/initialize', {
method: 'POST',
headers: {
Authorization: 'Bearer ' + process.env.PAYSTACK_SECRET_KEY,
'Content-Type': 'application/json',
},
body: JSON.stringify({
email,
amount: amount * 100,
metadata: { order_id: orderId },
}),
});
const data = await response.json();
const checkoutUrl = data.data.authorization_url;
// Generate QR code as a data URL
const qrDataUrl = await QRCode.toDataURL(checkoutUrl);
return {
qrImage: qrDataUrl,
reference: data.data.reference,
checkoutUrl,
};
}
When the customer scans this QR code, their phone opens the Paystack checkout page where they can complete the payment using their preferred method.
Get weekly developer tips
Join 25,000+ developers. Practical guides, job tips, and new content — straight to your inbox.
No spam. Unsubscribe anytime.
For retail stores, the QR payment flow typically looks like this:
// POS flow: generate QR and poll for completion
async function startQRPayment(amount, orderId) {
const qr = await generatePaymentQR(amount, 'pos@yourstore.com', orderId);
// Display qr.qrImage on the customer-facing screen
displayOnScreen(qr.qrImage, amount);
// Poll for payment completion
const result = await pollForCompletion(qr.reference, 120); // 2 minute timeout
if (result.status === 'success') {
printReceipt(orderId, amount, qr.reference);
clearScreen();
return { success: true, reference: qr.reference };
} else {
clearScreen();
return { success: false, reason: result.status };
}
}
async function pollForCompletion(reference, timeoutSeconds) {
const deadline = Date.now() + timeoutSeconds * 1000;
while (Date.now() < deadline) {
try {
const response = await fetch(
'https://api.paystack.co/transaction/verify/' + reference,
{
headers: {
Authorization: 'Bearer ' + process.env.PAYSTACK_SECRET_KEY,
},
}
);
const data = await response.json();
if (data.data.status === 'success') {
return { status: 'success', data: data.data };
}
if (data.data.status === 'failed') {
return { status: 'failed', data: data.data };
}
} catch (error) {
// Network error, continue polling
}
// Wait 3 seconds before next check
await new Promise(function(resolve) { setTimeout(resolve, 3000); });
}
return { status: 'timeout' };
}
For the in-store flow, polling is often more practical than webhooks because the cashier needs immediate feedback on the screen. The webhook still runs in the background as a backup to update your order database.
There are two types of QR codes you can use for payments:
Dynamic QR codes: Generated per transaction. Each code contains a specific amount and reference. The customer scans, the amount is pre-filled, and they just confirm. This is what you want for POS transactions where the amount is known.
Static QR codes: A permanent code that links to a payment page. The customer scans, then enters the amount themselves. This works for tip jars, donation boxes, or situations where the amount varies. You can implement this with a Paystack Payment Page link encoded into a QR code.
// Generate a static QR code for a payment page
const QRCode = require('qrcode');
async function generateStaticQR(paymentPageSlug) {
const paymentUrl = 'https://paystack.com/pay/' + paymentPageSlug;
const qrDataUrl = await QRCode.toDataURL(paymentUrl);
return qrDataUrl;
}
// Print this QR code on a sticker, poster, or receipt
// Customer scans and enters their own amount on the payment page
Static QR codes are great for physical businesses. Print them on a sign at the counter, on a menu, or on business cards. The customer can pay at their convenience without the cashier needing to generate a new code each time.
Dynamic QR codes are better for retail checkout where the amount is fixed per transaction. They reduce errors (the customer does not type the amount) and speed up the process.
QR payments trigger the same charge.success webhook as card or bank transfer payments. Your existing webhook handler works without changes.
// Your existing webhook handler works for QR payments
app.post('/webhooks/paystack', async (req, res) => {
// Verify webhook signature
const hash = crypto
.createHmac('sha512', process.env.PAYSTACK_SECRET_KEY)
.update(JSON.stringify(req.body))
.digest('hex');
if (hash !== req.headers['x-paystack-signature']) {
return res.sendStatus(401);
}
const event = req.body;
if (event.event === 'charge.success') {
const data = event.data;
const reference = data.reference;
const amount = data.amount;
const channel = data.channel; // "qr" for QR payments
// Verify the transaction
const verify = await fetch(
'https://api.paystack.co/transaction/verify/' + reference,
{
headers: {
Authorization: 'Bearer ' + process.env.PAYSTACK_SECRET_KEY,
},
}
);
const verified = await verify.json();
if (verified.data.status === 'success') {
await fulfillOrder(reference, amount);
}
}
res.sendStatus(200);
});
The only difference is the channel field in the transaction data, which will show "qr" for QR payments. If you track payment methods for analytics, capture this field. Otherwise, your webhook handler is channel-agnostic.
QR payments through Paystack have some limitations to be aware of:
Always offer QR alongside other payment methods, never as the only option. A customer whose bank does not support QR payments should be able to fall back to card, bank transfer, or USSD.
For the complete accept payments overview, see the accept payments guide. For other payment channels, see all Paystack payment channels.
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