Cổng thanh toán Stripe (Phần 6): Webhook Stripe

9 min read

Trong các bài trước, đặc biệt là phần 3 về cách Stripe hoạt động, phần 4 về Stripe Checkoutphần 5 về Payment Intent, chúng ta đã nhiều lần nhắc đến webhook như một phần quan trọng của Stripe. Khi thanh toán thành công, Stripe gửi webhook về backend. Khi subscription gia hạn, Stripe gửi webhook. Khi refund, dispute hoặc payout thay đổi trạng thái, Stripe cũng có thể gửi webhook.

Điều này cho thấy webhook không phải phần phụ trong tích hợp Stripe. Với một hệ thống thanh toán nghiêm túc, webhook là lớp kết nối quan trọng giữa Stripe và backend của doanh nghiệp.

Bài viết này sẽ giải thích webhook Stripe là gì, vì sao không nên chỉ dựa vào frontend callback, những event nào thường gặp và cách thiết kế webhook endpoint an toàn, ổn định trong dự án thực tế.

Webhook Stripe là gì?

Theo tài liệu webhooks của Stripe, webhook Stripe là cơ chế để Stripe gửi thông báo đến server của doanh nghiệp khi có sự kiện xảy ra trong tài khoản Stripe.

Thay vì backend liên tục hỏi Stripe “giao dịch này xong chưa?”, Stripe sẽ chủ động gửi event đến một URL do doanh nghiệp cấu hình. URL đó thường được gọi là webhook endpoint.

Ví dụ:

  • khách hàng thanh toán thành công;
  • thanh toán thất bại;
  • Checkout Session hoàn tất;
  • subscription được tạo;
  • invoice được thanh toán;
  • refund được xử lý;
  • dispute phát sinh;
  • payout đã chuyển về ngân hàng.

Mỗi sự kiện này có thể ảnh hưởng đến nghiệp vụ nội bộ. Vì vậy backend cần lắng nghe, xác thực và xử lý đúng.

Vì sao không nên chỉ dựa vào frontend callback?

Một lỗi phổ biến khi tích hợp Stripe là sau khi khách hàng quay về success_url, hệ thống lập tức đánh dấu đơn hàng đã thanh toán. Cách này đơn giản nhưng không đủ tin cậy.

Frontend callback có nhiều vấn đề:

  • khách hàng có thể đóng tab trước khi redirect;
  • trình duyệt có thể mất kết nối;
  • một số phương thức thanh toán xử lý bất đồng bộ;
  • người dùng có thể truy cập trực tiếp success page nếu hệ thống không kiểm tra;
  • backend không có bằng chứng server-to-server từ Stripe.

Webhook giải quyết vấn đề này bằng cách để Stripe gửi event trực tiếp đến backend. Với Stripe Checkout, Stripe cũng khuyến nghị dùng webhook để fulfill order sau thanh toán, thay vì chỉ phụ thuộc vào trang success. Backend nhận event, kiểm tra chữ ký, xác định giao dịch và cập nhật database.

Với thanh toán online, nên phân biệt rõ:

  • frontend dùng để hiển thị trải nghiệm cho khách hàng;
  • webhook dùng để xác nhận nghiệp vụ;
  • database nội bộ dùng để lưu trạng thái chính thức.

Các webhook event phổ biến

Tùy loại tích hợp, developer sẽ quan tâm đến các event khác nhau.

Với Stripe Checkout:

  • checkout.session.completed: khách hàng hoàn tất Checkout Session.
  • checkout.session.expired: Checkout Session hết hạn.

Với Payment Intent:

  • payment_intent.succeeded: thanh toán thành công.
  • payment_intent.payment_failed: thanh toán thất bại.
  • payment_intent.canceled: thanh toán bị hủy.

Với refund:

  • charge.refunded: giao dịch đã được hoàn tiền.
  • refund.created: refund được tạo.
  • refund.updated: refund thay đổi trạng thái.

Với dispute:

  • charge.dispute.created: phát sinh tranh chấp.
  • charge.dispute.updated: tranh chấp được cập nhật.
  • charge.dispute.closed: tranh chấp kết thúc.

Với payout:

  • payout.paid: payout đã được chuyển.
  • payout.failed: payout thất bại.

