Referral Links · Deep-link format
Every referral link comes with a ref_XXX token that we generate and store when you create the link on the dashboard. You can't change it, but you can append _cid and your own click identifier after it. We'll store the raw payload and Click ID on trader metadata so webhooks can send them later.
One sentence version
For Trading Layer to attribute a trader to a referral link, the /start payload must begin with our ref_XXX token. Add _cid and your click identifier after the token when you need click-level tracking.
RegEx
/^ref_[A-Za-z0-9]{20}/ + optional _cid suffixThe token (ref_ + exactly 20 hex characters = 24 chars total) is generated by the platform when you create a referral link. The parser only checks the start of the payload, so attribution is deterministic. A suffix that starts with _cid is parsed as Click ID and stored on trader metadata for later webhooks.
The matcher
This is the parser shape the bot uses to extract the referral token and Click ID. It is case-sensitive and only accepts the token at the start of the payload.
// Trading Layer parses only a fixed prefix at the start.
const REFERRAL_TOKEN = /^ref_[A-Za-z0-9]{20}/;
const CLICK_ID_MARKER = "_cid";
const rawStartPayload = ctx.message.text.replace(/^\/start\s*/, "").trim();
const match = rawStartPayload.match(REFERRAL_TOKEN);
if (match) {
const referralStartPayload = match[0]; // ref_03f1500eece04bf9ab5a
const suffix = rawStartPayload.slice(referralStartPayload.length);
const clickId = suffix.startsWith(CLICK_ID_MARKER)
? suffix.slice(CLICK_ID_MARKER.length) || null
: null;
}How the token is built
You don't pick the token — we do. Every referral link, at the moment it's created on the dashboard, is minted with a fresh, globally-unique identifier derived from a random UUID. It stays with the link for life.
ref_ (4) + 20 hexadecimal characters (the first 20 chars of a randomly-generated UUID, with dashes stripped).
Indexed with a unique constraint. Collisions across tenants are statistically impossible, so the token alone identifies the link — no secondary lookup needed.
The token can't be edited, renamed, or regenerated. If you need a new one, create a new referral link — archive the old one when you're done.
// apps/fullstack/src/server/api/lib/utils.ts
export function buildReferralStartPayload(): string {
// ref_ prefix + 20 hex characters from a random UUID (dashes stripped).
return `ref_${randomUUID().replace(/-/g, "").slice(0, 20)}`;
}
// Example output:
// ref_03f1500eece04bf9ab5a ← 24 characters, stored verbatim in the DBWhat you can change
This is where per-user / per-click tracking lives. Put _cid immediately after the 24-character token, then append the click-id from your ad network, affiliate, or campaign system.
# 1. What the dashboard gives you — the token is fixed at 24 characters.
https://t.me/your_bot?start=ref_03f1500eece04bf9ab5a
# 2. To tag each click, append _cid and your click identifier.
https://t.me/your_bot?start=ref_03f1500eece04bf9ab5a_cidJOPAPETUHA
# 3. Webhooks can send rawStartPayload and Click ID separately.
rawStartPayload = "ref_03f1500eece04bf9ab5a" + "_cidJOPAPETUHA"
^^^^^^^^^^^^^^^^^^^^^^^^ ^^^^^^^^^^^^^^^^^^^
fixed 24 chars (ours) Click ID marker + valueref_XXX.We read the first 24 characters, look that token up in your active referral links, and record a first-touch attribution row for the trader.
Configure webhook params for rawStartPayload and clickId. The Click ID is the suffix after _cid.
{clickid} in your link at serve time. Drop that macro after _cid and every trader who lands via an ad will arrive with the network's click-id attached. Webhooks can send that value as Click ID.Hard cap
The Telegram Bot API caps the /start parameter at 64 characters total. Anything beyond that is silently truncated before the bot ever sees it. Your budget, then, is what's left after our token.
ref_ + 20 hex)_cid marker uses four characters, leaving 36 for the Click ID value.The token always leads, so it's never the part Telegram chops — but a click-id that overflows your 36-char budget will be silently clipped from the right. Validate the final URL length when you compose it.
start payloads allows A-Z a-z 0-9 _ -. Spaces, dots, slashes, pipes, and URL-encoded bytes are either rejected at share time or stripped — they're not a safe transport for arbitrary strings. Base62 or hex click-ids work cleanly; base64 does not (it uses +, /, and =).Copy-pasteable
All URLs resolve to the same referral link. The examples with _cid also store a Click ID that can be sent by configured webhook params.
# Bare link — just use what the dashboard gives you
https://t.me/your_bot?start=ref_03f1500eece04bf9ab5a
# With a per-click identifier (e.g. Facebook click-id, affiliate subid)
https://t.me/your_bot?start=ref_03f1500eece04bf9ab5a_cidJOPAPETUHA
# Short user id — common pattern for attributing each ad impression
https://t.me/your_bot?start=ref_03f1500eece04bf9ab5a_cidu842817Known foot-guns
Most reports of 'attribution isn't working' trace back to one of these. Check them in order before opening a support ticket.
Payload exceeded 64 chars.
Telegram truncates. The token is safe (it leads and is always 24 chars) but your click-id gets clipped from the right and is stored incomplete. Validate the final URL length before you publish it.
Disallowed characters in the suffix.
Only A-Z a-z 0-9 _ - survive Telegram's validator. Base64 payloads (with + / =) and URL-encoded bytes get stripped or rejected — convert to hex or base62 first.
Link is archived.
Archived links are ignored by the attribution lookup even if the prefix parses. Historical attributions are preserved; new traders just fall through as unattributed.
Trader already attributed.
First-touch wins. Opening the bot with a different referral link doesn't re-attribute. The latest raw start payload and latest Click ID are still stored on trader metadata.
Wrong bot / tenant.
Start payloads are scoped to the tenant that owns the bot. A ref_XXX token minted on one tenant won't attribute against another tenant's bot.