Skip to main content

Dynamic QR Code

Overview

Dynamic QR Code is another powerful but complex integration to accept payments. This integration is suitable for merchants with engineering capabilities to embed Zalopay into their existing applications.

Payment flow

In the next sections, we guide you step by step to integrate Zalopay. You can try out our live demo application to get a quick overview. A complete example implementation (with NextJs) is in the Github repository.

Prerequisites

Before you begin, make sure the following works done for a smooth integration:

How it works

The payment flow is:

  1. You add a payment button to your web application.
  2. Your buyer clicks the button.
  3. Your backend server calls CreateOrder API to create a new payment order.
  4. Your frontend shows the QR code to the buyer based on the returned response.
  5. The buyer completes payment by scanning the QR code using the Zalopay app and approving the payment.
  6. Your backend server receives payment notifications.
  7. You show a confirmation message to the buyer.

In detail, our flow will look like this:



Integration

This section focuses on how to integrate dynamic QR code flow from Zalopay. A typical workflow includes the following steps:

  1. Add a payment button.
  2. Create an order.
  3. Present QR code.
  4. Show confirmation.

Step 1. Add a payment button

In Cart page, we add a button to redirect to Checkout page:

pages/cart/index.js (Cart page)
// For a  working example, please navigate to: 
// https://github.com/zalopay-samples/quickstart-nextjs-dynamic-qrcode

import Head from 'next/head';
import {useRouter} from "next/router";

const Cart = () => {
const [redirecting, setRedirecting] = useState(false);
const router = useRouter();

const redirectToCheckout = async (e) => {
e.preventDefault();
await router.push('/checkout');
};

return (
<>
<Head>
<title>My Shopping Cart </title>
</Head>
{/* ... */}
<button onClick={redirectToCheckout}>
{redirecting ? 'Redirecting...': 'Go to Checkout'}
</button>
{/* ... */}
</>
)
}
export default Cart;

In Checkout page, for the sake of brevity, we automatically invoke /api/create_order once the page is rendered.

pages/checkout/index.js (Checkout page)
import React, {useEffect, useState} from "react";
import axios from "axios";

const Checkout = () => {
// ...
useEffect(async () => {
// create ZLP order
const res = await axios.post('/api/create_order');
setQrCode(res.data.url);
setAppTransId(res.data.appTransID);
}, [])
// ...
}

Step 2. Create an order

After you have gathered all payment information and prepared for payment, you will need to submit your payment order to the Zalopay server using CreateOrder API.

Here are some examples of how you would handle /api/create_order in your backend:

/src/api/create_order/index.js (Backend API)
// For a  working example, please navigate to:
// https://github.com/zalopay-samples/quickstart-nextjs-dynamic-qrcode

import axios from "axios";
import CryptoJS from "crypto-js";
import moment from "moment";
import { configZLP } from "../config";

const embed_data = { zlppaymentid: "P271021" };
const items = [{}]; // todo: collect items from Cart page
const transID = Math.floor(Math.random() * 1000000);
const appTransID = `${moment().format('YYMMDD')}_${transID}`;
const order = {
app_id: configZLP.app_id,
app_trans_id: appTransID,
app_user: "user123",
app_time: Date.now(), // miliseconds
item: JSON.stringify(items),
embed_data: JSON.stringify(embed_data),
amount: 50000,
description: `Payment for the order #${transID}`,
bank_code: "zalopayapp",
callback_url: configZLP.callback_url
};

const data = [configZLP.app_id, order.app_trans_id, order.app_user, order.amount, order.app_time, order.embed_data, order.item].join("|");
order.mac = CryptoJS.HmacSHA256(data, configZLP.key1).toString();

axios.post(configZLP.endpoint + 'create', null, { params: order })
.then(result => {
res.status(200).json({
appTransID: appTransID,
url: result.data.order_url
});
})
.catch(err => console.log(err));

After Zalopay accepts your payment order, you would receive a response like this:

