Track Telegram Bot Starts Without Overwriting the First Known Source

7 min read

Pass a source through start, validate the campaign code, and keep first-known and last-known sources separate. Includes a local Python test and attribution limits.

Track a Telegram Bot's Start Source Without Losing the First One

Assign each placement a short code and a link such as https://t.me/YOUR_BOT_USERNAME?start=channel_a. When your bot receives /start channel_a, check the code against a registry and record the event. Store the first known source separately from the most recent known source, so a later start does not overwrite the first.

This attributes observed bot starts, not clicks. The code identifies the parameter that accompanied a start; it does not prove where someone found the link. The design below covers a bot you control in a private chat and includes a local Python test. It is not a guide to setting up a 24GO feature.

What Telegram sends to the bot

Telegram's deep-linking documentation describes passing a parameter through start. Allowed characters are A-Z, a-z, 0-9, _, and -; the parameter can be up to 64 characters long. A short code such as channel_a needs no encoding.

Replace YOUR_BOT_USERNAME with your bot's username. These are placeholders, not links to a demonstration service:

https://t.me/YOUR_BOT_USERNAME?start=channel_a
https://t.me/YOUR_BOT_USERNAME?start=webinar_b

The bot-link specification calls for a Start button when a parameter is supplied, including when the user has already started the bot. Confirm that your server receives the command after the user acts. Opening the link alone does not mean your server has received an event. Check the interface separately in each client you support.

Do not confuse start for a private chat with startgroup, which is used when adding a bot to a group. Arbitrary utm_source fields in a link do not replace handling start. If the bot needs a campaign identifier, send a payload it recognizes: the source code.

Define what a source means before storing it

Create a registry in which channel_a identifies one authorized placement and webinar_b another. Store each code's description, usage period, and owner on your server. Do not put phone numbers, email addresses, access tokens, or other sensitive data in the link. Base64url changes the representation of data; it does not encrypt it.

Keep the event log separate from the user's attribution record:

FieldWhat to storeRule
observed_start_atWhen the server received the commandDo not label it the click time
source_statusknown, missing, or invalid_or_unknownDo not invent a source when no code is present
source_codeA validated code, or no valueDo not trust arbitrary text
first_known_sourceThe first accepted codePreserve it on later starts
last_known_sourceThe most recent accepted codeUpdate only for a known code
“First known” is deliberately narrow. Someone may arrive without a code and later follow a webinar link. The webinar is then the first source you could identify, not necessarily their first interaction with the company. If you need to retain the first observed start regardless of its code, store it separately, including a missing status.

Use a stable internal user key and restrict access appropriately. In production, update the record atomically so concurrent events cannot both claim to be first. Delivery logs and event deduplication solve different problems; neither turns a repeated /start into a new lead.

A local test that preserves the first code

This example tests only your policy for an argument already extracted from /start. It does not run a bot, receive webhooks, or connect to a CRM. You need Python 3, with no extra dependencies. Save the code as attribution_test.py and run python attribution_test.py:

import json
import re

KNOWN = {"channel_a", "webinar_b"}
PATTERN = re.compile(r"[A-Za-z0-9_-]{1,64}")

def valid_payload(value):
return isinstance(value, str) and PATTERN.fullmatch(value) is not None

def observe(state, payload):
if payload is None or payload == "":
status = "missing"
elif not valid_payload(payload) or payload not in KNOWN:
status = "invalid_or_unknown"
else:
status = "known"
if state["first_known"] is None:
state["first_known"] = payload
state["last_known"] = payload
return status

assert valid_payload("a" * 64)
assert not valid_payload("a" * 65)
assert not valid_payload("has space")
state = {"first_known": None, "last_known": None}
inputs = [None, "channel_a", "webinar_b", "made_up", "", "webinar_b"]
statuses = [observe(state, value) for value in inputs]
assert statuses == ["missing", "known", "known", "invalid_or_unknown", "missing", "known"]
assert state == {"first_known": "channel_a", "last_known": "webinar_b"}
print(json.dumps({"statuses": statuses, "state": state}, sort_keys=True))

Expected output:

{"state": {"first_known": "channel_a", "last_known": "webinar_b"}, "statuses": ["missing", "known", "known", "invalid_or_unknown", "missing", "known"]}

Format validation and registry lookup are separate checks. A string of 64 letters passes the format check but is not a known source unless it appears in KNOWN. An unknown or missing code leaves previously accepted sources intact. Repeating webinar_b leaves both stored values unchanged.

This is a synthetic, in-memory test for one user. It does not demonstrate Telegram delivery, crash-safe storage, or concurrent processing. An AssertionError means the result differs from the stated policy: investigate the change rather than deleting the assertion. Do not run it with python -O, which disables assert statements.

Check your own bot before distributing links

  1. Add two codes to your registry and implement a /start handler that extracts the command argument and passes it to your validator. The example above does not implement that handler.
  2. Open the first link with a test account and complete the Start action if the client offers it. Confirm that the server received the expected command and stored channel_a in both fields.
  3. Open and start the second link using the same account. Expect first_known_source=channel_a and last_known_source=webinar_b. Do not create a second lead record merely because another start occurred.
  4. Send /start without an argument, then /start made_up. The event log should record the corresponding statuses while preserving the known sources.
  5. Repeat the test on the Telegram clients you support. Record the client, version, time, command received, and database result. Do not include your bot token in the report.
If no command arrives, determine whether the problem is link opening, update delivery, or filtering in your handler. If the command arrives but its code is missing, check argument extraction, the registry, and database writes. Neither problem establishes that the campaign has no demand.

What these records can and cannot tell you

A public link can be forwarded, and its code can be typed manually. A whitelist confirms that you recognize the code; it does not prove that the user came from the intended placement. Even a signed parameter does not prevent someone from copying the entire link. Do not use an ordinary campaign code to grant access, pay rewards, or confer other privileges.

You can report accepted commands and, separately, distinct internal users with a known code over an explicitly defined period. Repeated starts by one user are not new enquiries. Qualification needs its own criterion, and a sale needs separate confirmation from your sales system.

The bot alone does not provide a click-through-rate denominator: it does not show all link openings that never led to a start. Even your own redirect counter may include link previews and bots. Do not divide starts by incomparable view counts and call the result an audience-wide conversion rate.

Finally, receiving /start is not, by itself, consent to further marketing. Explain the purpose of data collection, restrict access, and set a retention period. Assess messaging permissions and applicable requirements separately. The outcome of this design is an auditable source for an observed start, not permission to message someone or proof of ROI.