Linking a Telegram Account to Your App With Deep Links
If your app has accounts and you want a Telegram bot to know which account it's talking to, the mechanism is a deep link: https://t.me/<bot_username>?start=<parameter>, where the parameter arrives with the /start command. Telegram's constraints are tight — "the parameter can be up to 64 characters long" and allowed characters are "A-Z, a-z, 0-9, _ and -" (Telegram docs). Put a one-time token in there, never a user ID, because the link is a shareable URL and whoever opens it becomes whoever the payload names. And store that token somewhere a restart can't erase, or your onboarding breaks every time you deploy.
The flow
Five steps, and the middle one is where the design lives:
- A signed-in user clicks Connect Telegram in your web app.
- You generate a short random token, store it with the account ID and an expiry, and build the link.
- They open
https://t.me/yourbot?start=TOKEN. - Your bot receives
/start TOKEN, looks it up, and binds that Telegram user to that account. - You delete the token and confirm in both places.
Everything meaningful is in the token: it's opaque, it's tied to one account server-side, it's used once, and it expires.
Why not put the user ID in the payload
Because the link is just a URL. It ends up in browser history, in a screenshot, in a shared chat, in a support ticket. If the payload says user-4102, then anyone who opens that link binds their Telegram account to user 4102 — and there's no authentication step to stop them.
A random token has none of that leverage. Someone who intercepts it gets a single, short-lived chance to link to an account they'd have to already know something about, and using it burns it.
Three properties to enforce:
- Random and opaque — generated with a cryptographic RNG, not a counter or a hash of the ID.
- Single-use — deleted the moment it's redeemed, so a replayed link does nothing.
- Short-lived — minutes, not days. Someone connecting an account does it immediately.
The 64-character limit is generous for this. A URL-safe random string comfortably fits, and the allowed character set matches what a base64url-encoded random value produces — Telegram explicitly recommends base64url if you need to encode structured content, though for this an opaque key is better than anything structured.
Store the token where a restart can't reach
Here's the failure specific to running in a container, and it's easy to ship without noticing.
The natural first implementation keeps pending tokens in a dictionary. Locally it works perfectly, because your process stays up between generating the link and the user clicking it.
Deployed, the container is replaced on every deploy and every configuration change — and the gap between "user clicks Connect" and "user opens Telegram and taps Start" is often a minute or more. A deploy in that window silently invalidates the token, and the user sees a failure with no explanation. It'll be intermittent, which makes it miserable to diagnose.
Put it in a table on a volume:
CREATE TABLE link_tokens (
token TEXT PRIMARY KEY,
account_id TEXT NOT NULL,
expires_at INTEGER NOT NULL
);
Delete on redemption, and prune expired rows from your background loop — small, but it's an unbounded table otherwise, and disk has an account-wide consequence.
If your bot and web app are separate apps
This is the design decision people hit next, and it has a firm answer.
Two applications cannot share a volume. So if your web app mints the token and your bot redeems it, a SQLite file on the web app's volume is invisible to the bot. Your options:
- Keep them in one app — the web server and the bot in one process. Simplest, and it's often the right call for a small project.
- Run a real database both apps connect to. Remember there's no private networking, so that connection crosses a public endpoint with credentials.
- Put an API between them, with the bot calling your web app to redeem tokens. That's an authenticated internal endpoint on a public URL, so treat it as such.
Decide this before you build the flow, because it's the difference between one process and three moving parts.
Confirm in both directions
Once linked, tell the user in both places — a message from the bot and a status change in the web app. Account linking is exactly the kind of thing people are unsure worked, and a confirmation prevents them repeating it and generating a second token.
Give them a way to unlink, too. It's one row to delete, and its absence is the sort of thing people notice when they're already annoyed.
The Discord equivalent
Different mechanism, same principles. Discord uses OAuth2 — you send the user to an authorization URL with the identify scope, and they come back with a code you exchange for their user ID.
The parallel to the token is the state parameter: generate it randomly, store it with the pending link, and verify it on return. It exists to stop someone handing your app a callback for an account you weren't expecting, and it should be single-use and short-lived for the same reasons.
Same storage rule applies — pending state in memory is lost on the next deploy, right in the middle of someone's login.
FAQ
How do I link a Telegram user to an account in my app?
Generate a one-time token, put it in a ?start= deep link, and have the bot redeem it when the user taps Start.
What are the deep link parameter limits?
Up to 64 characters, using A-Z, a-z, 0-9, _ and -. Base64url is recommended for encoded content.
Can I put the user ID in the deep link?
No. The link is shareable, so anyone who opens it would link to that account. Use a random single-use token instead.
Why does linking fail intermittently?
Pending tokens are probably in memory, and the container is recreated on every deploy — which can land between generating the link and the user tapping Start.
My bot and web app are separate apps — where does the token go?
Not a shared volume; that isn't possible. Use one app, a database both connect to, or an authenticated API between them.
What's the Discord equivalent?
OAuth2 with the identify scope, using a random single-use state parameter for the same purpose as the token.
Wrapping up
Account linking looks like a small feature and quietly contains an authentication decision: whatever is in that link is what your bot trusts. Make it a token that means nothing on its own, redeem it once, expire it quickly, and keep the pending state somewhere your next deploy can't erase. Then the flow is four database operations and a confirmation message — and it keeps working on the days you ship.