Product Management
Đăng nhập
ESC

Nhập từ khóa để tìm kiếm

↑↓ Di chuyển
Enter Mở
ESC Đóng

Test webhook & callback URL

Mở đầu — vì sao bài này quan trọng

Hãy tưởng tượng bạn là QA tại một sàn thương mại điện tử. Đội backend vừa tích hợp cổng thanh toán, và họ nói với bạn: "Khi khách thanh toán thành công, cổng sẽ gọi ngược về hệ thống của mình để cập nhật trạng thái đơn hàng." Cái "gọi ngược" đó chính là webhook — và nó là một trong những phần khó test nhất, cũng là phần dễ để lọt bug nhất trong toàn bộ vòng đời của một API.

Vì sao khó? Vì với API thông thường, bạn là người chủ động gửi request và nhận response — Postman rất giỏi việc này. Nhưng với webhook, mọi thứ đảo ngược: hệ thống bên ngoài mới là người gửi request, còn bạn là người nhận. Postman theo mặc định chỉ biết đi gõ cửa nhà người khác, chứ không biết cách ngồi đợi người khác gõ cửa nhà mình.

Đây chính xác là điểm nghẽn mà ghi chú gốc của bài đã chạm tới: "Postman gửi request, nhưng webhook...". Trong bài này bạn sẽ học cách vượt qua nghịch lý đó — cách bắt, kiểm tra nội dung, xác thực chữ ký, và mô phỏng webhook một cách có kỷ luật. Đây là kỹ năng phân biệt một QA biết "click Send" với một QA hiểu kiến trúc hệ thống bất đồng bộ.

Khái niệm cốt lõi

Webhook là gì và vì sao nó ngược đời

Webhook (còn gọi là reverse API hay HTTP callback) là cơ chế trong đó một dịch vụ chủ động gửi một HTTP request — hầu như luôn là POST — đến một URL mà bạn đã đăng ký trước, mỗi khi có một sự kiện (event) xảy ra. Ví dụ điển hình:

  • VNPay gửi POST về https://shop.vn/api/payment/vnpay-return khi khách thanh toán thành công.
  • GitHub gửi POST khi có người push code.
  • Giao Hàng Nhanh (GHN) gửi POST khi trạng thái vận đơn đổi sang "đã giao".
Điểm mấu chốt: với REST thông thường, quan hệ là request → response trong cùng một lượt (synchronous). Với webhook, quan hệ là sự kiện → callback ở một thời điểm sau đó, không xác định trước (asynchronous). Bạn không "gọi" webhook; bạn đợi nó.

Ba vai trò cần phân biệt rõ

Khi test webhook, luôn có ba nhân vật. Nhầm vai là nguyên nhân số một khiến QA bối rối:

  • Producer (bên phát) — dịch vụ tạo ra sự kiện và gửi callback. Ví dụ: cổng thanh toán, GitHub.
  • Endpoint / Receiver (bên nhận) — URL trong hệ thống của bạn, nơi tiếp nhận và xử lý callback.
  • Payload — nội dung JSON (hoặc form-encoded) mà producer gửi kèm, mô tả sự kiện.
Postman có thể đóng cả hai vai trò tùy tình huống: nó có thể giả làm producer để bắn payload thử vào endpoint của bạn, hoặc nó có thể giúp bạn quan sát payload thật mà producer gửi tới.

Ba câu hỏi cần trả lời khi test một webhook

Một bộ test webhook đầy đủ phải trả lời được:

  • Delivery (giao nhận): Endpoint của bạn có thực sự nhận được callback không? Có trả về đúng 200 OK để producer không retry (gửi lại) không?
  • Content (nội dung): Payload có đúng cấu trúc, đúng dữ liệu (số tiền, mã đơn, trạng thái) như kỳ vọng không?
  • Security (bảo mật): Callback này có thật sự đến từ producer hợp lệ không, hay là kẻ giả mạo? Đây là nơi signature verification (xác thực chữ ký) vào cuộc.

Vấn đề "localhost không nhìn thấy được"

