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.
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:
- Registered merchant account successfully and obtained
app_id
,mac_key
from Merchant Portal. - Understood the usage and specification of following APIs:
- The concept of callback and secure data transmission.
How it works
The payment flow is:
- You add a payment button to your web application.
- Your buyer clicks the button.
- Your backend server calls CreateOrder API to create a new payment order.
- Your frontend shows the QR code to the buyer based on the returned response.
- The buyer completes payment by scanning the QR code using the Zalopay app and approving the payment.
- Your backend server receives payment notifications.
- 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:
- Add a payment button.
- Create an order.
- Present QR code.
- Show confirmation.
Step 1. Add a payment button
In Cart page, we add a button to redirect to Checkout 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.
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:
// 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.
Please make sure you follow Zalopay guideline when you present our trademark.
Here is how Zalopay suggests you generate QR code:
// 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
.
- 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:
// 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:
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.