Webhook event-driven & retry
Hệ thống event tin cậy với HMAC signature verification, exponential backoff lên đến 12 giờ, và dead-letter queue cho events fail. Phù hợp tích hợp Shopee/TikTok Shop, đồng bộ Zalo OA, fan-out notification.
Connector — đầu nhận webhook
Trước tiên đăng ký endpoint của bạn để Zeni biết gửi event đến đâu:
bashcurl -X POST "https://zenicloud.io/api/v1/automation/connectors?ws=prod" \
-H "Authorization: Bearer $ZENI_TOKEN" \
-H "Content-Type: application/json" \
-d '{
"name": "shopee_order_sync",
"type": "webhook_outbound",
"target_url": "https://app.example.com/hooks/order",
"events": ["order.created", "order.paid", "order.shipped"],
"secret": "auto",
"retry": {
"max_attempts": 5,
"schedule": "exponential"
}
}'
Response:
json{
"connector_id": "conn_3a8f9b1c",
"name": "shopee_order_sync",
"secret": "whsec_HxJ4n2pKvL7wRtY9zM",
"verify_url": "https://zenicloud.io/api/v1/connectors/conn_3a8f9b1c/verify",
"status": "pending_verification"
}
Lưu secret ngay
Trường
secret chỉ trả về một lần — dùng để verify HMAC signature. Lưu vào env variable, không hardcode.Trigger event
Khi business logic của bạn cần fan-out, gửi event:
bashcurl -X POST "https://zenicloud.io/api/v1/automation/events?ws=prod" \
-H "Authorization: Bearer $ZENI_TOKEN" \
-H "Content-Type: application/json" \
-d '{
"event_type": "order.created",
"payload": {
"order_id": "ORD-2026-0042",
"amount_vnd": 1250000,
"customer_email": "khach@example.com",
"items": [
{"sku": "ABC-001", "qty": 2, "price": 625000}
]
}
}'
Zeni tự fan-out đến tất cả connectors đã subscribe order.created với retry policy.
Nhận webhook — verify HMAC
Khi Zeni POST đến URL của bạn, header gồm:
httpPOST /hooks/order HTTP/1.1
Host: app.example.com
Content-Type: application/json
X-Zeni-Event: order.created
X-Zeni-Event-Id: evt_8f3a9b1c2d4e
X-Zeni-Timestamp: 1714435200
X-Zeni-Signature: t=1714435200,v1=a3f8b2c1...
Verify HMAC trong Python
pythonimport hmac, hashlib, time
from flask import Flask, request, abort
app = Flask(__name__)
SECRET = b"whsec_HxJ4n2pKvL7wRtY9zM"
def verify_signature(payload, sig_header):
"""Verify HMAC SHA256 từ Zeni."""
parts = dict(p.split("=", 1) for p in sig_header.split(","))
timestamp = int(parts["t"])
signature = parts["v1"]
# Chống replay: chỉ chấp nhận event trong 5 phút
if abs(time.time() - timestamp) > 300:
return False
# Tính HMAC từ timestamp + body
signed = f"{timestamp}.{payload.decode()}".encode()
expected = hmac.new(SECRET, signed, hashlib.sha256).hexdigest()
return hmac.compare_digest(expected, signature)
@app.post("/hooks/order")
def handle_order():
sig = request.headers.get("X-Zeni-Signature")
if not verify_signature(request.data, sig):
abort(401, "Invalid signature")
event_type = request.headers["X-Zeni-Event"]
event_id = request.headers["X-Zeni-Event-Id"]
# Idempotency: kiểm tra event_id đã xử lý chưa
if event_already_processed(event_id):
return "", 200
# Xử lý event
data = request.get_json()
process_order(event_type, data)
mark_processed(event_id)
return "", 200
Verify HMAC trong Node.js
javascriptimport express from "express";
import crypto from "crypto";
const app = express();
const SECRET = process.env.ZENI_WEBHOOK_SECRET;
app.post("/hooks/order",
express.raw({ type: "application/json" }),
(req, res) => {
const sig = req.headers["x-zeni-signature"];
const parts = Object.fromEntries(
sig.split(",").map(p => p.split("="))
);
const timestamp = parseInt(parts.t);
// Chống replay 5 phút
if (Math.abs(Date.now() / 1000 - timestamp) > 300) {
return res.status(401).send("Stale");
}
const signed = `${timestamp}.${req.body.toString()}`;
const expected = crypto
.createHmac("sha256", SECRET)
.update(signed)
.digest("hex");
if (!crypto.timingSafeEqual(
Buffer.from(expected),
Buffer.from(parts.v1)
)) {
return res.status(401).send("Invalid signature");
}
const event = JSON.parse(req.body);
console.log("Received:", req.headers["x-zeni-event"], event);
res.status(200).send("ok");
}
);
Retry policy — exponential backoff
Khi endpoint trả non-2xx hoặc timeout (>10s), Zeni retry theo lịch:
Try 1
0s
ngay lập tức
Try 2
1m
sau 1 phút
Try 3
5m
sau 5 phút
Try 4
30m
sau 30 phút
Try 5
2h
sau 2 giờ
Try 6
12h
sau 12 giờ
Sau 6 lần fail, event chuyển sang Dead-Letter Queue.
Dead-Letter Queue (DLQ)
Events fail >5 lần được lưu DLQ trong 30 ngày. Bạn có thể inspect và replay thủ công:
bash# Liệt kê events trong DLQ
curl "https://zenicloud.io/api/v1/automation/dlq?ws=prod" \
-H "Authorization: Bearer $ZENI_TOKEN"
# Replay 1 event
curl -X POST "https://zenicloud.io/api/v1/automation/dlq/evt_8f3a/replay?ws=prod" \
-H "Authorization: Bearer $ZENI_TOKEN"
# Replay tất cả từ thời điểm
curl -X POST "https://zenicloud.io/api/v1/automation/dlq/replay-bulk?ws=prod" \
-H "Authorization: Bearer $ZENI_TOKEN" \
-d '{"event_type":"order.created","since":"2026-04-29T00:00:00Z"}'
Best practice cho receiver
- Luôn return 2xx nhanh — Zeni timeout sau 10s. Xử lý nặng phải đẩy vào queue nội bộ rồi return 200 ngay.
- Idempotency bằng Event-Id — lưu vào DB unique để tránh xử lý trùng.
- Verify signature MOI request — không bao giờ skip dù tin tưởng IP source.
- Log đủ thông tin — event_id, timestamp, response time để debug.
- Healthcheck endpoint — Zeni ping
HEAD /your-urlmỗi giờ; trả 200 là ok.
Event types phổ biến
| Event | Mô tả |
|---|---|
| order.created | Đơn hàng mới tạo |
| order.paid | Đơn hàng thanh toán xong |
| order.shipped | Đơn hàng đã giao |
| user.signup | Người dùng đăng ký |
| payment.success | Thanh toán VNPay/Momo thành công |
| payment.failed | Thanh toán thất bại |
| webhook.test | Test event để verify connector |
Tự định nghĩa event với prefix riêng, ví dụ my_app.user_promoted.
Chi phí
- Free — 10K events/tháng
- Starter — 100K events/tháng
- Pro+ — 1M events/tháng
- Vượt: $0.10 / 10K events