Обработка вебхуков Stripe: HMAC, идемпотентность и async-пайплайн
Stripe ретраит вебхуки до 72 часов при сбое. Обрабатывайте их правильно: проверяйте HMAC-подпись по сырому телу, записывайте ключ идемпотентности до любой мутации, отвечайте 200 за 30 секунд и продолжайте фулфилмент в фоновом пайплайне.
Обновлено: 2026-06-28
Кратко
Суть ответа
Обработка вебхуков Stripe: HMAC, идемпотентность и async-пайплайн. Функция получает raw body в event.body, считает HMAC с секретом Stripe из переменных окружения, проверяет timing-safe, возвращает 200, вызывает global.durable.startNew(). Оркестрация делает бизнес-логику.
Когда подходит и когда нет
- Обработка занимает больше 2–3 секунд
- Нужна идемпотентность при повторных доставках
На что обратить внимание
- Без сравнения HMAC с постоянным временем (timing-safe) возможны атаки по времени ответа и обход проверки подписи.
- Без ключа идемпотентности по event ID повторная доставка создаёт дубль записи в базе.
Ситуация: нагрузка и где обычно ломается
Что ломается в простых Stripe webhook-обработчиках
- Парсинг body до проверки HMAC — подпись не сходится даже на валидных событиях
- Синхронный фулфилмент: Stripe ретраит после 30 с → двойные списания
- Нет ключа идемпотентности: повторная доставка создаёт второй заказ
Stripe подписывает вебхуки HMAC-SHA256 по сырому телу. Если JSON.parse до проверки — нормализация пробелов ломает подпись. Если фулфилмент синхронный и дольше 30 с, Stripe ретраит — без идемпотентности это дубли побочных эффектов.
Когда простых рецептов недостаточно
Где теряются безопасность и идемпотентность
Без сравнения HMAC с постоянным временем (timing-safe) возможны атаки по времени ответа и обход проверки подписи.
Без ключа идемпотентности по event ID повторная доставка создаёт дубль записи в базе.
Как Inquir помогает в этом сценарии
Паттерн Stripe-вебхука на Inquir
Функция получает raw body в event.body, считает HMAC с секретом Stripe из переменных окружения, проверяет timing-safe, возвращает 200, вызывает global.durable.startNew(). Оркестрация делает бизнес-логику.
Секрет endpoint хранится в переменных окружения — не в коде. Ротация без редеплоя.
Что вы получаете на платформе
Ключевые части Stripe webhook-обработчика
Raw body для HMAC
Не парсить body до проверки подписи. event.body — строка; HMAC через stripe.webhooks.constructEvent() или timing-safe сравнение.
Ключ идемпотентности
Event ID из payload: upsert до любой мутации. Повторная доставка — 200 без второго фулфилмента.
Быстрый ACK
Вернуть 200 до фулфилмента. Stripe ждёт ответ в пределах 30 секунд.
Пайплайн для фулфилмента
global.durable.startNew() до return { statusCode: 200 } — email, инвентарь и ретраи вне HTTP-окна.
Что сделать дальше, по шагам
Как обработать Stripe webhook
Проверить подпись
stripe.webhooks.constructEvent(event.body, sig, secret) — выбрасывает при несовпадении.
Записать ключ идемпотентности
Upsert evt.id в таблицу событий. Если ключ уже есть — сразу вернуть 200 без фулфилмента.
Запустить оркестрацию и вернуть 200
global.durable.startNew('fulfill', undefined, { eventId, type, data }), затем return { statusCode: 200 } в пределах 30 с Stripe.
Пример кода
Stripe webhook handler
Complete pattern: raw body, signature verify, idempotency key, fast ACK, async fulfillment pipeline.
export async function handler(event) { const rawBody = event.body ?? ''; const sig = event.headers['stripe-signature'] ?? ''; let evt; try { evt = stripe.webhooks.constructEvent(rawBody, sig, process.env.STRIPE_WEBHOOK_SECRET); } catch (err) { return { statusCode: 400, body: `Webhook Error: ${err.message}` }; } const isNew = await db.webhookEvents.upsert({ id: evt.id, type: evt.type }); if (!isNew) return { statusCode: 200, body: 'duplicate' }; if (evt.type === 'payment_intent.succeeded') { await global.durable.startNew('stripe-fulfill', undefined, { intentId: evt.data.object.id, amount: evt.data.object.amount }); } return { statusCode: 200, body: JSON.stringify({ received: true }) }; }
Когда подходит и когда нет
Когда нужен async пайплайн для Stripe
Когда это уместно
- Обработка занимает больше 2–3 секунд
- Нужна идемпотентность при повторных доставках
Когда лучше выбрать другое
- Лёгкий вебхук с мгновенным ответом, без side-effects кроме записи в БД
Вопросы и ответы
Вопросы и ответы
Почему нужен raw body?
Stripe считает подпись по raw-байтам тела. После JSON.parse/stringify байты могут измениться — подпись не совпадёт.
Как хранить webhook secret?
Переменная окружения STRIPE_WEBHOOK_SECRET в настройках функции. Не коммитить в git.