Telegram Built-in Analytics
Telegram provides tools to analyze the results of ad placements using unique links. You can create a separate link for each ad placement. For channels and groups, join statistics are available directly within Telegram, while for bots and Mini Apps, you need to additionally configure link parameter saving and data collection on your project's side.
WARNING
One advertising placement = one unique link. If the same link is used in several ads, the traffic sources will be mixed and cannot be separated retroactively.
This type of analytics is separate from AdsGram campaign statistics and conversion tracking.
Part 1. For campaign managers on the advertiser side
1. Which link should you use?
To compare the results of different ad placements, use a unique link for each one.
For example, if you are promoting a project across three Telegram channels, create three separate links. This will allow you to determine which placements attract the most users.
The type of link depends on where the advertisement leads:
| Ad destination | Link format | Where to track results |
|---|---|---|
| Telegram channel or group | A separate invite link | Telegram: channel or group settings → Invite links |
| Bot | t.me/YourBot?start=ad_sep_anna | Your bot, if it stores the parameter |
| Mini App | t.me/YourBot?startapp=ad_sep_anna | Your app's server, if it stores the parameter |
Replace YourBot with your bot's username and ad_sep_anna with a unique parameter for the advertising placement.
2. Channels and groups: invite links
Create a separate invite link for each advertising placement.
- Open the channel or group and tap its name.
- Open the editing or management settings. In the desktop version, you can use ⋮ →
Manage channel/Manage group. - Open
Invite linksand select the option to create a new link. - Name the link after the placement, for example,
ad_sep_anna. The name is visible to administrators, not to users who open the link. - If needed, set an expiry date, a user limit, or require administrator approval.
- Use the link in only one advertising placement.
Where to check statistics: Telegram's Invite links screen shows how many users joined through each link. Open an individual link to see the list of users.
A regular public link such as t.me/yourchannel does not let you separate joins by advertising placement. Public channels can also create invite links, so use separate invite links in your ads.
3. Bots: links with the start parameter
Use the following link format:
https://t.me/<bot>?start=<parameter>For example:
https://t.me/YourBot?start=ad_sep_annaWhen a user opens the link and presses Start, the bot receives a command containing the parameter:
/start ad_sep_annaTelegram does not automatically report advertising attribution for these bot launches. The bot must save the parameter when the user record is first created.
Count only new users if your metric is new-user acquisition. An existing user opening an advertising link does not become a new user.
4. Mini Apps: links with the startapp parameter
For a bot's main Mini App, use:
https://t.me/<bot>?startapp=<parameter>For a Mini App created using /newapp in @BotFather, you can use:
https://t.me/<bot>/<app>?startapp=<parameter>For example:
https://t.me/YourBot?startapp=ad_sep_annaIf needed, add a display mode parameter:
&mode=compact— compact mode.&mode=fullscreen— fullscreen mode.
The Mini App receives the parameter in the start_param field. Your app's server must save it when the user first launches the app.
If a user opens the Mini App without an advertising link — for example, through the menu button or the bot profile — the advertising parameter may be absent. Treat that launch as organic unless another reliable attribution source is available.
5. Parameter rules
Maximum length:
startparameter — 64 charactersstartappparameter — 512 characters
Allowed symbols:
A-Z,a-z,0-9,_,-- Do not use spaces, Cyrillic characters, or periods. To avoid compatibility issues, stick to the permitted character set.
Do not include users' personal data in parameters.
Recommended naming pattern. Use a consistent naming convention so that placements are easy to identify in reports:
ad_<month>_<source>_<creative>Examples:
ad_sep_tgads_video1ad_sep_blogger_anna
6. Campaign links are not referral links
A referral link allows a user to invite other people and receive a reward.
An advertising link measures traffic from an advertising placement. By itself, it should not trigger a referral reward.
Keep the two types separate using prefixes:
ref_…— user referral links.ad_…— advertising placement links.
Creating a separate referral link for every advertising campaign is only a temporary workaround. It can cause referral rewards to be paid for advertising traffic and mix the statistics.
7. How to track clicks
Telegram does not provide click statistics for t.me links.
If you need to count clicks, send users through your own redirect before forwarding them to Telegram.
Example:
https://yourdomain.com/go/ad_sep_anna → https://t.me/YourBot?start=ad_sep_annaYour server records the click and redirects the user to the destination link. This requires your own implementation and does not replace tracking launches, new users, or channel joins.
8. Keeping a results spreadsheet
Create a spreadsheet with one row for each unique link.
Recommended columns:
| Field | What to record |
|---|---|
| Link parameter | For example, ad_sep_anna |
| Placement | Channel, post, influencer, or ad name |
| Dates | Placement period |
| Spend | Advertising cost |
| New users | Number of new users attributed to the link |
| Cost per user | Spend ÷ number of new users |
9. Pre-launch checklist
Before publishing an ad, make sure that:
- Each placement has its own link; links are not reused across placements.
- The parameter does not exceed the maximum length and contains only permitted characters.
- The link has been tested with a second account: the join is counted, or the parameter is saved by the bot or Mini App.
- The link, placement name, and spend have been added to the spreadsheet.
Part 2. Implementation for developers
The examples below use Python, SQL, and aiogram 3 for the bot. The same principles can be implemented with other technology stacks.
The code provides a foundation rather than a complete drop-in integration. Database access functions and some integration details must be adapted to your application.
10. What data to store
Attribution requires three entities:
- A registry of advertising links and campaigns.
- The user's acquisition source, stored when the user record is created.
- A history of campaign-link visits, including visits from existing users.
Example SQL schema:
-- One row per advertising link.
-- For example, parameter "ad_sep_anna" maps to code "sep_anna".
CREATE TABLE campaigns (
code text PRIMARY KEY,
name text NOT NULL,
created_at timestamptz NOT NULL DEFAULT now()
);
-- The user's acquisition source is set on creation.
-- NULL means organic.
ALTER TABLE users
ADD COLUMN campaign text REFERENCES campaigns (code);
-- All users who opened campaign links,
-- including both new and existing users.
CREATE TABLE campaign_visits (
code text REFERENCES campaigns (code),
user_id bigint,
first_at timestamptz NOT NULL DEFAULT now(),
PRIMARY KEY (code, user_id)
);The code field should contain a short campaign code, such as sep_anna. The name field describes the placement, for example, Anna's channel, September post.
The campaign_visits table tracks visits separately from the user's original acquisition source. The original source should not be overwritten every time the user opens another link.
11. Parse parameters for the bot and Mini App
Use a single parser for advertising and referral parameters.
import re
AD = re.compile(r"ad_([A-Za-z0-9_-]{1,61})")
REF = re.compile(r"ref_([A-Za-z0-9]{4,32})")
def parse_start(param: str | None) -> tuple[str, str] | None:
"""("ad", code), ("ref", code), or None for anything else."""
if not param:
return None
if m := AD.fullmatch(param):
return "ad", m[1]
if m := REF.fullmatch(param):
return "ref", m[1]
return NoneAfter parsing, check whether the resulting code exists in the campaigns table.
- Do not attribute an unknown code to an existing campaign. Treat the visit as organic or unattributed.
- Do not store unvalidated raw parameter values.
- Apply the same parsing rules to parameters received by the bot and the Mini App.
12. Bot: handling /start
When a user starts a bot, Telegram passes the parameter in the /start command arguments.
Example using aiogram 3:
@router.message(CommandStart())
async def start(message: Message, command: CommandObject):
source = parse_start(command.args)
# "/start ad_sep_anna" -> ("ad", "sep_anna")
async with db.transaction():
await create_user_if_new(message.from_user, source)
if source and source[0] == "ad":
await record_visit(source[1], message.from_user.id)The create_user_if_new function should create a user record only if the user does not already exist.
To prevent duplicate records, use an atomic operation such as:
-- create_user_if_new: atomic, safe when the bot and the Mini App ask at once
INSERT INTO users (id, campaign, referrer_id)
VALUES ($1, $2, $3)
ON CONFLICT (id) DO NOTHING
RETURNING id; -- a row back = new user; nothing = existing user, left as it isIf the query returns a row, the user was created. If it returns no row, the user already exists and their original acquisition source must remain unchanged.
To record a campaign-link visit, use:
-- record_visit
INSERT INTO campaign_visits (code, user_id)
VALUES ($1, $2)
ON CONFLICT DO NOTHING;If an existing user opens another link and receives /start ad_…, record the visit but do not overwrite their campaign field.
For referral attribution, set referrer_id only for new users. Do not allow users to refer themselves.
13. Mini App: handling the first launch
A Mini App can receive the start_param parameter as part of its Telegram Web App launch data.
On the frontend side, send Telegram's signed data to your server:
// Frontend: every request carries Telegram's signed init data
fetch("/api/me", { headers: { Authorization: `tma ${Telegram.WebApp.initData}` } })On the server, validate the authenticity of initData and its age before using the user ID or start_param.
Example validation function in Python:
import hashlib, hmac, json, time
from urllib.parse import parse_qsl
def check_init_data(
raw: str,
bot_token: str,
max_age: int = 86400
) -> dict:
fields = dict(
parse_qsl(
raw,
keep_blank_values=True,
strict_parsing=True
)
)
received = fields.pop("hash", "")
# every field except hash, "signature" included, sorted, one key=value per line
data = "\n".join(
f"{k}={v}" for k, v in sorted(fields.items())
)
secret = hmac.new(
b"WebAppData",
bot_token.encode(),
hashlib.sha256
).digest()
expected = hmac.new(
secret,
data.encode(),
hashlib.sha256
).hexdigest()
if not hmac.compare_digest(expected, received):
raise PermissionError("bad signature")
if time.time() - int(fields["auth_date"]) > max_age:
raise PermissionError("expired")
return {
"user": json.loads(fields["user"]),
"start_param": fields.get("start_param")
}After successful validation, follow the same process as for the bot (create_user_if_new and record_visit):
- Extract
userandstart_paramfrom the validated data. - Parse the parameter using
parse_start. - Create the user only if they do not already exist.
- Record the campaign-link visit if the parameter matches a known advertising campaign.
Security: use only validated initData. Do not trust initDataUnsafe or URL parameters as sources of user identity or attribution because they can be forged.
The start_param parameter is provided when the app is launched through the corresponding link. It may be absent on subsequent launches from the menu. Therefore, set the original acquisition source when the user is created, not on every app launch.
14. One user, two entry points
The bot and Mini App can share a single users table, with each user identified by their Telegram user ID.
For example, a user may start the bot first and then open the Mini App, or do the reverse.
Attribution rules:
- The first handler to create the user record sets the original acquisition source.
- The second handler must not overwrite that source.
- Subsequent visits can be recorded separately in
campaign_visits.
Use an atomic insert, such as INSERT … ON CONFLICT DO NOTHING. This helps avoid errors if requests from the bot and Mini App arrive at almost the same time.
15. Channels: tracking joins by invite link
If you need your own join tracking rather than relying solely on Telegram's interface, create a separate invite link for each campaign and save it together with the campaign code.
Example link creation in Python:
# Create a link per campaign and keep it with the code
link = await bot.create_chat_invite_link(
CHANNEL_ID,
name="ad_sep_anna"
) # name: 1–32 chars
save_channel_link("ad_sep_anna", link.invite_link)The invite-link name in this example is limited to 1–32 characters.
To receive join events, the bot must be an administrator of the channel and receive the relevant chat_member updates. If join requests require approval, it must also receive chat_join_request updates.
Example polling configuration:
# Joins come as chat_member updates: the bot must be an admin and ask for them
await dp.start_polling(
bot,
allowed_updates=[
"message",
"chat_member",
"chat_join_request"
]
)Example join handler:
@router.chat_member()
async def member(update: ChatMemberUpdated):
joined = (
update.old_chat_member.status in ("left", "kicked")
and update.new_chat_member.status == "member"
)
if joined and update.invite_link:
save_channel_join(
update.invite_link.name,
update.new_chat_member.user.id
)If a link requires administrator approval, handle chat_join_request. The invite link is available in update.invite_link; after checking the request, you can call approve_chat_join_request.
You can also record users leaving the channel to understand who stayed.
Telegram already shows administrators the number of joins per invite link. Your own database is useful if you want to combine channel-join data with bot-user information or other analytics.
16. Optional: tracking clicks with a redirect
To count clicks before users reach Telegram, create your own redirect endpoint.
Example in Python for Mini App:
@app.get("/go/{code}")
async def go(code: str, request: Request):
agent = request.headers.get("user-agent", "")
if code_exists(code) and "TelegramBot" not in agent: # skip Telegram's link preview crawler
record_click(code)
return RedirectResponse(
f"https://t.me/{BOT}?startapp=ad_{code}",
status_code=302
)This example validates the code and excludes requests whose User-Agent contains TelegramBot, to avoid counting Telegram link-preview requests as ordinary user clicks.
Adapt the implementation to your infrastructure and test it with real links. The redirect must lead to the correct bot or Mini App link with the appropriate parameter.
17. Example report
The following query reports the number of unique campaign-link visitors, new users, and activated users for each campaign.
SELECT
c.code,
c.name,
(
SELECT count(*)
FROM campaign_visits v
WHERE v.code = c.code
) AS visitors,
(
SELECT count(*)
FROM users u
WHERE u.campaign = c.code
) AS new_users,
(
SELECT count(*)
FROM users u
WHERE u.campaign = c.code
AND u.first_action_at IS NOT NULL
) AS active
FROM campaigns c
ORDER BY new_users DESC;
-- first_action_at: when the user first did what the campaign pays for (a task, a purchase)In this example, first_action_at is the time when a user first completes the target action that the advertiser pays for, such as completing a task or making a purchase.
The visitors metric represents users with a recorded visit to the corresponding campaign in campaign_visits. It is not the same as the number of link clicks unless clicks are recorded separately through a redirect.
18. Common pitfalls and limitations
- The bot receives the parameter only when the user presses
Start. Not everyone who clicks a link starts the bot, so the number of starts may be lower than the number of clicks. - Restarting does not mean a new user. If a user deletes the chat and starts the bot again, the parameter may be passed again, but the user is not new.
- Do not overwrite the original acquisition source. Use a separate visit table with timestamps to analyze subsequent visits.
- Separate new and existing accounts. If the goal is acquisition, repeat visits must not increase the number of new users.
- Exclude bots and self-referrals. Check
is_botand do not allow users to refer themselves. - Respect parameter limits. The total
startparameter length must not exceed 64 characters. Use short codes, such assep_anna, to leave room for thead_orref_prefix. - Keep referral and advertising attribution separate. Different prefixes and handling rules help prevent incorrect rewards and reporting.