A bot can observe a Telegram channel join only if it is an administrator and has explicitly subscribed to the chat_member update. That update names the joining user and, if they used an invite link, which link. Joins made through a chat folder invite link or an approved join request carry no link attribution at all.
Telegram channel attribution is treated as either impossible or trivial depending on who is describing it, and both descriptions are wrong. Telegram exposes a precise, documented set of join signals, and a precise set of blind spots. This page is the map of both, quoted from the Bot API reference, which documented Bot API 10.2 dated 14 July 2026 when we checked it.
Can a Telegram bot see who joins a channel?
Only under two conditions, both of which have to be arranged deliberately. The bot must be an administrator, and it must ask for the update by name. Telegram states this in the Update object reference:
"chat_member ChatMemberUpdated Optional. A chat member's status was updated in a chat. The bot must be an administrator in the chat and must explicitly specify "chat_member" in the list of allowed_updates to receive these updates."
The second condition is the one that catches people. chat_member is excluded from the default subscription set. Telegram's getUpdates reference states that an empty allowed_updates list means "receive all update types except chat_member, message_reaction, and message_reaction_count (default)". A bot that is a full administrator and receives nothing on join is almost always a bot that never listed the update.
Telegram has also been explicit that this is now the intended mechanism. Its changelog entry for Bot API 5.2 carries a warning: "Service messages about non-bot users joining the chat will be soon removed from large groups. We recommend using the "chat_member" update as a replacement."
What is in a ChatMemberUpdated?
Eight fields, three of which carry attribution information. Telegram describes the object as representing "changes in the status of a chat member", and the full field list is short enough to reproduce in full.
| Field | Type | Telegram's description |
|---|---|---|
chat | Chat | "Chat the user belongs to" |
from | User | "Performer of the action, which resulted in the change" |
date | Integer | "Date the change was done in Unix time" |
old_chat_member | ChatMember | "Previous information about the chat member" |
new_chat_member | ChatMember | "New information about the chat member" |
invite_link | ChatInviteLink | "Optional. Chat invite link, which was used by the user to join the chat; for joining by invite link events only" |
via_join_request | Boolean | "Optional. True, if the user joined the chat after sending a direct join request without using an invite link and being approved by an administrator" |
via_chat_folder_invite_link | Boolean | "Optional. True, if the user joined the chat via a chat folder invite link" |
That is the complete list. There are no other fields, and in particular there is no campaign field, no referrer and no source. Attribution has to be carried in the invite link itself, because the invite link is the only channel available.
The qualifier on invite_link is the whole design constraint: "for joining by invite link events only". A person who joins any other way produces a ChatMemberUpdated with no invite_link, and no amount of processing recovers what was never sent.
How do you attribute a join to a specific ad?
By minting an invite link per visitor before they arrive, and binding that link to the click record you already stored. When the join event arrives carrying that link, the join inherits the click's campaign, ad set and creative.
Telegram's createChatInviteLink gives you four levers:
| Parameter | Telegram's description | Why it matters for attribution |
|---|---|---|
name | "Invite link name; 0-32 characters" | 32 characters is enough for a campaign code, not for a click record. Treat it as a label, not a payload |
expire_date | "Point in time (Unix timestamp) when the link will expire" | Expiring a per-visitor link limits how long a shared screenshot can misattribute a join |
member_limit | "The maximum number of users that can be members of the chat simultaneously after joining the chat via this invite link; 1-99999" | Setting this to 1 is what makes a link genuinely per-person |
creates_join_request | "True, if users joining the chat via the link need to be approved by chat administrators. If True, member_limit can't be specified." | Manual approval and a member limit are mutually exclusive. You choose one |
The mutual exclusion in the last row is a real fork in the road. A link with member_limit set to 1 is single use and cannot be gated by approval. A link that creates join requests can be approved manually but can be used by any number of people, so it identifies a campaign rather than a person unless you mint one per visitor anyway.
One further constraint from the Bot API is easy to trip over: "Each administrator in a chat generates their own invite links. Bots can't use invite links generated by other administrators." Links created by a human admin in the Telegram app are not visible to your bot. Your bot has to create its own.
What does the invite link object tell you?
Less than you would expect. The Bot API's ChatInviteLink object exposes pending join requests but no cumulative usage count.
Its fields are invite_link, creator, creates_join_request, is_primary, is_revoked, name, expire_date, member_limit, pending_join_request_count, subscription_period and subscription_price. Telegram describes pending_join_request_count as the "Number of pending join requests created using this link".
There is no field for total joins through a link. That number exists in Telegram's underlying client API, where the chatInviteExported object carries a usage field, but it is not surfaced in the Bot API. The practical consequence: you count joins by counting chat_member updates as they arrive, not by polling a link for its total. Build the counter yourself, and make it idempotent, because a user who leaves and rejoins generates a second update.
Telegram notes that on the invite_link string, "If the link was created by another chat administrator, then the second part of the link will be replaced with "…"." So an invite link you read from an update is not necessarily a link you can reuse or display. Store the identifier you minted it against rather than relying on the string.
FlowTracker mints a link per click, binds it to the stored click record, listens for the join and carries the attribution through to the sale. No invite link bookkeeping by hand.
What about join requests?
Join requests are a separate update with a richer payload, and they give you something the plain join does not: a short window in which the bot may message the person directly.
Telegram's ChatJoinRequest carries chat, from, user_chat_id, date, an optional bio, an optional invite_link described as the "Chat invite link that was used by the user to send the join request", and an optional query_id. The interesting field is user_chat_id:
"Identifier of a private chat with the user who sent the join request. [...] The bot can use this identifier for 5 minutes to send messages until the join request is processed, assuming no other administrator contacted the user."
Telegram, Bot API, ChatJoinRequest. core.telegram.org/bots/api#chatjoinrequest. Bot API 10.2, 14 July 2026. Checked 14 August 2026.Five minutes is short, and it is a genuine opportunity: it is the one moment where you can open a private conversation with someone who has never messaged your bot. Telegram documented this change deliberately, noting in its changelog that "Bots are now allowed to contact users who sent a join request to a chat where the bot is an administrator with the can_invite_users administrator right - even if the user never interacted with the bot before."
To receive these updates at all, the bot needs a specific right: Telegram states the chat_join_request update requires that "The bot must have the can_invite_users administrator right in the chat to receive these updates."
Which joins can never be attributed?
Four kinds, and they are structural rather than fixable. Telegram does not publish a sentence saying so, but each follows directly from the field definitions quoted above.
- Chat folder invite links. The update sets
via_chat_folder_invite_linkto true and carries noinvite_link. You know the join happened and you know the route; you do not know the source. - Direct join requests without a link.
via_join_requestis true andinvite_linkis absent, because the person never used one. - Username search and forwarded links. A person who types your channel name, or opens a link screenshotted from someone else's chat, produces a join with no link attribution.
- Any join where your bot is not an administrator. No admin rights means no
chat_memberupdate, so the join is not observable at all.
There is a further asymmetry worth knowing. The Bot API provides no method that lists ordinary members. getChatAdministrators returns administrators, and getChatMember answers for one user ID at a time, with Telegram noting that it "is only guaranteed to work for other users if the bot is an administrator in the chat". getChatMemberCount returns a number and nothing else. You cannot enumerate a channel's membership and reconcile it against your click records after the fact. Attribution has to be captured at the moment of the join or not at all.
Does Telegram Ads give you a click ID?
No. Telegram's own advertising platform publishes no click identifier, no postback, no pixel and no conversion API. It measures joins internally and shows them as an aggregate chart in its dashboard.
Telegram describes what its statistics tab contains:
"Underneath, you will see a detailed graph showing views of your sponsored message and the number of times a user joined your channel or started the bot after viewing the sponsored message, able to be displayed in increments of minutes or days."
Telegram, Telegram Ads, Getting Started. promote.telegram.org/getting-started. Checked 14 August 2026.Telegram also states on its ads homepage that "Ads on Telegram do not rely on users' personal information and are based on the channels where they are shown." That is a design position, not an omission, and it explains the absence of an advertiser-facing identifier.
The practical consequence: Telegram Ads spend is measurable only inside Telegram's own dashboard, at aggregate level. Traffic from Meta, Google or TikTok into the same channel is measurable at person level, because those platforms do issue click identifiers and the bridge page can store them. Do not expect one reporting view to cover both cleanly.
One link per person, or one link per campaign?
Per person if you need to attribute revenue, per campaign if you only need to compare traffic sources. The tradeoff is real and choosing the heavier option by default is a mistake.
| One link per campaign | One link per visitor | |
|---|---|---|
| Setup effort | Minutes, done by hand | Needs a bridge page and a bot that mints links |
| Tells you joins per campaign | Yes | Yes |
| Tells you which ad produced a specific member | No | Yes |
| Survives someone forwarding the link | Attributes the friend to the campaign too | Contained, if member_limit is 1 |
| Supports revenue per source | No | Yes |
| Supports a conversion sent back to the ad platform | No | Yes |
Per-campaign links are genuinely useful and cost nothing, and for a channel that is not monetised per member they may be all you need. The moment you want to know which creative produced buyers rather than joiners, you need the per-visitor link, because that is the only structure that carries an identity rather than a label.
Putting it together
The working shape for a paid funnel into a Telegram channel is short:
- Bridge page stores the click. Click identifier, UTMs, timestamp, all written server side before the app switch. The reasons this cannot be skipped are in why the pixel cannot follow into a messaging app.
- Mint a per-visitor invite link.
member_limitof 1, a sensibleexpire_date, and anamethat carries a short campaign code for human readability. - Listen for
chat_member. Bot as administrator,chat_memberexplicitly inallowed_updates, and idempotent handling of rejoins. - Join on
invite_link. The link identifies the visitor, the visitor identifies the click, the click identifies the ad. - Return the conversion. Within the platform's upload window, which for Meta is seven days from the event, as set out in conversion upload windows compared.
Where the funnel runs through a bot rather than a channel, the start parameter replaces the invite link and gives you the same handshake with less machinery. Both routes are covered on the Telegram ad tracker page, and the community-specific version is in signal groups and communities.
Sources
Every link below was fetched and read on 14 August 2026. Where a vendor does not document something, this article says so rather than filling the gap with an estimate.
Last reviewed 14 August 2026