TEASEDocs
ProductsLinkTelegram gate

Troubleshooting

Symptom, cause, and how to check it for the Telegram gate's sharper edge cases.

The webhook returns 503 to every /start

Symptom. Telegram's delivery attempts to /api/tgbot/webhook all come back 503, for every bot on the platform, not just one tenant's.

Cause. Nothing authenticates the webhook at all — there's no environment-level webhook secret configured, and no gate row anywhere has a minted webhook_secret yet. The webhook fails closed on purpose: with no credential to check against, it refuses everything rather than silently accepting anything.

Check. turnstile_registry.any_webhook_configured — it's False only when both the env secret is empty and no row has a non-empty webhook_secret. Connecting any bot (which mints a per-row secret on first connect) or setting the env secret resolves this platform-wide.

The webhook returns 403 for one specific tenant's bot

Symptom. Other bots on the platform work; this one tenant's /start consistently 403s.

Cause. Webhook auth is configured platform-wide, but the X-Telegram-Bot-Api-Secret-Token header Telegram sends for this bot matches neither the environment secret nor any row's webhook_secret. In practice this means the webhook was never registered with this row's current secret, or the bot was disconnected (which tombstones the row but doesn't change its webhook_secret) and something is still delivering against a stale registration.

Check. Compare the row's stored webhook_secret against what Telegram's getWebhookInfo shows was last set for that bot. Fix it directly with Re-register the webhook — it re-registers using the row's own secret, so a mismatch resolves without touching the token.

Symptom. A button meant to carry attribution just opens the bare bot chat — no ?start= payload, so the resulting subscriber never links back to a click.

Cause. Either there's no bot connected for that owner and no environment fallback (bot_username_for_owner returns ""), or the button's URL isn't the bot's exact bare t.me/<bot> form — any query string or extra path segment on the configured URL means it never matches, so nothing rewrites it into a deep-link.

Check. tg_deeplink.is_turnstile_bot_url only matches https://t.me/<bot> or http://t.me/<bot> with no trailing path or query, case-insensitively, trailing slash tolerated. If the destination URL you configured has anything else appended, fix the URL itself rather than expecting the rewrite to handle it. Confirm a bot is actually connected for that owner via Connect your own turnstile bot.

PATCH /tg/gate returns 409 connect a bot first

Symptom. Any settings change — greeting, split, destinations, rules — is rejected with 409 before it's even validated.

Cause. There's no gate row for this owner yet, and there's nothing to auto-adopt: either this isn't the platform's home account (a regular tenant never inherits an environment bot), or it is the home account but the environment bot token itself is empty, so there's no live bot to migrate into a row.

Check. shape.env_migratable(settings) — true only when an environment bot token is present for the home account. If it's false and you're not that account, the fix is simply to connect a bot first: Connect your own turnstile bot.

A saved rule silently never fires even though PATCH accepted it

Symptom. A rule was saved successfully (no 422 at save time) but it never seems to match, even for /starts that should satisfy its conditions.

Cause. The runtime check that decides whether to evaluate a stored rule (rule_is_wellformed) is deliberately laxer than the check that validated it at save time — it doesn't re-verify the id's exact charset or that dest still points at a real destination. A rule can become malformed after the fact if a destination it points at was deleted or renamed via a separate edit, or via a direct database edit that bypassed the panel entirely. Rather than raising and breaking /start for everyone, the engine just skips a malformed rule and moves on to the next one.

Check. turnstile_rules.rule_is_wellformed — a rule fails this check when its dest no longer names a real destination, or its conds carry an unknown key or wrong-typed value. Re-open the rule in Route with rules, confirm its destination still exists, and re-save it — a clean re-save always produces a wellformed rule.

What's next

On this page