Обработка Stripe webhooks: HMAC, идемпотентность и async-пайплайн
Паттерн: raw body → HMAC SHA-256 → проверить ключ идемпотентности → ответить 200 → запустить пайплайн с бизнес-логикой. Stripe не делает повторную попытку, дублей нет.
Обновлено: 2026-04-20
Кратко
Суть ответа
Обработка Stripe webhooks: HMAC, идемпотентность и async-пайплайн. Функция получает raw body в `event.body`, считает HMAC с секретом Stripe из переменных окружения, проверяет timing-safe, возвращает 200, вызывает `global.durable.startNew()`. Оркестрация делает бизнес-логику.
Когда подходит и когда нет
- Обработка занимает больше 2–3 секунд
- Нужна идемпотентность при повторных доставках
На что обратить внимание
- Без сравнения HMAC с постоянным временем (timing-safe) возможны атаки по времени ответа и обход проверки подписи.
- Без ключа идемпотентности (`Stripe-Signature` header + event ID) повторная доставка создаёт дубль записи в базе.
Ситуация: нагрузка и где обычно ломается
Что ломается в простых Stripe webhook-обработчиках
- Parsing body before HMAC verification: signature mismatch on valid events
- Slow inline fulfillment: Stripe retries after 30s, causing double-charges
- Missing idempotency key: retry delivers event twice, two orders created
Если обработчик парсит body до проверки подписи — атакующий может отправить произвольные события. Stripe требует raw body для HMAC.
Когда упрощённых рецептов мало
Где при обработке вебхуков Stripe обычно теряются безопасность и идемпотентность (подпись, повтор доставки, таймауты)
Без сравнения HMAC с постоянным временем (timing-safe) возможны атаки по времени ответа и обход проверки подписи.
Без ключа идемпотентности (`Stripe-Signature` header + event ID) повторная доставка создаёт дубль записи в базе.
Как Inquir помогает в этом сценарии
Как Inquir помогает безопасно обрабатывать вебхуки Stripe: сырое тело запроса, HMAC и вынос бизнес-логики в пайплайн
Функция получает raw body в `event.body`, считает HMAC с секретом Stripe из переменных окружения, проверяет timing-safe, возвращает 200, вызывает `global.durable.startNew()`. Оркестрация делает бизнес-логику.
Секрет endpoint хранится в переменных окружения — не в коде. Ротация без редеплоя.
Что вы получаете на платформе
Ключевые части Stripe webhook-обработчика
Raw body
Не парсить body до проверки подписи. `event.body` — строка, как она пришла от Stripe.
HMAC SHA-256
timing-safe сравнение с `stripe.webhooks.constructEvent()` или ручным crypto.timingSafeEqual.
Ключ идемпотентности
Event ID из payload: `INSERT … ON CONFLICT DO NOTHING` до обработки.
Async оркестрация
`global.durable.startNew()` до `return { statusCode: 200 }` — бизнес-логика вне HTTP-окна.
Что сделать дальше, по шагам
Как обработать Stripe webhook
Проверить подпись
`stripe.webhooks.constructEvent(event.body, sig, secret)` — выбрасывает при несовпадении.
Проверить идемпотентность
Записать event.id в базу; конфликт — вернуть 200 без работы.
Ответить 200 и запустить оркестрацию
`global.durable.startNew('fulfill', undefined, { eventId, type, data })` → `return { statusCode: 200 }`.
Пример кода
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.