BV
All articles

n8n Webhook Tutorial: How to Receive, Process, and Route Incoming Data

n8n webhook nodes receive data from any external system and trigger powerful workflows in response. Here is how to set them up, route events, verify signatures, and go to production.

Muhammad Bilal
Muhammad Bilal Virk
11 min read
n8n Webhook Tutorial: How to Receive, Process, and Route Incoming Data

n8n's webhook capability is one of its most powerful features. A webhook endpoint in n8n can receive data from any system that sends HTTP requests — payment processors, CRMs, form tools, AI platforms, custom applications — and trigger any workflow in response. Understanding how n8n handles webhooks is foundational to building integrations that respond to real-world events in real time. If you're new to n8n itself, How to Build an n8n Workflow is the place to start.

This tutorial covers everything: setting up webhook nodes, understanding the data that arrives, filtering and routing based on event type, handling security, and the difference between test and production webhook behaviour.


How n8n Webhooks Work

When you add a Webhook node to an n8n workflow and activate it, n8n registers a unique URL at your instance. External systems send HTTP POST (or GET) requests to that URL. n8n receives the request, makes the data available to downstream nodes, and executes the rest of the workflow.

The webhook URL format for a self-hosted n8n instance looks like: https://your-n8n-domain.com/webhook/your-unique-path

For n8n Cloud, the format is similar with your cloud instance domain. If you haven't set up your instance yet, n8n Self-Hosted Setup covers that first.

Each webhook node has two URLs: a test URL and a production URL. The test URL is active only when you are in the n8n editor and have clicked "Listen for test event." The production URL is active when the workflow is activated. This distinction matters for how you configure the sending system.


Setting Up Your First Webhook

Create a new workflow in n8n. Click the plus button to add a node and search for Webhook. Select the Webhook node.

In the node settings:

HTTP Method. The most common is POST, which is what most systems use to send webhook payloads. Some systems (like certain form tools) use GET. Match what the sending system expects.

Path. The unique path for this webhook. n8n generates a UUID by default, but you can customise it to something readable: /form-submission, /retell-call-ended, /stripe-payment. Readable paths make multi-webhook setups easier to manage.

Authentication. None by default. For production webhooks, add authentication (covered below).

Response Mode. Whether to respond immediately with a fixed response or wait for the workflow to complete before sending the response. For most webhooks, respond immediately with a success acknowledgment — do not make the sender wait for your entire workflow to finish before confirming receipt. What the sending system is looking for is a 2xx status, and RFC 9110 is what defines each of those codes.

Copy the test URL and send a test payload from the external system. Click "Listen for test event" in n8n first — the node waits for the incoming request. Once the test payload arrives, n8n shows you the data structure and the webhook node is configured.


Understanding the Incoming Data

When a webhook payload arrives in n8n, it is available as the output of the Webhook node. The structure depends on what the sending system sends, but most webhooks deliver a JSON body.

The Webhook node output has this structure:

json
{
  "headers": {
    "content-type": "application/json",
    "x-signature": "sha256=abc123..."
  },
  "params": {},
  "query": {},
  "body": {
    "event": "call.ended",
    "call_id": "call_abc123",
    "caller_phone": "+14085551234",
    "duration": 187,
    "transcript": "Hello, I need to book..."
  }
}

In subsequent nodes, you access the body fields using expressions: {{ $json.body.event }}, {{ $json.body.caller_phone }}, {{ $json.body.duration }}.

Headers are accessible at {{ $json.headers['x-signature'] }} — useful for signature verification.


Routing Based on Event Type

Many systems send multiple event types to the same webhook URL. Retell AI sends call.started, call.ended, and call.failed events to the same endpoint. Stripe sends payment.succeeded, payment.failed, customer.created, and dozens more to the same URL.

