Telegram Webhook Secret-Token Checks and update_id Dedupe for Telegram→X Schedulers in 2026
A Telegram bot that schedules X Posts has two front doors: the outbound X API calls everyone worries about, and the inbound Telegram webhook that receives /schedule, /approve, and /cancel commands. If that inbound endpoint accepts forged requests, anyone who finds the URL can queue Posts on your account. If it processes a redelivered update twice, one approval can turn into two scheduled Posts. Telegram’s Bot API documents the tools to handle both: an optional secret_token that Telegram echoes back in a request header, and a sequential update_id meant for ignoring repeated updates. This guide covers how to use them in a Telegram→X scheduler like the ones discussed on AutoX. It complements our pieces on outbound create-Post idempotency and approval gates and token hygiene, and it does not repeat them. It is engineering hygiene, not a growth promise.
What Telegram actually documents about webhooks
Everything below comes from Telegram’s own pages, and you should re-check them before shipping because the Bot API changes often:
- Delivery and retries. The setWebhook reference says Telegram sends an HTTPS POST with a JSON-serialized
Updateto your URL. If the response status is anything other than 2XX, Telegram “will repeat the request and give up after a reasonable amount of attempts.” So a slow or failing handler gets the same update again. - secret_token. The same method takes an optional
secret_tokenof 1–256 characters, limited toA-Z,a-z,0-9,_, and-. When it is set, every webhook request includes the headerX-Telegram-Bot-Api-Secret-Tokencarrying that value. Telegram describes it as a way “to ensure that the request comes from a webhook set by you.” - update_id. In the Update object,
update_idis the update’s unique identifier. Identifiers start from a positive number and increase sequentially, which Telegram says is useful with webhooks to “ignore repeated updates or to restore the correct update sequence.” If there are no new updates for at least a week, the next identifier is picked at random instead of sequentially. Do not treat it as a counter that never resets. - Retention. According to Getting updates, undelivered updates are stored on Telegram’s side for no longer than 24 hours, and
getUpdatesand webhooks are mutually exclusive. - Network basics. Telegram’s webhook guide lists the requirements: IPv4, TLS 1.2 or newer, ports 443, 80, 88, or 8443, and incoming POSTs from the subnets
149.154.160.0/20and91.108.4.0/22(as published at the time of writing).
Verify the secret-token header before parsing anything
A webhook URL is not a secret. It shows up in logs, reverse-proxy configs, and sometimes in screenshots. The secret token is the part that proves Telegram sent the request. A practical pattern:
- Generate a high-entropy token using only the allowed character set, for example 64 URL-safe characters from a CSPRNG. Store it in your secret manager next to the bot token, never in source control. Pass it to
setWebhookassecret_token. - Check the header first. Before you parse JSON, touch your database, or log the body, read
X-Telegram-Bot-Api-Secret-Token. If it is missing or wrong, return 401 or 403 with an empty body. Rejecting early keeps forged traffic from costing you database reads or X API calls. - Compare in constant time. Use
hmac.compare_digestin Python orcrypto.timingSafeEqualin Node.js, not==. Note that Node’s function throws when the buffers have different lengths, so check the length first or compare fixed-length digests of both values. - Log carefully. Record that a rejection happened, with timestamp, source IP, and path. Never log the header value you received or the expected one.
- Rotate on purpose. When you rotate, call
setWebhookwith the new token and accept either the old or the new value for a short window. Telegram may still be retrying updates it signed with the old one.
The secret token does not replace your bot’s own authorization rules. A request can be genuinely from Telegram and still come from a user who should not be able to schedule Posts. Keep the operator allowlist and the approval gate from approval gates and API token hygiene in place. Subnet allowlisting at the edge is a reasonable extra layer, but Telegram publishes those ranges as current requirements, not permanent guarantees, so do not make it your only check.
Dedupe on update_id so one command acts once
Retries are where a Telegram→X scheduler goes wrong. Say the /approve handler calls the X API inline, X is slow, the request runs longer than Telegram is willing to wait, and Telegram redelivers. The naïve handler approves the same draft twice, and now there are two scheduled Posts. Here is how to prevent that:
- Durable claim. Before you take any action, atomically insert
update_id(scoped by bot id) into a store that supports create-if-absent, such as a Firestore document created in a transaction or a SQL row with a unique constraint. If the insert fails because the id already exists, return 200 and stop. - Keep claims for at least a day. Telegram holds undelivered updates for up to 24 hours, so a TTL of 48–72 hours covers redelivery comfortably. Do not try to infer duplicates from “highest id seen,” because ids can restart at a random value after a week with no updates.
- Ack fast, work later. Write the claim and a job record, return 200 right away, and let a worker do the X call. That removes the most common cause of redelivery, which is a webhook handler waiting on a third-party API.
- Dedupe at the action level as well.
update_idcatches Telegram redelivery, but it does not catch a user double-tapping an inline button. Those are two different updates. Make state transitions conditional, for example “approve only if status ispending,” so the second tap does nothing. For the outbound side, reuse the claim pattern from client-request-id idempotency. - Return 2XX for updates you have handled, even if you ignored them. Returning 500 for an update type you do not care about makes Telegram keep retrying it, and that delays everything behind it.
update_id. A durable claim turns the second delivery into a no-op instead of a second scheduled Post.Narrow the surface with allowed_updates and watch getWebhookInfo
Two more Bot API settings deserve a minute of your time:
- allowed_updates. Subscribe only to the update types the scheduler uses, which is usually
messageandcallback_query. Fewer update types mean less parsing code exposed to the internet and fewer chances to mishandle an unexpected payload. Telegram notes that changing this setting does not affect updates created before the call, so expect a brief tail of old types after a change. - max_connections. This defaults to 40 and accepts 1–100. If your worker pool or database cannot handle 40 concurrent webhook calls, lower it. Overload produces 5xx responses, which produce retries, which bring you back to the dedupe problem above.
- drop_pending_updates. This is useful after an incident, for example when a backlog of stale
/schedulecommands should not fire. Treat it as an operator decision you record, not something your deploy script does by default. - getWebhookInfo. Poll getWebhookInfo from a monitor. A rising
pending_update_countor a freshlast_error_dateorlast_error_messageusually shows a failing handler before users notice. Alert on those values, and never print the bot token in the alert.
Bot-side scheduling still has to respect X-side limits. Pair this with rate-limit and credit budgets and dead-letter and 429 backoff so a burst of valid commands does not become a burst of rejected Posts.
A minimal handler shape (pseudocode)
on POST /telegram/webhook:
got = header("X-Telegram-Bot-Api-Secret-Token") or ""
if not constant_time_equal(got, CURRENT) and not constant_time_equal(got, PREVIOUS_DURING_ROTATION):
return 403 # empty body, log the event without values
update = parse_json(body)
if not create_if_absent("tg_updates/" + BOT_ID + ":" + update.update_id, ttl=72h):
return 200 # already handled
enqueue(job_from(update)) # X API work happens in a worker
return 200
This is a shape, not a library. Adapt it to your framework, and keep the bot token and the secret token in a secret manager, as covered in secret storage for long-lived bots.
Practical checklist
- Set a high-entropy
secret_tokenonsetWebhook, store it in a secret manager, and rotate it with a short dual-accept window. - Check
X-Telegram-Bot-Api-Secret-Tokenfirst, compare in constant time, and reject with an empty 401/403. - Claim
update_idatomically with a TTL of at least 48 hours, and return 200 for duplicates. - Return 2XX quickly and move X API calls to a worker queue.
- Make approve and cancel transitions conditional on current state, which handles double taps.
- Limit
allowed_updates, sizemax_connections, and alert ongetWebhookInfoerrors.
Key takeaways
- Telegram retries any non-2XX webhook response, so a slow handler that calls X inline invites duplicate actions.
- The documented
secret_tokenandX-Telegram-Bot-Api-Secret-Tokenheader are the simplest way to reject forged webhook calls. Compare them in constant time. update_idexists to ignore repeated updates. Store it durably, and do not assume it never resets.- Inbound dedupe (Telegram) and outbound idempotency (X) are two separate controls, and a scheduler needs both.
Telegram Bot API parameters, header names, and network requirements summarized here come from Telegram’s public documentation as of 2026-10-05 and can change. Confirm them on core.telegram.org before relying on them. No affiliate links. Last verified 2026-10-05.