Skip to content

Telegram

Telegram works differently from Meta’s channels in four ways that shape its setup:

  • Herald registers the webhook itself. Telegram’s webhook is set per bot, so creating the channel account calls setWebhook. There is no dashboard step.
  • The bot is named in the URL. A Telegram update names the chat, never the bot it reached, so each bot delivers to /webhooks/telegram/<bot id>.
  • Authentication is a shared secret that Herald chooses and Telegram echoes back on every delivery in X-Telegram-Bot-Api-Secret-Token. It is not an HMAC of the body.
  • No 24-hour window and no templates. Any message can be sent at any time.
  1. Create the bot with @BotFather (/newbot) and note its token. The bot id is the number before the : in the token.

  2. Start Herald with the channel on.

    Terminal window
    HERALD_PUBLIC_BASE_URL="https://<your-herald-host>" \
    TELEGRAM_WEBHOOK_SECRET="<a secret only Herald and Telegram know>" \
    ./bin/herald # plus the required settings from the Quick Start

    The secret alone registers the channel. HERALD_PUBLIC_BASE_URL must be Herald’s real public HTTPS root, because that is what Telegram is told to call.

  3. Register the bot as a channel account. This one call also points the bot at Herald.

    Terminal window
    curl -X POST localhost:8081/admin/accounts \
    -H "X-Admin-Token: $HERALD_ADMIN_TOKEN" -H "Content-Type: application/json" \
    -d '{"tenant_id":"tenant-1","channel":"telegram",
    "external_id":"<bot id>",
    "credentials":{"bot_token":"<bot token>"}}'

    Herald calls setWebhook before saving the account, with url = <public base>/webhooks/telegram/<bot id>, the secret and allowed_updates: ["message"]. If Telegram refuses, the request fails with 502 and nothing is saved, so a 201 means the bot is already live. Without a public base URL or a secret, account creation is refused instead of leaving a bot that never receives.

  4. Bind it to an entry agent, the same way as on WhatsApp but with "channel":"telegram".

UpdateReaches the agent as
TextThe text
Photo, document, voice note, audio, videoThe caption, or a bracketed description when there is none. Photos keep the largest size
A contact the person shared about themselves[el usuario compartió su número de contacto], and the phone is stored as verified
A reply to an earlier messageThe text, plus the quoted message in context.reply_to
Edited messages, joins, polls…Not requested from Telegram

sender is the chat id. The message id is chat_id:message_id, because Telegram numbers messages per chat. contact_name is the sender’s first and last name, or their username.

When someone answers with Telegram’s “reply”, the agent receives the quoted message:

FieldContent
textoThe quoted message’s text or caption
message_idchat_id:message_id of the quoted message
del_botWhether the quoted message was the bot’s
enviado_elWhen it was sent, UTC

See Building Agents for how to use it.

On Telegram, sender is a chat id, so an agent that looks people up by phone finds nobody. The only way to learn the phone without inventing authentication is Telegram’s request_contact button: the person taps it and Telegram attaches the number of their own account.

Turn it on per binding:

Terminal window
curl -X PUT localhost:8081/admin/bindings/<binding id> \
-H "X-Admin-Token: $HERALD_ADMIN_TOKEN" -H "Content-Type: application/json" \
-d '{"config":{"pedir_contacto":true}}'

With pedir_contacto on, every text reply to someone without a verified phone carries a 📱 Compartir mi número button. When they tap it:

  1. Herald accepts the contact only if contact.user_id matches the sender. Otherwise anyone could verify someone else’s number by forwarding a contact.
  2. The phone is normalised with a leading + and stored for (tenant, channel, chat id).
  3. The agent runs with the bracketed query above and the phone in sender_phone, and so does every later message from that person.
  4. The button stops appearing.

It is off by default because the button replaces the person’s keyboard. A catalogue or an FAQ bot that never needs to identify anyone should leave it off.

Another service can ask which chat verified a phone with GET /api/v1/identities/resolve.

SendBot API method
TextsendMessage
Image, document, video, audiosendPhoto, sendDocument, sendVideo, sendAudio by public URL, with an optional caption
TemplateNot supported; fails permanently

The recipient is the chat id. Inbound files are fetched with getFile, up to 8 MiB, only when a forwarding binding needs them.

A 4xx from the Bot API is treated as permanent, except 401, 403, 408 and 429, which are retried. The bot token is part of every Bot API URL, so Herald strips URLs from network errors before they are stored in the outbox.

FlagEnvironmentDefault
--telegram-webhook-secretTELEGRAM_WEBHOOK_SECRETempty (channel off)
--public-base-urlHERALD_PUBLIC_BASE_URLempty
--telegram-api-base-urlTELEGRAM_API_BASE_URLhttps://api.telegram.org