Producer thật (VNPay, GitHub...) sống trên Internet công cộng. Chúng cần một URL công khai để gửi callback tới. Nhưng máy dev của bạn — http://localhost:8080 — thì thế giới bên ngoài không truy cập được. Đây là rào cản kỹ thuật lớn nhất. Có ba hướng giải quyết, ta sẽ đi sâu ở phần hướng dẫn:

  • Dùng một request-bin công khai (như Postman "Echo"/webhook.site) để bắt payload thật.
  • Dùng tunnel (ngrok, Cloudflare Tunnel) để phơi localhost ra Internet.
  • Mô phỏng producer bằng chính Postman, bắn payload giả vào endpoint — cách nhanh và kiểm soát tốt nhất cho regression test.

Tình huống thực tế

Ví dụ 1 — Tiki và cái webhook "im lặng" lúc 2 giờ sáng

Một đội QA ở Tiki test luồng thanh toán qua VNPay. Trên môi trường staging, đơn hàng thanh toán xong luôn hiển thị "Chờ xử lý" thay vì "Đã thanh toán". Đội dev khăng khăng "code webhook chạy đúng, test unit pass hết".

QA quyết định bắt payload thật. Họ tạm cấu hình VNPay sandbox trỏ callback về một endpoint webhook.site công khai, thực hiện một giao dịch, rồi xem VNPay thực sự gửi gì. Kết quả bất ngờ: VNPay gửi payload dạng form-urlencoded (vnp_ResponseCode=00&vnp_Amount=10000000&...) chứ không phải JSON. Trong khi đó code backend lại đang json_decode() body — nhận được null, xử lý thất bại lặng lẽ, và vẫn trả 200 OK nên VNPay không hề retry.

Bài học: đừng bao giờ giả định content-type của payload. Luôn bắt payload thật ít nhất một lần để xác nhận cấu trúc. Con số vnp_Amount cũng dạy một bài: VNPay gửi số tiền nhân 100 (10.000.000 nghĩa là 100.000 VND) — một chi tiết chỉ lộ ra khi bạn nhìn payload thật.

Ví dụ 2 — Sàn giao dịch giả mạo webhook và bài học chữ ký

Một startup fintech ở Singapore tích hợp cổng thanh toán Stripe. Bản test ban đầu của QA chỉ kiểm tra: "gửi POST vào /webhook/stripe với type=payment_intent.succeeded → đơn được đánh dấu đã trả tiền." Test pass, tính năng lên production.

Ba tuần sau, một kẻ tấn công phát hiện endpoint webhook public, và tự tay gửi một POST giả với payload "thanh toán thành công" cho đơn hàng của chính hắn — mà không hề trả đồng nào. Hệ thống tin ngay và giao hàng. Thiệt hại vài nghìn đô.

Nguyên nhân: endpoint không verify signature. Stripe gửi kèm header Stripe-Signature là một HMAC-SHA256 của body, ký bằng một webhook secret mà chỉ Stripe và server biết. Nếu server tính lại HMAC và so khớp, payload giả sẽ bị loại ngay. QA đã bỏ sót ca test quan trọng nhất: callback từ nguồn không hợp lệ phải bị từ chối (401/400).

Bài học: test webhook không chỉ là test "happy path" khi payload đúng. Ca test bảo mật — payload đúng cấu trúc nhưng sai/thiếu chữ ký phải bị chặn — quan trọng ngang, thậm chí hơn, ca thành công.

Ví dụ 3 — GHN và cơn bão retry

Một shop trên nền tảng của GHN nhận webhook cập nhật trạng thái vận đơn. Endpoint xử lý hơi chậm (gọi thêm một API nội bộ), mất ~40 giây. GHN có timeout 15 giây; không nhận được 200 kịp, nó coi như thất bại và retry — gửi lại cùng một sự kiện. Kết quả: một đơn "đã giao" được ghi log 5 lần, gửi 5 SMS cho khách, trừ kho 5 lần.

Bài học: webhook receiver phải idempotent (xử lý lặp cùng một sự kiện chỉ có tác dụng một lần) và phải trả 200 thật nhanh (nhận-rồi-xử-lý-sau). Khi test, QA cần chủ động gửi trùng payload (cùng event_id) và kiểm tra hệ thống chỉ xử lý một lần. Đây là ca test mà nếu chỉ "click Send một lần" bạn sẽ không bao giờ thấy.

Hướng dẫn từng bước

