# Guide to n8n Webhooks: Features and Best Practices

n8n workflows often end by telling another system that something happened: a lead was captured, an order was synced, or a long-running workflow finished or failed. In n8n, that call is made with the HTTP Request node. If your service is on the receiving end of those calls, you are running a webhook endpoint for n8n, even though n8n has no webhook product of its own on the sending side.

This guide covers how the HTTP Request node sends requests, how to authenticate them when n8n does not sign anything, the node settings that affect delivery, and the best practices for an endpoint that n8n workflows call. It is about requests that leave n8n. The Webhook trigger node, which receives requests into a workflow, is a different topic.

## What are n8n webhooks?

An n8n webhook, in this sense, is a request built by the workflow author. The HTTP Request node lets the author set the URL, the method, query parameters, headers and the body. n8n adds no event type, no event ID and no envelope, so the payload is whatever the workflow sends.

The method is the author's choice of `DELETE`, `GET`, `HEAD`, `OPTIONS`, `PATCH`, `POST` or `PUT`. The body content type is also a choice: Form URLencoded, Form-Data, JSON, an n8n Binary File, or Raw content with a content type you name.

n8n does not sign these requests. Authentication comes from an n8n credential attached to the node, which adds a header with a value only you and the receiver know.

## n8n HTTP Request node features

| Feature | Details |
| --- | --- |
| Sender | The HTTP Request node in any workflow, on n8n Cloud or self-hosted |
| Method | `DELETE`, `GET`, `HEAD`, `OPTIONS`, `PATCH`, `POST` or `PUT` |
| Body | Form URLencoded, Form-Data, JSON, n8n Binary File or Raw |
| Event name | None from n8n. Add your own field, such as `event` |
| Event ID | None from n8n. Add your own field, such as `id` |
| Signature | None |
| Authentication | Generic credentials: Header auth, Bearer auth, Basic auth, and others |
| Header names | Lowercased by default (the **Lowercase Headers** option) |
| Timeout | Optional. How long the node waits for the receiver's response headers |
| Batching | Optional. Items per batch and an interval between batches |
| Success | By default only a `2xx` response. **Never Error** treats any response as success |
| Source IPs | Not published. A self-hosted instance sends from wherever it runs |

## Event payloads

Because n8n defines no events, agree a payload shape with the people who build the workflows. A small envelope with a name, an ID and the data makes the receiver much simpler:

```json
{
  "event": "order.created",
  "id": "order-1042-created",
  "data": {
    "order_id": "1042",
    "total": 129.5,
    "currency": "GBP"
  }
}

```

Build the `id` in the workflow from something that is unique in your data, such as the record's own ID and the kind of change, so a repeated request carries the same key. Your receiver then routes on `event` and deduplicates on `id`.

## Setting up an n8n workflow to send webhooks

1. Generate a long random secret, for example with `openssl rand -hex 32`.
2. In n8n, add an **HTTP Request** node where the workflow should notify your service.
3. Set **Method** to `POST` and **URL** to your endpoint.
4. Under **Authentication**, choose **Generic Credential Type** and then one of:
  
  * **Header Auth**, with a header **Name** such as `X-Webhook-Secret` and your secret as the **Value**
  * **Bearer Auth**, with your secret as the **Bearer Token**. n8n sends it as `Authorization: Bearer <token>`
  * **Basic Auth**, with a username and password
5. Turn on **Send Body**, choose **JSON**, and enter your payload.
6. Run the node once and check the response your endpoint returned.

You can also add the header with **Send Headers**, but a credential keeps the secret in one place that every node can reuse.

## Securing n8n webhooks

With no signature, verification means checking the header your credential adds. Compare the whole value with the one you stored, in constant time, and fail closed if your secret is not configured.

```javascript
const crypto = require('crypto');
const express = require('express');

// The header your n8n credential sends. Node lowercases incoming header names.
const SECRET_HEADER = (process.env.N8N_WEBHOOK_HEADER || 'x-webhook-secret').toLowerCase();

function verifyN8nHeader(headers, expected) {
  const received = headers[SECRET_HEADER];
  if (typeof received !== 'string' || !expected) return false;

  const a = Buffer.from(received, 'utf8');
  const b = Buffer.from(expected, 'utf8');
  return a.length === b.length && crypto.timingSafeEqual(a, b);
}

const app = express();

app.post('/webhooks/n8n', express.raw({ type: '*/*' }), (req, res) => {
  const secret = process.env.N8N_WEBHOOK_SECRET;
  if (!secret) return res.sendStatus(500); // fail closed

  if (!verifyN8nHeader(req.headers, secret)) {
    return res.sendStatus(401);
  }

  const payload = JSON.parse(req.body.toString('utf8'));
  // Deduplicate on payload.id, route on payload.event, then process async.
  res.sendStatus(200);
});

```

The same check in Python:

```python
import hmac

def verify_n8n_header(headers, expected: str, header_name: str = "x-webhook-secret") -> bool:
    received = headers.get(header_name)
    if not received or not expected:
        return False
    return hmac.compare_digest(received.encode("utf-8"), expected.encode("utf-8"))

```

