Limits & Security¶
Webhooks are signed, rate-limited, and depth-capped. This page covers the security mechanics and plan-based quotas — the stuff that keeps forged URLs and runaway chains from ruining your afternoon.
Signature verification¶
Every webhook request must include a valid sig query parameter. No signature, no sandbox.
Algorithm: HMAC-SHA256 with your bot token as the key.
Payload format:
- User webhook:
userIdis the Telegram user ID - Global webhook:
userIdis empty ("") - Legacy signatures without
expiresare still accepted for backward compatibility
Invalid or missing signatures return 401 / 403 before your command executes. Tampering with command, options, user, or expires after signing breaks the signature too.
URL expiry¶
Pass expiresIn (seconds) when generating URLs for one-time or time-sensitive links:
The URL includes expires={unixTimestamp}. Requests after that time are rejected with 403.
Payload size limits¶
| Field | Max encoded length |
|---|---|
options | 5,000 characters |
params | 10,000 characters |
| Request body | 1 MB (platform default) |
Oversized payloads return 413. Keep options lean — it's signed and counted.
Webhook depth limit¶
User and global webhooks enforce a chain depth limit (default 10, configurable via TBH_MAX_CHAIN_DEPTH). This prevents runaway nested webhook calls — webhook A triggers webhook B triggers webhook C until the heat death of the universe.
Webapp requests are not subject to this depth check.
Plan-based rate limits¶
Each bot is limited by the owner's plan. These limits apply to webhooks, webapps, and public web combined per bot:
| Plan | Per minute | Per day |
|---|---|---|
| FREE | 15 | 5,000 |
| FREEMIUM | 30 | 5,000 |
| PREMIUM | 60 | 10,000 |
| ELITE | 120 | 20,000 |
Exceeded limits return 429:
(or a daily quota message)
Redirect prefetch limits¶
When redirect is set on getUrlFor or getGlobalUrl:
- URL must be HTTPS
- Fetch timeout: 5 seconds
- Response size cap applies (platform-enforced)
Prefetched content is exposed as the global content variable.
res.redirect() security¶
Response redirects via res.redirect() accept HTTPS URLs only. See Redirects.
Best practices¶
- Use
expiresInfor sensitive one-time links - Keep secrets in
options(signed), not unsignedparams - Use user webhooks for per-user mutations; global webhooks for system reads
- Return explicit
res.json()errors instead of relying on the default success body - For unsigned browser endpoints, use Webapps only when signing is not required