Webhook
What is the "Webhook" node
A Webhook node gives your scenario its own public URL. Any external system — a website, a CRM, a payment service, another bot, a script — can call that address with a plain HTTP request (GET or POST), and it will run the scenario just as if a user had messaged the bot.
Unlike the trigger node, which reacts to events inside a connected Telegram resource, a webhook reacts to events from outside — from any system capable of making an HTTP request. Unlike the API Request block, which the bot uses to call out to external services itself, a webhook is the point external services call into the bot.
When to use it
1. Receiving data from outside. An external system sends the webhook some data (e.g. "order paid", "form submitted on the website", "CRM event"), and the scenario saves it to the bot's database, updates the user's variables, and/or sends them a Telegram message. This is how a webhook works as an inbound integration point for any service that can send HTTP requests but has no dedicated bot module.
2. Serving data out — "bot as an API". An external system makes a GET request with parameters, the scenario runs blocks (e.g. Database, API Request, Integrations) to gather the needed data into variables — and the webhook returns it all as one JSON response. This turns the constructor into a lightweight API layer on top of your scenarios: no need to stand up a separate backend just to expose what a scenario can already compute.
Both patterns can be combined in a single node: accept the request's parameters, compute/save something, and return the result to the caller in the same call.
Adding it in the constructor
- On the canvas header, click the radio-signal icon (to the left of the "+" add-command button) — a purple "Webhook" node appears.
The Add Webhook button isn't available inside sub-scenarios — a webhook can only be placed in the bot's main scenario.
- Open the node — the settings panel opens on the right.
- Connect the webhook to a command: click "Create" to create and link a new command in one step, or pick an existing one in the "Go to command" field. That command runs on every request.
- Add the blocks you need to the linked command (text, database, API request, set variables, etc.) — they run whenever the webhook is called and can use the request's data through the webhook variable (see below).
- Publish the scenario — the production URL doesn't work until you do (see "Test and production URLs" below).
Webhook settings
Test and production URLs
The node has two URLs, switched via the Test URL / Production URL tabs in the settings panel:
| URL | When it works | Purpose |
|---|---|---|
Test URL (/webhook-test/…) |
Always | Captures test request data only — the scenario does not run (see "Sending a test request" below) |
Production URL (/webhook/…) |
Only after the scenario is published | The real call — runs the linked command |
Click either URL to copy it. Until the scenario is published, the Production URL tab shows "Publish the scenario to activate the production URL".
HTTP method and path
| Setting | Description |
|---|---|
| HTTP Method | GET or POST — a request with any other method gets 405 Method Not Allowed |
| Path | The part of the URL after the bot's address; auto-generated (a random unique id), editable. Uniqueness is checked within the bot — a conflicting path highlights the field in red |
Default User
Many scenario blocks (sending a message, working with a user's database record, etc.) need a user id. Rather than specifying it in every block, pick a default user — it's substituted automatically wherever the scenario needs one.
If no user is set, the scenario runs "anonymously": variables are computed and returned in the response, but no messages are sent anywhere — useful for a pure "bot as an API" setup.
If a user is set, messages from the linked command are additionally delivered to them on whichever platform they talk to the bot on (Telegram, the website widget, WhatsApp, Instagram, Messenger, email, or SMS — depending on how they were registered).
Authentication
| Mode | How it's checked |
|---|---|
| None | No check performed |
| Basic Auth | Login and password, sent via an Authorization: Basic … header |
| Header Auth | The value of a custom header (you choose the header's name) |
The login/password or header value is saved with its own "Save" button next to the fields — it's stored separately from the scenario schema and never appears in schema exports, templates, or the AI assistant. A request with missing or wrong credentials gets 401 Unauthorized.
Respond
| Mode | When the response is sent | What happens to the scenario |
|---|---|---|
| Immediately | Right away, before the scenario runs | The linked command runs after the response is sent — the caller doesn't wait for it to finish |
| When the scenario finishes | After the command has fully run | The response is built from the run's result (see below) |
In "When the scenario finishes" mode the webhook always returns JSON:
{
"success": true,
"variables": { "order_id": "A-114", "api": { "status": "paid" } },
"messages": [
{ "type": "text", "text": "Order A-114 is paid" }
]
}
variables— every variable the scenario computed by the time the command finished (including the request's own data).messages— the command's content blocks in the same shape they're sent to the user's messenger (text, media, buttons), regardless of whether a default user was set for actual delivery.
In "Immediately" mode you configure the fixed response yourself — status code, body, and headers (see "Additional settings").
Request variable
Incoming request data is saved into a variable whose name is set by the "Request variable" field (default webhook). Available properties:
| Variable | Value |
|---|---|
{{webhook.method}} |
The request's HTTP method |
{{webhook.path}} |
The request path |
{{webhook.ip}} |
Sender's IP address |
{{webhook.query.<name>}} |
A query-string parameter (?name=value) |
{{webhook.body.<name>}} |
A field from the request body (JSON or form data) |
{{webhook.headers.<name>}} |
A request header |
As with every constructor variable, nested fields are read with dot notation:
{{webhook.body.order.id}}, not{{webhook.body.order[0].id}}— see Variables.
Sending a test request
Before wiring up a real external system, it's worth checking exactly what data it will send — that's what the test URL is for.
-
In the settings panel, click "Listen for test event" — the constructor opens a 2-minute listening window.
-
Send a request to the Test URL any way you like:
From Postman / Insomnia:
- Create a new request using the method configured on the node (GET or POST).
- Paste the copied Test URL into the address bar.
- For GET, add parameters on the Params tab; for POST, add a body on the Body tab (JSON or form-data).
- Click Send.
From curl:
curl -X POST "https://<your-domain>/webhook-test/<slug>/<path>" \ -H "Content-Type: application/json" \ -d '{"order": {"id": 114, "status": "paid"}}' -
As soon as the request arrives, its data appears in the constructor — the listening window closes automatically.
The test URL never runs the scenario and has no side effects (nothing is written to the database, no messages are sent) — it exists purely to show you the real shape of the incoming data. The last captured request is kept and stays visible in the settings panel even after you switch to another node and come back — until the next test request overwrites it.
Using the test data in your scenario
Once a test request is captured, a "Available variables" table appears under the listen button, listing the actual field paths from the data received (e.g. {{webhook.query.order_id}}, {{webhook.body.customer.email}}), each shown next to the real example value that was received.
These variables are immediately available in autocomplete everywhere in the scenario: start typing {{webhook. in any block's text field (a message, an API request URL, a condition, etc.) and a list of the discovered fields appears. Clicking a variable in the table copies it to the clipboard.
A typical workflow:
- Send a test request with realistic data.
- Open the variables table and copy the paths you need.
- Use them in the linked command's blocks: for example, in a Set Variables block, save
{{webhook.body.order.id}}into a variable namedorder_id, then send it in a text block asOrder {{order_id}} received. - Send another test request to verify, if needed.
- Switch to the Production URL and publish the scenario.
Additional settings
Expand the "Additional settings" link at the bottom of the panel to reveal these:
| Setting | What it affects |
|---|---|
| Allowed Origins (CORS) | Comma-separated list of domains allowed to call the webhook from a browser (fetch/XHR from another site). Empty or * allows any domain. If a specific list is set and the request's Origin isn't on it, the browser blocks the caller from reading the response |
| Ignore Bots | When enabled, requests with typical link-previewer/crawler user agents (Slackbot, TelegramBot, facebookexternalhit, etc.) don't run the scenario — the webhook just replies 200 OK with an empty body. Useful when the webhook URL might get posted or forwarded in chat apps |
| IP(s) Allowlist | Comma-separated list of allowed IP addresses or CIDR ranges, e.g. 127.0.0.1, 192.168.1.0/24. A request from any other address gets 403 Forbidden. Empty allows any address |
| Response Code | The HTTP status code returned in "Immediately" mode |
| Response Data | The response body in "Immediately" mode. Supports {{webhook.*}} — the scenario hasn't run yet at this point, so only the request's own data is available, not variables computed by the command |
| Response Headers | Custom response headers for "Immediately" mode (name/value pairs, also supporting {{webhook.*}}). If you don't set a Content-Type, the constructor picks one automatically: application/json when the body is valid JSON, text/plain otherwise |
The request's IP address and authentication are checked before the scenario runs — if a request fails the Ignore Bots filter, the IP allowlist, or authentication, the linked command never runs at all.
Request log and export
At the bottom of the settings panel is a collapsible "Request log" section, showing the total record count in its header. Expanding it reveals:
- Filters — date range (from/to), status (all / success / errors), search across request parameters.
- A compact list of entries: time, status code, method, mode (test/production), execution duration, and a short summary of the parameters received and the response.
- Pagination — 30 records per page.
- Export — the download-icon button exports every record matching the current filters to a JSON file with full details (complete headers and request/response bodies, not truncated) — handy for investigating an incident or handing off to support.
Log records are kept for 7 days, then removed automatically by a daily cleanup job. For history beyond a week, export ahead of time.
