Telegram Trigger in n8n: How It Works and Bot Setup from Scratch

Telegram Trigger in n8n: How It Works and Bot Setup from Scratch

Telegram Trigger in n8n fails to catch messages 9 times out of 10 for one reason: Telegram can’t reach your n8n over HTTPS. Below is a working setup, configs, and diagnostic commands. No fluff.

Quick answer
To get Telegram Trigger working in n8n, you need four things:
  • A public domain with a valid HTTPS certificate on port 443, 80, 88, or 8443.
  • The WEBHOOK_URL variable in n8n pointing to that HTTPS address, plus websocket support in your reverse proxy.
  • A published workflow: on publish, n8n registers the webhook with Telegram itself via setWebhook.
  • One Telegram Trigger per bot. A second trigger or a test run overwrites the first one’s webhook.

10-second check: call getWebhookInfo in the Bot API. The url field should point to your n8n instance, and last_error_message should be empty.

Diagnosis: Telegram Trigger in n8n stays silent

You set up n8n. Added a Telegram Trigger. You message the bot, and n8n stays quiet. Or everything works in test mode, but after publishing the bot goes deaf. Sound familiar?

Telegram Trigger in n8n is simpler than it looks. It’s not magic, just a regular webhook. Telegram gets a message from a user and makes an HTTPS POST to the address it was told about earlier. If the address is wrong, the certificate is broken, or there’s a proxy in the way without the right headers, the message never reaches n8n. And n8n has no idea this happened.

This gives us the main rule of this article: check what Telegram knows about the webhook first, then dig into n8n. Ninety percent of problems are visible in a single API response.

What you’ll get

  • A working Telegram bot on self-hosted n8n in Docker behind nginx with Let’s Encrypt.
  • Understanding of how Telegram Trigger registers a webhook and why testing breaks production.
  • Ready-made bot logic: commands, inline buttons, callback responses.
  • An error table with causes and fix commands.
  • Backup, webhook monitoring, and safe upgrades.
  • Protection: UFW, fail2ban, SSH, webhook restriction by Telegram’s IP ranges.
  • Workarounds if you don’t have a public IP or Telegram is unreachable from your network.

Time: 40-60 minutes on a clean server, 5 minutes to diagnose an already-running setup.
Requirements: VPS with a public IP, a domain, DNS access, a Telegram account, basic Docker experience.

System requirements

Component Version Note
OS Ubuntu 24.04 LTS or Debian 12 Commands in this article are for Ubuntu 24.04
Docker Engine latest stable with Compose v2 plugin Use docker compose, not docker-compose
n8n 2.42.x stable Branch 2.x, uses Publish instead of the Active toggle
PostgreSQL 16 SQLite also works, but use Postgres for production
nginx 1.24 from the Ubuntu repository Reverse proxy and TLS termination
certbot from the repository Let’s Encrypt certificate
Resources 2 vCPU, 2 GB RAM, 20 GB disk Minimum for n8n with Postgres and a couple of bots

At the time of writing, n8n 2.42.x stable is current. Releases come out nearly every week, so check the latest releases on n8n’s GitHub before installing.

Ports

Port Protocol Purpose Externally accessible
443 TCP HTTPS: Telegram webhooks and n8n UI through nginx Yes
80 TCP Certificate issuance and renewal, redirect to HTTPS Yes
22 TCP SSH Only from the admin’s IP
5678 TCP n8n inside the container No, 127.0.0.1 only
5432 TCP PostgreSQL No, inside the docker network only

Architecture

Here’s how a message travels. The user writes to the bot. Telegram’s servers hit you on port 443. nginx terminates TLS and forwards the request to n8n on local port 5678. n8n runs the workflow, and the response goes back through api.telegram.org as an outgoing request.

%%{init: {
  'theme': 'base',
  'themeVariables': {
    'primaryColor': '#ffffff',
    'primaryTextColor': '#1e293b',
    'primaryBorderColor': '#94a3b8',
    'lineColor': '#64748b',
    'fontSize': '15px',
    'fontFamily': 'ui-sans-serif, system-ui, sans-serif'
  },
  'flowchart': {'curve': 'linear', 'nodeSpacing': 50, 'rankSpacing': 50}
}}%%
flowchart TD
    U["Telegram user"] --> T["Telegram servers"]
    T -->|HTTPS POST| N["nginx on 443"]
    N --> W["n8n on 127.0.0.1:5678"]
    W --> P["PostgreSQL"]
    W --> R["Bot workflow"]
    R -->|sendMessage| A["api.telegram.org"]
    A --> U
    style U fill:#f8fafc,stroke:#3b82f6,stroke-width:2px,color:#1e40af
    style N fill:#f8fafc,stroke:#22c55e,stroke-width:2px,color:#15803d
    style W fill:#f8fafc,stroke:#22c55e,stroke-width:2px,color:#15803d
    style R fill:#f8fafc,stroke:#22c55e,stroke-width:2px,color:#15803d

There are two traffic directions. Inbound: Telegram to you, requires public HTTPS. Outbound: you to api.telegram.org, just needs internet access. Usually the inbound direction breaks. But since 2026, in Russian networks the outbound direction has started breaking too, covered separately below.

How Telegram Trigger works: the logic under the hood

This part matters. Until you understand the mechanics, you’ll keep treating symptoms.

The Telegram Bot API offers two ways to deliver updates to a bot. First: polling, where the bot periodically calls getUpdates itself. Second: webhook, where Telegram pushes updates to your URL. Telegram Trigger in n8n only works through webhook. There’s no polling here.

What happens when you publish a workflow:

%%{init: {
  'theme': 'base',
  'themeVariables': {
    'primaryColor': '#f8fafc',
    'primaryTextColor': '#1e293b',
    'primaryBorderColor': '#94a3b8',
    'noteBkgColor': '#fefce8',
    'noteTextColor': '#713f12',
    'noteBorderColor': '#fbbf24',
    'actorBkg': '#f8fafc',
    'actorBorder': '#94a3b8',
    'actorTextColor': '#1e293b',
    'fontSize': '15px',
    'fontFamily': 'ui-sans-serif, system-ui, sans-serif'
  },
  'sequence': {
    'mirrorActors': false,
    'messageAlign': 'center',
    'actorMargin': 120,
    'width': 160,
    'noteMargin': 12
  }
}}%%
sequenceDiagram
    participant N as n8n
    participant T as Telegram API
    participant U as User
    N->>T: setWebhook url, allowed_updates, secret_token
    T->>N: ok true
    U->>T: Message to bot
    T->>N: POST /webhook/id/webhook
    Note over N: Checking secret header
    N->>T: HTTP 200 immediately
    Note over N: Running workflow
    N->>T: sendMessage
    T->>U: Bot response

Let’s go through the pitfalls step by step.

One bot, one webhook

A Telegram bot can have exactly one webhook address. Not two, not one per workflow. One. Every setWebhook call overwrites the previous address.

n8n states this directly in the node’s UI: only one Telegram Trigger can be active for a given bot at a time. Added a second workflow with the same token and published it? The first one goes deaf. No errors, no warnings, it just stops receiving updates.

