deep links
use telegram start payloads for referrals, onboarding, invite attribution, and group setup.
private chat links
const payload = "ref_67";
const url = "https://t.me/my_bot?start=" + encodeURIComponent(payload);read the payload
telegram delivers the payload as arguments to /start. yaebal's command matching
(in Composer.command(), @yaebal/commands, and the @yaebal/filters command filter) stages three fields on ctx:
ctx.command— the matched name, as typed.ctx.payload— the raw, trimmed remainder after the command. this is the field that maps 1:1 to a deep-link start parameter.ctx.args— the same remainder split on whitespace, as astring[](empty if there's nothing after the command). a deep-link payload never contains whitespace, soctx.payloadandctx.args[0]agree for deep links — reach forctx.argswhen a command takes several space-separated tokens instead.
import { createBot } from "yaebal";
const saveReferral = async (userId: number, code: string) => {};
const bot = createBot(process.env.BOT_TOKEN!);
bot.command("start", async (ctx) => {
// ctx.payload is the raw trimmed remainder after "/start " — exactly what
// telegram passed as the start parameter. ctx.args is the same thing split
// on whitespace, for commands that take multiple tokens.
const payload = ctx.payload;
if (payload.startsWith("ref_")) {
await saveReferral(ctx.from!.id, payload.slice(4));
}
await ctx.reply("welcome");
});filter deep links
@yaebal/filters ships dedicated filters for the deep-link
shapes telegram supports, so you don't have to parse ctx.payload by hand in every
handler:
start—/startrestricted to private chats.startGroup—/startfired in a group or supergroup, i.e. a?startgroup=link landed.deeplink(param)—/startwhose payload equalsparam(string) or matchesparam(regexp, stagingctx.match).
register the specific filter before a catch-all .command("start") —
filters run in registration order, and a broad handler earlier in the chain would otherwise
swallow every referral before the narrower filter gets a chance:
import { createBot } from "yaebal";
import { deeplink } from "@yaebal/filters";
const bot = createBot(process.env.BOT_TOKEN!)
// register before the plain /start handler — filters run in order, and
// a catch-all /start below would otherwise swallow every referral too
.filter(deeplink(/^ref_(\d+)$/), (ctx) =>
ctx.reply(`welcome! you were referred by user ${ctx.match[1]}`),
)
.command("start", (ctx) => ctx.reply("welcome!"));
bot.start();group and channel links
use startgroup when the link should add the bot to a group and carry setup
context, or startchannel for channels. both still arrive as a plain /start <payload> on the wire — telegram only differentiates them at the
link-building stage.
const url = "https://t.me/my_bot?startgroup=" + encodeURIComponent("team_67");
const channelUrl = "https://t.me/my_bot?startchannel=" + encodeURIComponent("team_67");distinguish where a /start landed with the chat-type filters from @yaebal/filters: isPrivate, isGroup, and isChannel narrow ctx.chat.type. startGroup is just and(isGroup, command("start")) — there's no
equivalent startChannel export yet, so compose it the same way:
import { createBot } from "yaebal";
import { and, command, isChannel } from "@yaebal/filters";
const bot = createBot(process.env.BOT_TOKEN!);
// there's no ready-made "startchannel" export — compose one from the filters
// that already exist. a channel deep link arrives as a channel_post, not a
// private message, so command("start") still matches it.
bot.filter(and(isChannel, command("start")), (ctx) => {
// ctx.chat.type is narrowed to "channel" here
return ctx.reply("channel setup: " + ctx.payload);
});safety rules
- treat payloads as untrusted user input.
- keep payloads short and url-safe.
- store attribution server-side if the payload would expose sensitive data.
- make referral writes idempotent; users can click the same link multiple times.
- order matters: register
deeplink()filters and other specific matchers before generic.command("start")handlers. - test the first-message path with @yaebal/test.