DEV Community

Cover image for How To Set Up QR Code Payments in Your App
uma victor for Flutterwave Engineering

Posted on

How To Set Up QR Code Payments in Your App

You’re at your favorite local restaurant, ready to pay for your meal. Instead of reaching for your cash or cards, you pull out your phone, scan a quick-response (QR) code on the table, and your contactless payment is complete in seconds. QR code-based payments are a massive global phenomenon, with millions of successful transactions being processed daily for various goods.

If you're a developer building mobile applications that handle payments, QR codes offer the opportunity for a faster and simplified payment process for your customers. No more complex checkout flows. QR codes reduce complex checkout flows and help minimize cart abandonment. While customers still need to authenticate their payments (via PIN, OTP, or biometrics), the process is significantly faster than traditional payment methods.

By the end of this guide, you'll know exactly how QR code payments work, how to implement them using Flutterwave's APIs, and the security practices that'll keep your users' transactions safe.

How Do QR Code Payments Work?

Before we jump into implementation, let's break down what happens when someone pays with a QR code. Understanding this flow will help you build better payment experiences.

QR Payment Flow

Here's the step-by-step process:

  1. QR Code Generation: Your app creates a unique QR code containing payment details like amount, merchant info, and transaction reference. Think of it as encoding all the payment information into a visual format that any smartphone camera can read.
  2. Customer Scans the Code: The customer opens their banking app, payment app, or even just their phone's camera and points it at your QR code. Most modern smartphones can read QR codes natively.
  3. Payment Information Extraction: Once scanned, the QR code reveals the embedded payment data. The customer's app now knows exactly what they're paying for, how much, and where the money should go.
  4. Payment Authorization: The customer reviews the transaction details and confirms the payment using their preferred method (mobile banking, digital wallet, or payment app).
  5. Transaction Processing: The payment flows through the banking network or payment processor. This happens in the background while your app waits for confirmation.
  6. Confirmation and Completion: Both you and the customer receive real-time notifications about the payment status. Your app gets webhook notifications to update the transaction status automatically.

The beauty of QR payments lies in their simplicity. Customers can complete QR code transactions in just two clicks, making them perfect for everything from retail stores to mobile apps.

Different QR Payment Methods

Before you set up QR payments in your app, you need to understand the different types of QR payments. Choosing the right model affects both your user experience and technical approach.

Static QR Codes
Static QR codes are reusable and don't contain a specific amount. Think of them like a digital "pay here" sign. They work by allowing the customer to scan your QR code, then enter the amount they want to pay. They’re perfect for scenarios where payment amounts vary. You can use them in:

  • Restaurant table payments where customers enter the total themselves
  • Donation boxes or tip jars
  • Service businesses with variable pricing

Dynamic QR Codes
Dynamic QR codes are generated for each transaction and contain specific payment details, including the exact amount. Your app generates a new QR code for each transaction, containing all the payment details. The customer just scans and confirms. No amount entry needed. You can use them in:

  • E-commerce checkouts
  • Bill payments
  • Any scenario where you know the exact amount upfront

Different QR payment models

Merchant-Presented vs. Customer-Presented QR Payment Modes
Both static and dynamic QR payment models can be in merchant-presented mode
or customer-presented mode.
Merchant-Presented Mode
In merchant-presented mode, your app displays the QR code, and the customer scans it with their banking app
Customer-Presented Mode
In customer-presented mode, the customer displays a QR code from their banking app, and you scan their code with your app. This is mostly common in in-person service businesses.

For most mobile app implementations, you'll use dynamic QR codes in merchant-presented mode, which is what we will be implementing with Flutterwave.

How To Set Up a QR Code for Payment with Flutterwave

Let's build a QR payment system. You'll create dynamic QR codes that customers can scan to pay instantly, handle real-time payment confirmations, and add proper security measures to protect transactions.

In this guide, we‘ll use Flutterwave's v3 API.

Prerequisites
Before you start, make sure you have:

  • A Flutterwave account
  • Your API keys from the Flutterwave dashboard API Keys

Step 1: Initialize the Payment Request
First, let's create a QR code payment request using Flutterwave's API:

    const initializeQRPayment = async (paymentData) => {
      const payload = {
        tx_ref: `qr-${Date.now()}`, // Unique transaction reference
        amount: paymentData.amount,
        currency: "NGN", 
        email: paymentData.customerEmail,
        fullname: paymentData.customerName,
        phone_number: paymentData.phone,
        is_nqr: "1", 
        redirect_url: "https://your-app.com/payment-callback"
      };

      try {
        const response = await fetch('https://api.flutterwave.com/v3/charges?type=qr', {
          method: 'POST',
          headers: {
            'Authorization': `Bearer ${process.env.FLW_SECRET_KEY}`,
            'Content-Type': 'application/json',
          },
          body: JSON.stringify(payload)
        });

        const result = await response.json();
        return result;
      } catch (error) {
        console.error('QR payment initialization failed:', error);
        throw error;
      }
    };
Enter fullscreen mode Exit fullscreen mode

