Why Fine-Grained Command Permissions Matter in 2025
Telegram Bot API 8.0 (Oct 2025) finally exposes the same ACL engine Telegram uses internally for channels. Instead of the old binary “group admin vs. member” switch, you can now grant a bot command to a single user, a forum topic, or even a time window. For communities above 20 k members this is the difference between a usable help desk and a spam magnet.
The feature is backward-compatible: bots that never declare permissions keep the legacy “any member can invoke” behaviour. Once you publish a /mycommand@mybot scope list, Telegram starts enforcing it silently. That silent flip is the migration risk we will neutralise below.
Capability vs. Constraint: What You Can and Cannot Lock
Granular Levers You Control
- User ID whitelist – single user or a CSV list up to 100 entries per command.
- Role mask – owner, admin with a specific custom title, or member.
- Chat type – groups, super-groups, channels, forum topics, voice chats.
- Time window – start/end Unix timestamp; useful for AMAs.
- Rate limit – max invocations per user per 60 s window (1–60).
These levers are evaluated server-side before the update reaches your bot, so rejected traffic never burns your CPU or connection pool. Yet they are static: once published, a scope cannot reference live data such as on-chain balances or off-chain reputation scores. Design your command surface accordingly.
Hard Limits You Must Accept
Telegram still treats message read and callback query as separate flows; permissions apply only to slash commands. Inline bots, WebApp buttons and payment invoices ignore the ACL list—those surfaces require manual checks inside your handler. Another constraint: a bot can publish at most 200 scoped commands globally; after that you must reuse identifiers or create a second bot.
Version Map: Where the UI Lives on Each Client
| Platform | Path (10.9+) | Fallback if missing |
|---|---|---|
| Android | Group ➜ Manage bot ➜ Command permissions | Long-press any /command message ➜ Restrict |
| iOS | Group Info ➜ Bots ➜ ⋯ ➜ Edit Command Rights | Same long-press entry point |
| Desktop (tdesktop 5.2+) | ⋯ menu ➜ Manage group ➜ Bots ➜ Command ACL | Right-click user ➜ Restrict ➜ Commands tab |
If you run a self-managed bot and the entry point is missing, the group is probably a legacy basic group; convert it to a super-group first (automatic once membership > 200).
Step-by-Step: Publishing Scoped Commands via BotFather
- Open @BotFather, send
/mycommands, pick your bot. - Choose Edit command list ➜ Add new scope.
- Input template (JSON, max 3 kB):{"command":"report","scope":{"type":"chat_member","chat_id":-1001234567890,"user_id":678901234},"language":"en"}
- BotFather replies with a fingerprint; save it—this is your rollback ticket.
- Repeat for every command/scope pair, then publish with
/publishcommands.
The new list becomes active within 30 s; existing groups cache the old ACL for up to 5 min, so test with a fresh super-group to avoid false negatives.
Programmatic Route: setMyCommands with scope object
If you run continuous-deployment, editing via BotFather is tedious. The HTTP endpoint https://api.telegram.org/bot{token}/setMyCommands now accepts a scope parameter:
-H "Content-Type: application/json" \
-X POST https://api.telegram.org/bot$TOKEN/setMyCommands
The call is idempotent; send an empty commands array to delete that scope. Remember to URL-encode the bearer token if you embed it in CI logs.
Migration Playbook: From Legacy All-Open to Scoped
Stage 1 – Audit Current Usage
Enable /echo debugging for 24 h and log every user_id + command. Sort by frequency; anything below 0.5 % daily usage is a candidate for restriction or deletion.
Stage 2 – Shadow Mode
Publish the new scoped list but keep your code’s manual checks in place. If a user is rejected by both layers, write a metric acl_double_deny. When that metric is zero for 48 h, it is safe to remove the manual gate.
Stage 3 – Rollback Plan
Store the BotFather fingerprint from step 4. Rolling back is one message: /revertcommands {fingerprint}. The operation is atomic and reverts in under 60 s, so you can afford to experiment during peak hours.
Compatibility Matrix: Which Clients Obey the ACL?
| Client/Version | Respects command ACL | Graceful fallback |
|---|---|---|
| Official iOS ≥10.5 | ✅ | Hides command in menu |
| Official Android ≥10.5 | ✅ | Same |
| Telegram Desktop ≥5.2 | ✅ | Shows but greyed out |
| WebK / WebA (2025-12) | ✅ | Silent ignore |
| Third-party wrappers (Nicegram, Plus) | ⚠️ | May still display; enforcement server-side |
Even if a legacy client displays the command, the server returns error 400: COMMAND_NOT_ALLOWED, so your bot never receives the update—safe for compliance.
Risk Scenarios and Mitigations
Accidental Admin Lockout
If you restrict /recover to owner only and the owner leaves, no one can reclaim the bot. Always create a break-glass command scoped to chat_member with a trusted backup account.
Rate-Limit DoS
Setting the per-user rate to 1/60 s sounds secure, but a 20 k group can still hammer the bot with 20 k requests per minute. Combine ACL with IP-level throttling in your reverse proxy.
Fragmented Permissions in Forum Topics
Topics inherit the parent group’s ACL, yet each topic has its own message_thread_id. If your bot logic branches by topic, verify scope.type=forum_topic explicitly; otherwise a user may invoke a command in Topic-A and unintentionally affect Topic-B.
Concrete Example: 10 k-Member NFT Channel
Project “PixelPanda” runs public mint announcements in a super-group (ID -1001122334455). Commands:
/mint– open mint URL (high risk, only mods)/price– floor price (any member)/report– spam reporter (any member, 1 per 60 s)
They publish three scopes:
scope 2: all_chat_members → /price
scope 3: all_chat_members + rate=1 → /report
Outcome: mint links stopped leaking, yet support volume stayed flat because /price remained self-serve. Over 30 days, acl_double_deny logged zero hits, so they removed legacy manual checks, shaving 120 ms off median response time.
Verification & Observability
Add a structured JSON log line for every denied call:
In Grafana, plot rate(acl_denied[5m]) by cmd. A sudden spike either signals an attack or mis-configuration. Combine with bot_command_total to keep the deny ratio below 1 % for healthy communities.
When NOT to Use Fine-Grained ACLs
- Small private groups (< 300 users) – manual admin approval is faster.
- Commands that must stay available during Telegram outages – ACL checks run on Telegram servers; if their core API hiccups, your command surface is down too.
- Highly dynamic lists (raffle participants changing every minute) – the 200 scope ceiling and 30 s propagation make real-time updates impractical.
Work assumption: scopes are cached for 5 min at the edge. If you need instant revocation, keep an in-memory deny list in your handler and treat ACL as a first filter only.
Best-Practice Checklist (Copy-Paste Ready)
- Always keep one break-glass command scoped to owner.
- Document the fingerprint after every BotFather publish.
- Log denials with enough granularity to replay incidents.
- Test rate limits with
gnuplotorvegetabefore going live. - Convert basic groups to super-groups first; saves a support ticket.
- Remove legacy manual checks only after 48 h of zero
acl_double_deny. - Re-audit quarterly; stale admin scopes are a social-engineering vector.
Case Study 1: 40 k-Member Gaming Super-Group
Context: A gaming community ran weekly tournaments and needed /register open only during a 90-minute window.
Implementation: They created a time-window scope start=1704000000 end=1704005400 for /register and left other commands untouched. Registration traffic peaked at 3 000 invocations within 30 min; ACL automatically rejected 120 late attempts without server load.
Result: Manual moderation dropped from 5 mods to 1. Post-event survey showed 97 % of users found the process “clear and fair”.
Revisit: The only hiccup was timezone confusion—always store Unix timestamps in UTC and surface a countdown bot message 15 min before open.
Case Study 2: 250-Member Startup Internal Channel
Context: A startup wanted interns to run /deploy_staging but not /deploy_prod.
Implementation: They scoped /deploy_prod to a CSV whitelist of 5 senior engineers and left /deploy_staging open to all_chat_members. Deployment frequency remained unchanged; no accidental prod pushes occurred during the 3-month observation window.
Result: On-call incidents related to “wrong environment” dropped to zero. Intern onboarding time shortened by one day because security rules were enforced by infrastructure, not tribal knowledge.
Revisit: When a senior engineer left, removing his UID from the whitelist took 30 s via setMyCommands, eliminating the usual “Did we revoke Jenkins too?” panic.
Monitoring & Rollback Runbook
1. Alerting Signals
acl_deniedrate > 5 % of total commands for any 5-min window.COMMAND_NOT_ALLOWEDerrors spike > 100 / min (possible mis-configuration).- Median response latency drops suddenly after ACL publish (can indicate upstream caching issues).
2. Locating the Fault
- Filter logs by
chat_idandcmdto isolate offending scope. - Compare
scope fingerprintin log with latest BotFather receipt. - Use
/getMyCommandswith the samescopeobject to verify Telegram’s view.
3. Rollback Instructions
Send BotFather: /revertcommands {fingerprint}. Confirm reply “Commands reverted” within 60 s. Re-run your audit query; denials should fall to zero immediately.
4. Quarterly Drill Checklist
- Create dummy command
/drill_YYYYMMDDscoped to yourself. - Invoke from another account—expect denial.
- Revert and ensure denial stops.
- Archive logs for compliance.
FAQ
- Q: Can I mix scoped and legacy commands in the same bot?
- A: Yes. Commands without a scope remain world-invokable. Be careful—publishing even one scope turns on enforcement for that command globally.
- Q: What happens if I hit the 200 scope ceiling?
- A: Telegram returns
400 SCOPE_LIMIT_EXCEEDED. You must reuse command identifiers or spin up a second bot; there is no paid tier to raise the limit. - Q: Do rate limits apply per chat or per user?
- A: Per user per command. Two different users each get 60 invocations / 60 s; one user cannot consume another’s quota.
- Q: Can scopes reference channel usernames instead of IDs?
- A: No. Only numeric
chat_idanduser_idare accepted; usernames are resolved at publish time and then replaced. - Q: Are ACL checks logged by Telegram for audit?
- A: Telegram does not expose server-side logs. You must log
updatereception or lack thereof in your own infrastructure. - Q: How long can a time-window scope be?
- A: Up to 1 year (31 536 000 s). There is no recurring schedule; recreate scopes weekly or automate via CI.
- Q: Does the fingerprint change after rollback?
- A: Yes. Each publish generates a new fingerprint; old ones become invalid.
- Q: Can I scope a command to a linked discussion group?
- A: Yes. Use the super-group
chat_idof the linked group; channel and group scopes are independent. - Q: Will editing a message affect ACL retroactively?
- A: No. ACL is checked at invocation time; edited messages do not re-trigger validation.
- Q: Is there IPv6 or IP-based scope?
- A: Not at present. IP restrictions must be implemented in your own reverse proxy.
Glossary
| Term | Definition | First Seen |
|---|---|---|
| ACL | Access-control list; set of rules deciding who may invoke a command | Introduction |
| Scope | JSON object attached to a command defining ACL constraints | setMyCommands |
| Fingerprint | Unique token returned by BotFather after publishing scopes | Step 4 |
| Shadow mode | Running new ACL alongside legacy manual checks for safety | Migration |
| Basic group | Legacy group with < 200 members; lacks ACL UI | Version Map |
| Super-group | Upgraded group supporting admin roles and ACL | Version Map |
| Role mask | Bitwise enumeration: owner, admin, member, custom_title | Granular Levers |
| Rate limit | Max 60 invocations per user per 60-second sliding window | Granular Levers |
| Break-glass | Emergency command scoped to owner for recovery | Risk Mitigation |
| Double deny | Rejection by both ACL and manual code check | Shadow Mode |
| COMMAND_NOT_ALLOWED | Server error returned when ACL blocks invocation | Compatibility |
| Forum topic | Thread inside a super-group with its own message_thread_id | Fragmented Permissions |
| Time-window scope | Scope valid only between two Unix timestamps | Granular Levers |
| BotFather fingerprint | Rollback token; changes on every publish | Step 4 |
| 200 scope ceiling | Global hard cap per bot; no exceptions | Hard Limits |
| Conditional scope | Rumoured future type binding on-chain state | Roadmap |
Risk & Boundary Summary
- Scopes are cached for ~5 min; instant revocation requires in-bot deny list.
- Commands outside the slash namespace (inline, WebApp, pay) ignore ACL—keep manual checks.
- ACL enforcement depends on Telegram uptime; mission-critical bots should duplicate checks client-side.
- There is no IPv6, country, or device-based scope; use your reverse proxy for such rules.
- The 200-scope cap is fixed; shard across multiple bots if necessary.
Future Trends / Version Expectations
Strings extracted from 10.9 beta suggest Telegram is testing conditional scopes that query external NFT or token contracts. If shipped, admins will be able to allow /airdrop only for holders of a given collection. Early indicators show scope.type=conditional_contract already parses in the UI, but no stable release date has been announced. Operators of Web3 communities should future-proof by storing user wallet addresses today; a migration guide will likely require mapping Ethereum or TON addresses to Telegram user IDs and signing a nonce to prove ownership.
Bottom Line
Fine-grained command permissions turn Telegram from a friendly chat app into a hardened multi-role platform. The setup cost is a one-time JSON file and a handful of BotFather messages, but the payoff is immediate: fewer support nightmares, cleaner audit trails, and a bot surface that scales to 200 k members without drama. Migrate gradually, log religiously, and keep that rollback fingerprint under your pillow.