For single-event webhooks, no routing is needed. For multi-event webhooks, use the Switch node (n8n's equivalent of a router with multiple outputs) immediately after the Webhook node.

Add a Switch node. Set the value to evaluate: {{ $json.body.event }}. Add output conditions:

  • Value equals call.ended — output 0
  • Value equals call.failed — output 1
  • Value equals call.started — output 2
  • Fallback output for anything else — output 3

Connect each output to a different branch of the workflow. The call.ended branch processes the call data and updates the CRM. The call.failed branch sends an alert. The call.started branch might log the call initiation. Unknown events route to a logging step for investigation.

This single-webhook, multi-branch architecture is cleaner than creating separate webhook URLs for each event type. One URL to configure in the sending system, one workflow to maintain.


Filtering Unwanted Events Early

Not every event that arrives at your webhook needs to trigger the full workflow. A Retell call.ended event with a duration of 5 seconds is probably a hang-up, not a real call worth processing. A Stripe event for a refund should be handled differently from a payment success.

Add an IF node immediately after the Switch node on the relevant branch:

For calls: {{ $json.body.duration }} greater than 30 (skip calls under 30 seconds).

For Stripe payments: {{ $json.body.type }} equals payment_intent.succeeded (only continue for successful payments).

The false output of the IF node either terminates (does nothing) or routes to a minimal logging step. The true output continues to the main processing logic.

Filtering early means your downstream API calls, CRM writes, and notification sends only happen for events that warrant them.


Verifying Webhook Signatures

Webhook endpoints that are publicly accessible should verify that incoming requests genuinely came from the expected sender. Most systems sign their webhook payloads with an HMAC signature in a request header. This is the same discipline covered in Python FastAPI Webhook Automation for backends that receive webhooks directly instead of through n8n.

In n8n, signature verification requires a Code node immediately after the Webhook node:

javascript
const crypto = require('crypto');

const payload = JSON.stringify($input.first().json.body);
const secret = $env.WEBHOOK_SECRET; // see the note below on enabling $env access
const signature = $input.first().json.headers['x-webhook-signature'];

const expectedSignature = crypto
  .createHmac('sha256', secret)
  .update(payload)
  .digest('hex');

const valid = signature
  && expectedSignature.length === signature.length
  && crypto.timingSafeEqual(Buffer.from(expectedSignature), Buffer.from(signature));

if (!valid) {
  throw new Error('Invalid webhook signature');
}

return $input.all();

If the signature does not match, the Code node throws an error and the workflow stops. This prevents unauthorised requests from triggering your automation.

Two things will trip you up here. The first is $env. n8n blocks environment variable access from inside Code nodes by default, and on n8n Cloud you cannot change that, so $env.WEBHOOK_SECRET comes back undefined and every signature check fails. On a self-hosted instance, set N8N_BLOCK_ENV_ACCESS_IN_NODE=false to allow it.

The second is the payload itself. JSON.stringify($json.body) does not reproduce the bytes the sender signed. n8n has already parsed the JSON by the time your Code node sees it, and re-serialising can reorder keys, change number formatting and strip whitespace, so your HMAC will not match theirs. Some senders are forgiving because their payloads happen to round-trip unchanged. Stripe and GitHub are not. Where the raw body matters, set the Webhook node to receive the request as raw or binary data and hash that instead of the parsed object.

Keep the webhook secret outside the workflow wherever the platform allows it. Anything pasted into a Code node travels with the workflow JSON every time it is exported or shared.


Responding to the Webhook

Most webhook senders expect a quick HTTP response confirming receipt. If your workflow takes more than a few seconds to run and the sender has a short timeout, they may retry, sending the same event multiple times.

For most n8n webhooks, configure the Webhook node to respond immediately with a fixed success response:

  • Response Code: 200
  • Response Mode: Immediately
  • Response Body: {"status": "received"}

The workflow then continues processing after sending the response. The sender gets their acknowledgment quickly and does not retry.

For webhooks where the sender needs a specific response that depends on the workflow results — some chatbot platforms, for example — set Response Mode to "When Last Node Finishes" and configure your final node to produce the required response format.


Test vs Production Webhooks

This is the most common source of confusion for n8n beginners.

Test webhook URL: Active only when the workflow editor is open and you have clicked "Listen for test event." When you close the editor or stop listening, the test URL stops receiving data. Use this URL during development only.

Production webhook URL: Active when the workflow is toggled to Active. Use this URL in production configurations. The production URL continues to receive events even when the editor is closed.

When you are ready to go live:

  1. Click the Active toggle in the top right of the n8n workflow editor
  2. Copy the production URL from the Webhook node (click the node, look for the production URL tab)
  3. Update the sending system's webhook configuration to use the production URL

A common mistake: configuring the sending system with the test URL, which stops working as soon as the workflow is no longer being actively tested.


Processing Webhook Data in a Real Workflow

Here is a complete example: receiving a Retell AI call.ended webhook and processing the call data — the n8n equivalent of the Make.com pattern in Make.com Webhook Tutorial.

  1. Webhook node receives the POST with call data
  2. Code node verifies the Retell signature
  3. IF node: duration greater than 30 seconds? If not, stop.
  4. Set node: extract caller_phone, call_id, duration, and transcript into clean variables
  5. HTTP Request node: search GoHighLevel API for contact by phone number
  6. Switch node: contact found or not found?
  7. Found branch: HTTP Request node to update GHL contact with call note and tag
  8. Not found branch: HTTP Request node to create new GHL contact
  9. HTTP Request node: send confirmation SMS via GHL
  10. HTTP Request node: post call summary to Slack

This workflow runs in under two seconds from webhook receipt to all actions completed. Every real caller gets a CRM record update, a confirmation message, and a team notification without any manual involvement.

The API Request Tester is useful for verifying what the GHL API returns for the contact search before wiring it into step 5.


Building Reliable Webhook Workflows

Add error handling to every external API call in the workflow. An n8n Error Trigger workflow that catches failures and sends a Slack alert catches problems before they result in missing CRM records or undelivered messages. This webhook-to-CRM pattern is one of the most common building blocks across the voice agent and automation stacks I build for clients.

If you want help building a specific webhook integration in n8n or troubleshooting a production webhook that is not behaving correctly, book a free 30-minute call. Bring the sending system, the payload structure, and the intended outcome and we will get it working.


Frequently Asked Questions

Why does my n8n webhook return a 404?

Almost always because the workflow is not active and you are calling the production URL, or because you are calling the test URL and nothing is listening. The two URLs differ by a single path segment: production is /webhook/your-path and test is /webhook-test/your-path. A 404 on the production path means the workflow toggle is off. A 404 on the test path means you have not clicked "Listen for test event", or the listening window has already closed. Check the path segment before you start debugging anything else, because the two mistakes look identical from the sender's side.

Does n8n queue webhook events while a workflow is deactivated?

No. Deactivating a workflow unregisters the production URL entirely, so requests get a 404 and the payload is gone. This is the opposite of how Make.com behaves, where webhooks queue while a scenario is off and replay as a burst when you switch it back on. If you need to deactivate an n8n workflow for maintenance and the sender does not retry, you will lose whatever arrives in that window. For anything you cannot afford to drop, keep the workflow active and put an IF node at the top that routes everything to a holding step instead of turning the workflow off.

What stops the same webhook event being processed twice?

Nothing, by default. Senders retry when they do not get a timely 200, and a retry carries the same payload, so a workflow that creates a CRM record will create two. Fix it in two places. Set the Webhook node to respond immediately rather than waiting for the workflow to finish, which removes most of the reason for a retry. Then make the workflow idempotent: take the event ID the sender provides, check it against a store of IDs you have already handled, and stop if it is there. Workflow static data works for low volume; a database or a Redis key with a short expiry is better once traffic is real.

Can one workflow have more than one webhook node?

Yes, and each registers its own URL, but only one of them fires per execution. This is useful when several related endpoints share downstream logic: a /lead-created webhook and a /lead-updated webhook can both feed the same enrichment and CRM branch. Be careful with expressions after the join point, because a field that exists on one payload may not exist on the other, and a missing field usually surfaces as an empty value written to your CRM rather than as an error. Add a Set node on each branch to normalise the payloads into the same shape before they merge.

Where can I see requests that never reached my workflow?

You cannot, and this catches people out. The executions list only records runs that actually started, so a request rejected with a 404, blocked by the webhook node's authentication, or stopped by a failed signature check leaves no trace in n8n's UI. If you are debugging a sender that insists it is posting and n8n insists it is not receiving, point the sender at a temporary capture endpoint such as a request-inspection service and look at what actually leaves their system. That will tell you the method, the headers and the body, which is usually enough to find the mismatch in under a minute.


If you would rather have this built than build it, I take on n8n and webhook integration work through Fiverr.

Muhammad Bilal
Muhammad Bilal Virk
AI automation engineer — building agents, workflows, and RPA that remove repetitive work.
Share
Newsletter

One email, when I ship something worth reading.

No cadence, no filler. Unsubscribe any time.

Free consultation

Want this built against your real numbers?

A 30-minute call to scope the workflow, agent, or automation you actually need.

Book a free consultation
Next step

Have a workflow that's burning hours every week?

Bring me one real bottleneck. I'll tell you whether it's worth automating, and what it would take.

Book 30 Minutes Call