Step 2: Display the QR Code to Your Customer
Once you get a successful response, Flutterwave returns a base64-encoded QR code image. Here's how to display it in your app:

    const displayQRCode = (qrImageData) => {
      // For web applications
      const qrImage = document.getElementById('qr-code-image');
      qrImage.src = qrImageData;

      // For mobile apps, you'll handle this differently
      // React Native example:
      // <Image source={{uri: `data:image/png;base64,${qrImageData}`}} />
    };

    // Usage example
    const processQRPayment = async (orderDetails) => {
      try {
        const paymentResponse = await initializeQRPayment({
          amount: orderDetails.total,
          customerEmail: orderDetails.customerEmail,
          orderId: orderDetails.id
        });

        if (paymentResponse.status === 'success') {
          // Display the QR code to the customer
          displayQRCode(paymentResponse.meta.authorization.qr_image);

          // Store transaction ID for verification
          const transactionId = paymentResponse.data.id; 
          localStorage.setItem('currentTransactionId', transactionId);
        }
      } catch (error) {
        // Handle error appropriately
        showErrorMessage('Unable to generate QR code. Please try again.');
      }
    };
Enter fullscreen mode Exit fullscreen mode

Step 3: Handle Payment Verification
QR payments occur outside your app, so you need to know when customers complete their payments and verify the transactions. Here's how to handle verification:

First, let's understand what you get back from Step 1. When you create a QR payment, Flutterwave returns something like this:

    // Response from initializeQRPayment
    {
      "status": "success",
      "message": "Charge initiated",
      "data": {
        "id": 8217804,                    // This is your transaction ID
        "tx_ref": "qr-1642123456789",
        "flw_ref": "FLWTK43726MCK1732546764469",
        "amount": 1000,
        "charged_amount": 1000,
        "app_fee": 5,
        "currency": "NGN",
        "status": "pending",
        "payment_type": "nibss-qr",
        "customer": {
          "id": 2539403,
          "name": "Billy Butcher",
          "email": "user@example.com",
          "phone_number": "080000000"
        }
      },
      "meta": {
        "authorization": {
          "qr_image": "data:image/jpeg;base64,iVBORw0KGgoAAAANS...",  // QR code is here!
          "mode": "qr",
          "validate_instructions": "The QR code provided is for demonstration purposes only."
        }
      }
    }
Enter fullscreen mode Exit fullscreen mode

Now you have two ways to know when the payment completes:

Option 1: Webhooks (Recommended)
Set up a webhook endpoint to get instant notifications. This is the best approach for user experience:

    // Webhook endpoint - gets called immediately when payment completes
    app.post('/webhook/flutterwave', (req, res) => {
      const secretHash = process.env.FLW_SECRET_HASH;
      const signature = req.headers["verif-hash"];

      // Always verify webhook signatures
      if (!signature || (signature !== secretHash)) {
        return res.status(401).send('Unauthorized');
      }

      const payload = req.body;

      if (payload.event === 'charge.completed') {
        // Always verify server-side even after webhook
        verifyTransactionServerSide(payload.data.id);
      }

      res.status(200).send('OK');
    });
Enter fullscreen mode Exit fullscreen mode

Option 2: Manual Verification (Fallback)
Sometimes you need to check payment status manually - maybe the webhook failed or the customer claims they paid:

    // Verify using the transaction ID from Step 1 response
    const verifyTransaction = async (transactionId) => {
      try {
        const response = await fetch(`https://api.flutterwave.com/v3/transactions/${transactionId}/verify`, {
          method: 'GET',
          headers: {
            'Authorization': `Bearer ${process.env.FLW_SECRET_KEY}`,
            'Content-Type': 'application/json',
          }
        });

        const result = await response.json();

        if (result.status === 'success' && result.data.status === 'successful') {
          // Payment verified - process the order
          processSuccessfulPayment(result.data);
          return true;
        }

        return false;
      } catch (error) {
        console.error('Verification failed:', error);
        return false;
      }
    };


    const checkPaymentStatus = async (transactionId) => {
      const isVerified = await verifyTransaction(transactionId);
      if (isVerified) {
        updateUI('Payment successful!');
      } else {
        updateUI('Payment still pending...');
      }
    };
Enter fullscreen mode Exit fullscreen mode

Always verify transactions server-side, even after receiving webhooks. Never trust payment status from your frontend alone.

QR Payments Security Best Practices

Security isn't optional when dealing with payments. Here are some key practices to look out for when integrating QR payments.

1. Verify Every Webhook Properly
Flutterwave sends payment notifications with a cryptographic signature. Before processing any webhook, check the verif-hash header against your secret hash using HMAC-SHA256.. Without this verification, attackers could send fake payment confirmations to your system. Set up your secret hash in your Flutterwave dashboard under Settings > Webhooks.

2. Always Verify Transactions Server-Side
Never trust payment status from your mobile app. After receiving a webhook or when a customer claims payment was successful, make a server-side call to Flutterwave's verification endpoint using the transaction ID. Check that the status is "successful," the amount matches what you expected, and the currency is correct.

3. Monitor Your Integration
Log all payment attempts, webhook receipts, and verification calls. Watch for patterns like multiple failed verification attempts or webhooks with invalid signatures. Set up alerts for unusual activity like payments from unexpected amounts or currencies.

4. Protect Your API Keys Like Passwords
Your Flutterwave secret key can process payments on your behalf. Store it securely on your server using environment variables or a secrets manager. Never embed secret keys in mobile app code where they can be extracted. All API calls requiring secret keys must originate from your secure backend server.

Wrapping Up

The benefits of QR code payments are clear: faster checkouts, reduced cart abandonment, better user experience, and access to valuable transaction data. Additionally, implementation is straightforward and secure because of Flutterwave's powerful API.

Remember the key points:

  • QR payments work through a simple scan-and-pay flow.
  • Flutterwave provides easy-to-use APIs for QR code generation.
  • Always verify payments server-side and implement proper security measures.
  • Use webhooks for real-time payment notifications.

Ready to transform how your customers pay? Explore Flutterwave's payment solutions and start building the future of payments today.

Top comments (0)