Skip to main content

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.

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:

  • 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

Payment flow

Flow 2: Payment and token return

Payment flow

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

  1. 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": ""
}
  1. 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:

ParamsData typeMax lengthDescription
binding_idString64The ID of the binding token has been approved to auto-debit.
statusInt1: Success, otherwise fail

Using this binding_id to query the binding status in case you didn't receive the binding callback from Zalopay.

  1. 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:

NoParamsData typeMax lengthDescription
1app_idIntThe app id of the merchant.
2app_trans_idString40The merchant's unique id for the binding.
3binding_typeString20Specify which type of binding. In this case, This value is CARD
4binding_idString64The id of binding has been confirmed in the Zalopay system. Merchant needs store binding_id value for using API unbinding
5pay_tokenString128The unique code that represents a customer's payment information. Merchant needs to store pay_token to use for subsequent payments
6server_timeIntServer timestamp in seconds.
7merchant_user_idString128The identifier field in Create binding request
8statusInt1: Active, 3: Cancelled
9msg_typeIntType of message:
1: The user confirms an agreement
2: The user updates the agreement
10card_idString128The unique identifier for a payment card, used to check for duplicate information
11masked_card_numberString20Masked user card number (Ex: "520032********5097")
12issuing_bank_logoString256Url to get logo of issuing bank
13issuing_bank_nameString64Bank 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

ParamData typeFormatExample
bindinginfoJSON 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

  1. 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.

  1. 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.

  1. 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"
}