All articles
Telegram-style chat update packets traveling through a secure HTTPS tunnel into a bot handler running on a developer laptop.
Telegram Bot APIwebhookslocalhostbot development

Test a Telegram Bot Webhook on Localhost

To test a Telegram bot webhook on localhost, expose your local server with a public HTTPS tunnel, call setWebhook with that URL, and validate Telegram's secret-token header on every request. This gives you real message, callback-query, and membership updates without deploying after every code change. The complete loop is: run the bot handler, start npx portpreview 3000, register the resulting URL, send your bot a message, and inspect the request locally.

Why Telegram cannot send updates directly to localhost

Telegram's Bot API sends webhook updates from Telegram infrastructure to an internet-reachable URL. localhost, 127.0.0.1, and private LAN addresses are not routable from that infrastructure. A localhost tunnel terminates HTTPS at a public address and forwards the unchanged HTTP request to your local port.

Telegram bots can receive updates in two mutually exclusive ways: long polling through getUpdates, or webhooks. The official setWebhook reference states that getUpdates is unavailable while an outgoing webhook is configured. If a polling process is still running, stop it before judging the webhook flow.

Build a local webhook endpoint

This Express example keeps the handler intentionally small. It checks the shared secret before touching the update, acknowledges quickly, and moves work outside the response path.

import express from 'express';
import crypto from 'node:crypto';

const app = express();
app.use(express.json({ limit: '1mb' }));

function sameSecret(received = '', expected = '') {
  const a = Buffer.from(received);
  const b = Buffer.from(expected);
  return a.length === b.length && crypto.timingSafeEqual(a, b);
}

app.post('/webhooks/telegram', (req, res) => {
  const received = req.get('x-telegram-bot-api-secret-token') || '';
  if (!sameSecret(received, process.env.TELEGRAM_WEBHOOK_SECRET)) {
    return res.sendStatus(401);
  }

  const update = req.body;
  res.sendStatus(200);
  queueMicrotask(() => handleUpdate(update));
});

app.listen(3000, () => console.log('Listening on http://localhost:3000'));

Telegram sends a JSON-serialized Update. Unlike HMAC-based providers, Telegram's secret_token feature does not sign the body. It places your chosen value in X-Telegram-Bot-Api-Secret-Token. The token proves that the sender knows the value used when the webhook was registered, but it does not provide a payload digest. TLS protects the request in transit.

Expose the endpoint with HTTPS

  1. Start the app and confirm curl -i http://localhost:3000/webhooks/telegram reaches the server, even if GET returns 404.
  2. Open a second terminal and run npx portpreview 3000.
  3. Copy the public HTTPS origin and append /webhooks/telegram.
  4. Keep the tunnel process running while Telegram delivers updates.

The Bot API accepts HTTPS webhook URLs. Telegram documents webhook support on ports 443, 80, 88, and 8443; a managed tunnel's public endpoint normally uses 443 even when the forwarded local process listens on 3000.

Register the Telegram webhook safely

Create a random secret containing only letters, digits, underscores, or hyphens. Telegram permits 1–256 characters. Do not reuse the bot token as this value.

export TELEGRAM_WEBHOOK_SECRET="$(openssl rand -hex 32)"

curl -sS -X POST \
  "https://api.telegram.org/bot${TELEGRAM_BOT_TOKEN}/setWebhook" \
  -d "url=https://YOUR-TUNNEL.portpreview.dev/webhooks/telegram" \
  -d "secret_token=${TELEGRAM_WEBHOOK_SECRET}" \
  -d 'allowed_updates=["message","callback_query"]' \
  -d "drop_pending_updates=true"

allowed_updates reduces noise and should list only update types the bot handles. drop_pending_updates=true is useful when starting a fresh local session, but it permanently discards queued updates, so omit it when those events matter. Telegram's Update documentation describes fields such as message, callback_query, and my_chat_member.

Confirm registration before debugging code

Use getWebhookInfo to separate configuration failures from handler failures:

curl -sS "https://api.telegram.org/bot${TELEGRAM_BOT_TOKEN}/getWebhookInfo"

Check url, pending_update_count, last_error_message, and last_error_date. An empty URL means registration did not stick. A growing pending count usually means Telegram cannot connect or your endpoint returns a non-2xx status. Send a direct message to the bot after registration; merely opening the chat does not necessarily create an update.

