Card Token
Overview
Card-on-file (so-called card tokenization) is a feature that enables customers to give consent to store their payment card details for future use, allowing for faster checkouts and ease of use for online payment:
- Support credit card
- Support ATM card with + 40 bank via NAPAS
Card-on-file payments are a popular choice across many different industries:
- Subscription: Customer provides consent to the business to bill their card periodically for a subscription
- Ecommerce: The cardholder's payment details are kept on file to avoid having them re-enter their card details for every order
- Travel: Stored card details often used to easily refund and issue fines for incidents, such as no-show charges, etc.
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:
- CreateOrder API
- QueryOrder API
- CreateBinding API
- Unbind API
- PayByToken API
- QueryPaymentToken API
The concept of callback and secure data transmission.
How it works
Flow 1: Card binding
Flow 2: Payment and token return
Integration
This section focuses on how to integrate card token flow from Zalopay. A typical workflow includes the following steps:
- Initiate a binding card.
- Pay using card token.
- Unbind an card token.
Step 1. Initiate a binding card
To user can check out using the card tokenization feature, the merchant must first initiate a request to create a new binding for an agreement that links ATM/CC of your user with your application.
Card binding flow
- From your server, call Create Binding API. For more specifying, please refer to the API Explorer for other critical parameters.
Here's an example of how you create a binding for an agreement:
/ For a working example, please navigate to:
// https://github.com/zalopay-samples/quickstart-nextjs-tokenized-payment
import axios from "axios";
import CryptoJS from "crypto-js";
import {
API_ROUTES,
configZLP,
ZLP_API_PATH
} from "../../config";
export default async function handler(req, res) {
if (req.method === 'POST') {
try {
const binding_data = "";
const bind = {
app_id: configZLP.app_id,
app_trans_id: req.body.appTransID,
req_date: Date.now(), // milliseconds
identifier: "ZLP User",
max_amount: 0,
binding_type: 'CARD',
binding_data: JSON.stringify(binding_data),
callback_url: configZLP.host + API_ROUTES.AGREEMENT_CALLBACK,
redirect_url: "http://localhost:3000/cart" // testing purpose
};
// app_id|app_trans_id|binding_data|binding_type|identifier|max_amout|req_date
const data = configZLP.app_id + "|" + bind.app_trans_id + "|" + bind.binding_data + "|" + bind.binding_type + "|" + bind.identifier + "|" + bind.max_amount + "|" + bind.req_date;
bind.mac = CryptoJS.HmacSHA256(data, configZLP.key1).toString();
axios.post(configZLP.zlp_endpoint + ZLP_API_PATH.AGREEMENT_BINDING, null, {
params: bind
})
.then(result => {
if (result.data.return_code === 1) {
res.status(200).json({
binding_token: result.data.binding_token,
binding_qr_link: result.data.binding_qr_link // web-based scenario
});
} else {
res.status(200).json({
error: true,
error_code: result.data.sub_return_code,
message: result.data.sub_return_message
});
}
})
.catch(err => console.log(err));
} catch (err) {
res.status(500).json({
statusCode: 500,
message: err.message
});
}
} else {
res.setHeader('Allow', 'POST');
res.status(405).end('Method Not Allowed');
}
}
After you would receive a response like this:
{
"return_code": 1,
"return_message": "Operation success.",
"sub_return_code": 1,
"sub_return_message": "Operation success.",
"binding_token": "",
"deep_link": "",
"binding_qr_link": "https://qcgateway.zalopay.vn/binding?binding_id=231120EVqxdkzxWxQQuVyqDpZ9dR0DW",
"short_link": ""
}
- Present website for binding card token
The binding response includes binding_qr_link
links that the merchant needs to navigate to the user can confirm the binding based on the merchant platform.
After confirmation, Zalopay will redirect to the merchant website or merchant app with params:
Params | Data type | Max length | Description |
---|---|---|---|
binding_id | String | 64 | The ID of the binding token has been approved to auto-debit. |
status | Int | 1: Success, otherwise fail |
Using this binding_id
to query the binding status in case you didn't receive the binding callback from Zalopay.
- Receive binding result
When your user completes the binding by input information Card on website and confirming, Zalopay will notify the binding result via callback with a POST request with Content-Type: application/json
.
The callback data includes the pay_token field, which the merchant will store in the system, for later payments using this token. For more specifying:
No | Params | Data type | Max length | Description |
---|---|---|---|---|
1 | app_id | Int | The app id of the merchant. | |
2 | app_trans_id | String | 40 | The merchant's unique id for the binding. |
3 | binding_type | String | 20 | Specify which type of binding. In this case, This value is CARD |
4 | binding_id | String | 64 | The id of binding has been confirmed in the Zalopay system. Merchant needs store binding_id value for using API unbinding |
5 | pay_token | String | 128 | The unique code that represents a customer's payment information. Merchant needs to store pay_token to use for subsequent payments |
6 | server_time | Int | Server timestamp in seconds. | |
7 | merchant_user_id | String | 128 | The identifier field in Create binding request |
8 | status | Int | 1: Active, 3: Cancelled | |
9 | msg_type | Int | Type of message: 1: The user confirms an agreement 2: The user updates the agreement | |
10 | card_id | String | 128 | The unique identifier for a payment card, used to check for duplicate information |
11 | masked_card_number | String | 20 | Masked user card number (Ex: "520032********5097 ") |
12 | issuing_bank_logo | String | 256 | Url to get logo of issuing bank |
13 | issuing_bank_name | String | 64 | Bank issues card (Ex: "Vietinbank ") |
Payment and token return flow
The Zalopay system provides a payment process and a token return flow. This flow is intended for users to make payments and confirm the binding option in the Zalopay Gateway.
In this process of payment and token return flow, when your call Create Order API
, the embed_data
needs to include the bindinginfo
parameter. The bindinginfo
data must contain the following details:
- agree_to_link: The user has agreed to save the payment information for the next time
- app_trans_id: The unique ID is used to initialize a binding request and is generated by the merchant system. It serves as a unique transaction ID for the application (TXID of the binding transaction). It is used for querying the payment token API. Example: 230313_123456.
- identifier: This refers to the user's identifier on the merchant system, which can be the merchant user's ID, phone number, email, etc. Example: 84903863801.
- binding_type: This parameter specifies the type of binding. Example: CARD.
- callback_url: The Zalopay tokenization system will notify the merchant system via this URL. Example: https://merchant.com/binding-result/
Specific embed_data's fields
Param | Data type | Format | Example |
---|---|---|---|
bindinginfo | JSON String | {"field_name":"value"} | {"bindinginfo":"{\"agree_to_link\":true,\"app_trans_id\":\"230313_123456\",\"identifier\":\"84903863801\",\"binding_type\":\"CARD\",\"callback_url\":\"https://merchant.com/binding-result/\"}"} |
Step 2. Pay using token
- Call
Create Order API
to create an order for agreement pay.
Here are some examples of how you would handle it 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 = {};
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.
- After merchants created the order for agreement pay successfully, you need to call
Pay By Token API
to notify Zalopay to process payment. Here's an example of how you pay by token:
// 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 {
configZLP,
ZLP_API_PATH
} from "../../config";
export default async function handler(req, res) {
if (req.method === 'POST') {
try {
const postData = {
app_id: configZLP.app_id,
identifier: "ZLP User",
binding_type: "CARD"
pay_token: req.body.payToken,
zp_trans_token: req.body.zpTransToken,
req_date: Date.now(), // milliseconds
};
// ap_pid|identifier|zp_trans_token|pay_token|req_date
const data = configZLP.app_id + "|" + postData.identifier + "|" + postData.zp_trans_token + "|" + postData.pay_token + "|" + postData.req_date;
postData.mac = CryptoJS.HmacSHA256(data, configZLP.key1).toString();
axios.post(configZLP.zlp_endpoint + ZLP_API_PATH.AGREEMENT_PAY, null, {
params: postData
})
.then(result => {
if (result.data.return_code === 1) {
res.status(200).json(result.data);
} else {
res.status(200).json({
error: true,
error_code: result.data.sub_return_code,
message: result.data.sub_return_message
});
}
})
.catch(err => console.log(err));
} catch (err) {
console.log(err)
res.status(500).json({
statusCode: 500,
message: err.message
});
}
} else {
res.setHeader('Allow', 'POST');
res.status(405).end('Method Not Allowed');
}
}
After you would receive a response like this:
{
"return_code": 3,
"return_message": "string",
"sub_return_code": 3,
"sub_return_message": "string",
"verification_url": "https://qcgateway.zalopay.vn/querypaybytokenstatus?order=eyJhcHBpZCI6MTAwMzMsInpwdHJhbnN0b2tlbiI6IkFDTG0xNkI4TFVsY1IxWEZldXBDTDQ5dyJ9"
}
The Pay by token response includes verification_url
links that the merchant needs to navigate to the URL for the user input OTP confirming paying.
- Callback agreement order
After payment has been processed successfully, a callback will be sent to the merchant for detail of the payment.
The logic to handle it same as Dynamic QR, you can refer here.
Step 3. Unbind an agreement
In case your user doesn't want to use the auto-debit feature, call Unbind API
with binding_id
returned when the binding is confirmed to Zalopay.
Example response:
{
"return_code": 0,
"return_message": "string",
"sub_return_code": 0,
"sub_return_message": "string"
}