Test URL vs production URL

Every trigger node in n8n has two addresses. The test one, prefixed with /webhook-test/, works while you’re listening for a test run in the editor. The production one, prefixed with /webhook/, works for a published workflow.

When you click Execute workflow in the editor, n8n registers the test address with Telegram. Production gets wiped. You finish testing, close the editor, and Telegram keeps sending updates to the test URL that nobody’s listening to anymore. Production is dead.

The practical rule: either unpublish the workflow while testing, or set up a second bot for testing. The second option is better. Bots are free, your nerves aren’t.

Secret token

The Bot API can pass a secret in the X-Telegram-Bot-Api-Secret-Token header. Current versions of n8n generate this secret themselves when registering the webhook and verify it on every incoming request. A request without the correct header gets dropped by the node.

This creates a trap: if you manually call setWebhook pointing to n8n’s address without a secret, n8n will reject the updates. Don’t touch the webhook manually while the workflow is published. Want to re-register it, unpublish and publish again.

Instant response and retries

Telegram expects a 2xx response from the webhook. Get an error or a timeout, and it retries delivery. Undelivered updates are held by Telegram for up to 24 hours, then discarded.

Telegram Trigger responds with 200 immediately on receipt, before the workflow even runs. So even a heavy workflow with a minute-long GPT call won’t trigger a redelivery. But if n8n is down, updates pile up at Telegram. n8n comes back up, and the whole queue lands at once. That’s where “the bot suddenly replied to 40 old messages” comes from.

Update types

In the Trigger On field, you choose which events to listen for. n8n passes this list into allowed_updates. The option with the asterisk means all types except chat_member, message_reaction, and message_reaction_count: Telegram doesn’t send these by default because there are too many of them.

Update type When it fires Chat ID in n8n expression Useful data
message Any new message: text, photo, file, sticker {{ $json.message.chat.id }} {{ $json.message.text }}
edited_message User edited a message {{ $json.edited_message.chat.id }} {{ $json.edited_message.text }}
callback_query Inline button pressed {{ $json.callback_query.message.chat.id }} {{ $json.callback_query.data }}
channel_post Post in a channel where the bot is admin {{ $json.channel_post.chat.id }} {{ $json.channel_post.text }}
inline_query User typed @bot query in any chat no chat {{ $json.inline_query.query }}
chat_member Member status changed, bot must be admin {{ $json.chat_member.chat.id }} {{ $json.chat_member.new_chat_member.status }}
my_chat_member Bot was added, removed, or blocked {{ $json.my_chat_member.chat.id }} {{ $json.my_chat_member.new_chat_member.status }}

Only pick what you actually process. Every extra type means extra workflow runs and clutter in the execution list.

Why the bot isn’t getting messages

It’s not as bad as you think. It’s actually much simpler: there are only seven causes, and each one is visible in the logs or in getWebhookInfo.

  1. No HTTPS, or n8n doesn’t know its own public address. Without WEBHOOK_URL, n8n registers the webhook as http://localhost:5678. Telegram won’t accept that address and returns a bad webhook error.
  2. Invalid certificate. Self-signed without being uploaded to setWebhook, expired, or missing the intermediate chain. Telegram can’t establish TLS and will queue updates instead.
  3. Unsupported port. Telegram webhooks only work on 443, 80, 88, and 8443. Port 5678 or 9443 won’t go through.
  4. Reverse proxy without websocket. The n8n editor receives events through a websocket. Without Upgrade headers, a test run hangs at Waiting for trigger event, even if the update arrived.
  5. Webhook conflict. A second workflow using the same bot, a test run overwriting production, a third-party script using the same token.
  6. Privacy mode in groups. By default, a bot in a group only sees commands and replies to its own messages. Regular text never reaches it.
  7. Network issues between Telegram and the server. A firewall blocking Telegram’s subnets, a hosting provider blocking inbound traffic, or the server sitting in a network where Telegram is restricted.
Telegram and Russian networks
Starting February 2026, Roskomnadzor has been gradually restricting Telegram in Russia, and from March 2026 the service has become practically unreachable from many Russian networks. Webhook integrations on Russian IPs work unreliably in both directions: Telegram may not reach your server, and your server may not reach api.telegram.org. If the bot is business-critical, host n8n, or at least the webhook entry point, on a foreign VPS. Options are covered in the alternatives section.

Recipe: setting up a Telegram bot in n8n from scratch

The plan: server, Docker, n8n with Postgres, nginx with a certificate, a bot in BotFather, a workflow. We’ll go step by step, checking each one before moving to the next.

Prerequisites

Before you start, check three things. Your domain, say n8n.example.com, has an A record pointing to the server’s IP. Ports 80 and 443 are open on the server. You have a sudo user, not root.

Where: your workstation or any machine with dig.


dig +short n8n.example.com A

Result: your server’s IP. Empty or a different address means DNS hasn’t propagated yet. Wait for TTL to expire, Let’s Encrypt won’t issue a certificate without a correct A record.

Where: n8n server, sudo user.


sudo apt update && sudo apt -y upgrade
sudo apt -y install ca-certificates curl jq nginx certbot ufw fail2ban
curl -fsSL https://get.docker.com | sudo sh
sudo usermod -aG docker "$USER"
newgrp docker
docker compose version

Result: the last command prints the Docker Compose v2 version. If it complains about an unknown command, the compose plugin didn’t install, check the official Docker install guide for Ubuntu.

Why the get.docker.com script and not the docker.io package from Ubuntu: the distro’s repository lags behind by several versions and often ships without the compose plugin. For production in a closed environment, add Docker’s own repository manually instead, as described in the same guide.

Step 1. Create a bot in BotFather

Open @BotFather in Telegram. Send /newbot. Set a display name and a username ending in bot. You’ll get a token: a number, a colon, and a long string of letters and digits.

Your bot token is a password
Whoever has the token controls the bot: reads updates, sends messages on its behalf, reassigns the webhook. Don’t commit the token to git, don’t paste it into group chats, don’t include it in screenshots. Leaked it? Issue a new one immediately with /revoke in BotFather. The old one stops working instantly.

Handle groups right away. If the bot needs to read regular messages in a group, disable privacy mode: in BotFather, run /setprivacy, select the bot, hit Disable. Then remove the bot from the group and add it back. The change doesn’t apply without re-adding the bot, and this trips everyone up.

Set up a second bot for testing right now, something like mycompany_test_bot. I’ll explain why in the troubleshooting section. Short version: so testing doesn’t break production.

Where: n8n server. Replace TOKEN with your own.


curl -s "https://api.telegram.org/botTOKEN/getMe" | jq

Result: JSON with ok: true and the bot’s data. If curl hangs until timeout, the server can’t reach api.telegram.org. No point moving forward until you fix the network.

Step 2. Directory structure and secrets

Keep everything in /opt/n8n. One directory, one compose file, one .env. A year from now you’ll thank yourself.

Where: n8n server.


sudo mkdir -p /opt/n8n/backup
sudo chown -R "$USER":"$USER" /opt/n8n
cd /opt/n8n
openssl rand -hex 32
openssl rand -hex 24
openssl rand -hex 24

