Skip to content

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 ​

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 destinationLink formatWhere to track results
Telegram channel or groupA separate invite linkTelegram: channel or group settings → Invite links
Bott.me/YourBot?start=ad_sep_annaYour bot, if it stores the parameter
Mini Appt.me/YourBot?startapp=ad_sep_annaYour 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.

Create a separate invite link for each advertising placement.

  1. Open the channel or group and tap its name.
  2. Open the editing or management settings. In the desktop version, you can use ⋮ → Manage channel / Manage group.
  3. Open Invite links and select the option to create a new link.
  4. Name the link after the placement, for example, ad_sep_anna. The name is visible to administrators, not to users who open the link.
  5. If needed, set an expiry date, a user limit, or require administrator approval.
  6. 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.

Use the following link format:

https://t.me/<bot>?start=<parameter>

For example:

https://t.me/YourBot?start=ad_sep_anna

When a user opens the link and presses Start, the bot receives a command containing the parameter:

/start ad_sep_anna

Telegram 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.

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_anna

If 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:

    • start parameter — 64 characters
    • startapp parameter — 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_video1
    • ad_sep_blogger_anna

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_anna

Your 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:

FieldWhat to record
Link parameterFor example, ad_sep_anna
PlacementChannel, post, influencer, or ad name
DatesPlacement period
SpendAdvertising cost
New usersNumber of new users attributed to the link
Cost per userSpend ÷ 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:

  1. A registry of advertising links and campaigns.
  2. The user's acquisition source, stored when the user record is created.
  3. A history of campaign-link visits, including visits from existing users.

Example SQL schema:

sql
-- 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.

python
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 None

After 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:

python
@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:

sql
-- 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 is

If 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:

sql
-- 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:

javascript
// 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:

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):

  1. Extract user and start_param from the validated data.
  2. Parse the parameter using parse_start.
  3. Create the user only if they do not already exist.
  4. 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.

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:

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:

python
# 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:

python
@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:

python
@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.

sql
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_bot and do not allow users to refer themselves.
  • Respect parameter limits. The total start parameter length must not exceed 64 characters. Use short codes, such as sep_anna, to leave room for the ad_ or ref_ prefix.
  • Keep referral and advertising attribution separate. Different prefixes and handling rules help prevent incorrect rewards and reporting.