Skip to content
All articles
8 min read

Telegram Deep Link Tracking: The Two Package IDs Nobody Expects

A Telegram deep link splitting into three Android package destinations and one iOS tg://resolve rewrite

You send a click ID into a Telegram bot through ?start=, and your postback comes back empty for a slice of Android installs that everyone insists is real traffic. The bot logged the /start command. It just never got your payload — or it did, and it came from a package your reporting never targeted in the first place. Both explanations produce the same symptom: a gap that looks like a tracking bug and is actually a routing gap.

Three ways to hand a value to Telegram

t.me links carry a value into Telegram through three different parameters, and they are not interchangeable:

ParameterWhere the value landsLimitCondition
?start=<payload>delivered to the bot as the /start command argumentup to 64 base64url charactersvisitor has to tap START
?startapp=<payload>Mini App init data, client-side onlyup to 512 charactersreaches your server only if the Mini App itself forwards and validates it
?startchannela bare flagno payload at allthere’s nothing here to attach a value to

?start=: built for exactly this job

This is the parameter that behaves the way most trackers expect a deep link parameter to behave. It’s delivered straight to the bot as the argument of the /start command, which means your bot logic reads it the same way it reads any other command argument — no separate init-data handling, no client-side code of your own required. The constraint that actually matters in practice is the 64-character cap: it’s base64url, which comfortably fits most internal click IDs, but it’s tight enough that a padded or verbose identifier can overrun it before you notice. And the value only arrives if the visitor taps START — a t.me link that opens a chat but never gets a tap sends no payload anywhere, regardless of what you put in the URL.

https://t.me/your_bot?start=dc_9Kx2Qz

reaches the bot as:

/start dc_9Kx2Qz

Nothing about that round trip changes based on platform or which Telegram package handled the tap — the payload arrives at the bot the same way whether Android routed it through the Play build, the telegram.org APK, or Telegram X, and whether it arrived through the tg://resolve rewrite on iOS. The one variable that changes what the bot sees is the payload itself.

?startapp=: a different animal entirely

?startapp= looks like a bigger version of ?start= — more characters allowed, up to 512 — and that similarity is exactly what causes the mistake. It doesn’t behave the same way. The value is delivered client-side, into a Mini App’s init data, not to a bot command handler. Nothing about that value reaches your server on its own. It reaches your server only if the operator’s own Mini App code reads that init data and forwards it, with its own validation, to wherever you’re collecting it. If you don’t control the Mini App, or the Mini App wasn’t built to do that forwarding, the payload can be sitting right there in the client and never once touch a backend you have access to.

?startchannel: nothing to carry

?startchannel is a bare flag. It takes no value at all. If the plan was to carry a click ID into a channel deep link the same way ?start= carries one into a bot, there’s no mechanism here to do that — the parameter simply isn’t built to hold a payload, and nothing you append to it changes that.

What happens on iOS: the tg://resolve rewrite

On iOS, a t.me link doesn’t need you to hand-build a custom-scheme URL. Two rewrites are confirmed to work:

https://t.me/<bot>?start=<payload>  ->  tg://resolve?domain=<bot>&start=<payload>
https://t.me/<channel>              ->  tg://resolve?domain=<channel>

The bot case is the one that matters for tracking: the start payload survives the rewrite intact, carried across as its own parameter on the tg://resolve URL rather than getting dropped or re-encoded along the way. If your t.me/<bot>?start=<payload> link works on Android, the same payload reaches the same bot on iOS without any extra handling on your side. The channel case rewrites the same way, minus a start payload — channels aren’t bots, so there’s nothing equivalent to carry.

The Android package trap: three IDs, one hidden in plain sight

This is where most setups quietly lose traffic without any error to show for it. Telegram on Android isn’t one package — it’s three:

SourcePackage
Play Store buildorg.telegram.messenger
The APK Telegram serves from its own websiteorg.telegram.messenger.web
Telegram Xorg.thunderdog.challegram

Most routing setups target only org.telegram.messenger, because that’s “the” Telegram package as far as the Play Store is concerned. But the APK that Telegram distributes directly from telegram.org — not through Play at all — carries a different package ID entirely. Read straight out of that APK’s own manifest: package=org.telegram.messenger.web, versionName 12.10.0. Anyone who installed Telegram from Telegram’s own website instead of the Play Store is running that package, full stop, and any package-based routing that only names org.telegram.messenger will never see them.

That’s not a rounding error in your Android numbers. It’s a visitor whose device genuinely has Telegram installed, correctly, from a source Telegram itself operates — and who is invisible to a chain built for one package ID.

Telegram X, org.thunderdog.challegram, is a third distinct client on top of that. It’s a separate app from a separate distribution, and worth including in the same package chain if any real share of your audience realistically uses it.

Why this looks like a tracking bug instead of a routing gap

Nothing about a missed package produces an error anywhere in the chain. The visitor taps your link, Android hands it to whichever Telegram package is installed, that app opens, the bot receives /start with the payload attached exactly as designed, and your bot logic runs. From Telegram’s side, everything worked. The gap only exists on your side, in the assumption baked into whatever routing or reporting logic decided which package to expect traffic from in the first place.

That’s what makes org.telegram.messenger.web specifically easy to miss for months: it isn’t a broken install, a bot misconfiguration, or a payload that failed to arrive. It’s traffic that was never counted as a distinct source because nobody wrote down that Telegram ships a second Android package outside Play. The /start argument, the 64-character limit, the iOS rewrite — none of that behaves any differently for this package than for the Play build. The only thing different is whether your own package list knows this source exists.

What actually reaches your postback

Put the three threads together and the picture is consistent: for /start-based tracking, your identifier survives the argument delivery to the bot, survives the iOS scheme rewrite intact, and then depends entirely on which Android packages your routing actually names. Miss org.telegram.messenger.web and you’re not failing to track a rare edge case — you’re failing to track every visitor who trusted Telegram’s own download page over the Play Store. For Mini Apps, ?startapp= reaches a server at all only if your own Mini App code does the forwarding; nothing external to that Mini App ever sees the value on its own.

A tracker generating the t.me link for a campaign needs a package list that includes org.telegram.messenger.web alongside the Play build, not because it’s an exotic edge case, but because it’s the same application served from a different door. DarkCore’s Telegram routing carries all three Android packages by default for exactly this reason: the traffic split between them isn’t noise to average away, it’s a population of real installs that a single-package chain simply never asks about.

What to check before you trust the numbers

Most of this is invisible until you go looking for it specifically, because none of it throws an error on Telegram’s side. A short checklist before a campaign goes live catches nearly all of it:

  • Confirm your click ID fits inside 64 base64url characters before it goes into ?start= — anything longer gets truncated or rejected depending on where the limit is enforced, and neither outcome is one you want discovered in production.
  • Don’t route ?startapp= payloads assuming they’ll show up server-side automatically. They only do if the Mini App you control explicitly forwards them.
  • If the goal is a payload carried into a channel link, stop looking at ?startchannel — it’s a flag, not a container, and no value you append to it will travel anywhere.
  • Add org.telegram.messenger.web to any Android package chain built for Telegram. Leaving it out doesn’t shrink your traffic; it just stops you from seeing the part that was always there.
  • If Telegram X has real share among your visitors, include org.thunderdog.challegram in the same chain rather than treating it as a separate, lower-priority case.
  • telegram start parameter
  • t.me deep link
  • telegram bot tracking
  • tg resolve scheme
  • org.telegram.messenger