docs · telegram

the channel with no gatekeeping.

A Telegram bot needs one token, no business verification, no credit card and no approval process. Delivery is free. The only thing it cannot do is message someone who has never spoken to the bot, and that turns out to be a feature.

Why this channel exists in this project

WhatsApp is the channel most users expect, and it is gated behind business verification, a dedicated SIM, a card that supports recurring international debit, and template review. That is fine for a company. It is a wall for an internal tool, a side project, a college project, or a launch that has not incorporated yet.

Telegram has none of that. You ask a bot for a token and you are sending messages the same minute, for free. The gateway treats both channels as first-class: same endpoints, same error shape, one channel field apart. Quota is metered against WhatsApp only, so Telegram traffic is never blocked by a spent monthly cap.

Operator setup, once

  1. Create the bot. Talk to @BotFather, send /newbot, choose a name and a username, and copy the token it gives you.

  2. Put the credentials in the environment. The username is what the gateway builds deep links from, so it must not include the leading @.

    TELEGRAM_BOT_TOKEN=123456:ABC-DEF...
    TELEGRAM_BOT_USERNAME=your_bot_username
    TELEGRAM_WEBHOOK_SECRET=a-long-random-string
  3. Register the webhook. Telegram has to be told where to deliver updates. The gateway ships a script for it.

    cd backend
    .venv/bin/python scripts/set_telegram_webhook.py https://api.example.com

    That resolves to {APP_URL}/telegram/webhookand attaches the secret, so the endpoint is not openly callable. If the secret and the registered value ever disagree, Telegram’s updates start failing and the bot goes silent.

The one bounce, and why it is a feature

Telegram bots can only message people who have started a chat with them. That is an anti-spam rule, and it cannot be bypassed. So the very first code to a phone bounces once, on purpose:

{
  "ok": false,
  "error": "user_not_linked",
  "link_url": "https://t.me/<bot_username>?start=<signed-token>"
}

What your application does:show a “connect telegram” button that opens link_url, then retry the same send. That is the entire integration. There is no “check link status” endpoint, because the 409 is the check, and the bounce consumed nothing: no code stored, no quota, no throttle.

What happens after the user taps the button:

  1. The bot opens in Telegram with /start and a signed token pre-filled.
  2. The bot replies with a one-time keyboard containing a single “share my number” button.
  3. The user taps it. The bot accepts the contact only if it belongs to the sender themselves. Nobody can link a friend’s number, or a stranger’s.
  4. The number is now linked, and your retry delivers the code on Telegram.

Linking is one-time per phone and platform-wide: once a user has connected to the bot, every application on the gateway can reach them. That is why the shared “connect” button is safe to show on the first bounce of any flow.

Watching it without setting anything up

The browser playground runs this exact flow: send on Telegram to an unlinked number, watch the 409 come back with a deep link, simulate the tap, then send again and see the code arrive. Nothing is sent and no bot is needed.

What it costs

Nothing, in both senses. Telegram charges for bot messages, so there is no per-message cost to pass on, and this project takes no cut and resells nothing. The only running cost is the machine you host the gateway on.

That is also the honest limit of this channel: Telegram reaches people who have Telegram. For consumer onboarding in a market where WhatsApp is the default messenger, you will eventually need the WhatsApp path as well. Running both is supported and costs one field.

Next