{
  "name": "Nodbot - Webhook idempotency with Postgres",
  "nodes": [
    {
      "parameters": {
        "path": "webhook-idempotency-to-postgres",
        "httpMethod": "POST",
        "responseMode": "responseNode"
      },
      "id": "962d77c4-cd65-435e-9fbf-315113e23e23",
      "name": "Webhook input",
      "type": "n8n-nodes-base.webhook",
      "typeVersion": 2,
      "position": [
        0,
        260
      ]
    },
    {
      "parameters": {
        "jsCode": "const crypto = require('crypto');\nconst src = $json.body ?? $json;\nconst provider = String(src.provider ?? 'unknown').trim();\nconst eventType = String(src.event_type ?? src.type ?? '').trim();\nconst eventId = String(src.event_id ?? src.id ?? '').trim();\n\nif (!eventType) throw new Error('Missing event_type for idempotency');\nif (!eventId) throw new Error('Missing event_id for idempotency');\n\nconst idempotencyKey = `${provider}:${eventType}:${eventId}`;\nconst payloadHash = crypto\n  .createHash('sha256')\n  .update(JSON.stringify(src.data ?? src))\n  .digest('hex');\n\nconst sql = `\nINSERT INTO webhook_idempotency (idempotency_key, provider, event_type, event_id, payload_hash, status, created_at)\nVALUES ($1, $2, $3, $4, $5, 'processing', now())\nON CONFLICT (idempotency_key) DO NOTHING\nRETURNING idempotency_key;\n`;\n\nreturn [{\n  json: {\n    idempotency_key: idempotencyKey,\n    provider,\n    event_type: eventType,\n    event_id: eventId,\n    payload_hash: payloadHash,\n    sql,\n    sql_params: [idempotencyKey, provider, eventType, eventId, payloadHash],\n    original_event: src\n  }\n}];"
      },
      "id": "877f7d8c-94b5-4b7b-9aa7-df79de511bb0",
      "name": "Validate signature",
      "type": "n8n-nodes-base.code",
      "typeVersion": 2,
      "position": [
        280,
        260
      ]
    },
    {
      "parameters": {
        "jsCode": "const crypto = require('crypto');\nconst src = $json.body ?? $json;\nconst provider = String(src.provider ?? 'unknown').trim();\nconst eventType = String(src.event_type ?? src.type ?? '').trim();\nconst eventId = String(src.event_id ?? src.id ?? '').trim();\n\nif (!eventType) throw new Error('Missing event_type for idempotency');\nif (!eventId) throw new Error('Missing event_id for idempotency');\n\nconst idempotencyKey = `${provider}:${eventType}:${eventId}`;\nconst payloadHash = crypto\n  .createHash('sha256')\n  .update(JSON.stringify(src.data ?? src))\n  .digest('hex');\n\nconst sql = `\nINSERT INTO webhook_idempotency (idempotency_key, provider, event_type, event_id, payload_hash, status, created_at)\nVALUES ($1, $2, $3, $4, $5, 'processing', now())\nON CONFLICT (idempotency_key) DO NOTHING\nRETURNING idempotency_key;\n`;\n\nreturn [{\n  json: {\n    idempotency_key: idempotencyKey,\n    provider,\n    event_type: eventType,\n    event_id: eventId,\n    payload_hash: payloadHash,\n    sql,\n    sql_params: [idempotencyKey, provider, eventType, eventId, payloadHash],\n    original_event: src\n  }\n}];"
      },
      "id": "ad150bb4-beb5-4fe5-802d-4cb9981c9e40",
      "name": "Build idempotency key",
      "type": "n8n-nodes-base.code",
      "typeVersion": 2,
      "position": [
        560,
        260
      ]
    },
    {
      "parameters": {},
      "id": "5cefb54c-232a-48b2-915e-bbb1025ecb36",
      "name": "Insert key in Postgres",
      "type": "n8n-nodes-base.postgres",
      "typeVersion": 2,
      "position": [
        840,
        260
      ]
    },
    {
      "parameters": {},
      "id": "e8c5275d-adba-427e-92e3-6bd493909086",
      "name": "New event gate",
      "type": "n8n-nodes-base.httpRequest",
      "typeVersion": 2,
      "position": [
        1120,
        260
      ]
    },
    {
      "parameters": {},
      "id": "c78a7093-e076-48c6-8e42-1f030eacd6b4",
      "name": "Business action",
      "type": "n8n-nodes-base.httpRequest",
      "typeVersion": 2,
      "position": [
        1400,
        260
      ]
    },
    {
      "parameters": {},
      "id": "aff98a75-8d4f-442c-9bf9-227470d4ccf9",
      "name": "Finalize status",
      "type": "n8n-nodes-base.httpRequest",
      "typeVersion": 2,
      "position": [
        1680,
        260
      ]
    },
    {
      "parameters": {},
      "id": "c426eab0-8573-4996-bd6d-a2395941bcf0",
      "name": "Respond",
      "type": "n8n-nodes-base.respondToWebhook",
      "typeVersion": 2,
      "position": [
        1960,
        260
      ]
    }
  ],
  "connections": {
    "Webhook input": {
      "main": [
        [
          {
            "node": "Validate signature",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Validate signature": {
      "main": [
        [
          {
            "node": "Build idempotency key",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Build idempotency key": {
      "main": [
        [
          {
            "node": "Insert key in Postgres",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Insert key in Postgres": {
      "main": [
        [
          {
            "node": "New event gate",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "New event gate": {
      "main": [
        [
          {
            "node": "Business action",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Business action": {
      "main": [
        [
          {
            "node": "Finalize status",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Finalize status": {
      "main": [
        [
          {
            "node": "Respond",
            "type": "main",
            "index": 0
          }
        ]
      ]
    }
  },
  "pinData": {},
  "settings": {
    "executionOrder": "v1"
  },
  "staticData": null,
  "tags": [
    "nodbot",
    "production",
    "template"
  ],
  "triggerCount": 1,
  "updatedAt": "2026-05-30T00:00:00.000Z",
  "versionId": "91bf14a8-693c-4134-89a7-e0de505163d4",
  "meta": {
    "templateCredsSetupCompleted": false
  }
}