Як тестувати вебхуки Stripe

Налаштовуємо ендпоінт у Stripe, дивимося, що саме приходить, перевіряємо Stripe-Signature на Node.js і Python та пересилаємо події на локальний застосунок.

Оновлено:

Stripe надсилає вебхуки, коли проходить оплата, продовжується підписка чи відкривається диспут. Щоб обробник працював надійно, треба бачити справжній payload і заголовки, правильно перевіряти підпис і швидко відповідати. Розберімо все по черзі, а події спершу ловитимемо на публічну адресу, щоб роздивитися їх ще до того, як вони дійдуть до коду.

Крок 1: отримайте адресу для перехоплення

Відкрийте Webhook Inspector і створіть адресу, наприклад:

https://webhook.voronyuk.com/abcd23efgh/stripe/webhook

Ловиться будь-який метод, підшлях і query. За замовчуванням адреса відповідає 200 з тілом {"ok":true,"request_id":N}, і Stripe вважає доставку успішною.

Крок 2: створіть ендпоінт у Stripe

  1. У Stripe Dashboard відкрийте Workbench і перейдіть на вкладку Webhooks.
  2. Натисніть Create an event destination і виберіть Your account.
  3. Позначте потрібні типи подій, наприклад payment_intent.succeeded, checkout.session.completed чи invoice.paid. Підписуйтеся лише на те, що справді обробляєте.
  4. Як тип призначення виберіть Webhook endpoint і вставте адресу перехоплювача.
  5. На сторінці ендпоінта відкрийте signing secret. Він починається з whsec_.

Stripe доставляє події лише на публічні HTTPS-адреси. Адреса перехоплювача під цю вимогу підходить: сертифікат валідний.

Тепер згенеруйте подію, наприклад через Stripe CLI:

stripe trigger payment_intent.succeeded

Запит одразу з’явиться в інспекторі: повний JSON у зручному вигляді і всі заголовки.

Крок 3: розберіть заголовок Stripe-Signature

Кожна подія має заголовок Stripe-Signature в один рядок:

Stripe-Signature: t=1492774577,v1=5257a869e7ecebeda32affa62cdca3fa51cad7e77a0e56ff536d0ce8e108d8bd
  • t — Unix-час.
  • v1 — підпис. Під час ротації секрету v1 може бути кілька.
  • У тестових подіях буває ще й v0. Усе, крім v1, ігноруйте.

Підпис — це HMAC-SHA256 від рядка {t}.{сире тіло} з ключем whsec_... вашого ендпоінта, у hex. Головне тут — сире тіло: щойно фреймворк розпарсить JSON і серіалізує його знову, байти зміняться і перевірка не пройде.

Крок 4: перевірка на Node.js

Офіційна бібліотека сама розбирає заголовок, рахує HMAC, порівнює підписи за сталий час і перевіряє, чи не застарів timestamp (допуск за замовчуванням — 5 хвилин):

const express = require('express');
const Stripe = require('stripe');

const stripe = new Stripe(process.env.STRIPE_SECRET_KEY);
const endpointSecret = process.env.STRIPE_WEBHOOK_SECRET; // whsec_...
const app = express();

// express.raw залишає тіло як Buffer — саме це потрібно constructEvent
app.post('/webhook', express.raw({ type: 'application/json' }), (req, res) => {
  let event;
  try {
    event = stripe.webhooks.constructEvent(
      req.body,
      req.headers['stripe-signature'],
      endpointSecret
    );
  } catch (err) {
    return res.status(400).send(`Webhook Error: ${err.message}`);
  }

  if (event.type === 'payment_intent.succeeded') {
    // поставте задачу в чергу, а не виконуйте її тут
  }
  res.json({ received: true });
});

app.listen(3000);

Оголосіть цей маршрут до глобального express.json() або виключіть його з-під нього, інакше тіло встигне розпарситися.

Крок 5: ручна перевірка на Python

Без SDK перевірка теж коротка. У прикладі на Flask сирі байти беремо через request.get_data():

import hashlib
import hmac
import json
import os
import time

from flask import Flask, abort, request

app = Flask(__name__)
SECRET = os.environ["STRIPE_WEBHOOK_SECRET"].encode()  # whsec_...
TOLERANCE = 300  # секунд


def verify(payload: bytes, header: str) -> bool:
    parts = [p.strip().split("=", 1) for p in header.split(",") if "=" in p]
    timestamp = next((v for k, v in parts if k == "t"), None)
    signatures = [v for k, v in parts if k == "v1"]
    if not timestamp or not signatures:
        return False
    if abs(time.time() - int(timestamp)) > TOLERANCE:
        return False
    signed = timestamp.encode() + b"." + payload
    expected = hmac.new(SECRET, signed, hashlib.sha256).hexdigest()
    return any(hmac.compare_digest(expected, s) for s in signatures)


@app.post("/webhook")
def webhook():
    payload = request.get_data()
    if not verify(payload, request.headers.get("Stripe-Signature", "")):
        abort(400)
    event = json.loads(payload)
    print(event["type"], event["id"])
    return "", 200


if __name__ == "__main__":
    app.run(port=3000)

