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

Webhook idempotency в n8n и Postgres: защита от повторных событий

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

AI summary: Практический workflow для webhook idempotency: принять событие, проверить подпись, собрать стабильный idempotency key, записать его в Postgres через unique constraint, выполнить бизнес-действие только для нового события и безопасно ответить повтору.
Шаблон для внедрения

Импортируйте workflow, замените credentials и прогоните тестовый payload до включения production.

Проблема: webhook-провайдеры повторяют события при timeout, 5xx или сетевых сбоях. Если n8n каждый раз выполняет бизнес-действие, один платеж, лид или статус может обработаться дважды.

Решение: выносим идемпотентность в Postgres: один `event_id` или HMAC-ключ получает unique index, повторные webhook-события не запускают бизнес-ветку, а возвращают уже известный результат.

Схема webhook idempotency в n8n и Postgres
Workflow проверяет подпись, записывает idempotency key в Postgres и выполняет бизнес-действие только для нового события.

Проблема: почему webhook-события приходят повторно

Повторный webhook — это нормальное поведение, а не баг. Платежные сервисы, маркетплейсы, CRM и формы повторяют POST, если не получили быстрый 200 OK. Иногда первый запрос успел изменить CRM, но ответ потерялся на сети, и провайдер присылает событие снова.

Идемпотентность нужна там, где повтор опасен: создание сделки, начисление доступа, отправка письма, смена статуса, списание бонусов. Цель workflow — не “поймать дубль в конце”, а не пустить повтор в бизнес-ветку с самого начала.

Архитектура workflow idempotency на Postgres

НодаРольЧто проверить
Webhook inputпринимает событие от провайдераevent_id, timestamp, signature, raw body
Validate signatureпроверяет HMAC или secretзащита от подделки и replay
Build idempotency keyсобирает стабильный ключprovider:event_type:event_id
Insert key in Postgresделает INSERT ON CONFLICTunique index и статус processing
Run business actionвыполняет только новое событиеCRM, склад, письмо, доступ
Finalize eventставит processed или failedduration, target_id, error message

В Postgres лучше хранить не только ключ, но и состояние: `processing`, `processed`, `failed`. Тогда можно отличить честный повтор от зависшего события и сделать ручное восстановление без повторного бизнес-действия.

Контракт входных данных webhook-события

{
  "event_id": "evt_2f3c6a99",
  "event_type": "lead.created",
  "provider": "tilda",
  "occurred_at": "2026-05-30T10:00:00Z",
  "signature": "sha256=7b5f...",
  "data": {
    "phone": "+7 (916) 555-12-34",
    "name": "Анна",
    "source": "landing"
  }
}

Лучший ключ — ID события от провайдера. Если его нет, используйте комбинацию provider, event_type, external_id и timestamp bucket, но обязательно документируйте компромисс: такой ключ может быть менее точным.

Code Node: idempotency key и SQL ON CONFLICT

const crypto = require('crypto');
const src = $json.body ?? $json;
const provider = String(src.provider ?? 'unknown').trim();
const eventType = String(src.event_type ?? src.type ?? '').trim();
const eventId = String(src.event_id ?? src.id ?? '').trim();

if (!eventType) throw new Error('Missing event_type for idempotency');
if (!eventId) throw new Error('Missing event_id for idempotency');

const idempotencyKey = `${provider}:${eventType}:${eventId}`;
const payloadHash = crypto
  .createHash('sha256')
  .update(JSON.stringify(src.data ?? src))
  .digest('hex');

const sql = `
INSERT INTO webhook_idempotency (idempotency_key, provider, event_type, event_id, payload_hash, status, created_at)
VALUES ($1, $2, $3, $4, $5, 'processing', now())
ON CONFLICT (idempotency_key) DO NOTHING
RETURNING idempotency_key;
`;

return [{
  json: {
    idempotency_key: idempotencyKey,
    provider,
    event_type: eventType,
    event_id: eventId,
    payload_hash: payloadHash,
    sql,
    sql_params: [idempotencyKey, provider, eventType, eventId, payloadHash],
    original_event: src
  }
}];
Минимальная схема таблицы Postgres

