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):

json
{
  "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.

Stripe delivers an event with a signature: the first attempt fails, Stripe retries, and the second one passes verification, deduplication and saving
Stripe delivers an event with a signature: the first attempt fails, Stripe retries, and the second one passes verification, deduplication and saving

Which events you need

EventWhen it arrivesWhat the shop does
checkout.session.completedthe customer finished Checkoutif payment_status is paid, the order becomes PAID
checkout.session.async_payment_succeededa delayed payment method was finally paidthe order becomes PAID
checkout.session.async_payment_faileda delayed payment failedtell the customer
checkout.session.expiredthe session expired unpaidrelease the order (optional)
payment_intent.succeededa 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.

The Express receiver: raw body and signature