Интеграция Stripe и n8n: webhooks, idempotency и CRM-события ¶
Обновлено: 2026-05-30
Импортируйте JSON в n8n, замените credentials, URL API, project/list IDs, поля и лимиты под вашу инфраструктуру.
Проблема: Stripe отправляет платежные события асинхронно и может доставлять webhook повторно. Если n8n сразу закрывает сделку или выдаёт доступ без idempotency, бизнес получает финансовые дубли.
Решение: Надёжная интеграция Stripe и n8n проверяет подпись webhook, различает event types, собирает idempotency key по event_id/payment_intent и обновляет CRM только после durable-записи в журнале.
Проблема: почему простая интеграция создаёт дубли и ручной хаос ¶
Stripe удобен для оплаты, подписок и checkout-сценариев, но платежное событие нельзя обрабатывать как обычную заявку. В production важны порядок событий, повторная доставка, возвраты, test mode, разные валюты и связь payment с order_id или deal_id.
Самая частая ошибка — принимать любой webhook как “оплачено”. В результате checkout.session.completed, payment_intent.succeeded и invoice.paid начинают дублировать действия: CRM закрывает сделку дважды, доступ выдаётся повторно, а менеджеры видят несколько одинаковых комментариев.
Архитектура workflow для n8n ¶
| Блок | Задача | Production-проверка |
|---|---|---|
| Stripe Webhook | принимает event object от Stripe | HTTPS endpoint, raw body, signature header |
| Verify and route | проверяет подпись и тип события | allowlist event types, test/live mode |
| Normalize payment | готовит payment_intent, amount, customer и metadata | order_id/deal_id в metadata |
| Check idempotency | фиксирует event_id или payment_intent в журнале | уникальный ключ в Postgres |
| Update CRM/access | обновляет сделку, заказ или entitlement | один бизнес-эффект на событие |
| Respond 2xx | подтверждает получение Stripe | без stack trace и секретов |
Для платежей важно разделить технический ответ Stripe и бизнес-действие. Webhook должен быстро вернуть 2xx, а тяжёлую обработку лучше делать после idempotency-записи.
Контракт входных данных ¶
{
"id": "evt_1PqTestStripe",
"object": "event",
"type": "payment_intent.succeeded",
"livemode": false,
"created": 1780123456,
"data": {
"object": {
"id": "pi_3PqPayment",
"object": "payment_intent",
"status": "succeeded",
"amount": 1290000,
"currency": "rub",
"customer": "cus_NodbotDemo",
"metadata": {
"order_id": "10492",
"deal_id": "crm-5581",
"source": "checkout"
}
}
}
}Минимальный production-контракт — event id, type, payment_intent id, amount/currency и metadata с order_id или deal_id. Не связывайте оплату с CRM только по сумме или email.
Code Node: нормализация, mapping и guard-условия ¶
const event = $json.body ?? $json;
const type = String(event.type ?? '').trim();
const allowed = ['payment_intent.succeeded', 'checkout.session.completed', 'invoice.paid', 'charge.refunded'];
if (!allowed.includes(type)) {
return [{ json: { action: 'ignore', reason: 'event_not_allowed', type } }];
}
const obj = event.data?.object ?? {};
const paymentIntent = String(obj.payment_intent ?? obj.id ?? '').trim();
const eventId = String(event.id ?? '').trim();
if (!eventId || !paymentIntent) throw new Error('No Stripe event_id or payment_intent');
const metadata = obj.metadata ?? {};
const orderId = String(metadata.order_id ?? '').trim();
const dealId = String(metadata.deal_id ?? '').trim();
if (!orderId && !dealId) throw new Error('Stripe metadata must contain order_id or deal_id');
return [{ json: {
action: type.includes('refunded') ? 'mark_refunded' : 'mark_paid',
idempotency_key: `stripe:${eventId}`,
payment_key: `stripe-payment:${paymentIntent}:${type}`,
event_id: eventId,
event_type: type,
payment_intent: paymentIntent,
amount: Number(obj.amount_received ?? obj.amount_paid ?? obj.amount ?? 0) / 100,
currency: String(obj.currency ?? '').toUpperCase(),
order_id: orderId,
deal_id: dealId,
livemode: event.livemode === true,
crm_comment: `Stripe ${type}: ${paymentIntent}`
}}];
Почему idempotency key для webhook отличается от Stripe Idempotency-Key
Stripe Idempotency-Key нужен для ваших исходящих POST-запросов к Stripe. Для входящих webhooks всё равно нужен свой durable-ключ по event_id или payment_intent, иначе повторная доставка события повторит бизнес-действие.
Готовый workflow JSON: скачать и импортировать ¶
Скачать готовый workflow JSON Скачать тестовый payload
{
"name": "Nodbot - Stripe webhook to CRM with idempotency",
"nodes": [
{
"name": "Stripe Webhook",
"type": "n8n-nodes-base.webhook",
"purpose": "Принять payment event"
},
{
"name": "Verify and route Stripe event",
"type": "n8n-nodes-base.code",
"purpose": "Проверить тип события и подпись"
},
{
"name": "Normalize payment event",
"type": "n8n-nodes-base.code",
"purpose": "Собрать payment contract"
},
{
"name": "Check idempotency",
"type": "n8n-nodes-base.postgres",
"purpose": "Не обработать event дважды"
},
{
"name": "Update CRM or access",
"type": "n8n-nodes-base.httpRequest",
"purpose": "Обновить сделку/заказ/доступ"
},
{
"name": "Respond 2xx",
"type": "n8n-nodes-base.respondToWebhook",
"purpose": "Подтвердить webhook"
}
],
"connections": "Stripe Webhook → Verify and route Stripe event → Normalize payment event → Check idempotency → Update CRM or access → Respond 2xx"
}
Пошаговая настройка связки ¶
- Создайте Stripe webhook endpoint только для нужных event types.
- Включите проверку подписи Stripe или выполняйте её в reverse proxy/Code Node.
- Передавайте order_id или deal_id в metadata при создании checkout/payment.
- Импортируйте workflow JSON и замените credentials, Postgres и CRM endpoint.
- Проверьте повторную доставку одного event и refund-сценарий.
Тесты перед production ¶
curl -X POST "https://YOUR-N8N-DOMAIN/webhook/integration-stripe-n8n-payment-webhooks" \
-H "Content-Type: application/json" \
--data @integration-stripe-n8n-payment-webhooks-payload.json- Повторный payload не создаёт дубль и возвращает тот же output key.
- Некорректный mapping останавливается до запроса к внешнему API.
- Пустые необязательные поля не ломают workflow.
- Ошибка API уходит в alert или DLQ с безопасным payload.
- Execution data не содержит секретов, токенов и лишних персональных данных.
Production-риски ¶
- Нет проверки подписи. Публичный webhook URL можно подделать.
- Любой event считается оплатой. Checkout, invoice и payment_intent начинают повторять бизнес-действия.
- Нет durable idempotency. После рестарта n8n повторный webhook снова обновит CRM.
- Metadata пустая. Платёж невозможно связать с заказом без ручного поиска.
- Refund не выделен в отдельную ветку. Возвраты начинают ломать оплаченные статусы.
Полезные ссылки и смежные материалы ¶
- Stripe Webhooks documentation
- Stripe Idempotent requests
- Stripe Events API
- Webhook signature validation
- Webhook idempotency to Postgres
- ЮKassa payment to CRM
Критерии готовности ¶
- Webhook signature проверяется до бизнес-логики.
- Event type allowlist не пропускает лишние события.
- Idempotency key записывается в durable storage.
- CRM обновляется только при наличии order_id или deal_id.
- Refund, payment success и invoice paid имеют отдельные правила.
Nodbot настроит Stripe + n8n: проверку подписи, idempotency, CRM/update access, refund-сценарии, alerts, retry и тестовые payload.
Обсудить Stripe-интеграцию