Создайте таблицу webhook_idempotency с unique index по idempotency_key и полями provider, event_type, event_id, payload_hash, status, target_id, error, created_at, processed_at. Это позволит отличать новый event от повтора.

Готовый workflow JSON: скачать и импортировать

Скачать готовый workflow JSON Скачать тестовый payload

{
  "name": "Nodbot - Webhook idempotency with Postgres",
  "nodes": [
    {
      "name": "Webhook input",
      "type": "n8n-nodes-base.webhook",
      "purpose": "Принять событие и заголовки"
    },
    {
      "name": "Validate signature",
      "type": "n8n-nodes-base.code",
      "purpose": "Проверить HMAC/secret и timestamp"
    },
    {
      "name": "Build idempotency key",
      "type": "n8n-nodes-base.code",
      "purpose": "Собрать provider:event_type:event_id"
    },
    {
      "name": "Insert key in Postgres",
      "type": "n8n-nodes-base.postgres",
      "purpose": "INSERT ON CONFLICT DO NOTHING"
    },
    {
      "name": "New event gate",
      "type": "n8n-nodes-base.if",
      "purpose": "Пустить только новую запись"
    },
    {
      "name": "Business action",
      "type": "n8n-nodes-base.httpRequest",
      "purpose": "Создать/обновить объект во внешней системе"
    },
    {
      "name": "Finalize status",
      "type": "n8n-nodes-base.postgres",
      "purpose": "Поставить processed или failed"
    },
    {
      "name": "Respond",
      "type": "n8n-nodes-base.respondToWebhook",
      "purpose": "Вернуть 200 повтору и новому событию"
    }
  ],
  "connections": "Webhook input → Validate signature → Build idempotency key → Insert key in Postgres → New event gate → Business action → Finalize status → Respond"
}

Пошаговая настройка Postgres unique key и n8n

  1. Создайте таблицу `webhook_idempotency` и unique index по `idempotency_key`.
  2. Импортируйте workflow и настройте Webhook URL с секретом.
  3. Добавьте проверку подписи или timestamp, если провайдер это поддерживает.
  4. Соберите ключ из provider, event_type и event_id до бизнес-действия.
  5. После успешного действия обновляйте status, target_id и processed_at.

Тесты перед production и проверка повторных webhook

curl -X POST "https://YOUR-N8N-DOMAIN/webhook/webhook-idempotency-to-postgres" \
  -H "Content-Type: application/json" \
  --data @webhook-idempotency-to-postgres-payload.json
  1. Отправьте один и тот же event_id дважды: бизнес-действие должно выполниться один раз.
  2. Измените payload при том же event_id и проверьте реакцию по payload_hash.
  3. Сымитируйте падение бизнес-API после insert и проверьте статус processing/failed.
  4. Отправьте webhook без event_id и убедитесь, что workflow не делает бизнес-действие.
  5. Проверьте ручной replay из Postgres по failed-событию.

Production-риски webhook idempotency

  • Ключ строится из всего payload. Незначительный порядок полей создаёт новый hash и дубль.
  • Нет unique index. Две одновременные доставки проходят проверку одновременно.
  • 200 возвращается до записи ключа. Провайдер считает событие обработанным, хотя n8n мог упасть.
  • Повтор считается ошибкой. Провайдер продолжает ретраи, хотя правильный ответ повтору — безопасный 200.
  • Нет статусов. Нельзя отличить обработанное событие от зависшего processing.

См. также webhook signature validation, retry/DLQ для HTTP Request, idempotency keys и ЮKassa webhook с платежами. Официальные документы: PostgreSQL unique constraints, PostgreSQL INSERT ON CONFLICT и n8n Webhook node.

Карточка результата webhook idempotency в Postgres
Визуальная карточка показывает idempotency key, статус записи и целевой объект.

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

  1. В Postgres есть unique index по idempotency_key.
  2. Повторный event_id не запускает бизнес-действие.
  3. События имеют статусы processing, processed и failed.
  4. Подпись или секрет webhook проверяется до idempotency.
  5. Есть процедура безопасного replay для failed-событий.
Нужно защитить webhook от дублей и гонок?

Nodbot настроит idempotency-слой на Postgres: unique key, подписи, статусы, replay, DLQ и мониторинг повторных событий.

Внедрить idempotency