Mở đầu — vì sao bài này quan trọng
Hãy tưởng tượng bạn đang viết spec cho một tính năng đơn giản: "Khi khách hàng thanh toán thành công qua cổng thanh toán, hệ thống của chúng ta phải cập nhật đơn hàng sang trạng thái Đã thanh toán và gửi email xác nhận." Nghe có vẻ rất bình thường. Nhưng câu hỏi kỹ thuật đằng sau nó lại không hề tầm thường: làm sao hệ thống của bạn biết được rằng việc thanh toán đã thành công? Khách hàng vừa bấm nút trên trang của VNPAY hay MoMo, tiền đã trừ, nhưng đó là chuyện xảy ra bên hệ thống của họ, không phải hệ thống của bạn.
Có hai cách trả lời câu hỏi này, và việc bạn chọn cách nào sẽ quyết định rất nhiều thứ: độ trễ của nghiệp vụ, chi phí hạ tầng, độ phức tạp khi xử lý lỗi, và cả tính đúng đắn của dữ liệu. Một Technical BA giỏi không chỉ viết "hệ thống nhận thông báo thanh toán" rồi để đó cho dev tự lo. Bạn cần hiểu cơ chế phía dưới đủ sâu để viết spec đúng, đặt câu hỏi đúng cho đối tác tích hợp, và lường trước những tình huống lỗi mà nếu bỏ sót sẽ gây thất thoát tiền thật.
Webhook chính là cơ chế trả lời cho câu hỏi trên trong hầu hết các hệ thống hiện đại. Đây là một trong những khái niệm tích hợp quan trọng nhất mà BA kỹ thuật phải nắm vững, bởi vì gần như mọi tích hợp với bên thứ ba — cổng thanh toán, đơn vị vận chuyển, nền tảng nhắn tin, dịch vụ ký số — đều dùng đến nó. Bài này sẽ giúp bạn hiểu webhook là gì, vì sao nó tốt hơn cách "hỏi liên tục" (polling), và quan trọng nhất là cách viết spec cho một tích hợp webhook một cách chuyên nghiệp.
Khái niệm cốt lõi
Webhook là gì?
Webhook bản chất là một HTTP callback: khi một sự kiện (event) xảy ra ở hệ thống A, hệ thống A sẽ chủ động gửi một HTTP request (thường là POST) đến một URL do hệ thống B đăng ký trước. URL đó gọi là endpoint hoặc callback URL.
Nói cách dễ hình dung: thay vì bạn cứ phải gọi điện hỏi nhà hàng "món của tôi xong chưa?" cứ mỗi 5 phút, bạn để lại số điện thoại và nói "khi nào xong thì gọi cho tôi nhé." Nhà hàng (hệ thống A) sẽ chủ động gọi bạn (gửi POST tới endpoint của hệ thống B) đúng vào lúc sự kiện xảy ra. Vì lý do này, webhook còn được gọi vui là "reverse API" hay "push API" — thay vì client gọi server, lần này server chủ động gọi đến client.
Một webhook request điển hình bao gồm:
- Phương thức và URL: ví dụ
POST https://shop.vn/api/webhooks/payment - Headers: chứa metadata như chữ ký số xác thực (signature), event type, request id để chống trùng.
- Body (payload): thường là JSON, mô tả sự kiện vừa xảy ra. Ví dụ:
{ "event": "payment.succeeded", "order_id": "DH2026-0042", "amount": 1290000, "currency": "VND", "paid_at": "2026-06-27T10:32:11+07:00" }
Khác gì với Polling?
Đây là so sánh cốt lõi mà BA cần nắm để giải thích cho stakeholder vì sao nên chọn webhook.
Polling là cách "hỏi liên tục": hệ thống B chủ động gọi API của hệ thống A theo một chu kỳ cố định (ví dụ mỗi 5 phút một lần) để hỏi "có gì mới không?". Ví dụ: cứ 5 phút, hệ thống đặt hàng gọi API của cổng thanh toán hỏi "đơn DH2026-0042 đã thanh toán chưa?".
Vấn đề của polling:
- Lãng phí: 99% các lần gọi sẽ nhận về câu trả lời "chưa có gì mới". Bạn gọi 288 lần một ngày cho một đơn hàng mà có thể chỉ một lần là có dữ liệu thật.
- Độ trễ cao: nếu sự kiện xảy ra ngay sau lần hỏi vừa rồi, bạn phải chờ tới gần 5 phút mới biết. Với thanh toán, 5 phút trễ là trải nghiệm tệ.
- Khó mở rộng: với 100.000 đơn hàng đang chờ, polling sẽ tạo ra lượng request khổng lồ, và nhiều cổng thanh toán sẽ giới hạn (rate limit) bạn.
Một nguyên tắc thực dụng để BA ghi nhớ: Dùng webhook khi sự kiện hiếm nhưng cần biết ngay (thanh toán, giao hàng thành công). Dùng polling khi không thể đặt endpoint công khai, hoặc khi đối tác không hỗ trợ webhook. Trong thực tế, nhiều hệ thống dùng cả hai: webhook là kênh chính, polling là kênh dự phòng để "đối soát" những webhook bị rớt.
Event-Driven API — bức tranh lớn hơn
Webhook là một biểu hiện cụ thể của tư duy event-driven (hướng sự kiện): hệ thống không hỏi trạng thái, mà phản ứng với sự kiện khi nó xảy ra. Thay vì "kéo" (pull) dữ liệu, ta "đẩy" (push) sự kiện. Một event mô tả "điều gì đã xảy ra" trong quá khứ: order.created, payment.succeeded, shipment.delivered. Tên event thường ở thì quá khứ vì nó là sự thật đã xảy ra, không thể đảo ngược.
Cần phân biệt: webhook là cơ chế giao tiếp event-driven giữa hai tổ chức/hệ thống khác nhau qua HTTP. Còn event-driven trong nội bộ một hệ thống lớn (giữa các microservice) thường dùng message queue như Kafka hoặc RabbitMQ — đó là chủ đề của các bài sau, ở bài này ta tập trung vào webhook qua HTTP.
Những thuộc tính kỹ thuật BA bắt buộc phải hiểu
Đây là phần làm nên sự khác biệt giữa BA "biết khái niệm" và BA "viết spec được":
- Retry (gửi lại): Mạng có thể rớt, endpoint của bạn có thể đang bảo trì. Bên gửi webhook tốt sẽ gửi lại nếu không nhận được 200, thường theo cơ chế exponential backoff (gửi lại sau 1 phút, 5 phút, 30 phút, vài giờ...). Điều này dẫn tới hệ quả quan trọng tiếp theo.
- At-least-once delivery (giao ít nhất một lần): Vì có retry, một event có thể được gửi nhiều lần. Bạn không được giả định mỗi webhook đến đúng một lần.
- Idempotency (tính bất biến khi lặp): Hệ thống nhận phải xử lý sao cho dù nhận cùng một event 5 lần thì kết quả vẫn như nhận 1 lần. Ví dụ: nhận webhook
payment.succeeded3 lần thì đơn hàng vẫn chỉ cộng tiền một lần. Đây là khái niệm sống còn (bài 29 sẽ đào sâu, nhưng trong ngữ cảnh webhook bạn phải nắm ngay). - Xác thực chữ ký (signature verification): Endpoint webhook là URL công khai, ai cũng có thể gửi POST đến. Kẻ xấu có thể giả mạo một webhook "payment.succeeded" để được giao hàng miễn phí. Vì vậy bên gửi ký payload bằng một secret key (thường dùng HMAC-SHA256), và bên nhận phải kiểm tra chữ ký trước khi tin.
- Thứ tự sự kiện (ordering): Webhook không đảm bảo đến đúng thứ tự. Event
order.updatedcó thể đến trướcorder.created. Spec phải nói rõ cách xử lý.
Tình huống thực tế
Ví dụ 1 — Tiki tích hợp cổng thanh toán: bài học về retry và idempotency
Giả sử một sàn thương mại điện tử kiểu Tiki tích hợp với cổng thanh toán VNPAY. Luồng thiết kế ban đầu của đội: khách thanh toán xong, VNPAY gửi webhook POST /webhooks/vnpay tới Tiki, Tiki cập nhật đơn sang "Đã thanh toán" và trừ kho.
Tháng đầu chạy êm. Đến một ngày cao điểm Sale 6/6, hệ thống Tiki bị chậm, một số endpoint webhook phản hồi sau 12 giây. VNPAY mặc định coi request không trả 200 trong vòng 10 giây là thất bại, nên gửi lại webhook đó. Kết quả: với khoảng 4.000 đơn, webhook bị gửi 2 lần. Vì đội chưa làm idempotency, hệ thống cộng nhầm — báo cáo doanh thu phình lên, và tệ hơn, một số khuyến mãi "hoàn tiền theo đơn" bị kích hoạt hai lần, gây thất thoát ước tính hàng chục triệu đồng trong vài giờ.
Bài học rút ra: BA viết spec tích hợp webhook bắt buộc phải có dòng "Hệ thống phải xử lý idempotent: lưu transaction_id của mỗi webhook đã xử lý; nếu nhận lại transaction_id đã có, trả về 200 nhưng KHÔNG xử lý nghiệp vụ lần nữa." Và phải hỏi đối tác: "Timeout của các anh là bao nhiêu giây? Cơ chế retry thế nào?" — đây là câu hỏi mà BA, chứ không phải dev, nên đặt ra trong buổi làm việc với đối tác.
Ví dụ 2 — Giao Hàng Nhanh và webhook trạng thái vận đơn: bài học về ordering
Một startup logistics kiểu Giao Hàng Nhanh (GHN) cung cấp webhook để báo trạng thái đơn cho các shop: picked_up (đã lấy hàng), in_transit (đang giao), delivered (đã giao), returned (hoàn). Một shop tích hợp và phát hiện hiện tượng kỳ lạ: thỉnh thoảng đơn hiển thị "Đã giao" rồi vài giây sau nhảy ngược về "Đang giao".
Nguyên nhân: webhook không đảm bảo thứ tự. Khi mạng có độ trễ chênh nhau, webhook delivered (phát lúc 14:30:05) đôi khi đến server của shop trước webhook in_transit (phát lúc 14:30:02). Shop xử lý theo kiểu "ghi đè trạng thái mới nhất nhận được", nên trạng thái cuối cùng bị sai.
Bài học rút ra: Spec phải định nghĩa cơ chế chống sự kiện đến sai thứ tự. Cách phổ biến: mỗi event mang một timestamp hoặc sequence_number; hệ thống nhận chỉ cập nhật trạng thái nếu timestamp của event mới hơn trạng thái hiện tại. BA cần ghi rõ trong spec: "Khi nhận webhook trạng thái vận đơn, chỉ cập nhật nếu event_timestamp lớn hơn last_updated_at đang lưu; nếu nhỏ hơn, bỏ qua (vẫn trả 200)." Một dòng spec như vậy phòng được nguyên một lớp bug khó tái hiện.
Ví dụ 3 — Ngân hàng số và xác thực chữ ký: bài học về bảo mật
Một ngân hàng số ở Đông Nam Á (kiểu Timo hoặc Cake) cho phép các đối tác fintech đăng ký webhook nhận thông báo khi có giao dịch vào tài khoản ảo của họ. Trong giai đoạn thử nghiệm, một đối tác để lộ URL endpoint. Một tester bảo mật (white-hat) đã thử gửi một POST giả mạo với payload { "event": "deposit.confirmed", "amount": 50000000 } trực tiếp tới endpoint đó. Vì endpoint không kiểm tra chữ ký, hệ thống đối tác tin là có 50 triệu vừa được nạp và cộng số dư.
May là phát hiện trong giai đoạn thử nghiệm. Sau sự cố, ngân hàng bắt buộc mọi webhook đều ký HMAC-SHA256 với secret riêng cho từng đối tác, và đối tác phải xác thực chữ ký trước khi xử lý.
Bài học rút ra: Với mọi webhook liên quan đến tiền hoặc dữ liệu nhạy cảm, spec phải có yêu cầu bảo mật rõ ràng: (1) xác thực chữ ký HMAC trên toàn bộ raw body trước khi parse; (2) từ chối request quá cũ (chống replay attack bằng cách kiểm tra timestamp trong khoảng cho phép, ví dụ ±5 phút); (3) endpoint chỉ chấp nhận HTTPS. BA không cần viết code HMAC, nhưng phải biết yêu cầu này tồn tại và đưa nó vào spec — vì nếu thiếu, hậu quả là rủi ro tài chính trực tiếp.
Hướng dẫn từng bước
Khi bạn được giao viết spec cho một tích hợp webhook (dù là nhận webhook từ đối tác hay phát webhook cho đối tác của mình), hãy đi theo các bước sau:
- Xác định danh sách sự kiện (event catalog): Liệt kê đầy đủ các event sẽ được gửi/nhận, đặt tên theo quy ước
resource.actionở thì quá khứ:payment.succeeded,payment.failed,order.cancelled. Với mỗi event, mô tả khi nào nó được phát.
- Định nghĩa payload (schema): Với mỗi event, mô tả cấu trúc body JSON: tên trường, kiểu dữ liệu, bắt buộc hay không, ví dụ mẫu. Luôn có các trường định danh:
event_id(duy nhất, để idempotency),event_type,timestamp, và id của tài nguyên liên quan.
- Định nghĩa hợp đồng phản hồi (response contract): Endpoint nhận trả về gì? Quy ước phổ biến: trả
200nghĩa là "đã nhận và sẽ xử lý"; mọi mã khác coi là thất bại và sẽ bị retry. Ghi rõ thời gian timeout mong đợi (thường endpoint phải phản hồi trong 5–10 giây).
- Định nghĩa cơ chế retry và xử lý lỗi: Bên gửi retry mấy lần, theo lịch nào? Sau bao nhiêu lần thất bại thì dừng và cảnh báo? Có cơ chế gửi lại thủ công (replay) không? Có dashboard để xem webhook đã gửi và trạng thái không?
- Định nghĩa yêu cầu idempotency: Ghi rõ hệ thống nhận phải lưu
event_idđã xử lý và bỏ qua bản trùng. Đây không phải gợi ý, mà là yêu cầu bắt buộc.
- Định nghĩa yêu cầu bảo mật: Chữ ký (thuật toán, header chứa chữ ký, secret quản lý ra sao), HTTPS bắt buộc, chống replay, có thể cả IP allowlist.
- Định nghĩa cơ chế đối soát (reconciliation): Vì webhook có thể bị rớt vĩnh viễn, luôn cần một cơ chế dự phòng — thường là một job định kỳ (polling/API đối soát) chạy mỗi vài giờ để so khớp trạng thái và bù những event bị thiếu. Đây là tấm lưới an toàn mà BA giỏi không bao giờ quên.
Lỗi thường gặp & mẹo
- Quên idempotency: Lỗi phổ biến và nguy hiểm nhất, đặc biệt với nghiệp vụ tiền. Luôn coi mỗi webhook có thể đến nhiều lần.
- Xử lý nghiệp vụ nặng ngay trong request webhook: Nếu endpoint vừa nhận webhook vừa gửi email, cập nhật kho, gọi API khác... thì rất dễ vượt timeout và bị retry. Mẹo: endpoint chỉ nên ghi nhận event vào hàng đợi nội bộ rồi trả 200 ngay, việc xử lý nặng để cho worker chạy nền.
- Tin payload mà không xác thực chữ ký: Coi như mở cửa cho giả mạo. Luôn verify trước, parse sau.
- Bỏ qua tình huống sai thứ tự: Đừng giả định webhook đến đúng trình tự. Dùng timestamp/sequence để bảo vệ.
- Không có cơ chế đối soát dự phòng: Webhook sẽ rớt vài lần, đó là chuyện bình thường của Internet. Không có reconciliation nghĩa là chấp nhận mất dữ liệu âm thầm.
- Endpoint trả 200 trước khi lưu thành công: Nếu bạn trả 200 rồi mới lưu, mà bước lưu lỗi, bên gửi tưởng thành công và không retry — event mất luôn. Mẹo: chỉ trả 200 sau khi đã ghi nhận chắc chắn (ít nhất là vào hàng đợi bền vững).
- Mẹo test: Khi chưa có endpoint thật, dùng các công cụ như webhook.site hoặc RequestBin để xem payload thật của đối tác trông ra sao; dùng ngrok để expose endpoint local ra Internet khi test với đối tác.
Bài tập thực hành
Tình huống: Công ty bạn vận hành một nền tảng đặt vé sự kiện. Bạn vừa ký hợp đồng tích hợp với cổng thanh toán MoMo. MoMo sẽ gửi webhook khi giao dịch hoàn tất.
- Viết event catalog cho ít nhất 3 event cần nhận từ MoMo (gợi ý: thanh toán thành công, thất bại, hoàn tiền). Đặt tên đúng quy ước.
- Thiết kế payload JSON mẫu cho event "thanh toán thành công", đảm bảo có đủ các trường phục vụ idempotency và đối soát.
- Viết một đoạn spec (3–5 câu) mô tả yêu cầu idempotency và yêu cầu xác thực chữ ký, đủ rõ để dev đọc và làm theo mà không cần hỏi lại.
- Liệt kê 5 câu hỏi bạn sẽ đặt cho đội kỹ thuật của MoMo trong buổi kick-off tích hợp (gợi ý: timeout, lịch retry, cơ chế ký, môi trường sandbox, API đối soát).
- Mô tả cơ chế đối soát dự phòng: nếu webhook "thanh toán thành công" bị rớt, làm sao hệ thống vẫn xác nhận được vé đã thanh toán trong vòng tối đa 15 phút?
Tóm tắt
Webhook là một HTTP callback: hệ thống nguồn chủ động POST tới endpoint của hệ thống đích mỗi khi một sự kiện xảy ra. So với polling (hỏi liên tục), webhook cho độ trễ gần như tức thời và tiết kiệm tài nguyên, nhưng đổi lại phức tạp hơn về xử lý lỗi. Webhook là biểu hiện cụ thể của tư duy event-driven trong giao tiếp giữa các hệ thống qua HTTP.
Là một Technical BA, bạn không cần code webhook, nhưng bạn phải viết spec cho nó đúng. Năm điều cốt lõi cần đưa vào mọi spec webhook: (1) idempotency — xử lý event lặp mà không sai nghiệp vụ; (2) xác thực chữ ký — chống giả mạo; (3) retry và hợp đồng phản hồi — hiểu rõ bên gửi gửi lại thế nào; (4) xử lý sai thứ tự — dùng timestamp/sequence; (5) đối soát dự phòng — tấm lưới an toàn cho những webhook bị rớt. Ba tình huống thật ở trên — cộng tiền hai lần, trạng thái nhảy ngược, và payload giả mạo — đều bắt nguồn từ việc bỏ sót một trong năm điều này. Khi bạn ghi đủ năm điều đó vào spec, bạn đã làm tốt phần quan trọng nhất của một BA kỹ thuật trong mảng tích hợp.