Result: three random strings. The first goes into n8n’s encryption key, the other two into Postgres passwords.

N8N_ENCRYPTION_KEY must not be lost
n8n uses this key to encrypt all credentials in the database, including your bot token. Lose the key, and a database backup turns into a useless pile of bytes: workflows will restore, but every credential will need to be re-entered from scratch. Save the key to a password manager right away, before the first launch.

Create the file /opt/n8n/.env. Substitute your domain and the generated strings.


N8N_VERSION=2.42.7
N8N_HOST=n8n.example.com
WEBHOOK_URL=https://n8n.example.com/
N8N_ENCRYPTION_KEY=paste_first_string_here
GENERIC_TIMEZONE=Europe/Moscow
TZ=Europe/Moscow

POSTGRES_USER=pgadmin
POSTGRES_PASSWORD=paste_second_string_here
POSTGRES_DB=n8n
POSTGRES_NON_ROOT_USER=n8n
POSTGRES_NON_ROOT_PASSWORD=paste_third_string_here

Where: n8n server.


chmod 600 /opt/n8n/.env

Why the version is pinned rather than latest: latest in production is a lottery. The container restarts overnight, pulls a new minor version with a database migration, and by morning the bot is down. We’ll upgrade deliberately, procedure at the end of the article.

Step 3. A dedicated database user

n8n shouldn’t connect to Postgres as superuser. The script below runs once, on the database’s first initialization, and creates an n8n user with rights limited to its own database.

Create the file /opt/n8n/init-data.sh:


#!/bin/bash
set -e
if [ -n "${POSTGRES_NON_ROOT_USER:-}" ] && [ -n "${POSTGRES_NON_ROOT_PASSWORD:-}" ]; then
psql -v ON_ERROR_STOP=1 --username "$POSTGRES_USER" --dbname "$POSTGRES_DB" <<EOSQL
CREATE USER ${POSTGRES_NON_ROOT_USER} WITH PASSWORD '${POSTGRES_NON_ROOT_PASSWORD}';
GRANT ALL PRIVILEGES ON DATABASE ${POSTGRES_DB} TO ${POSTGRES_NON_ROOT_USER};
GRANT CREATE ON SCHEMA public TO ${POSTGRES_NON_ROOT_USER};
EOSQL
else
echo "SETUP INFO: No non-root user credentials provided"
fi

Where: n8n server.


chmod +x /opt/n8n/init-data.sh

Step 4. docker-compose.yml

Create /opt/n8n/docker-compose.yml. Note the ports line: n8n listens on 127.0.0.1 only. It’s reachable from the outside solely through nginx.


services:
  postgres:
    image: postgres:16
    restart: unless-stopped
    environment:
      - POSTGRES_USER=${POSTGRES_USER}
      - POSTGRES_PASSWORD=${POSTGRES_PASSWORD}
      - POSTGRES_DB=${POSTGRES_DB}
      - POSTGRES_NON_ROOT_USER=${POSTGRES_NON_ROOT_USER}
      - POSTGRES_NON_ROOT_PASSWORD=${POSTGRES_NON_ROOT_PASSWORD}
    volumes:
      - db_storage:/var/lib/postgresql/data
      - ./init-data.sh:/docker-entrypoint-initdb.d/init-data.sh:ro
    healthcheck:
      test: ["CMD-SHELL", "pg_isready -h localhost -U ${POSTGRES_USER} -d ${POSTGRES_DB}"]
      interval: 5s
      timeout: 5s
      retries: 10

  n8n:
    image: docker.n8n.io/n8nio/n8n:${N8N_VERSION}
    restart: unless-stopped
    environment:
      - DB_TYPE=postgresdb
      - DB_POSTGRESDB_HOST=postgres
      - DB_POSTGRESDB_PORT=5432
      - DB_POSTGRESDB_DATABASE=${POSTGRES_DB}
      - DB_POSTGRESDB_USER=${POSTGRES_NON_ROOT_USER}
      - DB_POSTGRESDB_PASSWORD=${POSTGRES_NON_ROOT_PASSWORD}
      - N8N_HOST=${N8N_HOST}
      - N8N_PORT=5678
      - N8N_PROTOCOL=https
      - WEBHOOK_URL=${WEBHOOK_URL}
      - N8N_PROXY_HOPS=1
      - N8N_ENCRYPTION_KEY=${N8N_ENCRYPTION_KEY}
      - GENERIC_TIMEZONE=${GENERIC_TIMEZONE}
      - TZ=${TZ}
    ports:
      - "127.0.0.1:5678:5678"
    volumes:
      - n8n_storage:/home/node/.n8n
    depends_on:
      postgres:
        condition: service_healthy

volumes:
  db_storage:
  n8n_storage:

Three variables decide the fate of Telegram Trigger. WEBHOOK_URL tells n8n which address to hand to Telegram in setWebhook. N8N_PROTOCOL=https is needed for correct links in the UI. N8N_PROXY_HOPS=1 makes n8n trust the X-Forwarded-For header from exactly one proxy in front of it. Full list of variables in n8n’s deployment environment variables documentation.

Where: n8n server, /opt/n8n directory.


cd /opt/n8n
docker compose up -d
docker compose ps
docker compose logs n8n --tail 30
curl -s http://127.0.0.1:5678/healthz

Result: both containers show as running, postgres as healthy. The n8n logs include a line about the editor being ready. The last curl returns {“status”:”ok”}.

Step 5. nginx and the certificate

First we bring up plain HTTP so certbot can pass domain validation. Then we add HTTPS. Two passes, but no surprises and no certbot rewriting your configs on its own.

Create /etc/nginx/sites-available/n8n.conf with this temporary content:


server {
    listen 80;
    server_name n8n.example.com;

    location /.well-known/acme-challenge/ {
        root /var/www/html;
    }

    location / {
        return 301 https://$host$request_uri;
    }
}

Where: n8n server.


sudo ln -s /etc/nginx/sites-available/n8n.conf /etc/nginx/sites-enabled/n8n.conf
sudo rm -f /etc/nginx/sites-enabled/default
sudo nginx -t && sudo systemctl reload nginx
sudo certbot certonly --webroot -w /var/www/html -d n8n.example.com --agree-tos -m admin@example.com --no-eff-email

Result: certbot prints Successfully received certificate and the path to /etc/letsencrypt/live/n8n.example.com/. An error about unauthorized or connection refused almost always means the A record doesn’t point to this server, or port 80 is closed.

Now the full config. Overwrite /etc/nginx/sites-available/n8n.conf:


map $http_upgrade $connection_upgrade {
    default upgrade;
    ''      close;
}

server {
    listen 80;
    server_name n8n.example.com;

    location /.well-known/acme-challenge/ {
        root /var/www/html;
    }

    location / {
        return 301 https://$host$request_uri;
    }
}

