Account Linking & Security
Before a user can see any tickets in the Mini App, their Telegram account must be linked to a Console account. Linking is what lets the backend map an incoming Telegram identity to a real Console user — and therefore to that user's RBAC permissions and client access.
This is a single identity system shared by both Telegram bots. The same telegram_id mapping written here is what the Service Desk Bot uses to know which tickets are yours and the Ops Bot uses to attribute your Acknowledge/Resolve taps and scope what you can ask the AI Investigator. Link once (via /link on either bot, or the Console profile) and both work.
Why linking is required
The Mini App authenticates with a Telegram-issued initData payload, which only identifies the user's Telegram ID. The backend needs to know which Console user that Telegram ID belongs to. That mapping is stored on the Console user record (in the user's traits as telegram_id). Until it exists, the backend cannot issue a scoped JWT, and the Mini App shows a "not linked" screen.
Linking flow
Linking starts in the Console under Profile → Telegram, and uses a short-lived, single-use code so a user can prove they control both their Console account and their Telegram account. Tapping Link Telegram calls POST /api/v1/auth/telegram/generate-code (JWT-protected, so the request is already tied to the logged-in Console user). The backend returns an 8-character alphanumeric code stored in Valkey with a 5-minute TTL, keyed to that user, plus a one-tap deep-link URL.
There are two ways to redeem the code:
- One-tap (recommended) — tap Open Telegram. This opens the bot via the deep link
https://t.me/{botUsername}?start=<code>; the bot receives the code as the/startpayload and links the account automatically, then offers the Open Service Desk button. - Manual — copy the code and send
/link <code>to the bot yourself (useful if the deep link doesn't open).
Either way, the bot calls POST /api/v1/auth/telegram/link with the code and the user's Telegram ID. The backend looks up the code in Valkey, deletes it (so it can only be used once), and writes the Telegram ID onto the matching Console user's traits. The next time the user opens the Mini App, initData resolves to their Console account and a scoped JWT is issued.
Unlinking
A user can unlink from Profile → Telegram in the Console, which calls DELETE /api/v1/auth/telegram/link (JWT-protected) and removes the telegram_id from their traits. Sending /unlink to the bot replies with instructions pointing to this UI flow. Once unlinked, the Mini App returns to the "not linked" screen.
Security model
initData validation (HMAC-SHA256)
When the Mini App calls POST /api/v1/auth/telegram, it passes the raw Telegram initData. The backend validates it per the Telegram specification:
- It computes
HMAC-SHA256over the data-check string using a key derived from the bot token (HMAC("WebAppData", bot_token)), and compares it to the hash Telegram included. A mismatch means the payload was tampered with or not actually signed by Telegram → request rejected. - It checks
auth_date: the payload is rejected if it is more than 5 minutes old (or dated in the future). This bounds replay of a capturedinitDatato a 5-minute window.
Only after both checks pass does the backend look up the user by telegram_id and issue a JWT. If no linked user is found, the backend returns 403 telegram_not_linked and the Mini App shows the "not linked" screen.
JWT scoped to the linked user
The JWT the backend issues is the same kind of token a normal Console login produces — it carries the linked user's identity and therefore their full RBAC and client scoping. The Mini App cannot see, create, or modify anything the underlying Console user couldn't. There is no special "Telegram" permission tier; the integration simply rides on the linked user's existing authorization.
Rate limiting
The public Telegram auth routes are rate-limited per IP using the same auth rate limiter as other public auth endpoints:
| Route | Method | Auth | Notes |
|---|---|---|---|
/api/v1/auth/telegram | POST | Public | Rate-limited. Validates initData, issues JWT. |
/api/v1/auth/telegram/link | POST | Public | Rate-limited. Links via code. |
/api/v1/auth/telegram/generate-code | POST | JWT | Generates an 8-char link code. |
/api/v1/auth/telegram/link | DELETE | JWT | Unlinks the account. |
The two state-changing operations that act on a specific Console account — generating a code and unlinking — are JWT-protected, so they can only be performed by the authenticated owner of that account.
Link-code hardening
- Single-use — the code is deleted from Valkey the moment it is consumed.
- Short TTL — 5 minutes, after which the code expires automatically.
- Large keyspace — 8 alphanumeric characters give a very large combination space, making guessing impractical within the TTL.
- Duplicate-link protection — if a Telegram ID is already linked to another Console user, the link request is refused, preventing one Telegram account from being attached to multiple accounts.
The Telegram auth routes are only registered when the backend has a bot token configured (PROXIMA_TELEGRAM_BOT_TOKEN). On a deployment without a bot token, none of these endpoints exist. See Deployment.