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.
A landing/redirect Telegram button never gets a personal ?start= deep-link
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
- Back to Telegram gate for the full task list.