server {
    listen 443 ssl http2;
    server_name n8n.example.com;

    ssl_certificate     /etc/letsencrypt/live/n8n.example.com/fullchain.pem;
    ssl_certificate_key /etc/letsencrypt/live/n8n.example.com/privkey.pem;
    ssl_protocols TLSv1.2 TLSv1.3;

    client_max_body_size 50m;

    location / {
        proxy_pass http://127.0.0.1:5678;
        proxy_http_version 1.1;
        proxy_set_header Upgrade $http_upgrade;
        proxy_set_header Connection $connection_upgrade;
        proxy_set_header Host $host;
        proxy_set_header X-Real-IP $remote_addr;
        proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
        proxy_set_header X-Forwarded-Proto $scheme;
        proxy_buffering off;
        proxy_read_timeout 300s;
    }
}

Don’t mix it up: use fullchain.pem in ssl_certificate, not cert.pem. With cert.pem, a browser might still open the site because it fetches the intermediate certificate on its own. Telegram won’t do that and will fail certificate verification.

The Upgrade and Connection lines are exactly what provides websocket support. Without them, the n8n editor never receives the event about an incoming update, and a test run just waits forever.

Where: n8n server.


sudo nginx -t && sudo systemctl reload nginx
curl -sI https://n8n.example.com/healthz | head -n 1
sudo systemctl list-timers | grep certbot

Result: nginx -t prints syntax is ok, curl returns HTTP/2 200, the certbot.timer shows up in the list. The timer renews the certificate automatically. To make nginx pick up the new certificate, add a reload hook:


echo 'renew_hook = systemctl reload nginx' | sudo tee -a /etc/letsencrypt/renewal/n8n.example.com.conf
sudo certbot renew --dry-run

Where: n8n server. Result: dry-run ends with the line Congratulations, all simulated renewals succeeded.

Step 6. Credentials in n8n

Open https://n8n.example.com, create the owner account. Then:

  1. Left menu, Credentials section, Create button.
  2. Find Telegram API.
  3. Paste the token into the Access Token field.
  4. Save. n8n immediately tests the connection and shows Connection tested successfully.

Set up two credentials: “TG prod bot” and “TG test bot”. Boring names, but six months from now you won’t mix them up. Details in n8n’s Telegram credentials documentation.

Step 7. Bot logic: a router instead of ten triggers

Since a bot can only have one trigger, all your logic needs to live behind it. The right pattern: one router workflow with a Telegram Trigger, splitting updates across sub-workflows via the Execute Workflow node.

%%{init: {
  'theme': 'base',
  'themeVariables': {
    'primaryColor': '#ffffff',
    'primaryTextColor': '#1e293b',
    'primaryBorderColor': '#94a3b8',
    'lineColor': '#64748b',
    'fontSize': '15px',
    'fontFamily': 'ui-sans-serif, system-ui, sans-serif'
  },
  'flowchart': {'curve': 'linear', 'nodeSpacing': 50, 'rankSpacing': 50}
}}%%
flowchart TD
    T["Telegram Trigger"] --> S["Switch by update type"]
    S --> C["Commands /start /help"]
    S --> B["Button presses"]
    S --> F["Files and photos"]
    S --> X["Everything else"]
    C --> W1["Sub-workflow: commands"]
    B --> W2["Sub-workflow: buttons"]
    F --> W3["Sub-workflow: files"]
    X --> L["Reply: didn't understand"]
    style T fill:#f8fafc,stroke:#3b82f6,stroke-width:2px,color:#1e40af
    style S fill:#f8fafc,stroke:#22c55e,stroke-width:2px,color:#15803d
    style L fill:#f8fafc,stroke:#f97316,stroke-width:2px,color:#9a3412

Benefits of the router approach: each scenario lives on its own and can be edited without risking the others. You can test a sub-workflow with fixed data without touching the webhook at all.

Configuring Telegram Trigger

Parameter Value Why
Credential TG prod bot In the test copy of the workflow, use TG test bot
Trigger On Message, Callback Query Only what you actually process
Download Images/Files Off Enable only if you really process attachments
Restrict to User IDs Comma-separated IDs For internal bots: filters out other people’s updates at the entry point

Restrict to User IDs is underrated. For an internal bot used by five admins, it’s free protection. You’ll see your own user ID in the first execution, in the message.from.id field.

Switch: routing by type

Add a Switch node in Rules mode after the trigger. Rules are evaluated in order, the first match wins:

Output Condition Expression
Buttons exists {{ $json.callback_query }}
Commands String starts with / {{ $json.message.text }}
Files exists {{ $json.message.document ?? $json.message.photo }}
Fallback Enable the Fallback Output option all other updates

For commands inside the sub-workflow, add a second Switch matching exact text values. Keep in mind that in groups, a command arrives as /start@bot_name. Normalize it with this expression:


{{ $json.message.text.split(' ')[0].split('@')[0].toLowerCase() }}

Result: /start@mycompany_bot becomes /start, and /Help argument becomes /help.

Reply with inline buttons

Telegram node, Resource: Message, Operation: Send Message. Fields:

  • Chat ID: {{ $json.message.chat.id }}
  • Text: your reply text
  • Reply Markup: Inline Keyboard, with buttons using Callback Data like approve_42 and reject_42
  • Additional Fields, Parse Mode: HTML, if you need formatting

Callback Data is limited to 64 bytes. Cyrillic text takes two bytes per character. Put short identifiers there, not a half-screen JSON blob.

Handling a button press

When a user taps an inline button, Telegram shows a loading spinner on it until the bot confirms the press. Don’t confirm it, and the spinner hangs and disappears, leaving the user thinking the bot is broken.

As the first node in the buttons sub-workflow, use Telegram, Resource: Callback, Operation: Answer Query. Query ID: {{ $json.callback_query.id }}. You can add a short toast message. The rest of the logic follows.

Parsing the button data is easier with a Code node:


const cq = $input.first().json.callback_query;
const [action, id] = cq.data.split('_');
return [{
  json: {
    action,
    id,
    chatId: cq.message.chat.id,
    messageId: cq.message.message_id,
    userId: cq.from.id,
    userName: cq.from.username || cq.from.first_name
  }
}];

Result: clean fields on output, action, id, chatId. Then a Switch on action and a Telegram node with the Edit Message Text operation, to replace the buttons with “Approved by such-and-such user”. That way nobody can press the same button twice.

Sending limits

Telegram limits bots to roughly 30 messages per second total and one message per second per chat. In groups the limit is even lower, about 20 messages per minute. Sending out a batch of notifications, use Loop Over Items and a Wait of one second between sends. Otherwise you’ll hit a 429 Too Many Requests with a retry_after field.

Step 8. Testing on the test bot and publishing

Order that won’t break production:

  1. Download the workflow via the Download menu, import a copy, name it with a TEST suffix.
  2. In the copy, switch the credential on the trigger and all Telegram nodes to TG test bot.
  3. Click Execute workflow and message the test bot. The execution should appear right away.
  4. Once debugged, port the changes over to the production workflow and click Publish.

After Publish, n8n calls setWebhook with the production address. Let’s check Telegram agrees.

Where: n8n server.


curl -s "https://api.telegram.org/botTOKEN/getWebhookInfo" | jq

Result: something like this.


