How to Set Up a Telegram Bot Webhook: setWebhook, Secret Token and Troubleshooting
A Telegram bot webhook is an HTTPS URL on your server to which Telegram sends every update as a JSON POST request, so your bot no longer has to poll getUpdates. You register it with one call, setWebhook with your url and a secret_token, and check delivery with getWebhookInfo. This guide covers the requirements, a copy-paste handler that verifies the secret token, and fixes for the errors people hit most, including n8n on localhost.
Webhook vs getUpdates
| getUpdates (long polling) | Webhook | |
|---|---|---|
| Who connects | Your bot asks Telegram for updates | Telegram POSTs updates to your URL |
| Needs | Outgoing internet access only | A public HTTPS endpoint |
| Works on localhost | Yes | Only through a tunnel or proxy |
| Good for | Development, simple bots | Production, serverless, high traffic |
The two methods are mutually exclusive: while a webhook is set, getUpdates returns 409 Conflict. Either way, Telegram keeps undelivered updates for up to 24 hours.
Telegram webhook requirements
- HTTPS only. The official Bot API rejects plain HTTP URLs on any port.
- Ports 443, 80, 88 or 8443. Other ports don't work. For anything but 443, put the port in the URL, e.g.
https://example.com:8443/telegram/webhook. - TLS 1.2 or newer and a certificate whose CN or SAN matches your domain. Serve the full chain, including intermediate certificates.
- Self-signed certificates work if you upload the public certificate in the
certificateparameter. - IPv4. IPv6 is not supported for webhooks, and redirects are not supported either, so register the final URL.
- Firewall: requests come from
149.154.160.0/20and91.108.4.0/22; Telegram notes these ranges may change.
A local Bot API server lifts some of these limits (HTTP, any port), but with api.telegram.org the rules above apply.
Set the webhook URL with setWebhook
Generate a random secret first, for example with openssl rand -hex 32, then register the URL:
curl -X POST "https://api.telegram.org/bot123456789:AAEexampleTokenReplaceWithYours/setWebhook" \
-H "Content-Type: application/json" \
-d '{
"url": "https://example.com/telegram/webhook",
"secret_token": "replace-with-your-random-secret",
"allowed_updates": ["message", "callback_query"],
"drop_pending_updates": true,
"max_connections": 40
}'
On success the API answers {"ok": true, "result": true, "description": "Webhook was set"}.
| Parameter | Meaning |
|---|---|
url | Required. HTTPS URL for updates; an empty string removes the webhook. |
secret_token | 1–256 characters A-Z a-z 0-9 _ -, sent back in the X-Telegram-Bot-Api-Secret-Token header. |
allowed_updates | Update types to receive. If omitted, the previous setting is kept; an empty list means all types except chat_member, message_reaction and message_reaction_count. |
drop_pending_updates | Discard updates waiting for delivery. |
max_connections | Simultaneous HTTPS connections, 1–100 (default 40). Lower it to protect a small server. |
certificate | Public key certificate for self-signed setups, uploaded as a file. |
ip_address | Fixed IP address to send requests to instead of the one resolved via DNS. |
For a quick one-off setup, the browser address bar works too: https://api.telegram.org/bot<token>/setWebhook?url=https://example.com/telegram/webhook. With a self-signed certificate, upload it as multipart/form-data:
curl -F "url=https://203.0.113.10:8443/telegram/webhook" \
-F "certificate=@public.pem" \
"https://api.telegram.org/bot123456789:AAEexampleTokenReplaceWithYours/setWebhook"
Prefer a form? Fill in setWebhook in the Bot API Tester and execute it from your browser.
Check delivery with getWebhookInfo
curl "https://api.telegram.org/bot123456789:AAEexampleTokenReplaceWithYours/getWebhookInfo"
{
"ok": true,
"result": {
"url": "https://example.com/telegram/webhook",
"has_custom_certificate": false,
"pending_update_count": 14,
"last_error_date": 1793266500,
"last_error_message": "Wrong response from the webhook: 500 Internal Server Error",
"max_connections": 40,
"allowed_updates": ["message", "callback_query"]
}
}
urlis empty when no webhook is set and the bot uses getUpdates.- A
pending_update_countthat keeps growing means Telegram can't deliver: your endpoint is down, slow or returns errors. last_error_messageandlast_error_date(Unix time) describe the latest delivery failure: a timeout, a refused connection, a TLS problem or a non-2xx status from your server.
Run the same check in getWebhookInfo in the Bot API Tester whenever updates stop arriving.
Verify the X-Telegram-Bot-Api-Secret-Token header
Anyone who discovers your webhook URL can POST fake updates to it. With secret_token set, Telegram adds the secret to every request in the X-Telegram-Bot-Api-Secret-Token header; reject anything that doesn't match, using a constant-time comparison. Python with Flask (pip install flask):
import hmac
import os
from flask import Flask, abort, jsonify, request
app = Flask(__name__)
SECRET = os.environ["WEBHOOK_SECRET"]
@app.post("/telegram/webhook")
def telegram_webhook():
received = request.headers.get("X-Telegram-Bot-Api-Secret-Token", "")
if not hmac.compare_digest(received.encode(), SECRET.encode()):
abort(403)
update = request.get_json(silent=True) or {}
message = update.get("message") or {}
if "text" in message:
# reply inside the webhook response: no extra API call needed
return jsonify(method="sendMessage", chat_id=message["chat"]["id"],
text="Echo: " + message["text"])
return "", 200
The reply trick is documented by Telegram: you may answer a webhook request with a JSON body that contains method and its parameters. You can't see whether that call succeeded, so make a regular API request when you need the result. The same check in Node.js 18+ without dependencies (save as server.mjs):
import http from "node:http";
import crypto from "node:crypto";
const SECRET = Buffer.from(process.env.WEBHOOK_SECRET);
function hasValidSecret(req) {
const received = Buffer.from(req.headers["x-telegram-bot-api-secret-token"] ?? "");
return received.length === SECRET.length && crypto.timingSafeEqual(received, SECRET);
}
function handleUpdate(update) {
console.log("update", update.update_id); // your bot logic goes here
}
http.createServer((req, res) => {
if (req.method !== "POST" || req.url !== "/telegram/webhook") {
res.writeHead(404).end();
return;
}
if (!hasValidSecret(req)) {
res.writeHead(403).end();
return;
}
let body = "";
req.on("data", (chunk) => (body += chunk));
req.on("end", () => {
res.writeHead(200).end(); // acknowledge first, then process
try {
handleUpdate(JSON.parse(body));
} catch (err) {
console.error("bad update", err);
}
});
}).listen(8080);
Port 8080 isn't one of the four webhook ports, so run the app behind a reverse proxy such as nginx or Caddy that terminates TLS on 443 and forwards requests to it. Keep the secret in sync: if your server has a new secret but the webhook still has the old one, your own handler answers 403 and Telegram keeps retrying.
Respond with 2xx quickly
Telegram treats any response other than 2XX as a failed delivery: it repeats the request and gives up after a reasonable number of attempts, while new updates queue up behind it. Practical rules:
- Return 200 for every genuine update, including types you ignore and updates your code fails to process. Log the error instead of answering 500, or the same update keeps coming back.
- Acknowledge first and move slow work (LLM calls, file processing, broadcasts) to a background queue.
- Deduplicate by
update_id, because retries can deliver the same update again. - Use
max_connectionsto cap parallel requests on a small server.
deleteWebhook: switch back to getUpdates
curl "https://api.telegram.org/bot123456789:AAEexampleTokenReplaceWithYours/deleteWebhook?drop_pending_updates=true"
drop_pending_updates=true discards the backlog; leave it out to receive queued updates with getUpdates. Calling setWebhook with an empty url removes the webhook as well. Afterwards getWebhookInfo shows an empty url and polling works again.
Troubleshooting Telegram webhook errors
| Error | Cause and fix |
|---|---|
Bad Request: bad webhook: An HTTPS URL must be provided for webhook | The URL starts with http://, often a local n8n or dev server. Expose it over HTTPS. |
| setWebhook rejects the port | Only 443, 80, 88 and 8443 are accepted. Change the port or proxy through 443. |
409 Conflict from getUpdates | A webhook is active. Call deleteWebhook before polling. |
TLS or certificate errors in last_error_message | Incomplete chain, CN/SAN mismatch, or a self-signed certificate that wasn't uploaded. |
Wrong response from the webhook: … | Your server returned a non-2xx status: a wrong path, a crashing handler or a secret mismatch. |
| The webhook URL keeps changing | Another service calls setWebhook with the same token. A bot has exactly one webhook URL, so give each service its own bot. |
| No updates for group messages | Privacy mode is on, or the update type is missing from allowed_updates. |
Telegram webhook with n8n
The n8n Telegram Trigger receives updates through a webhook: it registers its own URL for your bot with setWebhook. Three consequences follow:
- n8n on localhost fails with
An HTTPS URL must be provided for webhook, because Telegram can't reach anhttp://localhostaddress. Make n8n reachable at a public HTTPS address (a reverse proxy with a certificate, or a tunnel) and configure n8n to use that address for its webhook URLs. - One bot, one webhook. Two workflows or two n8n instances using the same bot overwrite each other's webhook. Use a separate bot per workflow.
- getUpdates stops working for that bot while the trigger is active, so look up chat ids before you activate it.
Related guides: keep your bot token secret, since it is part of every API call your handler makes, and Telegram Bot API limits for sending replies at scale. New to bots? Start with how to create a Telegram bot.
Frequently asked questions
How do I set a webhook URL for my Telegram bot?
Call setWebhook, for example https://api.telegram.org/bot<token>/setWebhook?url=https://example.com/telegram/webhook. The URL must be public HTTPS on port 443, 80, 88 or 8443. Confirm the result with getWebhookInfo.
What is the secret token in a Telegram webhook?
An optional secret_token of 1–256 characters (A-Z, a-z, 0-9, _, -) that you pass to setWebhook. Telegram sends it in the X-Telegram-Bot-Api-Secret-Token header of every webhook request, so your handler can reject requests without it.
Can I use getUpdates and a webhook at the same time?
No, the two are mutually exclusive. While a webhook is set, getUpdates fails with 409 Conflict; call deleteWebhook to go back to polling.
Why does n8n show “An HTTPS URL must be provided for webhook”?
The n8n Telegram Trigger registers its own URL with setWebhook. If n8n runs on http://localhost, Telegram rejects that URL. Make n8n reachable at a public HTTPS address, for example through a reverse proxy or a tunnel, and let it register that address.
How do I delete a Telegram bot webhook?
Call deleteWebhook, optionally with drop_pending_updates=true to discard queued updates. Calling setWebhook with an empty url also removes it; getWebhookInfo then shows an empty url.
Can I use a self-signed certificate for a Telegram webhook?
Yes. Upload the public certificate in PEM format as the certificate parameter using multipart/form-data. Certificates from a trusted CA need no upload, but your server must send the full chain, including intermediates.
Related guides
How to Create a Telegram Bot: Step by Step with BotFather and Python
How to create a Telegram bot step by step: register it with @BotFather, get the token, test it with getMe and send a first message with curl or Python.
Telegram Bot API 10.0–10.3: Rich Messages, Ephemeral Messages, Guest Mode and More
What's new in Telegram Bot API 10.0–10.3: guest mode, rich messages, streamed drafts, ephemeral messages, join request queries, with JSON examples.
Telegram Bot API Limits: Rate Limits, 429 Errors, File Sizes and Message Length
Telegram Bot API rate limits explained: 30 msg/s, 1 msg/s per chat, 20/min in groups, handling 429 retry_after, plus file size and message length limits.
How to Send a Message with the Telegram Bot API (sendMessage)
Telegram Bot API send message guide: sendMessage with curl, Python and JavaScript, HTML and MarkdownV2 formatting, inline buttons and error fixes.
Telegram Bot Token: How to Get It, Find It Again, Check It and Keep It Safe
Telegram bot token guide: get one from @BotFather, find the token of an existing bot, check it with getMe, revoke a leaked token and store it safely.
How to Get a Telegram Chat ID for a User, Group, Channel or Topic
How to get a Telegram chat ID for a user, group, supergroup, channel or forum topic with getUpdates, plus fixes for an empty result and 409 Conflict.
How to Test a Telegram Mini App: From Browser Mock to Real Telegram
Test a Telegram Mini App step by step: browser mock mode, localhost via HTTPS tunnel, test environment, DevTools per platform, initData validation.