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):
Params | Data type | Description |
---|---|---|
data | Json String | The callback data. The format may vary depending on the specific product. |
mac | string | Confirmation of order information, using key2 (had been provided) to verify the order |
type | int | Callback type - type=1 : Order - type=2 : Agreement |
The callback data for the specified product is presented as follows:
Order callback data
Params | Data type | Description |
---|---|---|
app_id | Int | Order's app_id |
app_trans_id | String | Order's app_trans_id |
app_time | Int64 | Order's app_time |
app_user | String | Order's app_user |
amount | Int64 | Amount received (VND) |
embed_data | Json String | Order's embed_data. For example: {"promotioninfo":"","merchantinfo":"merchant-defined data"} |
item | Json Array | Order's item. For example: [{"itemid":"knb","itename":"plantA","itemprice":198400,"itemquantity":1}] |
zp_trans_id | Int64 | Zalopay's Transaction code |
server_time | Int64 | Zalopay's Transaction time (unix timestamp in milliseconds)' |
channel | Int | Payment channels. |
merchant_user_id | String | Zalopay user, who paid for the order |
user_fee_amount | Int64 | Fee (VND) |
discount_amount | Int64 | Discount (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.
Params | Data type | Description |
---|---|---|
app_id | Int | The app id of the merchant. |
app_trans_id | String | The merchant's unique id for the binding. |
binding_id | String | The id of binding has been confirmed in the Zalopay system. |
pay_token | String | The public token is used when doing auto-debit. |
server_time | Int | Server timestamp in seconds. |
merchant_user_id | String | The identifier field in bind request. |
status | Int | Type of message: 1: The user confirms an agreement 2: The user updates the agreement |
msg_type | Int | 1: Success, otherwise fail |
zp_user_id | String | The identifier of Zalopay user per merchant app_id. |
masked_user_phone | String | Masked 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
Params | Data type | Description |
---|---|---|
appId | String | The app identifier is provided by Zalopay. |
mcRefId | String | The merchant order's reference ID. |
amount | Int | Amount of order. |
zpTransId | Int | The Zalopay transaction id. |
serverTime | Int | Server timestamp in seconds. |
channel | Int | Payment channels. |
zpUserId | String | The identifier of Zalopay user per merchant. |
userFeeAmount | Int | Fee amount. |
discountAmount | Int | Discount amount. |
userChargeAmount | Int | Amount 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
Params | Data type | Description |
---|---|---|
return_code | Int | 1: Success 2: Failed |
return_message | String | Description of return code |
For the ZOD product, the expected response format should be as follows:
Params | Data type | Description |
---|---|---|
returnCode | Int | 1: Success 2: Failed |
returnMessage | String | Description 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 isHmacSHA256
key2
: provided by Zalopay at registrationcallback_data
: is data requested by Zalopay to Merchant's callback API when Zalopay has successfully collected money from customers.
Examples
- NodeJs
- Python
- Go
// 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");
});
# coding=utf-8
# Python 3.6
from flask import Flask, request, json # pip3 install Flask
import hmac, hashlib
app = Flask(__name__)
config = {
'key2': 'eG4r0GcoNtRGbO8'
}
@app.route('/callback', methods=['POST'])
def callback():
result = {}
try
cbdata = request.json
mac = hmac.new(config['key2'].encode(), cbdata['data'].encode(), hashlib.sha256).hexdigest()
# check if the callback is valid (from Zalopay server)
if mac != cbdata['mac']:
# callback is invalid
result['return_code'] = -1
result['return_message'] = 'mac not equal'
else:
# payment success
# merchant update status for order's status
data_json = json.loads(cbdata['data'])
print("update order's status = success where app_trans_id = " + data_json['app_trans_id'])
result['return_code'] = 1
result['return_message'] = 'success'
except Exception as e
result['return_code'] = 0 # callback again (up to 3 times)
result['e'] = str(e)
# returns the result for Zalopay server
return json.jsonify(result)
// go version go1.11.1 linux/amd64
package main
import (
"encoding/json"
"fmt"
"log"
"net/http"
"github.com/zpmep/hmacutil"
)
// App config
var (
key2 = "eG4r0GcoNtRGbO8"
)
func main() {
mux := http.DefaultServeMux
mux.HandleFunc("/callback", func(w http.ResponseWriter, r *http.Request) {
defer r.Body.Close()
var cbdata map[string]interface{}
decoder := json.NewDecoder(r.Body)
decoder.Decode(&cbdata)
requestMac := cbdata["mac"].(string)
dataStr := cbdata["data"].(string)
mac := hmacutil.HexStringEncode(hmacutil.SHA256, key2, dataStr)
log.Println("mac =", mac)
result := make(map[string]interface{})
// check if the callback is valid (from Zalopay server)
if mac != requestMac {
// callback is invalid
result["return_code"] = -1
result["return_message"] = "mac not equal"
} else {
// payment success
result["return_code"] = 1
result["return_message"] = "success"
// merchant update status for order's status
var dataJSON map[string]interface{}
json.Unmarshal([]byte(dataStr), &dataJSON)
log.Println("update order's status = success where app_trans_id =", dataJSON["app_trans_id"])
}
// returns the result for Zalopay server
resultJSON, _ := json.Marshal(result)
fmt.Fprintf(w, "%s", resultJSON)
})
log.Println("Server is listening at port :8888")
http.ListenAndServe(":8888", mux)
}
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
Value | Payment channel |
---|---|
36 | Visa/Master/JCB |
37 | Bank Account |
38 | Zalopay Wallet |
39 | ATM |
41 | Visa/Master Debit |