Đừng tin kết quả trả về ngay trên trình duyệt — nguồn sự thật là webhook từ cổng.
Luồng:
1. Tạo đơn trạng thái pending trong DB, sinh order_id riêng.
2. Gọi cổng tạo phiên checkout, đính order_id vào metadata; redirect user sang trang thanh toán của cổng.
3. Cổng gọi webhook về server khi có kết quả (payment.succeeded...). Đây mới là chỗ cập nhật đơn thành paid, không phải trang redirect success (user có thể đóng tab).
Bảo vệ webhook:
- Verify chữ ký header (Stripe-Signature) bằng webhook secret → chống giả mạo request.
- Chống xử lý trùng: cổng gửi at-least-once, một event có thể đến nhiều lần. Lưu event_id đã xử lý vào bảng có unique constraint; nếu đã tồn tại thì trả 200 và bỏ qua (idempotent consumer).
- Trả 2xx nhanh; xử lý nặng đẩy sang queue. Nếu trả lỗi, cổng sẽ retry — nên handler phải idempotent.
verifySignature(req.headers["stripe-signature"], secret, rawBody) // throws if forged
const r = await db.query(
"INSERT INTO processed_events(event_id) VALUES($1) ON CONFLICT (event_id) DO NOTHING",
[event.id],
)
if (r.rowCount === 0) return res.sendStatus(200) // duplicate event, skip
await fulfillOrder(event.data.object.metadata.order_id)
res.sendStatus(200)Lỗi hay gặp: cập nhật đơn dựa vào redirect thay vì webhook; không verify chữ ký; xử lý trùng làm cộng credit 2 lần.