{
  "ok": true,
  "result": {
    "url": "https://n8n.example.com/webhook/4f1c2b7e-0d8a-4b4e-9a51-2f6c9d3e8a10/webhook",
    "has_custom_certificate": false,
    "pending_update_count": 0,
    "max_connections": 40,
    "ip_address": "203.0.113.25",
    "allowed_updates": ["message", "callback_query"]
  }
}

url starts with /webhook/, not /webhook-test/. pending_update_count is zero. There’s no last_error_date or last_error_message field. This is a healthy bot.

Verification: make sure everything works

We check from the bottom up: services, network, webhook, execution. If a given layer is broken, there’s no point checking the layers above it.

What to check Command Expected
Containers docker compose ps running, postgres healthy
n8n is alive curl -s http://127.0.0.1:5678/healthz {“status”:”ok”}
HTTPS from outside curl -sI https://n8n.example.com/healthz HTTP/2 200
Outbound to Telegram curl -s api.telegram.org/botTOKEN/getMe ok: true
Webhook getWebhookInfo production url, zero errors
Requests arrive grep webhook in nginx’s access.log POST returning 200 from Telegram’s subnets

Where: n8n server.


cd /opt/n8n
docker compose ps
curl -s http://127.0.0.1:5678/healthz; echo
curl -sI https://n8n.example.com/healthz | head -n 1
sudo tail -f /var/log/nginx/access.log | grep --line-buffered '/webhook/'

Leave tail running and message the bot. You should see a line with a POST, code 200, and an IP from the 149.154.160.0/20 or 91.108.4.0/22 range. No line at all means Telegram never reaches nginx: check DNS, firewall, port, or certificate. A line with 4xx or 5xx means it got through, but nginx or n8n responded with an error.

Checking the certificate the way Telegram sees it:

Where: any external machine, not the server itself.


echo | openssl s_client -connect n8n.example.com:443 -servername n8n.example.com 2>/dev/null | openssl x509 -noout -subject -issuer -dates
echo | openssl s_client -connect n8n.example.com:443 -servername n8n.example.com 2>/dev/null | grep -E 'Verify return code'

Result: Verify return code: 0 (ok) and a notAfter date in the future. Code 21 unable to verify the first certificate means nginx is pointing to cert.pem instead of fullchain.pem.

n8n logs if execution is failing:

Where: n8n server, /opt/n8n directory.


docker compose logs n8n --since 15m | grep -iE 'error|telegram|webhook'

And the last level: the Executions section in the n8n UI. Every update that gets through the trigger leaves a record there. Records exist but there’s no reply, that means the problem is in the nodes after the trigger, not in the webhook.

Troubleshooting: Telegram Trigger errors and fixes

“It just started working” in my book means “it’s going to break on a Friday evening.” So we fix things by algorithm, not by restarting things blindly.

%%{init: {
  'theme': 'base',
  'themeVariables': {
    'primaryColor': '#ffffff',
    'primaryTextColor': '#1e293b',
    'primaryBorderColor': '#94a3b8',
    'lineColor': '#64748b',
    'fontSize': '15px',
    'fontFamily': 'ui-sans-serif, system-ui, sans-serif'
  },
  'flowchart': {'curve': 'linear', 'nodeSpacing': 50, 'rankSpacing': 50}
}}%%
flowchart TD
    A["Bot is silent"] --> B["getWebhookInfo"]
    B --> C{"url empty?"}
    C -->|yes| D["Workflow not published"]
    C -->|no| E{"url has webhook-test?"}
    E -->|yes| F["Test overwrote prod: republish"]
    E -->|no| G{"Is there a last_error_message?"}
    G -->|yes| H["Check error table"]
    G -->|no| I{"Record in Executions?"}
    I -->|no| J["Update type or Restrict to User IDs"]
    I -->|yes| K["Error in nodes after the trigger"]
    style A fill:#f8fafc,stroke:#f97316,stroke-width:2px,color:#9a3412
    style D fill:#f8fafc,stroke:#22c55e,stroke-width:2px,color:#15803d
    style F fill:#f8fafc,stroke:#22c55e,stroke-width:2px,color:#15803d
    style H fill:#f8fafc,stroke:#ef4444,stroke-width:2px,color:#b91c1c
    style J fill:#f8fafc,stroke:#22c55e,stroke-width:2px,color:#15803d
    style K fill:#f8fafc,stroke:#22c55e,stroke-width:2px,color:#15803d
Error Cause Fix
Bad Request: bad webhook: An HTTPS URL must be provided for webhook WEBHOOK_URL isn’t set, or is set with http Set WEBHOOK_URL=https://domain/ and recreate the container
Waiting for trigger event hangs forever No websocket in the reverse proxy, or a test run overwrote the webhook of another bot Add Upgrade and Connection headers in nginx
Works only in test or only in production One webhook per bot, test and prod overwrite each other Separate test bot, or unpublish during testing
SSL error: certificate verify failed Incomplete chain, expired, self-signed certificate Use fullchain.pem in nginx, check renewal
Wrong response from the webhook: 502 Bad Gateway n8n is down, or nginx points to the wrong port docker compose ps, check proxy_pass
Wrong response from the webhook: 403 Forbidden Webhook was set manually without a secret, or nginx’s allow/deny rules block it Republish the workflow, check allow rules
Wrong response from the webhook: 404 Not Found Workflow deleted or unpublished, but the webhook is still registered Publish the workflow, or remove the webhook with deleteWebhook
Connection timed out in last_error_message Port closed, hosting provider blocking inbound, or network blocking Telegram Check UFW and the hosting provider’s firewall, consider a foreign VPS
409 Conflict: terminated by other getUpdates request Something else is polling the bot via getUpdates at the same time Find and stop the other process using the same token
401 Unauthorized Token revoked, or pasted with a stray space Reissue the token in BotFather, update the credential
Bot in a group doesn’t see regular messages Privacy mode is enabled /setprivacy Disable and re-add the bot to the group
Bot replied to dozens of old messages n8n was down, Telegram built up a queue Re-register the webhook with drop_pending_updates

Bot works only in test or only in production

Cause: one webhook per bot. Whoever last called setWebhook gets all the updates.

Fix: close the test run tab. Unpublish the workflow and publish it again: n8n re-registers the production address. Check getWebhookInfo, the url should have /webhook/ without -test.

Where: n8n server.


curl -s "https://api.telegram.org/botTOKEN/getWebhookInfo" | jq -r '.result.url'

Waiting for trigger event and silence

Cause: the update arrived at n8n, but the editor never found out because the websocket isn’t proxied. Second, less common cause: you’re listening with a test run but writing to a different bot.

Fix: check that your location block has proxy_http_version 1.1 and the Upgrade and Connection headers. If there’s another proxy in front of nginx, say on a router or in Cloudflare, it needs to pass through the websocket too.

Where: n8n server.


sudo nginx -T 2>/dev/null | grep -nE 'proxy_http_version|Upgrade|connection_upgrade'

Result: three to four lines with these directives. Empty output means the config isn’t the right one, or isn’t linked in sites-enabled.

Certificate error