Với subscription:

  • customer.subscription.created;
  • customer.subscription.updated;
  • customer.subscription.deleted;
  • invoice.paid;
  • invoice.payment_failed;
  • invoice.finalized.

Không nhất thiết phải xử lý tất cả event ngay từ đầu. Nhưng hệ thống nên xác định rõ event nào là nguồn sự thật cho từng nghiệp vụ.

Thiết kế webhook endpoint

Webhook endpoint thường là một API public, ví dụ:

POST /stripe/webhook

Endpoint này cần làm các việc chính. Theo Stripe, webhook endpoint production nên dùng HTTPS, nhận POST request và nên trả về mã 2xx nhanh trước khi xử lý các tác vụ quá nặng:

  1. Nhận raw body từ request.
  2. Lấy Stripe signature từ header.
  3. Xác thực event bằng webhook secret.
  4. Kiểm tra event type.
  5. Xử lý nghiệp vụ tương ứng.
  6. Trả HTTP 2xx nếu xử lý hoặc ghi nhận thành công.

Một điểm quan trọng là xác thực chữ ký thường cần raw body. Nếu framework tự parse JSON trước khi xác thực, chữ ký có thể không khớp. Đây là lỗi khá phổ biến khi tích hợp webhook trong Express, NestJS hoặc các framework backend khác.

Xác thực webhook signature

Webhook endpoint là URL public nên không thể tin mọi request gửi đến đó. Backend cần xác thực rằng event thật sự đến từ Stripe.

Stripe cung cấp webhook signing secret. Khi gửi event, Stripe kèm chữ ký trong header Stripe-Signature. Backend dùng signing secret để verify event theo hướng dẫn xác thực chữ ký webhook của Stripe.

Nếu chữ ký không hợp lệ, backend nên từ chối request và không xử lý nghiệp vụ.

Mục tiêu của bước này:

  • tránh request giả mạo;
  • tránh cập nhật đơn hàng sai;
  • tránh bị gọi webhook endpoint tùy ý;
  • đảm bảo event đến từ Stripe.

Không nên bỏ qua bước verify chỉ vì webhook đang chạy trong môi trường test. Thói quen đúng nên được áp dụng từ đầu để tránh quên khi lên production.

Idempotency: tránh xử lý trùng event

Stripe có thể gửi lại webhook event nếu endpoint không phản hồi thành công hoặc xảy ra lỗi mạng. Vì vậy backend phải xử lý idempotent.

Idempotent nghĩa là cùng một event được gửi nhiều lần thì kết quả nghiệp vụ vẫn chỉ xảy ra một lần.

Ví dụ:

  • không tạo hai đơn hàng từ một giao dịch;
  • không gửi hai lần quyền truy cập khóa học;
  • không cộng doanh thu hai lần;
  • không gửi email xác nhận quá nhiều lần;
  • không tạo hai invoice nội bộ cho một payment.

Cách làm phổ biến là lưu event.id của Stripe vào database. Nếu hệ thống gọi Stripe API từ backend, developer cũng nên hiểu thêm về idempotent requests trong Stripe để tránh tạo tác vụ trùng trong các request nhạy cảm. Trước khi xử lý event, backend kiểm tra event này đã được xử lý chưa. Nếu đã xử lý, trả về thành công nhưng không chạy lại nghiệp vụ.

Ngoài event.id, hệ thống cũng nên kiểm tra trạng thái hiện tại của order. Ví dụ, nếu order đã paid, webhook payment_intent.succeeded gửi lại không nên làm thay đổi sai dữ liệu.

Order fulfillment nên xử lý thế nào?

Order fulfillment là bước hệ thống thực hiện sau khi thanh toán thành công. Tùy sản phẩm, fulfillment có thể là:

  • mở quyền truy cập khóa học;
  • gửi email chứa link tải file;
  • tạo đơn giao hàng;
  • nâng cấp tài khoản;
  • kích hoạt subscription;
  • ghi nhận booking;
  • gửi thông báo cho đội vận hành.

Với Stripe Checkout, event thường dùng là checkout.session.completed. Tuy nhiên, tùy mô hình thanh toán, có thể cần kiểm tra thêm trạng thái payment hoặc invoice.

