Leyla paid for the order and immediately closed the tab: the "Thank you" page never opened. How will your server know the money has arrived? From Stripe, not from the browser: Stripe itself sends a request to your server — a webhook. In this lesson we build a webhook receiver in Express: we verify the signature so nobody can fake a "payment", mark the order PAID in one transaction together with an event for RabbitMQ, survive retries and mixed-up event order, and learn to test it all on our own computer with the Stripe CLI.
Why webhooks
A lot happens in a payment outside your request: the customer enters a 3-D Secure code, the bank confirms the payment later, you refund money from the Dashboard, the customer disputes the payment. Asking Stripe "well, was it paid?" every second is wasteful. A webhook works the other way round: when something happens, Stripe itself sends an HTTP POST request to your address with a description of the event. An analogy: an SMS from the bank "a transfer arrived" is more reliable than the sender saying "I paid".
An event looks like this (shortened):
{
"id": "evt_1Rk4ZtAb",
"object": "event",
"type": "checkout.session.completed",
"created": 1789000000,
"livemode": false,
"data": {
"object": {
"id": "cs_test_a1B2c3",
"object": "checkout.session",
"payment_status": "paid",
"amount_total": 5000,
"currency": "azn",
"client_reference_id": "42"
}
}
}id is unique (repeats are told apart by it), type says what happened, and data.object holds the object itself — here a Checkout Session.
Which events you need
| Event | When it arrives | What the shop does |
|---|---|---|
checkout.session.completed | the customer finished Checkout | if payment_status is paid, the order becomes PAID |
checkout.session.async_payment_succeeded | a delayed payment method was finally paid | the order becomes PAID |
checkout.session.async_payment_failed | a delayed payment failed | tell the customer |
checkout.session.expired | the session expired unpaid | release the order (optional) |
payment_intent.succeeded | a PaymentIntent was paid (lesson 041) | the order becomes PAID with your own payment form |
For Checkout we take the session events: they carry your client_reference_id and metadata. payment_intent.succeeded has no session metadata unless you passed payment_intent_data.metadata. Subscribe only to the events you need: Stripe does not recommend listening to everything.