Cause: most often cert.pem instead of fullchain.pem. Less commonly, the certificate expired because certbot renewed it but nginx never reloaded.

Fix: correct the path, reload nginx, and clear the error by republishing the workflow.

Where: n8n server.


sudo grep -n 'ssl_certificate ' /etc/nginx/sites-available/n8n.conf
sudo certbot certificates
sudo nginx -t && sudo systemctl reload nginx

Queue of old updates

Cause: n8n was unreachable, and Telegram queued updates for up to 24 hours. Once it’s back, the whole batch arrives at once.

Fix: if you don’t need the old messages, clear the queue. First unpublish the workflow, then delete the webhook with the drop_pending_updates flag, then publish again.

Clearing the queue is irreversible
The drop_pending_updates flag deletes all undelivered updates with no way to get them back. If the queue could contain requests, button presses waiting on approval, or files from users, they’ll disappear silently. Check pending_update_count first and decide whether you actually need that queue.

Where: n8n server. The workflow is already unpublished.


curl -s "https://api.telegram.org/botTOKEN/getWebhookInfo" | jq '.result.pending_update_count'
curl -s "https://api.telegram.org/botTOKEN/deleteWebhook?drop_pending_updates=true" | jq

Result: ok: true, description Webhook was deleted. After that, click Publish, and n8n registers the webhook again with an empty queue.

409 Conflict from getUpdates

Cause: somewhere there’s an old script running on aiogram or python-telegram-bot with the same token. It calls deleteWebhook on startup and switches to polling. n8n’s webhook silently dies. This takes a while to find because the script is usually running in a screen session on a server everybody forgot about.

Fix: the fastest way to find the culprit is to reissue the token. The old process dies with a 401, and someone will show up complaining. That’s how you find out where it lives.

Bot stays silent in a group

Cause: privacy mode. The bot only gets commands, mentions, and replies to its own messages.

Fix: /setprivacy in BotFather, Disable, then remove the bot from the group and add it back. Alternative: make the bot a group admin, admins see everything without disabling privacy mode.

Rolling back after you’ve broken everything

It happens. Rollback order: webhook, containers, data. Workflows and credentials live in Postgres, so rolling back the container to an old version without rolling back the database doesn’t always help. More on this in the upgrade section.

Where: n8n server, /opt/n8n directory.


curl -s "https://api.telegram.org/botTOKEN/deleteWebhook" | jq
docker compose down
sed -i 's/^N8N_VERSION=.*/N8N_VERSION=PREVIOUS_VERSION/' .env
docker compose up -d

Deleting the webhook during rollback keeps Telegram from hammering a dead server and racking up errors. Updates will wait in the queue for up to 24 hours and arrive once the workflow is published again.

Alternatives to Telegram Trigger

Telegram Trigger isn’t the only way. Sometimes a different option is honestly better. Pick based on your situation.

Option When it fits Downsides
Telegram Trigger, the default choice You have public HTTPS, one bot with one router workflow One trigger per bot, needs a public address
Webhook node with manual setWebhook You need full control: your own path, your own secret, your own update list You write the secret validation yourself
Schedule plus getUpdates via HTTP Request No public address, a one-minute delay is acceptable Delay, extra executions, manual offset tracking
Cloudflare Tunnel or similar n8n at home or in an office without a public IP Dependency on an external service, needs websocket support
Foreign proxy VPS n8n in a Russian network, Telegram unstable from there One more server to maintain
Separate bot on aiogram Complex dialogs, state management, high load Code, deployment, and maintenance instead of a visual workflow
Telegram node only, no trigger The bot only sends notifications and never receives anything Can’t accept commands

Webhook node with manual registration

A regular Webhook node in n8n accepts any POST. You call setWebhook yourself against its production URL and set secret_token yourself. The upside: the path is predictable, say /webhook/tg-prod, and it’s easy to lock down in nginx by Telegram’s subnets.

Where: n8n server. SECRET is a string of letters, digits, hyphens, and underscores, up to 256 characters.


curl -s "https://api.telegram.org/botTOKEN/setWebhook" \
  -d "url=https://n8n.example.com/webhook/tg-prod" \
  -d "secret_token=SECRET" \
  -d 'allowed_updates=["message","callback_query"]' | jq

In the workflow, after the Webhook node, add an IF: header {{ $json.headers[‘x-telegram-bot-api-secret-token’] }} equals your secret. If it doesn’t match, route to a dead end. Configure the Webhook node for Respond Immediately so Telegram gets a 200 right away. Method details in the Bot API documentation.

Scheduled polling

A Schedule node once a minute, then an HTTP Request to getUpdates with an offset parameter equal to the last update_id plus one. Store the offset in a Data Table or in Postgres. Works entirely without a public address. The obvious downside: the bot replies with up to a minute’s delay, and an execution runs every single minute. Tolerable for a notification bot with a “confirm” button, not for a conversation.

n8n in a Russian network

If your n8n server has to stay in Russia, split the traffic. Inbound: nginx on a foreign VPS accepts the webhook and proxies it to n8n through a VPN tunnel. Outbound: n8n reaches api.telegram.org through an HTTP proxy on the same VPS. For outbound traffic, n8n has built-in HTTPS_PROXY and NO_PROXY variables.

Add these to the n8n service’s environment in docker-compose.yml:


      - HTTPS_PROXY=http://10.10.0.1:3128
      - NO_PROXY=localhost,127.0.0.1,postgres

Don’t skip NO_PROXY. Without it, n8n will try to reach its own database through the proxy, and you’ll get some very interesting errors. Keep in mind the proxy applies to all outgoing HTTPS requests from every node, not just Telegram.

Why Telegram Trigger is still the default choice

It registers the webhook itself, generates and verifies the secret itself, and re-registers the address on every publish. Fewer manual steps, fewer places to make a mistake. A manual Webhook node makes sense when you need strict path-based filtering in nginx or several independent environments on one instance.

Prevention: security, backups, monitoring

It only takes one small mistake to cause big trouble. One forgotten allow all line in nginx exposes the n8n editor, with all its tokens, to the entire internet. We lock things down in layers.

UFW

Docker and UFW
Docker writes its own iptables rules that bypass UFW. A port published as 5678:5678 without an address will be reachable from the internet even if UFW denies it. That’s why n8n’s port is bound to 127.0.0.1 in docker-compose.yml. Don’t remove that binding.

Where: n8n server. Replace 203.0.113.10 with your own admin IP. Don’t lose your SSH session: keep a second tab open until you’ve verified.


sudo ufw default deny incoming
sudo ufw default allow outgoing
sudo ufw allow from 203.0.113.10 to any port 22 proto tcp
sudo ufw allow 80/tcp
sudo ufw allow 443/tcp
sudo ufw enable
sudo ufw status verbose

Result: Status active, 22 allowed only from your IP, 80 and 443 allowed from everywhere.

Restricting the webhook to Telegram’s subnets

Telegram sends webhooks from the 149.154.160.0/20 and 91.108.4.0/22 subnets. Keep the current list cross-checked against Telegram’s webhook documentation page. The Telegram Trigger path has a predictable shape, /webhook/UUID/webhook, which you can match with a regex.

