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.

10 phút đọcCập nhật 2026-05-22
Nghe bài viết
Để Claude đọc bài cho bạn — vừa nghe vừa làm việc khác
Mục lục bài viết(15)
Sơ đồ Web API tổng quát — client gửi request, server response. Webhook là biến thể trong đó server bên kia chủ động gọi endpoint của bạn

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ạnhAPI thườngWebhook
Ai chủ độngBạn (gọi server)Server bên kia (gọi bạn)
Khi nào gọiKhi bạn cần biếtKhi có sự kiện xảy ra
LatencyCó thể delay = chu kỳ pollingRealtime (< 1 giây)
Hiệu suấtTố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ơnCầ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ầnVai trò
URL endpointBạn config sẵn: https://vietcodex.com/api/webhook/casso
Method POSTWebhook luôn POST (không bao giờ GET)
Body JSONPayload sự kiện — data thực
Signature headerChứng thực request thật từ Casso, không bị tamper
Event type headerLoại sự kiện (payment.received, payment.failed...)

Server bạn phải:

  1. Verify signature trong header
  2. Parse body JSON
  3. Lookup event trong DB (đã xử lý chưa? idempotency)
  4. Trigger business logic (mark order PAID, gửi mail, gửi Telegram)
  5. Trả về 200 OK trong < 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ầnDelay sau lần trướcTổng thời gian
1(lần đầu)0
230 giây30s
35 phút~5m
430 phút~36m
52 giờ~2h
6-104-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ây

Webhook handler nhẹ + nhanh. Business logic nặng để qua queue.

Webhook phổ biến ở Việt Nam

