Webhook là gì — cách Casso, Stripe, Zalo 'gõ cửa' web của bạn
API là bạn HỎI server. Webhook là server BÁO bạn khi có sự kiện. Hiểu webhook giúp bạn tự động hoá thanh toán, gửi noti Telegram khi có đơn mới, sync data realtime giữa các SaaS — mà không cần code polling mỗi 5 giây.
Mục lục bài viết(15)

Sơ đồ Web API tổng quát — webhook là pattern "ngược chiều" của mô hình này: thay vì BẠN gọi server bên kia (như API thường), server bên kia chủ động gửi HTTP POST tới URL của bạn khi có sự kiện. Còn gọi là "reverse API" hay "HTTP callback". Nguồn: Wikimedia Commons (CC BY-SA 4.0).
Hiểu đơn giản nhất
Tưởng tượng bạn order pizza từ Now.vn.
Cách 1 — API polling (như API thường):
Cứ 5 phút bạn lại gọi điện hỏi: "Pizza đã đi chưa?". 5 phút sau gọi lại: "Đã đi chưa?". Quán bận, bạn tốn pin điện thoại. Gọi 30 lần mới nhận được câu "Đã đi rồi anh ơi".
Cách 2 — Webhook (như callback):
Bạn nói shipper: "Khi pizza tới cửa thì bấm chuông". Bạn ngồi yên làm việc khác. Pizza tới → chuông kêu → bạn ra mở cửa.
Webhook = "chuông cửa" trong thế giới web. Bạn config 1 URL với nhà cung cấp dịch vụ (Casso, Stripe, GitHub, Zalo), khi có sự kiện xảy ra, họ tự "bấm chuông" (gửi HTTP POST tới URL bạn). Bạn không cần liên tục hỏi.
| Khía cạnh | API thường | Webhook |
|---|---|---|
| Ai chủ động | Bạn (gọi server) | Server bên kia (gọi bạn) |
| Khi nào gọi | Khi bạn cần biết | Khi có sự kiện xảy ra |
| Latency | Có thể delay = chu kỳ polling | Realtime (< 1 giây) |
| Hiệu suất | Tốn — polling 99% là "không có gì mới" | Tiết kiệm — chỉ gọi khi có thật |
| Phức tạp | Đơn giản hơn | Cần verify signature + retry handling |
Tại sao bạn cần biết
- Tự động hoá payment trong 30 phút thay vì 3 ngày. Casso webhook báo "đơn VC202600001 vừa nhận 800k" → server bạn tự đánh dấu PAID + gửi Telegram cho admin. Không cần nhân viên check sao kê bank manual.
- Tích hợp realtime giữa SaaS. User đăng ký trên Zalo → webhook Zalo OA → server bạn lưu lead → đẩy vào CRM HubSpot qua webhook khác. Cả chuỗi automation không cần code custom polling.
- Phân biệt SaaS chuẩn vs nghiệp dư khi đánh giá. SaaS không có webhook = chỉ làm việc với người, không làm việc với hệ thống khác. Đối với enterprise integration, webhook là check-list cứng.
- Hiểu được lỗi 'webhook retry' trong dashboard Stripe/Casso. "Webhook delivered" = OK. "Webhook failed (status 500)" = server bạn lỗi, cần fix.
- Audit security đúng. Webhook không verify signature là lỗ Critical (vietcodex.com vừa fix M6 — Casso webhook constant-time compare).
Anatomy của một webhook delivery
Khi có sự kiện, nhà cung cấp webhook gửi:
POST /api/webhook/casso HTTP/1.1
Host: vietcodex.com
Content-Type: application/json
secure-token: WB_xxxxxxxxxxxxxxxxxxxx
X-Casso-Event: payment.received
X-Casso-Signature: t=1716000000,v1=5257a869e7ecebeda32a...
User-Agent: Casso-Webhook/2.0
{
"id": 123456,
"tid": "FT2026000123456",
"description": "VC2026000001",
"amount": 800000,
"when": "2026-05-22 10:30:00"
}5 thành phần cần verify:
| Thành phần | Vai trò |
|---|---|
| URL endpoint | Bạn config sẵn: https://vietcodex.com/api/webhook/casso |
| Method POST | Webhook luôn POST (không bao giờ GET) |
| Body JSON | Payload sự kiện — data thực |
| Signature header | Chứng thực request thật từ Casso, không bị tamper |
| Event type header | Loại sự kiện (payment.received, payment.failed...) |
Server bạn phải:
- Verify signature trong header
- Parse body JSON
- Lookup event trong DB (đã xử lý chưa? idempotency)
- Trigger business logic (mark order PAID, gửi mail, gửi Telegram)
- Trả về
200 OKtrong < 5 giây
Security: 3 cách verify webhook authentic
1. Shared secret token (đơn giản nhất)
Nhà cung cấp gửi token cố định trong header. Bạn so với token đã config.
const incoming = req.headers.get("authorization");
const expected = process.env.CASSO_WEBHOOK_SECRET;
if (incoming !== expected) return 401;⚠️ Phải dùng constant-time compare (crypto.timingSafeEqual), KHÔNG === thường — chống timing side-channel attack. (Đây là M6 audit vietcodex.com vừa fix.)
2. HMAC signature (chuẩn cao hơn)
Nhà cung cấp ký body bằng HMAC-SHA256 với secret, gửi chữ ký trong header. Bạn ký lại body bên mình rồi so:
import { createHmac, timingSafeEqual } from "node:crypto";
const body = await req.text();
const incoming = req.headers.get("x-signature");
const computed = createHmac("sha256", process.env.STRIPE_WEBHOOK_SECRET)
.update(body)
.digest("hex");
if (!timingSafeEqual(Buffer.from(incoming), Buffer.from(computed))) {
return 401;
}Lợi ích so với (1): kể cả attacker biết secret hash, không tự ký request giả mạo được vì không có secret gốc.
Stripe, GitHub, Slack, Zalo OA đều dùng pattern HMAC.
3. IP allowlist (bổ sung)
Chỉ accept request từ IP range của Stripe/Casso. List IP công bố trong docs nhà cung cấp.
⚠️ Đừng dùng một mình IP allowlist — IP có thể spoof trong vài kịch bản. Kết hợp với (1) hoặc (2).
Idempotency — webhook gửi 2 lần không double-charge
Webhook có thể gửi trùng vì nhiều lý do:
- Lần đầu fail (network), nhà cung cấp retry
- Server bạn xử lý xong nhưng response 500 do bug khác
- Bug của nhà cung cấp gửi nhầm 2 lần
Pattern idempotency — webhook trùng KHÔNG được tạo side-effect lần 2:
// Webhook handler
async function handleCassoWebhook(payload) {
// Check idempotency by event ID
const exists = await db.query.payments.findFirst({
where: eq(payments.cassoTransactionId, payload.id),
});
if (exists) {
return { ok: true, deduped: true }; // Đã xử lý — bỏ qua
}
// Process new event
await markOrderPaid(payload.description, payload.amount);
await db.insert(payments).values({ cassoTransactionId: payload.id, ... });
}Idempotency key có thể là: event ID, transaction ID, hoặc combine (event_type, resource_id).
Retry mechanism — webhook không bao giờ "mất"
Nhà cung cấp tốt có retry policy:
| Lần | Delay sau lần trước | Tổng thời gian |
|---|---|---|
| 1 | (lần đầu) | 0 |
| 2 | 30 giây | 30s |
| 3 | 5 phút | ~5m |
| 4 | 30 phút | ~36m |
| 5 | 2 giờ | ~2h |
| 6-10 | 4-24 giờ | ~24h |
Server bạn down 1 giờ → khi up, nhà cung cấp tự gửi lại các webhook đã miss. Bạn không mất event.
Tuy nhiên, endpoint webhook PHẢI trả 200 trong < 5 giây — không thì coi là fail. Pattern chuẩn:
export async function POST(req: Request) {
// 1. Verify signature (10ms)
if (!verifySignature(req)) return 401;
// 2. Parse body (1ms)
const payload = await req.json();
// 3. Push job vào queue (5ms)
await queue.add("process-casso-webhook", payload);
// 4. Trả 200 NGAY (tổng < 20ms)
return new Response("ok", { status: 200 });
}
// Background worker xử lý queue — có thể mất 5-30 giâyWebhook handler nhẹ + nhanh. Business logic nặng để qua queue.
Webhook phổ biến ở Việt Nam
| Nhà cung cấp | Webhook event chính | Use case |
|---|---|---|
| Casso | Tiền vào tài khoản bank | Auto verify payment cho ecommerce/SaaS VN |
| VNPay | Payment success/failed | Mark order PAID realtime |
| MoMo | Payment notification | Tương tự VNPay |
| Zalo OA | Tin nhắn user mới, user follow OA | Chatbot tự động, lead capture |
| Zalo Pay | Payment events | E-commerce VN ngoài bank |
| GHN / GHTK / Viettel Post | Đơn cập nhật trạng thái (đã pickup, đang giao, đã giao) | Tracking + noti khách |
| Telegram Bot API | Tin nhắn nhận được | Bot CSKH, internal admin notify |
| GitHub | Push, PR, issue, star | Auto-deploy, CI/CD trigger |
| Stripe | Subscription renewed, payment failed, refund | International SaaS billing |
Ví dụ thực tế: vietcodex.com tích hợp Casso
Flow thanh toán đơn hàng tại vietcodex.com:
- Khách checkout → tạo order với
status=PENDING_PAYMENT, mã đơnVC2026000001 - Hiện QR VietQR cho khách quét → chuyển tiền với nội dung
VC2026000001 - Khách chuyển bank Vietcombank 800.000đ với nội dung kia
- Bank thông báo Casso → Casso parse description =
VC2026000001 - Casso gửi webhook POST
https://vietcodex.com/api/webhook/cassokèm secure-token - Server vietcodex.com:
- Verify
secure-tokenquatimingSafeEqual(M6 audit) - Parse body, tìm order
VC2026000001 - So amount khớp → mark PAID
- Provision tenant cho khách (
POST /api/admin/orders/:id/provision) - Gửi Telegram cho admin: "💰 Đơn 001 vừa thanh toán 800k"
- Gửi email cho khách: "Đã nhận thanh toán, đăng nhập admin-hub"
- Verify
- Server trả 200 trong < 50ms → Casso đánh dấu webhook delivered
- Khách thấy trang status update "Đã thanh toán" qua WebSocket (Soketi)
Toàn flow ~3-10 giây từ lúc chuyển tiền đến lúc khách thấy site update. Không người gác.
Cái gì có thể sai
| Vấn đề | Nguyên nhân | Cách fix |
|---|---|---|
| Webhook không nhận được | URL config sai, firewall block | Check Stripe/Casso dashboard → Webhook attempts; whitelist IP |
| Webhook retry liên tục, server quá tải | Endpoint xử lý > 5s không trả 200 | Push vào queue, trả 200 ngay |
| Khách bị charge 2 lần | Không có idempotency check | Add unique constraint trên transaction_id + check trước khi process |
| Webhook bị giả mạo (fake payment) | Không verify signature | Implement HMAC-SHA256 verify ở mọi webhook |
| Webhook delivered nhưng business logic fail | Endpoint trả 200 trước khi xử lý xong | OK pattern nhưng cần dead-letter queue cho failed jobs |
| Server local không nhận webhook test | Không có public URL | Dùng cloudflared tunnel hoặc ngrok để expose localhost |
| Webhook body bị middleware modify | Express body-parser, Next.js JSON parse trước khi verify HMAC | Verify HMAC trên raw body trước khi JSON.parse |
Webhook vs Polling vs WebSocket — chọn cái nào?
| Pattern | Bạn cần biết khi nào | Latency | Phù hợp với |
|---|---|---|---|
| Polling (call API định kỳ) | Periodic check | 5s - 5 phút | Status không quá quan trọng — cập nhật giá crypto, thời tiết |
| Webhook (server-to-server PUSH) | Sự kiện thật xảy ra | < 1s | Payment, order, integration — bên gửi và bên nhận đều có server |
| WebSocket (browser realtime) | Trong session user | < 100ms | Chat realtime, live notification trong UI, dashboard live data |
| Server-Sent Events (SSE) | Server stream một chiều xuống browser | < 100ms | Live log, AI streaming response (ChatGPT, Claude) |
Server-to-server → webhook. Server-to-browser → WebSocket hoặc SSE.
Tóm tắt 1 dòng
Webhook = "chuông cửa" — server bên kia gửi HTTP POST cho bạn khi có sự kiện (payment, order, user signup). Khác API thường (bạn chủ động hỏi), webhook là reactive (chờ event). Verify bằng HMAC signature (chuẩn cao) hoặc shared secret + constant-time compare. Endpoint phải nhanh + idempotent + trả 200 trong < 5 giây — business logic nặng để qua queue.
Đọc tiếp
- API là gì? Tại sao mọi app, mọi web hiện đại đều cần — webhook là pattern "API ngược" — bạn expose endpoint, server khác gọi
- Mật khẩu được "băm" thế nào trong database — HMAC signature cùng concept với password hash (one-way + có secret)
- Frontend vs Backend khác nhau ở đâu — webhook 100% là chuyện giữa 2 backend, frontend không liên quan
- HTTPS, SSL, TLS — vì sao web không có ổ khoá xanh thì khách bỏ chạy — webhook URL bắt buộc HTTPS