Add this block in the port-443 server, before location /:


    location ~ ^/webhook(-test)?/[0-9a-f-]+/webhook$ {
        allow 149.154.160.0/20;
        allow 91.108.4.0/22;
        deny all;

        proxy_pass http://127.0.0.1:5678;
        proxy_http_version 1.1;
        proxy_set_header Host $host;
        proxy_set_header X-Real-IP $remote_addr;
        proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
        proxy_set_header X-Forwarded-Proto $scheme;
    }

Don’t mix it up: other n8n trigger nodes may use a path in the same /webhook/UUID/webhook shape. If your instance also runs GitHub or other triggers, their requests will get a 403 under this rule. In that case, either list their subnets too or switch them to a Webhook node with a custom path.

It’s also worth locking down the n8n editor if it’s only used from the office or over VPN. In location / add an allow for office addresses and deny all. Telegram’s webhooks will keep flowing through the regular location block above.

Where: n8n server.


sudo nginx -t && sudo systemctl reload nginx
curl -s -o /dev/null -w '%{http_code}\n' -X POST https://n8n.example.com/webhook/00000000-0000-0000-0000-000000000000/webhook

Result: run from the server or your workstation, curl returns 403. That means the rule is working. Then message the bot and confirm it replies.

SSH hardening

Create /etc/ssh/sshd_config.d/10-hardening.conf. Replace the deploy username with your own. Before applying this, make sure key-based login already works.


PermitRootLogin no
PasswordAuthentication no
KbdInteractiveAuthentication no
PubkeyAuthentication yes
MaxAuthTries 3
LoginGraceTime 30
AllowUsers deploy
X11Forwarding no

Where: n8n server, in an open second SSH session.


sudo sshd -t && sudo systemctl reload ssh

Result: sshd -t stays silent, meaning the config is valid. Without closing your current session, open a new one and verify login.

fail2ban

Create /etc/fail2ban/jail.local:


[DEFAULT]
bantime = 1h
findtime = 10m
maxretry = 5
ignoreip = 127.0.0.1/8 ::1 203.0.113.10

[sshd]
enabled = true
backend = systemd

[nginx-botsearch]
enabled = true
port = http,https
logpath = /var/log/nginx/access.log

Where: n8n server.


sudo systemctl enable --now fail2ban
sudo systemctl restart fail2ban
sudo fail2ban-client status

Result: sshd and nginx-botsearch both show up in the jail list. Putting your own IP in ignoreip is mandatory, otherwise one day you’ll ban yourself after five sudo password typos.

Webhook monitoring

The nastiest way a bot breaks is silently: everything’s green, containers are running, but the webhook has been pointing nowhere for a while. We catch this with a script that checks getWebhookInfo every five minutes.

First, save the token in a separate file with locked-down permissions.

Where: n8n server.


sudo mkdir -p /etc/tg-monitor
echo 'TOKEN' | sudo tee /etc/tg-monitor/token >/dev/null
sudo chmod 600 /etc/tg-monitor/token

Create /usr/local/bin/tg-webhook-check.sh:


#!/bin/bash
# Telegram bot webhook check. Exit 0 = OK, 1 = problem.
set -u
TOKEN=$(cat /etc/tg-monitor/token)
EXPECTED_PREFIX="https://n8n.example.com/webhook/"
MAX_PENDING=20
MAX_ERROR_AGE=600

INFO=$(curl -s --max-time 15 "https://api.telegram.org/bot${TOKEN}/getWebhookInfo")
if [ -z "$INFO" ] || [ "$(echo "$INFO" | jq -r '.ok')" != "true" ]; then
  echo "CRIT: api.telegram.org unreachable or token invalid"
  exit 1
fi

URL=$(echo "$INFO" | jq -r '.result.url')
PENDING=$(echo "$INFO" | jq -r '.result.pending_update_count')
ERR_DATE=$(echo "$INFO" | jq -r '.result.last_error_date // 0')
ERR_MSG=$(echo "$INFO" | jq -r '.result.last_error_message // ""')
NOW=$(date +%s)

case "$URL" in
  "$EXPECTED_PREFIX"*) ;;
  *) echo "CRIT: webhook points to the wrong place: '$URL'"; exit 1 ;;
esac

if [ "$PENDING" -gt "$MAX_PENDING" ]; then
  echo "WARN: $PENDING updates in queue"
  exit 1
fi

if [ "$ERR_DATE" -gt 0 ] && [ $((NOW - ERR_DATE)) -lt "$MAX_ERROR_AGE" ]; then
  echo "WARN: recent webhook error: $ERR_MSG"
  exit 1
fi

echo "OK: $URL, pending=$PENDING"
exit 0

Where: n8n server.


sudo chmod 755 /usr/local/bin/tg-webhook-check.sh
sudo /usr/local/bin/tg-webhook-check.sh

Result: a line with OK and the webhook address. From here, hook the script into your monitoring. For Zabbix that’s a UserParameter, for something simpler just cron with an email alert.

A cron example with an email alert. Requires mailutils or any sendmail-compatible agent configured.

Where: n8n server, file /etc/cron.d/tg-webhook-check.


*/5 * * * * root /usr/local/bin/tg-webhook-check.sh >/tmp/tg-check.out 2>&1 || mail -s "TG webhook problem" admin@example.com < /tmp/tg-check.out

Don’t send an alert about a broken Telegram bot through that same Telegram bot. Sounds obvious. I’ve seen it done three times.

In Zabbix, also add a web scenario for https://n8n.example.com/healthz expecting code 200, plus a trigger for certificate expiry. The certificate usually expires on a Sunday night when you’re out of reach.

Backups

What Location Frequency Why
PostgreSQL database db_storage volume Daily Workflows, credentials, execution history
.env /opt/n8n/.env On every change Encryption key and passwords
Workflow JSON export via n8n CLI Daily Quickly restore a single workflow without a full database rollback
Configs docker-compose.yml, init-data.sh, nginx, cron On change Rebuild the server from scratch in 15 minutes

Create /usr/local/bin/n8n-backup.sh:


#!/bin/bash
set -euo pipefail
cd /opt/n8n
set -a; . ./.env; set +a

STAMP=$(date +%F_%H%M)
DEST=/opt/n8n/backup/$STAMP
mkdir -p "$DEST"

# Database dump in custom format
docker compose exec -T postgres pg_dump -U "$POSTGRES_USER" -d "$POSTGRES_DB" -Fc > "$DEST/n8n.dump"

# Export workflows and encrypted credentials
docker compose exec -T n8n sh -c 'rm -rf /home/node/.n8n/export && mkdir -p /home/node/.n8n/export/wf /home/node/.n8n/export/cred'
docker compose exec -T n8n n8n export:workflow --backup --output=/home/node/.n8n/export/wf/
docker compose exec -T n8n n8n export:credentials --backup --output=/home/node/.n8n/export/cred/
docker compose cp n8n:/home/node/.n8n/export "$DEST/export"

# Configs
cp .env docker-compose.yml init-data.sh "$DEST/"
cp /etc/nginx/sites-available/n8n.conf "$DEST/"

