telegram-bots
Use when building, adding or auditing a Telegram bot on the HTTP Bot API: polling or webhooks, update dedup, keyboards, Telegram Stars payments, files and rate limits. Not for user accounts (telegram-userbots) or the web layer (telegram-miniapps). Opt out: no telegram, без телеграма. Triggers - "telegram bot", "bot api", "aiogram", "setWebhook", "telegram stars", "телеграм бот", "вебхук телеграм", "звёзды телеграм".
How do I install this agent skill?
npx skills add https://github.com/ssheleg/telegram-dev --skill telegram-botsIs this agent skill safe to install?
- Gen Agent Trust Hubpass
This skill provides safe and technically sound documentation for building Telegram bots, including best practices for security and reliability. It contains a Python test fixture that demonstrates idempotency and crash recovery invariants without using dangerous operations or external dependencies.
- Socketpass
No alerts
- Snykwarn
Risk: MEDIUM · 2 issues
What does this agent skill do?
Telegram bots — the official Bot API
Telegram holds the chat. Your database holds everything you will act on later. Every serious bot defect is an update that was processed twice, or one that was never delivered because nobody asked for it.
The Bot API is not where bots fail. They fail at the seam: the update redelivered after a crash, the member event that never arrives because of a default nobody read, the payment confirmed from the wrong signal, the broadcast that trips 429 at user 31 and silently stops.
Read against the Bot API on 2026-08-25, at version 10.3 (released
2026-08-24). Re-check core.telegram.org/bots/api-changelog before quoting a
version: this API ships roughly monthly. Re-checked 2026-10-19: 10.3 is still
the latest. For a machine to read, Telegram serves the docs as Markdown at the
same path plus .md: core.telegram.org/bots/api.md, one file per method
(/bots/api/methods/getupdates.md) and a types index (/bots/api/types.md).
That is official and parseable, but it is Markdown, not a JSON schema.
Deep material, loaded on demand:
| Read | When |
|---|---|
references/updates-and-delivery.md | wiring polling or a webhook — the delivery contract, ordering, dedup, and the migration between them |
references/payments-stars.md | taking money — Stars, invoices, the pre-checkout window, refunds, subscriptions |
references/limits-and-files.md | sending at volume, or moving files — every published limit and what to do at each |
references/frameworks.md | choosing or auditing a library — aiogram, grammY, Telegraf, python-telegram-bot, and what each hides |
Runnable, and shipped beside this file:
fixtures/update_delivery.py — nine invariants a
correct handler holds, each with the mutant that makes it fail. python3 fixtures/update_delivery.py --self-test watches a redelivered update processed
twice, an update lost to a crash at the offset seam, an acked update lost to a
dead worker, a redelivery answered "duplicate" about work that never happened, a
reply doubled by a crash before the done mark, a redelivered outbox row sent
twice, one charge granted twice across two updates, a reply dropped on 429, and
one payment granted twice. Standard library only.
Which API you are on, decided once
| You need | Use | Why |
|---|---|---|
| A bot users add, in chats it is in | Bot API — this skill | HTTP, a token, no phone number, no ban risk |
| To read history a bot cannot see, or act as a person | MTProto — telegram-userbots | a user account, with everything that costs |
| A web interface inside Telegram | Mini App — telegram-miniapps | a page plus initData you must verify |
A bot cannot read messages in a group without privacy mode off or a mention,
see a chat it was never added to, read history from before it joined, act on
behalf of a user, or download a file over 20 MB. Wanting any of those is the
signal to read telegram-userbots — and to read its refusal first, because a
user account is a liability a bot token is not.
The update is the event, and it arrives at least once
# aiogram 3.x — the shape, not the framework
async def webhook(update: Update, db) -> web.Response:
await db.inbox_put(update.update_id, raw_body) # INSERT on a primary key,
return web.Response(status=200) # durable BEFORE the ack
# off the request path: a worker takes a pending row, does the work, marks it
# done — and a crashed attempt leaves the row pending for the next sweep
update_idis the only idempotency key you get. It is stable across redeliveries and it identifies an update — but it is IDENTITY, not a forever guarantee of monotonicity: after ~a week idle the counter can restart from a new random base, so never compare update_ids across a long gap to decide "newer". Nothing else in an update identifies it: two identical messages a second apart are two events, and the same event delivered twice is one.- Inbox before ack — a claim is a receipt, not completion. The
INSERTon a primary key (never aSELECTthen anINSERT: under a webhook up tomax_connections, default 40, deliveries of one update can be in flight at once) writes a durable row the 200 is only then allowed to answer for. Work runs off the request under a worker lease: a worker that dies leaves the row pending and the sweep retries it. Claim-then-work inside the request is the quiet data-loss shape — the crash leaves the claim standing, Telegram's redelivery reads "duplicate", and the update is gone while every fixture stays green. - Updates are kept for 24 hours and no longer. A bot that is down for a day has lost them, and nothing will say so — reconcile from your own state, never from the assumption that the queue drained.
- Measured across eight live Telegram bots on this machine on 2026-08-25:
zero of eight deduplicate on
update_id. Long polling hides it until the first crash between "work done" and "offset confirmed".
allowed_updates drops three types by default
await bot.set_webhook(
url=f"{origin}/tg/{SECRET_PATH}",
secret_token=WEBHOOK_SECRET, # 1-256 chars
allowed_updates=["message", "callback_query", "chat_member",
"pre_checkout_query", "my_chat_member"],
drop_pending_updates=False,
)
The default — an empty list, and the value you get by not passing the parameter —
means "all update types except chat_member, message_reaction and
message_reaction_count". So a bot that tracks joins and leaves receives
nothing, the request returns ok: true, and the logs are clean. Name every
type you handle, explicitly, and re-run setWebhook when you add one:
allowed_updates is set at subscription time, not at handler time.
Webhook or polling — and never both
getUpdates will not work while a webhook is set. That is the whole
migration hazard: a local getUpdates run against a production token silently
takes over, or fails, depending on which side moved last. One token, one
consumer.
- The webhook is the callback, so it needs the same defences as a payment
webhook: verify
X-Telegram-Bot-Api-Secret-Tokenagainst the value you set withsecret_token, in constant time, and answer 401 with no detail when it fails. The URL is not a secret; the header is. - Answer fast, work later. Telegram retries on non-2xx and on a slow reply,
and a retry is a second delivery of the same
update_id. Acknowledge, then process — the claim above is what makes that safe. drop_pending_updates=Trueis a decision, not a cleanup. It discards everything queued, including payments already made.- Long polling is correct for development and for low-volume bots; a webhook is
correct when you have a public HTTPS endpoint and care about latency. Both are
in
references/updates-and-delivery.md.
Limits are a design constraint, not an error path
| Limit | Value | What it means for the design |
|---|---|---|
| One chat | ~1 message/second | bursts are tolerated, then refused |
| One group | 20 messages/minute | a chatty group bot needs a queue |
| Bulk, all users | ~30 messages/second | a broadcast to 100k users is hours, not a loop |
| Paid broadcasts | up to 1000/second | 0.1 Stars per message past the free 30/s |
On 429 the response carries parameters.retry_after in seconds. Sleep exactly
that long and retry the same call — a fixed backoff either wastes the window or
trips the next one. A broadcast is a job with a rate limiter and a resume point,
not a for loop; if it cannot resume, a crash at user 60 000 restarts at zero
and every earlier user is messaged twice.
Money: the successful payment is the payment
Digital goods and services are sold exclusively in Telegram Stars, currency
code XTR, and provider_token is left empty for them.
sendInvoice / createInvoiceLink → pre_checkout_query → successful_payment
answer within 10s deliver here
answerPreCheckoutQueryhas a ten-second window. Miss it and the transaction is cancelled — so that handler validates against your own state and nothing slow. Anything that can take a second belongs aftersuccessful_payment.- Deliver on
successful_payment, never on the pre-checkout. Pre-checkout is a question; the payment has not happened yet. refundStarPaymentexists and a refund is not a deletion. Claw back what you granted, keyed on the charge id, exactly the way a card refund is handled — the arithmetic and the ordering are the same problemstripe-billingcovers.
Full flow, subscriptions and the transaction ledger:
references/payments-stars.md.
Files have a ceiling, and it is lower than you think
- Download via
getFile: 20 MB. Above that the Bot API refuses, and no framework works around it. - Upload: 50 MB through the cloud Bot API.
- A local Bot API server removes both ceilings and changes the file paths
your code receives. That is an infrastructure decision, not a flag —
references/limits-and-files.md. file_idis not a URL and not stable across bots. It is valid for your bot only; store it to resend cheaply, never as an archival reference.
What belongs in your database
| Fact | Where it lives | Why |
|---|---|---|
update_id seen | yours, unique | the only idempotency key |
chat_id, user_id | yours | Telegram will not list your users for you |
| the message you sent | yours, with message_id | editing later needs both ids |
| entitlement bought with Stars | yours, keyed on the charge id | a refund must find it |
file_id | yours, as a cache | cheap resend, not an archive |
Telegram is not a database and offers no way to enumerate the people who have started your bot. If you did not write it down when it happened, it is gone.
A second channel, when the audience can lose Telegram. A bot is a channel
you do not own. If your users live where access to Telegram is restricted, or
could become so, plan for the day it stops reaching them: key users on your own
id rather than on chat_id alone, ask for a second contact (email, a web
account) at a moment it is useful to the user, and keep the product reachable
outside Telegram. A block then costs a channel, not the user list.
Before you ship
- Every handler is idempotent on
update_id(§ The update is the event). - The webhook verifies the secret header and answers 401 without detail (§ Webhook or polling).
allowed_updatesnames every type you handle — the default is not "all" (§allowed_updatesdrops three types).- 429 sleeps for
retry_after, and a broadcast can resume (§ Limits). - Goods are delivered on
successful_payment, not on pre-checkout (§ Money).
Triggers
More phrases that route here: "grammy", "telegraf", "python-telegram-bot", "getUpdates", "pre_checkout_query", "бот апи", "Telegram-бот", "Telegram-бота", "телеграм-бота".
How can the creator link this skill?
Add the canonical catalog link to the repository README so users can inspect current installs and available audits. The publishing guide covers the complete discovery path.
<a href="https://skillzs.dev/skills/ssheleg/telegram-dev/telegram-bots">View telegram-bots on skillZs</a>