Skip to main content
n8n is the most capable of the three no-code tools: it can reply in the same request, call the response_url, and upload files.

Set up the trigger

1

Add a Webhook node

Set HTTP Method to POST. Copy the Production URL into your bot’s webhook settings.
2

Set Response Mode

Change Respond to Using ‘Respond to Webhook’ Node.
Leave this on the default, and n8n instantly replies Workflow was started — before your workflow has done anything. We know that exact body and block it. So you get no bot message and no error, just silence. This one setting is the top cause of “my n8n bot says nothing”.
3

Add a Switch node

Route on {{ $json.body.event }}. You will want at least webhook.verify and message.created.
The Webhook node puts the payload under body, so the visitor’s text is {{ $json.body.message.text }}.

Pass verification

On the webhook.verify branch, add a Respond to Webhook node:
  • Respond With: JSON
  • Response Body:
Press Verify in your bot settings while the workflow is active. Extra keys in the reply are fine, so n8n’s own additions do no harm.
The Test URL only listens while you watch the canvas. Verify against the Production URL, with the workflow turned on.

Reply in the same request

For anything under 10 seconds, end with a Respond to Webhook node:
  • Respond With: JSON
  • Response Body:
Plain string labels are fine — we build action_id values from them (Track package becomes track_package).

Reply later

An AI Agent or a slow API will blow past the 10-second limit. Confirm first, answer later.
1

Confirm right away

Put a Respond to Webhook node straight after the trigger, and set Respond With to No Data. The typing dots stay on.
2

Do the slow work

Your AI Agent, HTTP calls, database lookups — up to 45 minutes.
3

POST the answer

Add an HTTP Request node:
  • Method: POST
  • URL: {{ $('Webhook').item.json.body.response_url }}
  • Body Content Type: JSON
  • Specify Body: Using JSON
  • JSON:
No auth header — the key is already in the URL.
Writing $('Webhook').item.json.body.response_url matters: after an AI Agent node, plain $json no longer holds the first payload.

Why the mid

A mid turns the POST into an update. If n8n retries the node, or the workflow runs twice, you get one message instead of two. Build it from event_id, and it stays the same across retries of one event.

Show progress

POST to the same response_url again and again, with the same mid:
The visitor sees one bubble change, instead of three piling up.

Send a file

Two HTTP Request nodes.
1

Upload

  • Method: POST
  • URL: {{ $('Webhook').item.json.body.response_url }}/files
  • Body Content Type: Form-Data (multipart/form-data)
  • Add a parameter of type n8n Binary File, named file
The reply contains file.file_id.
2

Post the message

  • URL: {{ $('Webhook').item.json.body.response_url }}
  • JSON:

Handle button clicks

Clicks arrive at the same webhook with event set to interaction.button_clicked. Add a Switch branch and read:
  • {{ $json.body.action_id }} — which action
  • {{ $json.body.value }} — your data
  • {{ $json.body.message_metadata }} — whatever you attached to the message
Reply the same two ways: a Respond to Webhook node, or a POST to that event’s own response_url.

Fixing problems

The Webhook node’s Respond setting is still on its default, so n8n replied Workflow was started before your nodes ran. Switch it to Using ‘Respond to Webhook’ Node.
Check the workflow is active and you are using the Production URL. The wizard shows what we received — usually n8n’s default body, which means your Respond node never ran.
Retries. Set a mid built from event_id, and re-posts become updates.
You are doing the work before replying. Confirm with No Data first, then POST to the response_url.
You mapped an object into a text field. {{ $json.output }} must be text. Use {{ JSON.stringify($json.output) }} if you really want the JSON.