{
"return_code": 1,
"return_message": "...",
"sub_return_code": 1,
"sub_return_message": "...",
"zp_trans_token": "20090400000112548Y3z18",
"order_url": "https://sbgateway.zalopay.vn/openinapp?order=eyJ6cHRyYW5zdG9rZW4iOiIyMDA5MDQwMDAwMDExMjU0OFkzejE4IiwiYXBwaWQiOjI1NTN9",
"order_token": "ptazBLb128DJ6MSe4d-I2okWQpO7FdUwK9VA4MNqFliUIO1SM3E8ElOsum-iie61rou4A1lblWwddvCwKObzFpo0xJRX4AgP6moSsVqKsxM8K-Duix5ZdH3HFNN07fk7"
}

If you get a response with return_code that differs from 1, consult the reference for status codes to troubleshoot.

Step 3. Present QR code

Once Zalopay completed the payment order creation, it's time for you to present the payment details to your buyer so that they can complete the payment. You will need to translate order_url to a QR code, this can be done by any QR Code generator library which you feel comfortable with.

info

Please make sure you follow Zalopay guideline when you present our trademark.

Here is how Zalopay suggests you generate QR code:

pages/checkout/index.js (CheckoutPage)
// For a  working example, please navigate to: 
// https://github.com/zalopay-samples/quickstart-nextjs-dynamic-qrcode

import { QRCode } from 'antd';

<div id="qr-code">
<QRCode value={result.url}/>
</div>

Step 4. Show confirmation

When your buyer completes the payment by scanning the QR code and confirming the payment, Zalopay will notify the payment completion via callback with a POST request with Content-Type: application/json.

caution
  • It's important to verify that the request actually came from Zalopay by using the callback key to validate the callback's data.
  • For security reasons, your callback url must have the same domain as your server.
  • Your callback endpoint must be publicly accessible.
  • Due to technical issues such as network timeout, your service is unavailable to serve the callback request... Zalopay may not be able to notify you via the callback.

Here are some examples of how you can do it:

api/callback/index.js
// For a  working example, please navigate to: 
// https://github.com/zalopay-samples/quickstart-nextjs-dynamic-qrcode

import CryptoJS from "crypto-js";
import { configZLP } from "../config";

export default async function handler(req, res) {
if (req.method === 'POST') {
try {
let result = {};
try {
let dataStr = req.body.data;
let reqMac = req.body.mac;

let mac = CryptoJS.HmacSHA256(dataStr, configZLP.key2).toString();
console.log("mac =", mac);

if (reqMac !== mac) {
result.return_code = -1;
result.return_message = "mac not equal";
} else {
let dataJson = JSON.parse(dataStr, configZLP.key2);
console.log(`💰 Payment Callback received!`);
console.log("✅ Update order's status = success where app_trans_id =", dataJson["app_trans_id"]);

result.return_code = 1;
result.return_message = "success";
}
} catch (ex) {
result.return_code = 0;
result.return_message = ex.message;
}

res.json(result);
} catch (err) {
res.status(500).json({statusCode: 500, message: err.message});
}
} else {
res.setHeader('Allow', 'POST');
res.status(405).end('Method Not Allowed');
}
}

We strongly recommend that you actively query the payment status. This can be done by creating a request to QueryOrder API.

Our sample code to query the order you just created:

api/query_status/index.js
import axios from "axios";
import CryptoJS from "crypto-js";
import qs from "qs";
import { configZLP } from "../config";

let postData = {
app_id: configZLP.app_id,
app_trans_id: appTransId, // Input your app_trans_id
}

let data = [postData.app_id, postData.app_trans_id, configZLP.key1].join("|"); // appid|app_trans_id|key1
postData.mac = CryptoJS.HmacSHA256(data, configZLP.key1).toString();

let postConfig = {
method: 'post',
url: configZLP.endpoint + 'query',
headers: {
'Content-Type': 'application/x-www-form-urlencoded'
},
data: qs.stringify(postData)
};

const result = await axios(postConfig).then(response => {
return response.data
})
if (returnCode === 1) {
console.log(`💰 Payment received!`);
console.log("✅ Update order's status = success where app_trans_id =", postData["app_trans_id"]);
}

An order will be expired and be marked as INVALID after 15 minutes after its creation.

  • You can stop querying order status after this time box and treat the latest status as its final status.
  • If you didn't receive any callback, you could mark this payment as failed.

After getting the final order status from Zalopay, now you can present to your buyer the latest payment status.

See also

What's next

  • Finish coding? Verify your integration with testing.