Ta sẽ đi qua hai kịch bản: (A) bắt webhook thật để quan sát, và (B) mô phỏng producer bằng Postman để đưa vào bộ regression.

Bước 1 — Bắt payload thật với một request-bin

Vào webhook.site (hoặc dùng "Postman Echo") để lấy một URL công khai duy nhất, ví dụ https://webhook.site/abc-123. Cấu hình producer (VNPay sandbox, GitHub...) gửi callback về URL này. Kích hoạt một sự kiện thật, rồi quan sát:

  • Method — có đúng là POST không?
  • Headers — content-type là gì (application/json hay application/x-www-form-urlencoded)? Có header chữ ký (X-Signature, Stripe-Signature...) không?
  • Body — copy toàn bộ để dùng làm "mẫu vàng" (golden sample) cho bước mô phỏng.

Bước 2 — Nếu cần callback về localhost, dựng tunnel

Khi bạn muốn producer gọi thẳng vào máy dev, chạy một tunnel:

ngrok http 8080

ngrok trả về một URL public như https://a1b2.ngrok-free.app. Đăng ký URL này làm callback ở producer. Bây giờ mọi callback từ Internet sẽ được chuyển tiếp về localhost:8080 của bạn, và bạn có thể debug ngay trong máy.

Bước 3 — Mô phỏng producer bằng Postman (cho regression)

