Telegraft

Bot API reference

Two ways to receive an update, one of them for production

A bot receives updates either by calling `getUpdates` in a loop (long polling) or by registering an HTTPS endpoint with `setWebhook` and having Telegram POST to it. They are mutually exclusive: calling `getUpdates` while a webhook is set returns an error until the webhook is deleted.

Webhooks vs long polling: the exact figures

Webhook ports
443, 80, 88 or 8443, HTTPS only
Production choice
Webhook. Long polling is a development tool
Mutually exclusive
getUpdates fails while a webhook is registered
Transport
HTTPS on port 443, 80, 88 or 8443
Handler contract
Respond 200 immediately; process asynchronously
Diagnostics
getWebhookInfo reports pending count and last error

As of 2025-10-01, Telegram Bot API 13.4

What it means in practice

Long polling holds a request open until an update arrives or a timeout expires, then immediately reopens it. It needs no public address, no certificate and no inbound firewall rule, which is exactly why it is the right choice on a laptop. Nothing about it scales: every instance polling the same token competes for the same updates, the process must stay alive to receive anything, and a restart leaves a window where updates queue server-side.

A webhook inverts the relationship. Telegram POSTs each update to an HTTPS URL you register once, which means the bot is a request handler rather than a long-lived process — and that is what makes serverless runtimes viable. It requires a valid certificate on port 443, 80, 88 or 8443, a publicly reachable address, and a handler that responds quickly.

That last requirement is the one that catches people. Telegram treats a slow or failing endpoint as unhealthy and will retry, so a handler that performs the actual work before responding produces duplicate processing under load. The correct shape is to acknowledge immediately with 200 and do the work afterwards — on Cloudflare Workers that is `ctx.waitUntil`, on Node an explicit queue. Responding first is not an optimisation; it is what stops one slow database write becoming three deliveries of the same message.

Switching between them is a live operation with a sharp edge. `deleteWebhook` accepts `drop_pending_updates`, and whether to use it is a real decision rather than a default: dropping discards everything queued while the endpoint was down, which is correct after a long outage of a chat bot and wrong for a payment bot where the queue may contain a completed checkout. `getWebhookInfo` reports the pending count and the last error, and it is the first thing to check when a bot has gone quiet.

For production the answer is a webhook, without much nuance. It is cheaper to run, it survives restarts without losing updates, it scales horizontally because Telegram is doing the distribution, and it is a prerequisite for the secret-token validation that stops anyone else posting fabricated updates at your endpoint.

Handling it in code

// Acknowledge first, work afterwards. The order is the whole point.
export default {
  async fetch(request: Request, env: Env, ctx: ExecutionContext): Promise<Response> {
    if (request.headers.get('X-Telegram-Bot-Api-Secret-Token') !== env.WEBHOOK_SECRET) {
      return new Response('Not found', { status: 404 })
    }

    const update = await request.json()

    // Telegram retries anything slow or failing. Doing the work before responding is
    // how one slow write becomes three copies of the same message.
    ctx.waitUntil(handleUpdate(update, env))
    return new Response('ok')
  },
}
The 200 goes out before the work starts. Everything else about webhook reliability follows from getting this ordering right.

Questions this raises

Can I run long polling and a webhook at the same time?

No. They are mutually exclusive by design: while a webhook is registered, `getUpdates` returns an error. This is the usual cause of a local bot that suddenly receives nothing — a webhook is still set from a previous deploy, and `deleteWebhook` is the fix.

What happens to updates while my webhook endpoint is down?

Telegram queues them and retries for a period, then discards them. `getWebhookInfo` reports how many are pending and what the last error was, which makes it the first diagnostic to run rather than the last.

Should I use drop_pending_updates when redeploying?

It depends on what the queue might contain, and it is worth deciding deliberately rather than copying a snippet. Dropping is right for a conversational bot after a long outage, where replaying an hour of stale messages confuses people. It is wrong for a payment or booking bot, where a pending update may be the only record that a checkout completed.

Is a self-signed certificate acceptable?

Telegram supports uploading a self-signed certificate with setWebhook, but there is rarely a reason to now that free automated certificates are universal. Using a normal certificate removes an entire class of expiry and trust-chain failure that is unpleasant to diagnose from the outside.

Related limits