Webhooks
Webhooks và chữ ký
Tin nhắn của khách hàng được đưa vào hệ thống qua webhook. Mỗi nền tảng áp dụng một cơ chế ký request riêng và không nền tảng nào giống nền tảng nào. Trang này mô tả chính xác các bước xác thực hệ thống thực hiện.
Hai nguyên tắc nền tảng
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 và 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.
Cơ chế chống xử lý trùng
Mọi nền tảng đều có thời điểm gửi lại cùng một sự kiện, do kết nối không ổn định, do phản hồi của hệ thống về muộn hoặc do cơ chế thử lại theo lịch của nền tảng. Mỗi tin nhắn đưa vào hàng đợi mang một mã tác vụ hình thành từ mã kênh và mã tin nhắn phía nền tảng, nhờ đó các sự kiện lặp lại được gộp thay vì khiến trợ lý phản hồi khách hàng hai lần.
Nếu doanh nghiệp tự xây dựng luồng tương tự, nên triển khai cơ chế này ở tầng hàng đợi thay vì ở handler HTTP, bởi handler phải hoàn tất trong vài giây và không đủ thời gian để tra cứu cùng thiết lập khoá.
Tra cứu địa chỉ webhook
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.