Перейти к содержанию

Интеграция Stripe и n8n: webhooks, idempotency и CRM-события

Обновлено: 2026-05-30

AI summary: Problem/Solution-гайд по Stripe и n8n: как принимать платежные события, проверять подпись, обрабатывать event один раз и безопасно обновлять CRM, доступ или заказ.
Готовый blueprint для внедрения

Импортируйте 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 и n8n для webhook, idempotency и CRM-синхронизации
Схема показывает безопасный маршрут платежного события Stripe до обновления CRM или доступа.

Проблема: почему простая интеграция создаёт дубли и ручной хаос

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 от StripeHTTPS endpoint, raw body, signature header
Verify and routeпроверяет подпись и тип событияallowlist event types, test/live mode
Normalize paymentготовит payment_intent, amount, customer и metadataorder_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"
}

Пошаговая настройка связки

  1. Создайте Stripe webhook endpoint только для нужных event types.
  2. Включите проверку подписи Stripe или выполняйте её в reverse proxy/Code Node.
  3. Передавайте order_id или deal_id в metadata при создании checkout/payment.
  4. Импортируйте workflow JSON и замените credentials, Postgres и CRM endpoint.
  5. Проверьте повторную доставку одного 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
  1. Повторный payload не создаёт дубль и возвращает тот же output key.
  2. Некорректный mapping останавливается до запроса к внешнему API.
  3. Пустые необязательные поля не ломают workflow.
  4. Ошибка API уходит в alert или DLQ с безопасным payload.
  5. Execution data не содержит секретов, токенов и лишних персональных данных.

Production-риски

  • Нет проверки подписи. Публичный webhook URL можно подделать.
  • Любой event считается оплатой. Checkout, invoice и payment_intent начинают повторять бизнес-действия.
  • Нет durable idempotency. После рестарта n8n повторный webhook снова обновит CRM.
  • Metadata пустая. Платёж невозможно связать с заказом без ручного поиска.
  • Refund не выделен в отдельную ветку. Возвраты начинают ломать оплаченные статусы.
Карточка результата Stripe sync с payment intent, суммой и idempotency key
Пример результата: платеж обработан один раз, сделка закрыта и повторный webhook не создаёт дубль.

Критерии готовности

  1. Webhook signature проверяется до бизнес-логики.
  2. Event type allowlist не пропускает лишние события.
  3. Idempotency key записывается в durable storage.
  4. CRM обновляется только при наличии order_id или deal_id.
  5. Refund, payment success и invoice paid имеют отдельные правила.
Нужно подключить Stripe без финансовых дублей?

Nodbot настроит Stripe + n8n: проверку подписи, idempotency, CRM/update access, refund-сценарии, alerts, retry и тестовые payload.

Обсудить Stripe-интеграцию