Skip to main content

Callback

Overview

Callback or Webhook is a mechanism that automatically notifies merchants of events related to Zalopay transactions. Third-party users and developers can manage these callbacks. They are user-defined HTTP callbacks that allow two independent online applications to communicate.

Flow

When a certain transaction is successful:

  • Zalopay Server will notify to Merchant Server via Callback URL which was previously registered e.g. via createOrder API.
  • Merchant Server uses key2 (provided by Zalopay) to validate callback data.
  • Merchant can check the callback log in the Sandbox environment on Merchant Portal

Request

Your callback endpoints should be accessible by Zalopay Server.

  • Method: POST
  • Content-Type: application/json

The endpoint should accept data with the following schema (apply for all products):

ParamsData typeDescription
dataJson StringThe callback data. The format may vary depending on the specific product.
macstringConfirmation of order information, using key2 (had been provided) to verify the order
typeintCallback type
- type=1: Order
- type=2: Agreement

The callback data for the specified product is presented as follows:

Order callback data
ParamsData typeDescription
app_idIntOrder's app_id
app_trans_idStringOrder's app_trans_id
app_timeInt64Order's app_time
app_userStringOrder's app_user
amountInt64Amount received (VND)
embed_dataJson StringOrder's embed_data.
For example: {"promotioninfo":"","merchantinfo":"merchant-defined data"}
itemJson ArrayOrder's item.
For example: [{"itemid":"knb","itename":"plantA","itemprice":198400,"itemquantity":1}]
zp_trans_idInt64Zalopay's Transaction code
server_timeInt64Zalopay's Transaction time (unix timestamp in milliseconds)'
channelIntPayment channels.
merchant_user_idStringZalopay user, who paid for the order
user_fee_amountInt64Fee (VND)
discount_amountInt64Discount (VND)

Example:

{
"data": "{\"app_id\":2638,\"app_trans_id\":\"230407_13583500399\",\"app_time\":1680850715576,\"app_user\":\"Zalopay\",\"amount\":50000,\"embed_data\":\"{}\",\"item\":\"[]\",\"zp_trans_id\":230407000006575,\"server_time\":1680850894407,\"channel\":38,\"merchant_user_id\":\"Bs8KJXf2GKyfVv_fnxG7IwG5VbQ8H_qGsOUW2UJAJSM\",\"zp_user_id\":\"Bs8KJXf2GKyfVv_fnxG7IwG5VbQ8H_qGsOUW2UJAJSM\",\"user_fee_amount\":0,\"discount_amount\":0}",
"mac": "761c9be42fd419b7078e4cf42e5034238ced266b2e094bfd4ce242f99e61b172",
"type": 1
}
Tokenization/Agreement callback data

The callback data encompasses details regarding confirmed or updated agreements.

ParamsData typeDescription
app_idIntThe app id of the merchant.
app_trans_idStringThe merchant's unique id for the binding.
binding_idStringThe id of binding has been confirmed in the Zalopay system.
pay_tokenStringThe public token is used when doing auto-debit.
server_timeIntServer timestamp in seconds.
merchant_user_idStringThe identifier field in bind request.
statusIntType of message:
1: The user confirms an agreement
2: The user updates the agreement
msg_typeInt1: Success, otherwise fail
zp_user_idStringThe identifier of Zalopay user per merchant app_id.
masked_user_phoneStringMasked user phone (Ex: masked_user_phone: "****6938").

Example:

{
"data": "{\"app_id\":2638,\"app_trans_id\":\"230407_13221300383\",\"binding_id\":\"230407qQe7vGnqp0agyforLAy0D2b1x3\",\"pay_token\":\"NJLHNTRLZDETZGQYZI0ZNTBKLTGWZJKTMTKXYJHLMWI5NTFI\",\"merchant_user_id\":\"84903863801\",\"zp_user_id\":\"Bs8KJXf2GKyfVv_fnxG7IwG5VbQ8H_qGsOUW2UJAJSM\",\"masked_user_phone\":\"****3801\",\"server_time\":1680848564,\"status\":1,\"msg_type\":1,\"expiry_timestamp_in_ms\":1996467763977}",
"mac": "bed74dc38e023d7402297107e1e518b16731d1689ebf77120872e12d9e36882a",
"type": 2
}
ZOD callback data
ParamsData typeDescription
appIdStringThe app identifier is provided by Zalopay.
mcRefIdStringThe merchant order's reference ID.
amountIntAmount of order.
zpTransIdIntThe Zalopay transaction id.
serverTimeIntServer timestamp in seconds.
channelIntPayment channels.
zpUserIdStringThe identifier of Zalopay user per merchant.
userFeeAmountIntFee amount.
discountAmountIntDiscount amount.
userChargeAmountIntAmount charged from user.

