---
title: "Webhook idempotency в Postgres и n8n | Nodbot"
source_url: "https://nodbot.ru/workflows/webhook-idempotency-to-postgres/"
canonical_url: "https://nodbot.ru/workflows/webhook-idempotency-to-postgres/"
language: "ru"
content_type: "WorkflowTemplate"
section: "workflows"
generated_at: "2026-05-30"
word_count_source: 1028
---

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

## AI summary

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

## Best used for

Полноценный Problem/Solution-мануал для внедрения в n8n: импортировать workflow JSON, настроить API, выполнить production-тесты и передать решение команде.

## Table of contents

- Проблема: где ломается сценарий
- Архитектура workflow
- Контракт входных данных
- Code Node: нормализация и контроль
- Готовый workflow JSON
- Пошаговая настройка
- Тесты перед production
- Production-риски
- Полезные ссылки и смежные workflow
- Критерии готовности

## Key topics

- webhook idempotency
- n8n
- Postgres
- unique key
- ON CONFLICT
- HMAC
- deduplication
- replay

## Source outline

Webhook idempotency в n8n и Postgres: защита от повторных событий ¶ Обновлено: 2026-05-30 AI summary: Практический workflow для webhook idempotency: принять событие, проверить подпись, собрать стабильный idempotency key, записать его в Postgres через unique constraint, выполнить бизнес-действие только для нового события и безопасно ответить повтору. Шаблон для внедрения Скачать workflow JSON Скачать test payload Скопировать curl Импортируйте workflow, замените credentials и прогоните тестовый payload до включения production. Содержание Проблема: где ломается сценарий Архитектура workflow Контракт входных данных Code Node: нормализация и контроль Готовый workflow JSON Пошаговая настройка Тесты перед production Production-риски Полезные ссылки и смежные workflow Критерии готовности Проблема: webhook-провайдеры повторяют события при timeout, 5xx или сетевых сбоях. Если n8n каждый раз выполняет бизнес-действие, один платеж, лид или статус может обработаться дважды. Решение: выносим идемпотентность в Postgres: один `event_id` или HMAC-ключ получает unique index, повторные webhook-события не запускают бизнес-ветку, а возвращают уже известный результат. 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 CONFLICT unique index и статус processing Run business action выполняет только новое событие CRM, склад, письмо, доступ Finalize event ставит processed или failed duration, 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 ¶ Создайте таблицу `webhook_idempotency` и unique index по `idempotency_key`. Импортируйте workflow и настройте Webhook URL с секретом. Добавьте проверку подписи или timestamp, если провайдер это поддерживает. Соберите ключ из provider, event_type и event_id до бизнес-действия. После успешного действия обновляйте 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 Отправьте один и тот же event_id дважды: бизнес-действие должно выполниться один раз. Измените payload при том же event_id и проверьте реакцию по payload_hash. Сымитируйте падение бизнес-API после insert и проверьте статус processing/failed. Отправьте webhook без event_id и убедитесь, что workflow не делает бизнес-действие. Проверьте ручной replay из Postgres по failed-событию. Production-риски webhook idempotency ¶ Ключ строится из всего payload. Незначительный порядок полей создаёт новый hash и дубль. Нет unique index. Две одновременные доставки проходят проверку одновременно. 200 возвращается до записи ключа. Провайдер считает событие обработанным, хотя n8n мог упасть. Повтор считается ошибкой. Провайдер продолжает ретраи, хотя правильный ответ повтору — безопасный 200. Нет статусов. Нельзя отличить обработанное событие от зависшего processing. Полезные ссылки и смежные workflow ¶ См. также webhook signature validation , retry/DLQ для HTTP Request , idempotency keys и ЮKassa webhook с платежами . Официальные документы: PostgreSQL unique constraints , PostgreSQL INSERT ON CONFLICT и n8n Webhook node . Визуальная карточка показывает idempotency key, статус записи и целевой объект. Критерии готовности ¶ В Postgres есть unique index по idempotency_key. Повторный event_id не запускает бизнес-действие. События имеют статусы processing, processed и failed. Подпись или секрет webhook проверяется до idempotency. Есть процедура безопасного replay для failed-событий. Нужно защитить webhook от дублей и гонок? Nodbot настроит idempotency-слой на Postgres: unique key, подписи, статусы, replay, DLQ и мониторинг повторных событий. Внедрить idempotency

## Test payload

```json
{
  "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"
  }
}
```

## Key implementation snippet

```javascript
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
  }
}];
```

## Importable workflow structure

```json
{
  "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"
}
```

## Retrieval hints

- Использовать HTML как canonical source.
- Markdown удобен для LLM-ответов, извлечения workflow-контракта, кода и чеклистов.
- Для ссылок пользователю отдавать canonical URL.