Крок 6: пересилання подій на localhost

Коли формат зрозумілий, віддайте події локальному обробнику. У панелі Пересилання вкажіть ціль http://localhost:3000. Підшлях дописується до цілі, тож .../abcd23efgh/stripe/webhook прийде на http://localhost:3000/webhook. Можна скористатися relay через браузер (сервер має дозволяти CORS) або CLI-агентом:

WI_TOKEN='<токен>' inspector listen https://webhook.voronyuk.com/abcd23efgh/stripe --to localhost:3000

Точну команду з вашим токеном скопіюйте з панелі. Агент сам перепідключається і доганяє пропущені запити. Детальніше про всі варіанти — у гайді як тестувати вебхуки локально.

Альтернатива: stripe listen

Власний CLI Stripe теж уміє пересилати події:

stripe listen --forward-to localhost:3000/webhook

Він виводить окремий whsec_... для цієї сесії. Поки тестуєте через CLI, використовуйте в обробнику саме його: це не той самий секрет, що в ендпоінта з Dashboard.

Повтори, дублікати й час відповіді

  • Відповідайте 2xx одразу. Спершу підтвердіть отримання, а обробляйте вже в черзі. Повільний обробник ловить тайм-аут, і доставка вважається невдалою.
  • Повтори. У live-режимі Stripe повторює невдалу доставку до трьох днів з експоненційною затримкою. У sandbox — тричі протягом кількох годин.
  • Редиректи — це помилка. Відповідь 3xx вважається невдалою доставкою.
  • Дублікати трапляються. Зберігайте оброблені event.id і пропускайте повтори. Побічні ефекти робіть ідемпотентними.
  • Порядок не гарантовано. Не розраховуйте, що invoice.created прийде раніше за invoice.paid.

Щоб перевірити поведінку при збоях, задайте для адреси власну відповідь, скажімо статус 500 або затримку, і стежте за вкладкою Event deliveries у Stripe. А будь-яку перехоплену подію можна повторити прямо з тестера вебхуків Stripe, без нової оплати.

Схожі гайди: вебхуки GitHub і вебхуки Shopify.

Часті питання

Як тестувати вебхуки Stripe локально?

Або запустіть stripe listen --forward-to localhost:3000/webhook, або зареєструйте у Workbench публічну адресу перехоплювача і пересилайте з неї на localhost через relay чи CLI-агент. Другий варіант зберігає історію подій, які можна повторити.

Чому не проходить перевірка підпису Stripe?

Майже завжди тіло змінилося до перевірки, найчастіше його розпарсив middleware. Передавайте в constructEvent сирі байти. І перевірте, чи той whsec_ ви взяли: у ендпоінта з Dashboard і в сесії stripe listen секрети різні.

Де знайти webhook secret у Stripe?

У Workbench відкрийте вкладку Webhooks, виберіть ендпоінт і покажіть signing secret. Він починається з whsec_.

Скільки Stripe повторює невдалі вебхуки?

У live-режимі — до трьох днів з експоненційною затримкою. Події з sandbox повторюються тричі протягом кількох годин.

Який статус має повертати обробник вебхука Stripe?

Будь-який 2xx, і якомога швидше. Інші коди, редиректи й тайм-аути вважаються помилкою, і Stripe повторить доставку.

Спробуйте зараз

Отримайте безкоштовний URL для вебхуків і дивіться, як приходять запити, — без реєстрації.

Отримати URL для вебхуків

Інші гайди

Localhost

Як тестувати вебхуки локально

Чому Stripe, GitHub та інші сервіси не бачать ваш localhost і як зловити справжній вебхук на публічній адресі, а потім переслати його собі на машину.

GitHub

Як тестувати вебхуки GitHub

Налаштовуємо вебхук репозиторію, розбираємо заголовки GitHub, правильно перевіряємо X-Hub-Signature-256 і повторно доставляємо події під час налагодження.

MCP

Тестування вебхуків із Claude і ChatGPT через MCP

Підключіть Webhook Inspector як MCP-сервер, і AI-асистент сам створить URL, дочекається вебхука й розбере його разом із вами.

Shopify

Як тестувати вебхуки Shopify

Створюємо вебхук магазину або підписку застосунку, розбираємо заголовки X-Shopify, перевіряємо X-Shopify-Hmac-Sha256 і враховуємо тайм-аути та повтори Shopify.

Slack

Як тестувати Slack Events API та interactivity

Дивимося, що Slack надсилає на Request URL, проходимо перевірку url_verification, валідуємо X-Slack-Signature і розкодовуємо form-encoded payload від interactivity.

Telegram

Як тестувати вебхук Telegram-бота

Спрямовуємо бота на публічну адресу через setWebhook, роздивляємося сирі апдейти від Telegram і пересилаємо їх боту, що працює на вашій машині.

Twilio

Як тестувати вебхуки Twilio для SMS і дзвінків

Спрямовуємо номер Twilio на адресу-перехоплювач, читаємо form-encoded параметри, відповідаємо TwiML без власного сервера і перевіряємо X-Twilio-Signature через офіційні бібліотеки.