For Bearer auth, set the header to `authorization` and store the full value, `Bearer <token>`. For Basic auth, do the same with `Basic ` followed by the base64 of `username:password`.

Other n8n credentials don't fit this model. Digest auth is a challenge your server has to run, OAuth1 and OAuth2 tokens come from your own authorisation server, and Query auth or Custom auth values in the query string or body are not headers. If you want a header check, use Header, Bearer or Basic auth.

A static header proves the caller knows your secret. It does not prove the body is unchanged, and the header is identical on every request, so a captured request can be sent again.

> **Make n8n webhooks production-ready.** [Hookdeck Event Gateway](/event-gateway) checks your n8n credential header before anything reaches your code, queues every request durably, and lets you replay what failed.

## n8n webhook limitations and pain points

### No signature, only a static header

**The Problem:** An endpoint that a workflow calls without a credential accepts anything posted to it. With a credential, anyone who captures one request can send more.

**Why It Happens:** The HTTP Request node signs nothing. Its credentials add a fixed header, and that header is the same on every request.

**Workarounds:**

* Attach a Header, Bearer or Basic auth credential to every HTTP Request node that calls your service.
* Serve the endpoint over HTTPS and never log the header value.
* To rotate, accept both the old and new values for a short time, update the credential in n8n, then remove the old value.

**How Hookdeck Can Help:** Hookdeck's n8n source type checks a header for you. Enter the header name and the exact value, including any `Bearer ` or `Basic ` prefix, and Hookdeck rejects requests without it before they reach your service.

### No event ID or event name unless you add one

**The Problem:** Your receiver can't tell one kind of request from another, or a repeated request from a new one.

**Why It Happens:** n8n sends only what the author puts in the node. Re-running a failed execution, or a workflow that processes the same item twice, sends the same data again with nothing to mark it as a repeat.

**Workarounds:**

* Put an `event` field and an `id` field in every payload, and document the shape for everyone who builds workflows.
* Store each `id` you process and skip repeats.

**How Hookdeck Can Help:** Hookdeck's [deduplication](/docs/deduplication) can drop repeated requests based on the fields you choose, such as the body's `id`, and [filters](/docs/filters) can route on `event`.

### The method and format are not fixed

**The Problem:** A handler that expects a JSON `POST` breaks when someone builds a workflow with `PUT` or a form body.

**Why It Happens:** Every node is configured by hand, and n8n offers seven methods and five body types.

**Workarounds:**

* Agree on `POST` with a JSON body for every workflow that calls your service.
* Return a clear `4xx` for anything else so the author sees the failure in n8n.

**How Hookdeck Can Help:** Hookdeck's n8n source starts with `POST` as its allowed method. If a workflow needs another method, add it to the source.

### Delivery depends on how the node is configured

**The Problem:** A slow response or an outage on your side ends with lost data, or with a workflow that reports success when nothing arrived.

**Why It Happens:** n8n has no delivery policy of its own for these requests. The **Timeout** option is how long the node waits for your response headers. By default the node succeeds only on a `2xx`, but **Never Error** makes every response count as success.

**Workarounds:**

* Leave **Never Error** off on nodes that notify your service, so failures show up in n8n.
* Respond quickly and do the work after you return `200`.
* Use an Error Trigger workflow in n8n to alert someone when a request fails.

**How Hookdeck Can Help:** Hookdeck accepts each request as soon as it arrives and stores it, so the node sees a fast `2xx` even while your service is down. You then [retry](/docs/retries) and replay requests on your own schedule, and [issues](/docs/issues) alert you when delivery to your service starts failing.

## Best practices

### Use a credential, not a plain header

A credential lets you change the secret in one place for every node that uses it.

### Agree a payload contract

Decide on an envelope with `event`, `id` and `data`, and use it in every workflow. A receiver with a known shape is easy to test and hard to break.

### Fail closed

Return `401` on a missing or wrong header, and reject everything if your own secret is not configured.

### Check headers case-insensitively

n8n lowercases header names by default. Most frameworks already treat header names case-insensitively, but if you read raw header names, compare them in lower case.

### Acknowledge fast and process asynchronously

Return `200` after the header check and queue the work, so a slow job doesn't run into the node's timeout. See [why to process webhooks asynchronously](/webhooks/guides/why-implement-asynchronous-processing-webhooks).

### Make handlers idempotent

Processing the same request twice should have the same result as processing it once. See our [guide to webhook idempotency](/webhooks/guides/implement-webhook-idempotency).

## Conclusion

Requests from n8n's HTTP Request node carry exactly what the workflow author configures, with no signature, event name or ID added by n8n. Attach a Header, Bearer or Basic auth credential to every node that calls your service, compare the full header value in constant time, and agree a payload with `event` and `id` fields so your receiver can route and deduplicate.

[Hookdeck Event Gateway](https://hookdeck.com) verifies that header and stops requests that fail it, stores every request before your service sees it, and retries on your terms, so a slow receiver doesn't turn into a failed workflow.

[Get started with Hookdeck](https://dashboard.hookdeck.com/signup) for free and handle n8n webhooks reliably in minutes.