tar -czf "/opt/n8n/backup/n8n_$STAMP.tar.gz" -C /opt/n8n/backup "$STAMP"
rm -rf "$DEST"
chmod 600 "/opt/n8n/backup/n8n_$STAMP.tar.gz"

# Keep 14 days locally
find /opt/n8n/backup -name 'n8n_*.tar.gz' -mtime +14 -delete
echo "Backup done: /opt/n8n/backup/n8n_$STAMP.tar.gz"

Where: n8n server.


sudo chmod 700 /usr/local/bin/n8n-backup.sh
sudo /usr/local/bin/n8n-backup.sh
echo '30 3 * * * root /usr/local/bin/n8n-backup.sh >> /var/log/n8n-backup.log 2>&1' | sudo tee /etc/cron.d/n8n-backup

Result: an archive with the date appears in /opt/n8n/backup. Cron will create a new one every night at 3:30.

A backup on the same server isn’t a backup, it’s a copy. Ship the archives out: rsync or rclone to another server or an S3-compatible storage. The archive contains the encryption key, so the storage needs to be locked down too.

Restoring

Test a restore at least once before you actually need it. A backup that’s never been restored is just a hypothesis.

Where: n8n server or a new server with the same docker-compose.yml and .env from the archive.


cd /opt/n8n
tar -xzf backup/n8n_DATE.tar.gz -C /tmp
cp /tmp/DATE/.env /tmp/DATE/docker-compose.yml /tmp/DATE/init-data.sh /opt/n8n/
docker compose up -d postgres
sleep 10
set -a; . ./.env; set +a
docker compose exec -T postgres pg_restore -U "$POSTGRES_USER" -d "$POSTGRES_DB" --clean --if-exists --no-owner --role="$POSTGRES_NON_ROOT_USER" < /tmp/DATE/n8n.dump
docker compose up -d
docker compose logs n8n --tail 30

Result: n8n starts up with the same workflows and credentials. Published workflows re-register their webhooks on startup. Check getWebhookInfo to confirm the address is correct.

Upgrading n8n

Born in Docker, upgrades aren’t scary, as long as you backed up first.

What to check before upgrading:

  • Read the release notes between your current and target versions on GitHub, look for breaking changes and anything about Telegram.
  • Run a fresh database backup with the script above.
  • Write down the current version, you’ll need it for rollback.
  • Upgrade to stable, not beta. Beta is for a staging box.

Where: n8n server, /opt/n8n directory.


cd /opt/n8n
grep N8N_VERSION .env
sudo /usr/local/bin/n8n-backup.sh
sed -i 's/^N8N_VERSION=.*/N8N_VERSION=NEW_VERSION/' .env
docker compose pull n8n
docker compose up -d n8n
docker compose logs n8n -f --tail 50

Result: the logs show database migrations running, if any, and n8n reports readiness. Then check healthz, getWebhookInfo, and send a test message to the bot.

Rollback after a database migration
Newer n8n versions often migrate the database schema. An older version with a newer schema may fail to start or behave incorrectly. So rollback always involves two actions: revert the version in .env and restore the database dump taken before the upgrade. Just switching the image tag back, without restoring the database, isn’t actually a rollback.

Moving from the 1.x branch to 2.x is its own story. The 2.0 branch shipped as a security-hardening release, with changes to default behavior. Do that kind of upgrade on a copy first, with a separate test bot, and only then in production.

FAQ: common questions about Telegram Trigger in n8n

Why doesn’t Telegram Trigger in n8n work after setup?

In most cases, Telegram can’t deliver the update to your server. Run getWebhookInfo: an empty url means the workflow isn’t published, an address with webhook-test means a test run overwrote production. A filled-in last_error_message field names the cause directly, cross-check it with the error table above.

How do I verify Telegram Trigger is working correctly?

Three checks. getWebhookInfo shows a production url with /webhook/ and zero errors. nginx’s access.log shows a POST returning 200 from Telegram’s subnets when you message the bot. The Executions section in n8n shows a new execution.

What do I do if n8n shows Bad Request: bad webhook: An HTTPS URL must be provided for webhook?

n8n is handing Telegram an address with http or localhost. Set the WEBHOOK_URL variable to a public HTTPS address, configure TLS on the reverse proxy, and recreate the container with docker compose up -d. Then unpublish the workflow and publish it again.

How is Telegram Trigger different from a regular Webhook node?

Telegram Trigger calls setWebhook itself on publish, generates and verifies the secret token itself, and parses the update type for you. A Webhook node accepts any POST, you register the webhook with Telegram manually, and you write the secret check yourself. In exchange, you get a predictable path and full control over the parameters.

Can I use multiple Telegram Triggers with one bot?

No. A bot has one webhook, and whichever workflow was published last grabs all the updates. Build one router workflow with a single trigger and split the logic across sub-workflows using Execute Workflow. Set up a separate bot for testing.

Why does a test run hang at Waiting for trigger event?

The reverse proxy isn’t passing through the websocket, so the editor never learns an update arrived. Add proxy_http_version 1.1 and the Upgrade and Connection headers in nginx. The second common cause: you’re messaging the production bot while listening on the test one, or vice versa.

How do I find the chat id in n8n?

Send the bot any message and open the Telegram Trigger execution. The Chat ID is in the message.chat.id field. It’s positive for private chats, negative for groups, and starts with -100 for supergroups and channels.

Why doesn’t the bot see messages in a group?

Privacy mode is enabled: the bot only sees commands and replies to its own messages. Disable it in BotFather with /setprivacy and re-add the bot to the group. Alternatively, make the bot a group admin, admins see everything without needing privacy mode disabled.

Do I need a public IP for a Telegram bot on n8n?

Telegram Trigger needs an address reachable over the internet via HTTPS. A public IP isn’t strictly required: a tunnel or a proxying VPS works fine. If inbound access is impossible altogether, fall back to polling with Schedule and getUpdates.

What’s next

We covered how an update actually travels: from the user through Telegram’s servers to your nginx and into n8n. We set up n8n in Docker with Postgres behind HTTPS, set WEBHOOK_URL correctly, and enabled websocket in the proxy. We built the bot using the “one trigger, one router” pattern, with commands, buttons, and callback replies. We locked the server down with a firewall, fail2ban, and webhook restriction by Telegram’s subnets.

Now you have a bot that survives a reboot, an upgrade, and a 3am crash. Monitoring catches a dead webhook before users do, backups restore along with the encryption key, and when something breaks there’s a five-minute algorithm instead of guesswork. The one habit worth keeping: bot goes silent, check getWebhookInfo first. Not a restart, not a reinstall, just one API call.

Still not working?
If the bot is still silent, drop a comment and we’ll sort it out. Include the getWebhookInfo output with the token redacted, your n8n version, and the relevant piece of your nginx config. Those three things reveal the cause nine times out of ten.

Оставайтесь на связи

Рецепты от IT-боли. Без воды, без рекламы, без маркетинговой шелухи.

Подписаться на IT-Аптеку →
Поделитесь:

Leave a Comment

Your email address will not be published. Required fields are marked *

Scroll to Top