Tips > Data, APIs & Webhooks

Handle Webhook Idempotency with Deduplication Keys

Webhook senders often retry on timeout or network errors, sending the same event multiple times.

TipIntermediate2 min read

Webhook senders often retry on timeout or network errors, sending the same event multiple times. Without idempotency handling, you will process duplicates -- charging customers twice, sending duplicate emails, or creating duplicate records. Check for an idempotency key before processing.

Real-world example: A payment gateway retries a payment.completed event three times. Your workflow should process it exactly once.

// Code node: "Check Idempotency"
// Place this immediately after the Webhook node

const idempotencyKey = $input.first().json.headers['x-idempotency-key']
  || $input.first().json.headers['x-request-id']
  || $input.first().json.body.event_id;

if (!idempotencyKey) {
  return [{
    json: {
      duplicate: false,
      idempotencyKey: null,
      warning: 'No idempotency key provided',
      data: $input.first().json.body
    }
  }];
}

// This value will be checked against the datastore in the next node
return [{
  json: {
    idempotencyKey,
    data: $input.first().json.body
  }
}];
```text
After the Code node, query your datastore (Google Sheet, Postgres, Redis):

```sql
-- Postgres node: Check if this event was already processed
-- Use parameterized queries ($1) to prevent SQL injection
SELECT idempotency_key, processed_at
FROM webhook_events
WHERE idempotency_key = $1
LIMIT 1;
-- Query Parameter 1: {{ $json.idempotencyKey }}
```text
Then use an IF node: if the query returned a row, respond with `200 OK` (acknowledge but skip). If no row, continue processing and insert the key when done:

```sql
-- Postgres node: Mark event as processed (after successful processing)
-- Use parameterized queries to prevent SQL injection
INSERT INTO webhook_events (idempotency_key, processed_at, event_type)
VALUES ($1, NOW(), $2);
-- Query Parameter 1: {{ $json.idempotencyKey }}
-- Query Parameter 2: {{ $json.data.event_type }}
```text
> **Danger: Never use string interpolation in SQL**
>
> Always use parameterized queries (`$1`, `$2`) instead of `'{{ $json.value }}'` in PostgreSQL nodes. String interpolation is vulnerable to SQL injection -- a malicious webhook payload could execute arbitrary SQL. See [Security Tip 7](../security/index.md) for details.

This guarantees exactly-once processing. The sender gets `200` for duplicates (so it stops retrying) and the event is processed only on the first delivery.

**Related:** [Set a Unique Encryption Key and Back It Up](../security-best-practices/01-set-a-unique-encryption-key-and-back-it-up.md) | [Use the HTTP Request Node as a Universal Connector](../integration-patterns/01-use-the-http-request-node-as-a-universal-connector.md)

Want this running in your stack?

I build production n8n and Cloudflare automation for teams — the same engineering behind HarperFlow. Fixed-price, escrow-protected, US-based.