Webhooks
Webhooks và chữ ký
Tin nhắn của khách đi vào hệ thống qua webhook. Mỗi nền tảng ký request một kiểu khác nhau, và không nền tảng nào ký giống nền tảng nào. Trang này ghi đúng những gì chúng tôi kiểm tra.
Hai nguyên tắc quyết định mọi thứ
Trước khi đi vào từng nền tảng, có hai điều chi phối toàn bộ thiết kế phần này. Hiểu hai điều đó thì phần còn lại chỉ là chi tiết.
1. Chữ ký tính trên RAW BODY
Chữ ký luôn được tính trên đúng chuỗi byte mà nền tảng gửi đi. Nếu bạn để framework parse JSON rồi JSON.stringify lại để ký, kết quả gần như chắc chắn sai: thứ tự khoá có thể đổi, khoảng trắng biến mất, ký tự Unicode bị escape khác đi, số 1.0 thành 1. Chỉ cần lệch một byte là HMAC khác hoàn toàn.
Vì vậy máy chủ của chúng tôi giữ lại raw body của mọi request và xác thực trên đó, rồi mới dùng bản đã parse để đọc nội dung.
2. Trả 200 thật nhanh
Meta, Telegram và các nền tảng khác sẽ thử lại dồn dập, rồi vô hiệu hoá webhook nếu bạn phản hồi chậm. Nên trong handler chúng tôi chỉ làm ba việc: xác thực chữ ký, bóc payload, đẩy vào hàng đợi. Việc gọi model và trả lời khách chạy ở worker phía sau.
Hệ quả bạn nên biết: kết quả { "ok": true, "queued": n } nghĩa là đã nhận, không phải đã trả lời xong. Sự kiện không khớp kênh nào cũng trả 200 (kèm queued: 0) chứ không trả lỗi — báo lỗi chỉ khiến nền tảng dội lại vô ích.
Bảng tổng hợp
| Nền tảng | Đường dẫn | Header | Cách ký |
|---|---|---|---|
| Telegram | /webhooks/telegram/{channelId} | X-Telegram-Bot-Api-Secret-Token | So sánh trực tiếp với secret token đã đặt lúc setWebhook (không phải HMAC). |
| Meta — Messenger, Instagram DM, WhatsApp | /webhooks/meta | X-Hub-Signature-256 | sha256=HMAC-SHA256(rawBody, app_secret) |
| Zalo OA | /webhooks/zalo | X-ZEvent-Signature | mac=SHA256(app_id + rawBody + timestamp + app_secret) |
| Shopee | /webhooks/shopee | Authorization | HMAC-SHA256(callbackUrl + "|" + rawBody, partner_key) |
| TikTok Shop | /webhooks/tiktok | Authorization | HMAC-SHA256(app_key + rawBody, app_secret) |
Chữ ký sai luôn trả 401 unauthenticated. Mọi phép so sánh chữ ký đều dùng hàm so sánh thời gian hằng định để không rò rỉ thông tin qua thời gian phản hồi.
Telegram
Telegram là ngoại lệ dễ chịu: không có HMAC. Lúc đăng ký webhook, ta gửi kèm một secret_token ngẫu nhiên; Telegram lặp lại token đó ở header của mọi update.
X-Telegram-Bot-Api-Secret-Token: <secret sinh riêng cho từng kênh>Mỗi kênh có secret riêng, và địa chỉ webhook cũng riêng theo kênh — /webhooks/telegram/{channelId} — nên không cần định tuyến theo nội dung payload. Secret được lưu mã hoá cùng thông tin đăng nhập của kênh.
Điều này cũng có nghĩa: ai biết địa chỉ webhook nhưng không biết secret thì không bắn được update giả vào hệ thống.
Meta — Messenger, Instagram DM, WhatsApp
Meta chỉ cho khai một callback URL cho cả ứng dụng, và gộp sự kiện của mọi page, mọi số điện thoại, mọi khách hàng vào chung một endpoint. Nên ở đây có thêm bước định tuyến.
Bắt tay lúc đăng ký
Meta gọi GET /webhooks/meta với hub.mode, hub.verify_token và hub.challenge. Nếu verify token khớp, ta trả về nguyên văn chuỗi hub.challenge — không bọc JSON, vì Meta so sánh từng ký tự. Không khớp thì 403 forbidden.
Xác thực sự kiện
X-Hub-Signature-256: sha256=<hex>
expected = HMAC_SHA256(key = app_secret, data = rawBody)
hợp lệ ⇔ header bắt đầu bằng "sha256=" và phần hex khớp expectedĐịnh tuyến về đúng kênh
Sau khi chữ ký hợp lệ, mỗi entry được đưa về đúng tổ chức dựa trên trường định danh:
| object | Loại kênh | Định tuyến theo |
|---|---|---|
| page | facebook_messenger | entry.id (page_id) |
| instagram_dm | entry.id | |
| whatsapp_business_account | changes[].value.metadata.phone_number_id |
Zalo OA
Cũng là một URL chung cho cả ứng dụng. Zalo dùng SHA-256 nối chuỗi (không phải HMAC), và có tham gia của timestamp lấy từ chính body.
X-ZEvent-Signature: mac=<hex>
expected = SHA256(app_id + rawBody + timestamp + app_secret)
trong đó timestamp = body.timestamp (đọc từ payload đã parse)Sau khi hợp lệ, sự kiện được định tuyến theo oa_id, hoặc recipient.id nếu payload không có oa_id. Lát cắt hiện tại chỉ xử lý tin nhắn văn bản từ người dùng (event_name = user_send_text); các loại sự kiện khác được nhận và bỏ qua, vẫn trả 200.
Shopee
Shopee ký kèm chính URL callback đã đăng ký trên Open Platform. Đây là điểm dễ vấp nhất khi triển khai: nếu địa chỉ thật khác địa chỉ đã đăng ký dù chỉ một dấu gạch chéo cuối, chữ ký sẽ không bao giờ khớp.
Authorization: <hex>
expected = HMAC_SHA256(key = partner_key, data = callbackUrl + "|" + rawBody)
callbackUrl = https://api.chattudong.com/webhooks/shopeeChú ý: header ở đây là Authorization nhưng không có tiền tố Bearer — giá trị là chuỗi hex trần. Sau khi hợp lệ, sự kiện định tuyến theo shop_id.
TikTok Shop
Tương tự Shopee ở chỗ dùng header Authorization, nhưng chuỗi được ký thì khác: nối app key vào trước raw body, không có dấu phân tách.
Authorization: <hex>
expected = HMAC_SHA256(key = app_secret, data = app_key + rawBody)Định tuyến theo shop_id trong payload.
Chống xử lý trùng
Nền tảng nào cũng có lúc gửi lại cùng một sự kiện — mạng chập chờn, phản hồi của ta về muộn, hoặc họ retry theo lịch. Mỗi tin nhắn vào hàng đợi mang một mã việc dựng từ mã kênh và mã tin nhắn phía nền tảng, nên sự kiện lặp lại bị gộp thay vì khiến bot trả lời khách hai lần.
Nếu bạn tự dựng luồng tương tự, hãy làm điều đó ở tầng hàng đợi chứ đừng làm ở handler HTTP: handler phải kết thúc trong vài giây, không đủ thời gian để tra cứu và khoá.
Lấy địa chỉ webhook ở đâu
Mở /dashboard/kenh, chọn kênh, phần Địa chỉ webhook hiện đúng URL cần dán vào cấu hình của nền tảng. Với các kênh dùng webhook cấp ứng dụng (Meta, Zalo, Shopee, TikTok), URL là chung cho cả nền tảng và chúng tôi tự định tuyến theo định danh shop/page như mô tả ở trên.
Kênh gặp sự cố xác thực token phía nền tảng sẽ hiện lỗi ngay trên trang chi tiết kênh, và API trả 422 channel_needs_reauth — lúc đó cần kết nối lại kênh. Danh sách mã lỗi đầy đủ ở API Reference.