Leyla оплатила заказ и сразу закрыла вкладку: страница «Спасибо» так и не открылась. Как ваш сервер узнает, что деньги получены? От Stripe, а не от браузера: Stripe сам отправляет запрос на ваш сервер — вебхук. В этом уроке мы построим приёмник вебхуков в Express: проверим подпись, чтобы никто не подделал «оплату», отметим заказ PAID в одной транзакции вместе с событием для RabbitMQ, переживём повторы и перепутанный порядок событий и научимся проверять всё на своём компьютере через Stripe CLI.

Зачем нужны вебхуки

Многое в платеже происходит вне вашего запроса: клиент вводит код 3-D Secure, банк подтверждает платёж позже, вы возвращаете деньги из Dashboard, клиент оспаривает платёж. Спрашивать Stripe «ну что, оплатили?» каждую секунду расточительно. Вебхук устроен наоборот: когда что-то происходит, Stripe сам отправляет на ваш адрес HTTP-запрос POST с описанием события (event). Аналогия: SMS от банка «вам поступил перевод» надёжнее, чем слова отправителя «я заплатил».

Событие выглядит так (в сокращении):

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 уникален (по нему отличают повторы), type говорит, что случилось, а в data.object лежит сам объект — здесь Checkout Session.

Stripe доставляет событие с подписью: первая попытка не удалась, Stripe повторяет, а вторая проходит проверку, дедупликацию и сохранение
Stripe доставляет событие с подписью: первая попытка не удалась, Stripe повторяет, а вторая проходит проверку, дедупликацию и сохранение

Какие события нужны

СобытиеКогда приходитЧто делает магазин
checkout.session.completedклиент завершил Checkoutесли payment_status равен paid, заказ становится PAID
checkout.session.async_payment_succeededотложенный способ оплаты наконец оплачензаказ становится PAID
checkout.session.async_payment_failedотложенный платёж не прошёлсообщить клиенту
checkout.session.expiredсессия истекла без оплатыосвободить заказ (по желанию)
payment_intent.succeededоплачен PaymentIntent (урок 041)заказ PAID при своей форме оплаты

Для Checkout берём события сессии: в них приходят ваши client_reference_id и metadata. У payment_intent.succeeded метаданных сессии нет, пока вы не передали payment_intent_data.metadata. Подписывайтесь только на нужные события: Stripe не советует слушать всё подряд.

Приёмник в Express: сырое тело и подпись