Skip to content

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:

{userId}:{command}:{JSON.stringify(options)}[:{expires}]
  • User webhook: userId is the Telegram user ID
  • Global webhook: userId is empty ("")
  • Legacy signatures without expires are 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:

Webhook.getUrl("action", { expiresIn: 3600 })  // valid 1 hour

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:

{
  "status": "error",
  "message": "Rate limit exceeded: Too many requests per minute"
}

(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 expiresIn for sensitive one-time links
  • Keep secrets in options (signed), not unsigned params
  • 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

See also