Một flow thực tế:

  1. Khách hàng tạo order, trạng thái pending.
  2. Backend tạo Checkout Session kèm order_id trong metadata.
  3. Stripe gửi checkout.session.completed.
  4. Backend verify signature.
  5. Backend tìm order theo metadata.
  6. Backend kiểm tra order chưa được fulfill.
  7. Backend cập nhật order thành paid.
  8. Backend chạy fulfillment.
  9. Backend lưu event đã xử lý.

Flow này rõ ràng, dễ debug và giảm rủi ro xử lý trùng.

Xử lý refund và dispute

Hệ thống thanh toán không chỉ có giao dịch thành công. Khi vận hành thật, doanh nghiệp sẽ gặp refund và dispute.

Refund là hoàn tiền cho khách hàng. Refund có thể do khách yêu cầu, doanh nghiệp chủ động hoàn tiền hoặc giao dịch cần đảo lại vì lý do vận hành.

Dispute là tranh chấp thanh toán. Ví dụ, khách hàng báo với ngân hàng rằng giao dịch không hợp lệ. Khi đó doanh nghiệp cần cung cấp bằng chứng hoặc chấp nhận mất giao dịch tùy trường hợp.

Webhook giúp backend cập nhật các trạng thái này:

  • đơn hàng đã refund toàn phần;
  • đơn hàng đã refund một phần;
  • giao dịch đang bị dispute;
  • dispute đã thắng hoặc thua;
  • cần đội vận hành xử lý.

Nếu không theo dõi refund và dispute, dashboard nội bộ có thể lệch so với Stripe. Khi cần tra cứu sâu hơn, bạn có thể tham khảo Stripe disputesStripe refunds. Điều này ảnh hưởng đến báo cáo doanh thu, chăm sóc khách hàng và kế toán.

Logging và monitoring webhook

Webhook là phần dễ bị bỏ quên nhưng rất quan trọng khi có sự cố.

Nên log các thông tin:

  • event.id;
  • event.type;
  • thời gian nhận event;
  • Stripe object id liên quan;
  • order id nội bộ;
  • kết quả xử lý;
  • lỗi nếu có.

Ngoài log, nên có cơ chế cảnh báo khi webhook lỗi liên tục. Trong live mode, Stripe có cơ chế retry webhook khi endpoint không phản hồi đúng, nhưng backend vẫn cần tự quan sát để phát hiện lỗi sớm.

Một webhook endpoint tốt không chỉ “chạy được”, mà còn phải dễ tra cứu khi khách hàng báo đã thanh toán nhưng hệ thống chưa mở quyền.

FAQ

Webhook Stripe có bắt buộc không?

Với hệ thống thanh toán nghiêm túc, webhook gần như là bắt buộc. Nó giúp backend nhận trạng thái thanh toán trực tiếp từ Stripe thay vì phụ thuộc vào trình duyệt của khách hàng.

Stripe có gửi lại webhook không?

Có. Nếu endpoint không phản hồi thành công hoặc có lỗi, Stripe có thể retry. Vì vậy backend cần thiết kế idempotent.

Có nên xử lý nhiều logic nặng ngay trong webhook không?

Không nên để webhook chạy quá lâu. Có thể ghi nhận event, cập nhật trạng thái quan trọng, sau đó đẩy các tác vụ nặng sang queue/job nếu hệ thống lớn.

Kết luận

Webhook là phần quyết định độ tin cậy của tích hợp Stripe. Frontend giúp khách hàng có trải nghiệm thanh toán tốt, nhưng webhook mới là nơi backend xác nhận thanh toán, cập nhật đơn hàng, xử lý refund, theo dõi dispute và đồng bộ trạng thái với Stripe.

Khi triển khai webhook, developer cần chú ý xác thực chữ ký, xử lý idempotent, log đầy đủ và xác định rõ event nào là nguồn sự thật cho từng nghiệp vụ. Đây là nền tảng quan trọng trước khi đi tiếp sang các mô hình phức tạp hơn như subscription và Stripe Billing.

Nguồn tham khảo

Bài viết liên quan

Avatar photo

Leave a Reply

Your email address will not be published. Required fields are marked *