Example:

{
"data": "{\"appId\":\"15011\",\"mcRefId\":\"LZD201230_23423453\",\"amount\":30000,\"zpTransId\":210126000000814,\"serverTime\":1611633042737,\"channel\":38,\"zpUserId\":\"200601589000506\",\"userFeeAmount\":0,\"discountAmount\":0,\"userChargeAmount\":30000}",
"mac": "dd163f1bc82a92eb8c9619337f47298fee0c95b1c764767ad18f358f0e8120a9",
"type": 1
}

Response

ParamsData typeDescription
return_codeInt1: Success
2: Failed
return_messageStringDescription of return code

For the ZOD product, the expected response format should be as follows:

ParamsData typeDescription
returnCodeInt1: Success
2: Failed
returnMessageStringDescription of return code

Implementation

Validation

Your callback URLs can be susceptible from hacker attacks. It's a must for you to validate if a callback is valid with HMAC.

reqmac = HMAC(hmac_algorithm, key2, callback_data.data);
if (reqmac == callback_data.mac) {
// valid callback
} else {
// invalid callback
}

in which:

  • hmac_algorithm: is a security method registered by Merchant with Zalopay, the default is HmacSHA256
  • key2: provided by Zalopay at registration
  • callback_data: is data requested by Zalopay to Merchant's callback API when Zalopay has successfully collected money from customers.

Examples

// Node v10.15.3
const CryptoJS = require("crypto-js"); // npm install crypto-js
const express = require("express"); // npm install express
const bodyParser = require("body-parser"); // npm install body-parser
const app = express();

const config = {
key2: "eG4r0GcoNtRGbO8",
};

app.use(bodyParser.json());

app.post("/callback", (req, res) => {
let result = {};

try {
let dataStr = req.body.data;
let reqMac = req.body.mac;

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

// check if the callback is valid (from Zalopay server)
if (reqMac !== mac) {
// callback is invalid
result.return_code = -1;
result.return_message = "mac not equal";
} else {
// payment success
// merchant update status for order's status
let dataJson = JSON.parse(dataStr, config.key2);
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; // callback again (up to 3 times)
result.return_message = ex.message;
}

// returns the result for Zalopay server
res.json(result);
});

app.listen(8888, function () {
console.log("Server is listening at port :8888");
});

For other examples, please navigate to https://github.com/orgs/Zalopay-samples/repositories

Recommendation

Retry

Due to technical issues such as network timeout or your service unavailability, Zalopay may not be able to notify you via the callback. After 15 minutes from the time of the order establishment, if you still do not receive a callback from Zalopay, the merchant needs to call QueryOrder API proactively to get the final result.

Idempotence

Callback endpoints may receive duplicate events, which can cause issues if the events are processed multiple times. To ensure that these duplicate events don't cause problems, it's important to make the event processing idempotent. This means that processing the same event multiple times should have the same result as processing it just once. One way to achieve this is by tracking the events that have been processed and avoiding reprocessing events that have already been tracked.

Using HTTPS

By using an HTTPS URL for your callback endpoint, you can ensure that the connection between the client and server is secure and encrypted, providing protection against potential security threats. With HTTPS, data transmitted between the server and client is encrypted, preventing attackers from intercepting and reading sensitive information.

CSRF protection

Web frameworks such as Rails, Django, or others may automatically verify that each POST request includes a CSRF token to safeguard against cross-site request forgery attempts. Although this is a crucial security feature to protect both you and your users, it may hinder the processing of valid events on your website. In such cases, you may have to exclude the callback route from CSRF protection.

For example with Express.js:

const express = require("express");
const cookieParser = require("cookie-parser");
const csrf = require("csurf");
const app = express();

// Set up middleware
app.use(express.urlencoded({ extended: false }));
app.use(cookieParser());

const protectCSRF = csrf({ cookie: true });

// Set up CSRF protection, excluding your callback path
app.use((req, res, next) => {
if (req.url === "your_callback_path") {
next();
} else {
protectCSRF(req, res, next);
}
});

// Do some action to create csrf token and attach to the cookie here

app.listen(8080);

Others

Payment channels

ValuePayment channel
36Visa/Master/JCB
37Bank Account
38Zalopay Wallet
39ATM
41Visa/Master Debit