Khi đã có "mẫu vàng", bạn không cần producer thật mỗi lần test nữa. Tạo một request trong Postman đóng vai producer:

  • Method: POST
  • URL: endpoint webhook của bạn (https://staging.shop.vn/api/webhook/payment)
  • Body: dán payload mẫu đã bắt được
  • Headers: sao chép content-type và các header cần thiết
Đây chính là câu trả lời cho nghịch lý mở đầu: Postman không đợi được webhook, nhưng nó giả làm bên phát rất tốt — nhanh, lặp lại được, không phụ thuộc hệ thống bên thứ ba.

Bước 4 — Tự sinh chữ ký hợp lệ trong Pre-request Script

Nếu endpoint verify chữ ký, payload giả của bạn cũng phải có chữ ký hợp lệ. Dùng thư viện crypto có sẵn trong sandbox để tính HMAC ngay trước khi gửi:

const secret = pm.environment.get("webhook_secret");
const body = pm.request.body.raw;
const signature = CryptoJS.HmacSHA256(body, secret).toString();
pm.request.headers.upsert({ key: "X-Signature", value: signature });

Bước 5 — Viết Tests kiểm tra endpoint phản hồi đúng

Sau khi gửi payload mô phỏng, viết assertions cho response mà endpoint trả về:

pm.test("Endpoint trả 200 để không bị retry", function () {
    pm.response.to.have.status(200);
});

pm.test("Phản hồi nhanh dưới ngưỡng timeout của producer", function () { pm.expect(pm.response.responseTime).to.be.below(3000); });

pm.test("Body xác nhận đã nhận sự kiện", function () { pm.expect(pm.response.text()).to.include("received"); });

Bước 6 — Kiểm tra tác động phía sau (side-effect)

Webhook thường không trả dữ liệu — nó gây ra thay đổi trạng thái. Vì vậy test chưa xong ở response. Thêm một request nối tiếp để xác minh side-effect thực sự đã xảy ra:

// Sau khi gửi webhook payment.succeeded,
// gọi GET /orders/{id} để xác nhận trạng thái đã đổi
pm.test("Đơn hàng chuyển sang PAID sau webhook", function () {
    const order = pm.response.json();
    pm.expect(order.status).to.eql("PAID");
});

Bước 7 — Bổ sung các ca âm (negative cases)

Bộ test webhook chỉ hoàn chỉnh khi có các ca "phải bị từ chối":

  • Payload đúng cấu trúc nhưng thiếu/sai chữ ký → kỳ vọng 400/401, và trạng thái đơn không đổi.
  • Gửi trùng cùng event_id hai lần → kỳ vọng chỉ xử lý một lần (idempotency).
  • Payload thiếu field bắt buộc → endpoint xử lý an toàn, không crash.

Lỗi thường gặp & mẹo

Lỗi 1 — Tưởng Postman "lắng nghe" được webhook. Postman là HTTP client, không phải server; nó không thể mở một cổng để nhận callback. Đừng phí thời gian tìm nút "listen". Hãy dùng request-bin để bắt, và dùng Postman để mô phỏng phát.

Lỗi 2 — Đoán sai content-type. Rất nhiều cổng thanh toán VN (VNPay) gửi form-urlencoded, không phải JSON. Luôn bắt payload thật một lần trước khi viết test.

Lỗi 3 — Quên ca chữ ký. Test happy-path pass không có nghĩa endpoint an toàn. Callback giả mạo là rủi ro bảo mật thật, đặc biệt với webhook thanh toán. Ca "sai chữ ký phải bị chặn" là bắt buộc.

Lỗi 4 — Bỏ qua idempotency. Producer sẽ retry khi timeout. Nếu bạn không test gửi trùng, bug trừ kho/gửi SMS nhiều lần sẽ chỉ lộ ra trên production.

Lỗi 5 — Ký nhầm nội dung. HMAC phải tính trên đúng chuỗi body thô mà endpoint sẽ nhận. Nếu Postman tự chỉnh khoảng trắng/format JSON sau khi bạn ký, chữ ký sẽ lệch. Mẹo: ký trên raw body dạng chuỗi cố định, đừng để môi trường biến đổi nó.

Mẹo — Lưu golden payload thành file. Giữ mỗi mẫu payload thật (payment success, payment fail, refund...) thành một biến hoặc file dữ liệu, để tái sử dụng cho data-driven testing sau này.

Mẹo — Đặt biến webhook_secret theo environment. Mỗi môi trường dev/staging có secret khác nhau; đừng hardcode.

Bài tập thực hành

  • Bắt payload thật: Tạo một URL trên webhook.site, cấu hình VNPay sandbox (hoặc bất kỳ producer nào bạn có) gửi callback về đó, kích hoạt một giao dịch, và ghi lại: method, content-type, có header chữ ký không, và cấu trúc body. So sánh với những gì bạn đoán trước đó.
  • Mô phỏng producer: Dựng một request Postman đóng vai producer, dùng đúng payload đã bắt, gửi vào endpoint webhook trên staging. Viết 3 assertion: status 200, responseTime dưới 3s, body chứa xác nhận.
  • Tự ký chữ ký: Thêm Pre-request Script tính HMAC-SHA256 của body với một webhook_secret và gắn vào header. Xác nhận endpoint chấp nhận.
  • Ca bảo mật: Nhân bản request ở bài 3 nhưng đổi một ký tự trong chữ ký. Viết assertion kỳ vọng endpoint trả 401 và trạng thái đơn không đổi.
  • Idempotency: Gửi cùng một payload (cùng event_id) hai lần liên tiếp. Sau đó gọi API kiểm tra để chứng minh side-effect chỉ xảy ra một lần.

Tóm tắt

Webhook đảo ngược mô hình quen thuộc: thay vì bạn gọi API, hệ thống bên ngoài gọi bạn — một cách bất đồng bộ, tại thời điểm không định trước. Postman không thể "lắng nghe" callback, nhưng nó giải quyết trọn vẹn bài toán theo hai hướng: dùng request-bin/tunnel để bắt và quan sát payload thật, rồi mô phỏng producer để đưa webhook vào bộ regression lặp lại được.

Một bộ test webhook đạt chuẩn phải trả lời ba câu hỏi — giao nhận (endpoint có nhận, có trả 200 nhanh không), nội dung (payload đúng cấu trúc và dữ liệu không), bảo mật (chữ ký có hợp lệ không) — và luôn kèm hai ca sống còn thường bị bỏ quên: chữ ký sai phải bị chặngửi trùng chỉ xử lý một lần. Ba câu chuyện Tiki, fintech Singapore và GHN nhắc ta rằng bug webhook hiếm khi nằm ở happy path; nó ẩn trong content-type sai, chữ ký bị bỏ qua, và retry không idempotent. Nắm chắc những điểm này, bạn đã kiểm soát được phần "ngược đời" nhất của API testing.

Học xong bài này rồi? Tạo tài khoản miễn phí để lưu lại — lần sau vào là biết ngay đang dở ở đâu, và học hết khóa thì có chứng chỉ. Lưu tiến độ của tôi