Nhà cung cấpWebhook event chínhUse case
CassoTiền vào tài khoản bankAuto verify payment cho ecommerce/SaaS VN
VNPayPayment success/failedMark order PAID realtime
MoMoPayment notificationTương tự VNPay
Zalo OATin nhắn user mới, user follow OAChatbot tự động, lead capture
Zalo PayPayment eventsE-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 APITin nhắn nhận đượcBot CSKH, internal admin notify
GitHubPush, PR, issue, starAuto-deploy, CI/CD trigger
StripeSubscription renewed, payment failed, refundInternational 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:

  1. Khách checkout → tạo order với status=PENDING_PAYMENT, mã đơn VC2026000001
  2. Hiện QR VietQR cho khách quét → chuyển tiền với nội dung VC2026000001
  3. Khách chuyển bank Vietcombank 800.000đ với nội dung kia
  4. Bank thông báo Casso → Casso parse description = VC2026000001
  5. Casso gửi webhook POST https://vietcodex.com/api/webhook/casso kèm secure-token
  6. Server vietcodex.com:
    • Verify secure-token qua timingSafeEqual (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"
  7. Server trả 200 trong < 50ms → Casso đánh dấu webhook delivered
  8. 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ânCách fix
Webhook không nhận đượcURL config sai, firewall blockCheck Stripe/Casso dashboard → Webhook attempts; whitelist IP
Webhook retry liên tục, server quá tảiEndpoint xử lý > 5s không trả 200Push vào queue, trả 200 ngay
Khách bị charge 2 lầnKhông có idempotency checkAdd unique constraint trên transaction_id + check trước khi process
Webhook bị giả mạo (fake payment)Không verify signatureImplement HMAC-SHA256 verify ở mọi webhook
Webhook delivered nhưng business logic failEndpoint trả 200 trước khi xử lý xongOK pattern nhưng cần dead-letter queue cho failed jobs
Server local không nhận webhook testKhông có public URLDùng cloudflared tunnel hoặc ngrok để expose localhost
Webhook body bị middleware modifyExpress body-parser, Next.js JSON parse trước khi verify HMACVerify HMAC trên raw body trước khi JSON.parse

Webhook vs Polling vs WebSocket — chọn cái nào?

PatternBạn cần biết khi nàoLatencyPhù hợp với
Polling (call API định kỳ)Periodic check5s - 5 phútStatus 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< 1sPayment, order, integration — bên gửi và bên nhận đều có server
WebSocket (browser realtime)Trong session user< 100msChat realtime, live notification trong UI, dashboard live data
Server-Sent Events (SSE)Server stream một chiều xuống browser< 100msLive 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

Câu hỏi thường gặp

Webhook và API khác nhau cụ thể ở đâu?
API: BẠN gọi server khác (active — bạn chủ động). Webhook: server khác gọi BẠN khi có sự kiện (reactive — bạn passively chờ). Ví dụ: API VNPay 'tra trạng thái đơn 123' = bạn gọi VNPay. Webhook VNPay 'đơn 123 vừa thanh toán xong' = VNPay gọi bạn. Pattern API hợp khi bạn cần data ngay lập tức. Pattern webhook hợp khi bạn cần biết khi có thay đổi, không muốn poll API mỗi 5 giây.
Webhook có bảo mật không? Sao biết request đó là Casso/Stripe gửi, không phải kẻ giả mạo?
3 cách phổ biến để verify: (1) Shared secret — bạn config secret với nhà cung cấp, webhook gửi secret trong header `Authorization: Bearer xxx` hoặc `secure-token: xxx`. (2) HMAC signature — webhook ký body bằng HMAC-SHA256 với secret, gửi chữ ký trong header `X-Signature`; bạn ký lại body bên mình rồi so. (3) IP allowlist — chỉ accept request từ IP của Casso/Stripe. Bắt buộc dùng ít nhất một cái — không thì ai cũng có thể fake webhook 'đã thanh toán' để chiếm hàng.
Nếu server bạn down lúc webhook gửi tới, sao biết để xử lý đơn?
Nhà cung cấp webhook tốt (Stripe, Casso, Zalo) có cơ chế **retry**: gửi lần đầu fail (timeout, 500, 503) → đợi 30s gửi lại, đợi 5 phút gửi lại, đợi 30 phút gửi lại... thường 5-10 lần trong 24 giờ. Bạn cần: (1) endpoint webhook trả 200 NHANH (dưới 5 giây) ngay cả khi xử lý chưa xong (queue background job); (2) idempotent — webhook gửi 2 lần cùng event không double-charge khách; (3) log mọi webhook nhận được để debug. Pattern này gọi là 'at-least-once delivery'.
Webhook có giới hạn nào không? Có thể nhận hàng trăm sự kiện/giây?
Có. Mỗi webhook endpoint thường handle 50-500 request/giây tuỳ server. Nếu nhà cung cấp gửi burst lớn (Black Friday Stripe gửi 1000 đơn/giây): bạn phải dùng queue (Redis, BullMQ, RabbitMQ). Endpoint webhook chỉ làm 2 việc: (1) verify signature; (2) push vào queue + trả 200. Background worker xử lý queue tốc độ riêng. Đừng làm DB write nặng hoặc gọi API khác trong endpoint webhook — sẽ timeout và webhook retry liên tục.
Có thể tự build webhook gửi từ web của tôi cho web khác không?
Hoàn toàn được — và nên. Nếu SaaS bạn build có sự kiện đáng quan tâm (đơn hàng mới, user signup, payment), expose webhook cho khách của bạn integrate. Pattern chuẩn: (1) UI cho khách config webhook URL + chọn events; (2) lưu URL + secret trong DB; (3) khi event xảy ra, push job vào queue gọi URL với HMAC signature; (4) retry với exponential backoff; (5) UI hiển thị history + status các webhook đã gửi. Stripe webhook docs là mẫu chuẩn để học.
Webhook URL có cần HTTPS không?
Bắt buộc 99% trường hợp. Stripe, Casso, Zalo Pay, GitHub — tất cả nhà cung cấp chuẩn đều chỉ accept webhook URL HTTPS. Lý do: webhook chứa data nhạy cảm (payment info, customer email) — HTTP gửi qua mạng công cộng = ai sniff cũng đọc được. Để test local, dùng ngrok/cloudflared tunnel để có HTTPS URL tạm: `cloudflared tunnel --url http://localhost:3000`. KHÔNG dùng ngrok URL cho production — chỉ dev.
Có webhook chuẩn nào cho Zalo OA không?
Có — Zalo Official Account Webhook. Setup tại developers.zalo.me → OA → Webhook URL. Zalo gửi event: tin nhắn mới từ user, user follow OA, user gửi attachment. Header verify bằng `X-ZEvent-Signature` (HMAC-SHA256). Lưu ý: VN có Zalo Pay webhook (riêng cho payment) cũng tương tự pattern. So với Stripe ở mức trưởng thành, docs Zalo ít chi tiết hơn — phải đọc code SDK + test thật. Webhook Zalo OA là cơ sở để build chatbot tự động trả lời, sync conversations sang CRM.