Callback
Tổng quan
Callback hoặc Webhook là một cơ chế tự động thông báo cho các nhà bán hàng về các sự kiện liên quan đến giao dịch Zalopay. Người dùng và nhà phát triển bên thứ ba có thể quản lý các Callback này. Chúng là các phản hồi HTTP được xác định bởi người dùng cho phép hai ứng dụng trực tuyến độc lập giao tiếp với nhau.
Luồng thực hiện
Khi một giao dịch cụ thể thành công:
- Máy chủ Zalopay sẽ thông báo đến Máy chủ Merchant thông qua Callback URL đã đăng ký trước đó, ví dụ CreateOrder API.
- Máy chủ Merchant sử dụng key2 (do Zalopay cung cấp) để xác nhận dữ liệu callback.
- Merchant có thể kiểm tra callback log trong môi trường Sandbox trên Merchant Portal
Request
Các điểm cuối gọi lại (callback endpoints) của bạn phải có thể truy cập được bởi máy chủ Zalopay.
- Phương thức:
POST
- Content-Type:
application/json
Điểm cuối này nên chấp nhận dữ liệu theo cấu trúc sau (áp dụng cho tất cả sản phẩm):
Tham số | Kiểu dữ liệu | Mô tả |
---|---|---|
data | Json String | Dữ liệu gọi lại (callback data) có thể thay đổi định dạng tùy thuộc vào sản phẩm cụ thể. |
mac | string | Xác nhận thông tin đơn hàng bằng cách sử dụng key2 (đã được cung cấp) để xác minh đơn hàng. |
type | int | Loại gọi lại (callback type): - type=1 : Order - type=2 : Agreement |
Dữ liệu gọi lại (callback data) cho sản phẩm cụ thể được trình bày như sau:
Dữ liệu gọi lại (callback data) cho đơn hàng (Order) là như sau:
Tham số | Kiểu dữ liệu | Mô tả |
---|---|---|
app_id | Int | app_id của đơn hàng |
app_trans_id | String | app_trans_id của đơn hàng |
app_time | Int64 | app_time của đơn hàng |
app_user | String | app_user của đơn hàng |
amount | Int64 | Số tiền đã nhận được (VND) |
embed_data | Json String | embed_data của đơn hàng. Ví dụ: {"promotioninfo":"","merchantinfo":"merchant-defined data"} |
item | Json Array | Oitem của đơn hàng. Ví dụ: [{"itemid":"knb","itename":"plantA","itemprice":198400,"itemquantity":1}] |
zp_trans_id | Int64 | Mã giao dịch của Zalopay. |
server_time | Int64 | Thời gian giao dịch của Zalopay (thời gian Unix dạng timestamp tính bằng millisecond). |
channel | Int | Kênh thanh toán. |
merchant_user_id | String | Người dùng Zalopay, người đã thanh toán cho đơn hàng. |
user_fee_amount | Int64 | Số tiền phí (VND) |
discount_amount | Int64 | Số tiền giảm giá (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
}
Dữ liệu gọi lại (callback data) cho quá trình thanh toán sử dụng token/liên kết hợp đồng (agreement) là như sau:
Dữ liệu gọi lại (callback data) bao gồm chi tiết về các thỏa thuận được xác nhận hoặc cập nhật.
Tham số | Kiểu dữ liệu | Mô tả |
---|---|---|
app_id | Int | App ID của người bán. |
app_trans_id | String | ID duy nhất của người bán để ràng buộc (binding). |
binding_id | String | ID của quá trình ràng buộc (binding) đã được xác nhận trong hệ thống Zalopay. |
pay_token | String | Token công khai (public token) được sử dụng khi thực hiện tự động trừ tiền (auto-debit). |
server_time | Int | Thời trên máy chủ (server timestamp) tính bằng giây. |
merchant_user_id | String | Trường thông tin định danh (identifier field) trong yêu cầu ràng buộc (bind request). |
status | Int | Loại tin nhắn (message type): 1: Người dùng xác nhận một thỏa thuận 2: Người dùng cập nhật thỏa thuận |
msg_type | Int | 1: Thành công, còn lại là thất bại. |
zp_user_id | String | Định danh của người dùng Zalopay theo app_id của người bán. |
masked_user_phone | String | Số điện thoại của người dùng được che giấu (Ex: masked_user_phone: "****6938"). |
Ví dụ:
{
"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
}
Dữ liệu gọi lại (callback data) cho ZOD là như sau:
Tham số | Kiểu dữ liệu | Mô tả |
---|---|---|
appId | String | Bộ nhận dạng ứng dụng (app identifier) được cung cấp bởi Zalopay. |
mcRefId | String | ID tham chiếu của đơn hàng của người bán (merchant order's reference ID). |
amount | Int | Số tiền của đơn hàng. |
zpTransId | Int | ID giao dịch của Zalopay (Zalopay transaction ID). |
serverTime | Int | Thời gian giao dịch của Zalopay (thời gian Unix dạng timestamp tính bằng millisecond). |
channel | Int | Kênh thanh toán. |
zpUserId | String | Định danh của người dùng Zalopay theo từng người bán |
userFeeAmount | Int | Số tiền phí (VND) |
discountAmount | Int | Số tiền giảm giá (VND) |
userChargeAmount | Int | Số tiền được thu từ người dùng. |
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
}
Phản hồi (response)
Tham số | Kiểu dữ liệu | Mô tả |
---|---|---|
return_code | Int | 1: Thành công 2: Thất bại |
return_message | String | Mô tả về mã trả về |
Đối với sản phẩm ZOD, định dạng phản hồi dự kiến như sau:
Tham số | Kiểu dữ liệu | Mô tả |
---|---|---|
returnCode | Int | 1: Thành công 2: Thất bại |
returnMessage | String | Mô tả về mã trả về |
Cài đặt
Xác thực (validation)
URL callback của bạn có thể bị tấn công bởi hacker. Để đảm bảo tính hợp lệ của một callback, việc xác thực bằng HMAC là bắt buộc.
reqmac = HMAC(hmac_algorithm, key2, callback_data.data);
if (reqmac == callback_data.mac) {
// valid callback
} else {
// invalid callback
}
trong đó:
hmac_algorithm
: là phương pháp bảo mật được đăng ký bởi Merchant với Zalopay, giá trị mặc định làHmacSHA256
key2
: được Zalopay cung cấp trong quá trình đăng ký.callback_data
: là dữ liệu mà Zalopay yêu cầu từ API gọi lại của Merchant khi Zalopay đã thành công thu tiền từ khách hàng.
Ví dụ
- 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)
}
Để xem các ví dụ khác, vui lòng truy cập vào đường dẫn sau: https://github.com/orgs/zalopay-samples/repositories
Đề xuất
Thử lại (Retry)
Do các vấn đề kỹ thuật như mất kết nối mạng hoặc không khả dụng dịch vụ, Zalopay có thể không thể thông báo cho bạn thông qua callback. Sau 15 phút kể từ thời điểm thiết lập đơn hàng, nếu bạn vẫn không nhận được callback từ Zalopay, người bán cần gọi API QueryOrder API một cách chủ động để lấy kết quả cuối cùng.
Idempotence (Tính đơn nhất)
Các điểm cuối callback có thể nhận được các sự kiện trùng lặp, điều này có thể gây ra vấn đề nếu các sự kiện được xử lý nhiều lần. Để đảm bảo rằng các sự kiện trùng lặp này không gây ra vấn đề, việc xử lý sự kiện cần phải đơn nhất (idempotent). Điều này có nghĩa là việc xử lý cùng một sự kiện nhiều lần sẽ có cùng kết quả như việc xử lý nó chỉ một lần. Một cách để đạt được điều này là theo dõi các sự kiện đã được xử lý và tránh xử lý lại các sự kiện đã được theo dõi.
Sử dụng HTTPS
Bằng cách sử dụng URL HTTPS cho điểm cuối callback, bạn có thể đảm bảo rằng kết nối giữa máy khách và máy chủ là an toàn và được mã hóa, cung cấp bảo vệ chống lại các mối đe dọa an ninh tiềm ẩn. Với HTTPS, dữ liệu được truyền giữa máy chủ và máy khách được mã hóa, ngăn chặn kẻ tấn công từ việc chặn và đọc thông tin nhạy cảm.
Bảo vệ chống lại CSRF
Các framework web như Rails, Django hoặc các framework khác có thể tự động xác minh rằng mỗi yêu cầu POST bao gồm mã thông báo CSRF (CSRF token) nhằm bảo vệ chống lại các cuộc tấn công giả mạo yêu cầu xuyên trang. Mặc dù đây là một tính năng bảo mật quan trọng để bảo vệ bạn và người dùng của bạn, nhưng nó có thể gây trở ngại trong việc xử lý các sự kiện hợp lệ trên trang web của bạn. Trong những trường hợp như vậy, bạn có thể loại trừ đường dẫn callback khỏi bảo vệ CSRF.
Ví dụ với 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);
Khác
Kênh thanh toán
Giá trị | Kênh Thanh Toán |
---|---|
36 | Visa/Master/JCB |
37 | Bank Account |
38 | Zalopay Wallet |
39 | ATM |
41 | Visa/Master Debit |