How to Receive Webhooks from n8n Workflows (Self-Hosted, Step by Step)
Receive webhooks sent by n8n HTTP Request nodes on your own server: Header Auth credential setup, constant-time token validation, Docker setup, and RabbitMQ routing, verified against a real n8n workflow execution.
By the end of this guide you will have a self-hosted endpoint that receives webhooks from your n8n workflows, authenticates every delivery with a secret header, and routes the payloads to a log, RabbitMQ, or any other destination. Everything below was verified end to end against a real n8n instance (v2.36) and the real Core Webhook Module Docker image: an actual n8n workflow sent the deliveries quoted here.
How n8n sends webhooks
n8n is best known for receiving webhooks, but every non-trivial workflow also needs to send them: notify another system when a pipeline finishes, push events into a queue, fan results out to services that are not n8n nodes. The tool for that is the HTTP Request node.
What an HTTP Request delivery looks like on the wire (captured from a real execution): a POST with User-Agent: n8n, Content-Type: application/json, your custom headers, and whatever JSON body the node builds. There is no built-in payload signing; authentication is whatever headers you configure, which is why the receiver should require a secret header and compare it in constant time.
Useful node options, quoted from the n8n docs: authentication via a Generic Credential Type (including Header auth, which stores "Name"/"Value" pairs of header parameters as a reusable credential), Send Body with Body Content Type set to JSON, a Timeout in milliseconds, and batching controls (Items per Batch, Batch Interval) when a workflow emits many items.
Prerequisites
- Docker on any machine (your laptop is fine for the test run)
- A public HTTPS URL for production, unless n8n and your receiver share a network. Two ways to get one:
- HTTPS for webhooks with nginx and Let's Encrypt if you have a public server
- Receiving webhooks through Cloudflare Tunnel if you do not want to open inbound ports
Step 1: Configure the receiver
Create a working directory with two files.
webhooks.json:
{
"n8n_events": {
"data_type": "json",
"module": "log",
"header_auth": {
"header_name": "X-Webhook-Token",
"api_key": "{$N8N_WEBHOOK_TOKEN}",
"case_sensitive": true
}
}
}
connections.json:
{}
What each parameter does:
n8n_eventsis the webhook ID; it becomes the URL path (/webhook/n8n_events).module: logprints each authenticated payload to the container log. Zero dependencies for the first run; we swap in RabbitMQ at the end.header_authrequires theX-Webhook-Tokenheader on every request and compares it in constant time.case_sensitive: truemakes the token value match exactly.{$N8N_WEBHOOK_TOKEN}pulls the token from an environment variable, so it never lives in a config file.
Generate a strong token (or use our webhook secret generator):
openssl rand -hex 32
Step 2: Run it
docker run -d --name webhook-gateway -p 8000:8000 \
-v "$PWD/webhooks.json:/app/webhooks.json:ro" \
-v "$PWD/connections.json:/app/connections.json:ro" \
-e N8N_WEBHOOK_TOKEN="your-generated-token" \
spiderhash/webhook:latest
Confirm it is up:
docker logs webhook-gateway
# ... INFO: Application startup complete.
The receiver now answers on http://localhost:8000/webhook/n8n_events, with auto-generated API docs at http://localhost:8000/docs.
Step 3: Test it locally
Simulate exactly what the n8n node will send:
cat > payload.json <<'EOF'
{"event": "deploy_finished", "service": "api", "version": "2.4.1", "status": "success", "triggered_by": "n8n"}
EOF
curl -s -w "\nHTTP %{http_code}\n" -X POST http://localhost:8000/webhook/n8n_events \
-H "Content-Type: application/json" \
-H "X-Webhook-Token: your-generated-token" \
--data-binary @payload.json
Expected result:
{"message":"200 OK"}
HTTP 200
Now prove the authentication works. A wrong token:
{"detail":"Invalid API key in header: X-Webhook-Token"}
HTTP 401
And a request with no token header at all:
{"detail":"Missing required header: X-Webhook-Token"}
HTTP 401
Those are the exact responses the gateway returns. docker logs webhook-gateway shows the accepted payload.
Step 4: Build the sending workflow in n8n
Add an HTTP Request node where your workflow should notify the outside world:
- Method:
POST - URL:
https://your-domain/webhook/n8n_events - Authentication:
Generic Credential Type> Header Auth, then create a credential with NameX-Webhook-Tokenand Value your token. The credential is stored encrypted by n8n and reused across workflows, instead of the token sitting in every node. - Send Body: on, Body Content Type:
JSON, and build the payload from your workflow data with expressions, for example:
{
"event": "deploy_finished",
"service": "{{ $json.service }}",
"version": "{{ $json.version }}",
"status": "success",
"triggered_by": "n8n"
}
Run the workflow once with Execute workflow; the node output shows the gateway's {"message":"200 OK"} response, and docker logs webhook-gateway shows the payload arriving with user-agent: n8n.
For a quick one-off test you can also skip the credential and toggle Send Headers with a header parameter named X-Webhook-Token; for anything permanent, prefer the Header Auth credential.
Troubleshooting
401 {"detail":"Invalid API key in header: X-Webhook-Token"}: the credential value andN8N_WEBHOOK_TOKENdiffer. They must be byte-identical; watch for trailing whitespace pasted into the credential.401 {"detail":"Missing required header: X-Webhook-Token"}: the node has no Header Auth credential selected and no matching header under Send Headers, or a proxy in between strips custom headers.- The node fails with a timeout on big payloads or slow networks: raise the node's Timeout option (milliseconds). The gateway acknowledges in milliseconds, so a timeout here usually points at the network path.
- A loop of items hammers the endpoint: the HTTP Request node sends one request per item by default. Use Items per Batch and Batch Interval to pace bulk sends, or aggregate items into one payload before the node.
Going to production
Printing to logs is not a pipeline. Swap the module for a real destination; here is the RabbitMQ variant, verified end to end (a real n8n workflow execution landed as a message in the n8n_events queue):
webhooks.json:
{
"n8n_events": {
"data_type": "json",
"module": "rabbitmq",
"connection": "rabbitmq_local",
"module-config": {
"queue_name": "n8n_events"
},
"header_auth": {
"header_name": "X-Webhook-Token",
"api_key": "{$N8N_WEBHOOK_TOKEN}",
"case_sensitive": true
}
}
}
connections.json:
{
"rabbitmq_local": {
"type": "rabbitmq",
"host": "{$RABBITMQ_HOST:localhost}",
"port": "{$RABBITMQ_PORT:5672}",
"user": "{$RABBITMQ_USER:guest}",
"pass": "{$RABBITMQ_PASS:guest}"
}
}
The same pattern works for 17 other destinations: archive workflow events to S3, publish to Kafka, or fan out to several at once with webhook chaining. Built-in rate limiting and retry handling cover the operational edges. The full authentication reference, including header auth and 11 other methods, is in the docs.