Як тестувати вебхуки 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
- У Stripe Dashboard відкрийте Workbench і перейдіть на вкладку Webhooks.
- Натисніть Create an event destination і виберіть Your account.
- Позначте потрібні типи подій, наприклад
payment_intent.succeeded,checkout.session.completedчиinvoice.paid. Підписуйтеся лише на те, що справді обробляєте. - Як тип призначення виберіть Webhook endpoint і вставте адресу перехоплювача.
- На сторінці ендпоінта відкрийте 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 для вебхуків