You've seen the registration call in passing since lesson 1.
This lesson makes you the operator of it: what setWebhook really stores, which
secret carries the trust, how to inspect and repair a live registration, and why dev and prod
can run at the same time without stealing each other's mail. It ends with you reading your own
bot's registration, live.
Which leaked secret would let a stranger impersonate you to your own bot — forge an update saying "chat 421337842 says: delete everything" — and what's the fix?
setWebhook(url) writes
exactly one record on Telegram's side, keyed by bot token: "deliver this bot's updates by
POSTing to this URL." Three properties of that record drive everything operational:
getUpdates fails with 409 Conflict — Telegram refuses to serve the
same queue two ways. Delivery mode is a switch, not a mixture.deleteWebhook, silently drag production's
real updates onto your laptop. A second bot account is the only clean way to have both delivery
modes alive at once.
Same call, two triggers — you saw both files in chat; here's the operational logic:
server/plugins/register-webhook.tsexport default defineNitroPlugin(async () => {
if (env.vercelEnv !== 'production') return // preview deploys: hands off
// …
const url = `${env.publicBaseUrl}/api/telegram/webhook/${env.telegramWebhookSecret}`
await bot.api.setWebhook(url)
})
Automatic: a Nitro plugin runs at server boot — on Vercel, that means each
cold start. The VERCEL_ENV gate is the whole safety story: without it, every
preview deploy's first request would boot the server, self-register its throwaway URL, and
steal production's queue (property 1 above).
Manual (npm run set-webhook): the identical call from your
machine. It exists because of a genuine chicken-and-egg: the plugin only runs on a cold start,
a cold start only happens when traffic arrives, and no traffic ever arrives until Telegram
knows the URL. After the very first production deploy, someone has to make the first
move — that's this script.
One deliberate omission worth knowing about: setWebhook also accepts a
secret_token parameter —
Telegram would then echo it back in an X-Telegram-Bot-Api-Secret-Token header on
every POST, and you'd verify the header instead of hiding the path. It's the "official" way to
authenticate inbound webhooks; this repo skips it and relies on the unguessable path alone.
Both are legitimate; the header survives your URL appearing in logs, the path needs zero extra
code. Your next bot can choose either — just choose one on purpose.
getWebhookInfo is
the diagnostic view of the stored record — callable from a browser, since bot API methods are
plain HTTPS GETs:
https://api.telegram.org/bot<TOKEN>/getWebhookInfo
The fields that matter, and what they tell you:
url — what's actually registered right now. Empty string = no webhook =
polling mode possible. Wrong host = something hijacked or a stale registration.pending_update_count — mail Telegram is holding that you haven't 200-ed.
Nonzero and growing = your endpoint is failing (lesson 4's silent-outage scenario).last_error_message / last_error_date — Telegram's own note about
the most recent failed delivery ("connection refused", "wrong response…"). The first place to
look when the bot "just stopped answering".
Open two browser tabs with getWebhookInfo — one with your prod
token, one with your dev token (both are in .env). Predict
before you look: what should url be in each? You should see your production
URL + secret in one, and an empty url in the other — the two delivery modes of
lessons 1 and 5, live. If prod's pending_update_count is nonzero, tell your
teacher — that's a real finding. (Treat these URLs as secrets: they contain the token —
don't paste them anywhere public.)
npm run set-webhook
once by hand. Verify with getWebhookInfo.TELEGRAM_WEBHOOK_SECRET in the env, redeploy (or run the script). One move does
both halves: the route now 404s the old path, and Telegram now POSTs the new one.getWebhookInfo first —
last_error_message and pending_update_count usually name the
culprit before you open a single log.deleteWebhook,
optionally with drop_pending_updates: true if the held backlog would replay
confusingly. Only then does getUpdates stop 409-ing.1 · A teammate opens a preview deploy of a bot that lacks the VERCEL_ENV gate. Production goes quiet. What exactly happened, and where's the proof?
2 · You accidentally start npm run bot:dev with the PROD token in TELEGRAM_DEV_BOT_TOKEN while production's webhook is registered. What happens?
3 · Fresh project, first-ever production deploy just finished. Why can't you simply wait for the auto-register plugin to do its job?
Questions the lesson provokes — should your next bot use secret_token instead of
the path? what does drop_pending_updates lose exactly? — ask your teacher (me)
directly, any time.