Bonaventure OgetoBy Bonaventure Ogeto|

Accepting QR Payments Through Paystack

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.

How QR Payments Work on Paystack

A QR payment on Paystack follows this sequence:

  1. Your server initializes a transaction with the amount and customer email.
  2. Paystack generates a QR code containing the payment details (amount, merchant, reference).
  3. The customer opens their banking app and scans the QR code.
  4. The banking app shows the payment details and asks the customer to confirm.
  5. The customer confirms (using their banking app PIN, fingerprint, or face ID).
  6. The bank debits the customer and notifies Paystack.
  7. Paystack fires a charge.success webhook to your server.
  8. Your server verifies the transaction and grants value.

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.

Generating QR Codes for Payment

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.

In-Store QR Payment Flow

For retail stores, the QR payment flow typically looks like this:

  1. The cashier enters the total amount on the POS system or tablet.
  2. The system generates a QR code and displays it on the customer-facing screen.
  3. The customer scans the QR code with their banking app.
  4. The system polls for payment confirmation or waits for the webhook.
  5. On confirmation, the system prints a receipt and clears the transaction.
// 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.

Static vs Dynamic QR Codes

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.

Webhook Handling for QR Payments

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 Payment Availability and Limitations

QR payments through Paystack have some limitations to be aware of:

  • Bank support varies. Not all banks support QR scan-to-pay. The customer needs a banking app that supports scanning Paystack QR codes. If their bank does not support it, they need an alternative payment method.
  • Country availability. QR payment availability depends on the country and the specific banks operating there. Check the Paystack documentation for current QR support in your market.
  • Customer education. Many customers are unfamiliar with QR payments. You may need signage or instructions explaining how to scan and pay, especially in markets where QR payments are newer.
  • Offline limitations. The customer needs an internet connection on their phone to scan the QR code and complete the payment. Unlike USSD, QR payments are not offline-friendly on the customer side.

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.

Stay Up to Date

QR payment support on Paystack is expanding as more banks adopt the technology. We update these guides as new QR features and bank partnerships launch.

Join the McTaba newsletter for Paystack engineering updates.

Sign up for the McTaba newsletter

Key Takeaways

  • QR payments work by generating a code that the customer scans with their banking app. The bank processes the payment directly, and Paystack receives confirmation. No card details, no USSD codes, no manual transfers.
  • Paystack can generate QR codes as part of the checkout flow. When QR is available as a payment channel, the checkout page shows the QR code alongside other payment options.
  • QR payments are ideal for in-person scenarios: retail stores, market stalls, events, and service businesses. The customer scans, confirms on their phone, and the payment is done.
  • Your server receives the same charge.success webhook for QR payments as for any other channel. Your webhook handler does not need channel-specific logic.
  • QR codes can be displayed on screens (POS, tablet, phone) or printed on paper (receipts, invoices, signage). Dynamic QR codes change per transaction. Static QR codes link to a payment page where the customer enters the amount.
  • Not all banks support QR scan-to-pay. Availability depends on the customer bank and the country. Always offer alternative payment methods alongside QR.

Frequently Asked Questions

Which banks support QR payments through Paystack?
QR payment support depends on the country and the specific banks that have enabled scan-to-pay in their apps. Paystack works with partner banks to enable QR payments. Check the Paystack documentation or your dashboard for the current list of supported banks in your market.
Can I use QR payments for online stores, not just in-person?
Yes. You can display a QR code on your website during checkout. The customer scans it with their phone and completes the payment via their banking app. This is useful when the customer is browsing on a desktop computer but wants to pay with their phone. However, if the customer is already on their phone, scanning a QR code on the same device is awkward. In that case, direct them to the Paystack checkout page instead.
How long is a dynamic QR code valid?
Dynamic QR codes are tied to a Paystack transaction, and transactions have a limited validity window. The exact timeout depends on the Paystack access code expiry, which is typically a few hours. For POS applications, generate a fresh QR code for each sale rather than reusing old ones.
Do QR payments have different fees than card payments?
Fee structures may differ between payment channels. QR payments, card payments, and bank transfers each have their own fee schedule depending on your Paystack account agreement and country. Check your Paystack dashboard for the specific fees that apply to each channel.
Can I customize the QR code appearance?
If you generate the QR code yourself using a library like qrcode (encoding the Paystack checkout URL), you can customize colors, add a logo in the center, and adjust the size. If you use the QR code generated by the Paystack checkout page, the appearance is controlled by Paystack.

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