Handle updates without causing retries

Acknowledge before slow work

Return a 2xx response as soon as the request is authenticated and durably accepted. Database exports, AI calls, and third-party APIs should run asynchronously. Telegram retries unsuccessful requests after non-2xx responses, so slow synchronous work can create duplicates.

Deduplicate with update_id

Every update has an update_id. Store processed IDs with an expiration window or enforce a unique database key. A retry must not send a second payment receipt, create a duplicate ticket, or execute the same callback twice.

Model every update type explicitly

Not every update contains message.text. Callback buttons arrive under callback_query; channel posts and membership changes have other fields. Branch on the present top-level field and treat unknown types as valid no-ops rather than throwing.

Security rules for local Telegram bot testing

  • Validate the secret header first. Reject missing or incorrect values before logging or parsing sensitive fields.
  • Keep tokens out of URLs and logs. The Bot API token in the registration command is a credential. Avoid shell history on shared systems and rotate an exposed token through BotFather.
  • Use an unguessable route and secret. The route is defense in depth; the secret header is the actual application check.
  • Limit captured data. Messages can contain names, usernames, phone numbers, files, and private conversation text. Redact logs and delete local captures when finished.
  • Never disable authentication in development. A public tunnel is public. Local code should exercise the same checks as production.

See the broader localhost tunnel security guide for access-control and data-retention practices.

Troubleshoot common Telegram webhook failures

Telegram reports a certificate or connection error

Use the tunnel's HTTPS URL, not its local HTTP target. Confirm the tunnel is active and the URL has not changed. If you provide your own self-signed certificate instead, Telegram requires uploading the public certificate as a file; a managed TLS endpoint avoids that setup.

The endpoint returns 401

Compare the secret passed to setWebhook with the environment variable used by the process. Header names are case-insensitive, but proxies or middleware can strip custom headers. Inspect the incoming headers without printing the secret value.

No requests arrive

Run getWebhookInfo, verify the registered path exactly matches your route, and ensure no firewall blocks the tunnel's local connection. If you recently used polling, confirm the webhook URL is now populated. Trigger an actual update by messaging the bot.

Updates arrive repeatedly

Log status and response time. Exceptions after receiving the request may turn an intended 200 into a 500. Return 200 promptly, make processing idempotent, and use controlled webhook replay rather than waiting for provider retries during debugging.

Test callback queries and files, not only text

A useful bot test matrix covers more than message.text. Send a photo with a caption, share a contact, edit a message, and press an inline-keyboard button. For callback queries, call answerCallbackQuery promptly so the client stops showing its progress indicator, then perform slower work separately. File updates contain identifiers; downloading the bytes is a second Bot API operation and should not delay the webhook response.

Keep fixtures made from sanitized updates for unit tests, but preserve the full transport path for at least one test of each supported type. A fixture proves your dispatcher understands a payload; a real tunneled delivery also proves registration, TLS, headers, body parsing, and acknowledgement behavior. When adding a new allowed_updates entry, call setWebhook again and verify getWebhookInfo reflects the intended configuration.

Remove the webhook after the local session

curl -sS -X POST \
  "https://api.telegram.org/bot${TELEGRAM_BOT_TOKEN}/deleteWebhook" \
  -d "drop_pending_updates=false"

Deleting the webhook lets you return to getUpdates. If the tunnel URL changes on the next session, call setWebhook again. For additional diagnostics, follow the general local webhook debugging workflow.

Frequently asked questions

Can Telegram send a bot webhook directly to localhost?
No. Telegram cannot route requests to localhost or a private LAN address. Use a public HTTPS tunnel that forwards requests to your local bot server.
How do I authenticate Telegram bot webhook requests?
Pass a random secret_token to setWebhook and compare every request's X-Telegram-Bot-Api-Secret-Token header with that value using a timing-safe comparison.
Why does my Telegram webhook receive duplicate updates?
Telegram retries unsuccessful deliveries. Return 2xx quickly and deduplicate work by update_id so retries cannot repeat side effects.
Can I use getUpdates while a Telegram webhook is active?
No. Telegram's Bot API does not allow getUpdates while an outgoing webhook is configured. Delete the webhook before returning to long polling.