n8n Webhook Tutorial:
10 Minutes to Your First Real Automation
Every serious n8n workflow I run starts with a webhook. Not a schedule trigger, not a button click, a webhook. Once you understand why, the rest of n8n starts making a lot more sense.
What's in this guide
A webhook is a URL that listens. Everything else in n8n stacks on top of that.
A webhook is a URL that listens. That's it. When something happens elsewhere โ a form submission, a Stripe payment, a GitHub push โ it makes a POST request to that URL. n8n catches it and your workflow fires.
The key difference from a schedule trigger (which polls on a timer) is that webhooks are event-driven. They react to things as they happen. This is why they're the foundation of every serious automation stack: you're not asking "did anything happen?" every 5 minutes. You're being told the moment it does.
Webhooks are how I get instant Telegram alerts when a lead submits a form, when a Stripe payment goes through, and when someone pings my n8n agent endpoint. They fire within milliseconds of the event.
In n8n, you create a webhook by dropping in a Webhook node as your trigger. n8n gives you a URL. You paste that URL into whatever tool you want to react to. Done.
Build the thing first. Understanding follows from doing, not from reading more.
Let's build something real: a workflow that catches a form submission and sends you a Telegram message with the contact's name and email. This is the workflow I run on every lead capture form. Three nodes, takes 8 minutes, saves you hours of inbox checking.
You'll see two URLs:
Test URLโ for testing while building (only works when you click "Listen for test event")Production URLโ the live URL (only works when workflow is Active)
Set HTTP Method to
POST. Leave everything else as default for now.
Then open a terminal and fire a test payload:
curl -X POST "YOUR_TEST_URL_HERE" \
-H "Content-Type: application/json" \
-d '{"name": "Alex", "email": "alex@example.com", "message": "Interested in your services"}'
Or use a tool like Insomnia or Postman if you prefer a GUI.You should see the payload appear in n8n. The webhook node shows you the exact structure โ this is what you'll reference in the next nodes.
In the Set node, add fields:
- Name:
contact_nameโ Value:{{ $json.body.name }} - Name:
contact_emailโ Value:{{ $json.body.email }} - Name:
messageโ Value:{{ $json.body.message }}
Why the Set node? Because referencing
$json.body.name in every subsequent node is annoying and error-prone. Clean your data once, early, and everything downstream is simpler.
Message text:
๐ New Lead!
Name: {{ $json.contact_name }}
Email: {{ $json.contact_email }}
Message: {{ $json.message }}
Time: {{ $now.toISO() }}
If you prefer email, swap in the Gmail node. Slack? Use the Slack node. The webhook and Set nodes stay the same.
Operation:
Append RowMap columns:
- Column A (Date):
{{ $now.toISO() }} - Column B (Name):
{{ $json.contact_name }} - Column C (Email):
{{ $json.contact_email }} - Column D (Message):
{{ $json.message }}
Now you have a permanent log of every lead. This has saved me more times than I can count.
Switch from the Test URL to the Production URL. Paste the Production URL into your form's webhook field (Typeform, Tally, Gravity Forms, Webflow, custom form โ they all have a webhook setting).
Test with a real form submission. The workflow fires. Telegram message arrives. Sheet logs. You've shipped your first real automation.
Testing webhooks locally is where beginners get stuck. Here's the actual fix.
This is where beginners get stuck. If you're running n8n locally (not cloud, not VPS), external tools can't reach your localhost URL. The form doesn't know where http://localhost:5678 is.
Three options, in order of preference:
Option 1: Use n8n Cloud for testing. n8n Cloud gives you a real HTTPS URL immediately. It's $20/month. If you're self-hosting but need to test external webhook triggers, spin up a free trial or pay the month.
Option 2: Cloudflare Tunnel (free, my recommendation for self-hosters). This creates a public HTTPS URL that tunnels to your local n8n instance. Setup:
# Install cloudflared
brew install cloudflare/cloudflare/cloudflared # macOS
# or download binary from cloudflare.com/products/tunnel
# Create a quick tunnel (no account needed for testing)
cloudflared tunnel --url http://localhost:5678
# You'll get a URL like: https://random-words-here.trycloudflare.com
# Use that as your webhook base URL in n8n
The tunnel URL changes every restart. For permanent self-hosting, set up a named Cloudflare Tunnel with your domain. That's what I run โ a Hetzner CX22 ($6/month) with Cloudflare Tunnel in front of it. All webhooks hit the public URL, tunnel routes to n8n. Solid, free on the Cloudflare side.
Option 3: ngrok. Same idea as Cloudflare Tunnel but ngrok is the original. Free tier has limits and the URL changes on restart. Works fine for development.
Self-hosting tip: Once you move to a VPS with a domain, you don't need tunneling. Your n8n instance has a real public IP. Set up an nginx reverse proxy with SSL (Let's Encrypt makes this free) and your webhook URLs are clean https://n8n.yourdomain.com/webhook/... URLs permanently.
Every tool sends a different payload shape. Knowing why saves you an hour of debugging each time.
This is the part that trips people up. The payload structure varies depending on what's sending it. Here's how it actually works in n8n:
The $json structure
When a webhook fires, n8n wraps the incoming data. How you reference it depends on where the data is:
$json.bodyโ the POST body (this is what you want 99% of the time)$json.headersโ request headers (useful for auth tokens, content-type)$json.queryโ URL query parameters (for GET webhooks)$json.paramsโ URL path parameters
So if Typeform sends {"form_response": {"answers": [...], "variables": {...}}}, you'd access the first answer as $json.body.form_response.answers[0].text. Use the n8n expression editor โ it shows you the live structure when you have test data.
Different tools send different shapes
Here's the actual structure for the tools I use most:
Tally forms: Sends $json.body.data.fields โ an array of field objects. Each has a label and value. You usually loop over them or find by label.
Stripe webhooks: $json.body.type tells you the event type (payment_intent.succeeded, customer.subscription.created, etc.). The data is at $json.body.data.object.
GitHub webhooks: $json.body.ref for the branch, $json.body.repository.full_name for the repo, $json.body.head_commit.message for the commit message.
Typeform: $json.body.form_response.hidden.email if you pass hidden fields. $json.body.form_response.answers array for actual responses.
Always use the Set node to clean your data early
The Set node is not glamorous. It saves you from yourself anyway.
I say this to everyone who asks me about workflow debugging: clean your data as the first thing after the webhook. Extract the fields you need, rename them to something human, and every downstream node becomes trivial to build.
Without this: {{ $json.body.form_response.answers[2].text }} โ fragile, unreadable, breaks if answer order changes.
With a Set node: {{ $json.contact_email }} โ obvious, clean, won't break if the form adds a question.
5 webhook patterns I actually run (copy the structure, not just the concept)
Already want these workflows?
The lead capture webhook from this guide is in the n8n Pack
Plus 13 more production workflows โ email triage, Stripe ops, YouTube repurposing, Reddit lead monitoring. Every node is annotated so you know exactly what to swap for your tools.
Get all 14 workflows โ $97 โOne-time ยท Instant download ยท 30-day guarantee
The four mistakes behind almost every "my webhook isn't firing" post
I've replied to a lot of webhook debugging threads. Same four issues come up constantly. I call these The Four Fires: test vs production URL confusion, missing webhook response, sync vs async mode mismatch, and hardcoded payload paths. Fix these four and you'll never be stuck on a webhook again.
1. Testing with the production URL. The production URL only works when the workflow is Active. The test URL only works while you're in the editor with "Listen for test event" active. Using the wrong one is responsible for roughly half the "my webhook isn't firing" posts. Check which URL you're using before posting for help.
2. Forgetting to respond to the webhook. By default, n8n keeps the HTTP connection open waiting to respond. If you have a long-running workflow (AI processing, multiple API calls), the sending service might time out. Add a "Respond to Webhook" node early in your flow (before the slow stuff) to immediately send {"status": "received"} and let the workflow continue asynchronously. This is especially important for Stripe and Twilio webhooks which require a fast response.
3. Not understanding synchronous vs asynchronous response. If your webhook needs to return data to the caller (like an AI agent responding to a query), you need to enable "Respond to Webhook" and set it to return the final output. This requires the workflow to complete before responding. Know which mode you need.
4. Hardcoding payload paths without test data. You guessed at what the payload structure looks like and hardcoded $json.body.data.email. The actual field is $json.body.email. Always fire a real test event first, look at the actual structure in the n8n node inspector, then build your expressions. Never guess.
What to build next (once the basics are running)
If you've followed this guide, you now have a working webhook trigger, a Set node that cleans the data, and downstream notifications. That's a real automation. It runs while you're doing other things.
The next directions worth exploring:
Add an IF node to route different payloads differently. New customer vs returning customer means different welcome emails. Urgent lead vs low-priority means different notification channels. One webhook can branch into multiple paths.
Add AI processing. Webhook catches a forwarded email, an AI node classifies it, the workflow responds based on category. The webhook is the input. AI is the brain. An action is the output. This is how I run my Telegram bot: every message hits a webhook, my workflow decides what to do, the Respond to Webhook node sends back a formatted reply.
Webhooks are the first skill in n8n. Everything else stacks on top of them. The gap between "cron job that polls every five minutes" and "system that reacts to real events instantly" is exactly one Webhook node. Once you understand that gap, you start seeing it everywhere, and you stop building the slow version on purpose.
If you want the workflow JSON for the lead capture setup (plus 13 others from my production stack), I packaged them all up:
Get 14 webhook-driven workflows you can actually deploy
The n8n Automation Starter Pack is $97 one-time and includes the lead capture flow from this guide plus 13 more annotated workflows, so you can copy proven webhook patterns instead of rebuilding them from scratch.
See Google Workspace MCP โOne-time purchase ยท Instant download ยท 30-day money-back guarantee ยท No subscription.
Need someone to tell you what to fix before you build more webhook spaghetti?
If the real problem is not the node, but the whole workflow shape, the Agent OS Audit is the better next step. AiMe reviews your current automation, finds the first useful fix, and tells you what to ignore for now.
See Google Workspace MCP โ48-hour async review ยท no call required ยท built for messy real setups