Telegram interactivity
Add inline buttons, handle taps, edit and react to messages, send media and voice notes, and message other bots over Telegram.
Turn a one-way Telegram bot into an interactive one. Your agent can put buttons under a message, receive the tap back as a normal inbound message, edit what it already sent, react with an emoji, send media and voice notes, and message another bot. Every one of these rides the same send call and MCP tools you already use for plain text.
This builds on connecting a Telegram bot. If your inbox is
active, you're ready.
Sending media (photos, PDFs, files)
Attach files to a Telegram send exactly as you would on email — the same attachments
field. Each attachment is an object: inline {filename, contentBase64} or a storage
reference {objectId} of a Dairo object you own. Dairo routes each to its native Telegram
type by content type:
- an image (
image/*) → sendPhoto (rendered inline), - a video (
video/*) → sendVideo (native inline player), - OGG/Opus audio (
audio/ogg,audio/opus) → sendVoice (a real voice-note bubble — perfect for an agent's spoken reply), - other audio (
audio/*) → sendAudio (the music player), - anything else (PDF, doc, …) → sendDocument (a file card),
- 2+ files → a sendMediaGroup album (mixed sets fall back to separate native sends),
- the
text/htmlbody becomes the caption (at most 1,024 characters — a longer one is a400, so send long text as its own message); a send with an attachment needs no body, so a caption-less voice note or photo is valid.
Native up to 50 MB, link over that. Dairo fetches a storage {objectId} and uploads it
to Telegram natively — up to Telegram's 50 MB bot-upload cap — so it bypasses the
inline-JSON size limit entirely (upload a 20 MB video to a bucket, reference it by objectId,
and it lands in the chat). Over 50 MB, a send returns a 413: create a
Dairo share link for the file and include the link in your message text instead (Dairo won't
auto-add it) — the same pattern as email.
A single photo/document can also carry buttons; an album (2+ files) cannot (Telegram albums
have no keyboard), so buttons + multiple attachments is a 400.
Receiving media
When someone sends the bot a photo, document, video, audio, or voice message, Dairo
downloads the file and lands it as a first-class attachment on the inbound mailbox
message — the same shape as an inbound email attachment (its own storage object, with a
messageId join key). The message's caption becomes the text body. A media-only message
(no caption) still arrives, carrying only the attachment. Your normal read loop sees it;
no special casing.
Native message types (location, poll, dice, …)
Beyond text + media, a Telegram send can carry a telegram object for the native message
types — exactly one per send:
telegram.<field> | Sends |
|---|---|
location: {latitude, longitude, livePeriod?} | a map point (sendLocation) |
venue: {latitude, longitude, title, address} | a venue (sendVenue) |
contact: {phoneNumber, firstName, lastName?} | a phone contact (sendContact) |
poll: {question, options[], isAnonymous?, type?, allowsMultipleAnswers?, correctOptionId?} | a poll or quiz — type:"quiz" + correctOptionId makes it a quiz (sendPoll) |
dice: {emoji?} | an animated 🎲 🎯 🏀 ⚽ 🎳 🎰 (sendDice) |
sticker: "<file_id or https URL>" | a sticker (sendSticker) |
videoNote: "<file_id or https URL>" | a round video note (sendVideoNote) |
Files route by content type too: an image/gif attachment becomes an animation
(sendAnimation), an image/webp becomes a sticker.
Voice messages from text
Your agent can send a real Telegram voice note from pure text — no audio file. Dairo synthesizes the speech (Dairo TTS) and delivers it as a native voice bubble:
text— required, up to 4000 characters (~4 minutes of speech).voiceId— optional: a catalog slug (browse them withGET /v1/telegram/voices), a raw Dairo TTS voice id, or omit it for the default voice (adrian, a clean narrator).
Voice notes are delivered in the background: the send returns status: "queued" immediately,
then Dairo synthesizes and delivers the note, so long text never times out the request. Read
the final sent/failed outcome with GET /v1/messages/{id}.
Browsing voices
Dairo ships a catalog of thousands of voices — celebrity and character voices alongside
neutral professionals. Each carries a name, description, best language, tags, and a featured
flag. Browse it:
Each row returns { slug, name, description, bestLanguage, languages, gender, tags, featured, popularity },
ordered by popularity. Pass a row's slug as the voiceId on a voice send; celebrity and
character voices carry featured: true.
Built-in slash commands
Every connected Telegram bot registers a small set of built-in commands that Dairo
answers itself — no agent code needed. They autocomplete in the Telegram / menu:
| Command | What it does |
|---|---|
/start | A warm, personal welcome, written fresh by Dairo for the chat that verified the bot. Every other chat gets the default welcome. |
/status | A link to the Dairo system status page. |
/usage | This bot's sends over the last 24h (never the rest of your account). Answers only the chat that verified the bot. |
/whoami | This inbox's identity (bot handle + inbox id). Answers only the chat that verified the bot. |
/help | The command list. |
These are read-only and handled entirely by Dairo. Every other command (your own
/deploy, /summary, …) and every normal message flows to your agent as a normal
inbound — so you can implement your own commands by branching on the inbound text. New bots
get the built-ins registered automatically on connect.
Inline buttons on a send
Add a buttons field to any Telegram send. It is a 2-D array — an array of
rows, each row an array of buttons. Each button is exactly one of:
url— a link button. Tapping it opens the URL (http/https/tgonly).callback— an action button. Tapping it surfaces the tap back to your agent (see Handling a tap). Thecallbackstring is your own opaque value — Dairo stores it and hands it back on the tap.
The row layout you send is the layout Telegram renders — two buttons side by side, then a full-width link button below.
Buttons work on a text or an html (rich) body. The grid is bounded:
| Limit | Value |
|---|---|
| Rows | up to 8 |
| Buttons per row | up to 8 |
Button text | 1–64 characters |
callback value | up to 200 characters (stored by Dairo, never on the wire) |
Your callback string never travels on the Telegram wire — Telegram hard-caps
callback_data at 64 bytes, so Dairo sends a compact reference and keeps your full
callback value (and the button label) on the outbound message. The tap round-trips
your real payload.
Tap feedback
By default, tapping a callback button flashes a small confirmation toast — a check
mark plus the button's label (e.g. ✓ Approve) — so a tap is never silent. Override it
per button:
feedback— the toast text to show on tap (up to 200 characters).alert— settrueto show a modal the user must dismiss, instead of a toast.
The toast/alert is answered inside Telegram's brief callback window, so it is decided at send time, per button. For a richer, stateful response — checking off the choice, swapping the keyboard, rewriting the text — edit the message when you receive the tap.
Handling a button tap
When someone taps a callback button, Dairo does three things:
- Answers the tap immediately, so the tapper's client stops its loading spinner.
- Persists an inbound message into your mailbox and fires the
message.receivedwebhook — your agent's normal read loop sees it, no special casing. ItssubjectisButton tapped: <label>and itstextBodyis your originalcallbackvalue. - Records a
message.button.tappedevent in the event stream.
The inbound message carries the tap details in channelMetadata:
The message.button.tapped event in the stream carries the same facts in its data:
Use sourceMessageId to tie the tap back to the exact send it belongs to, and
buttonCallback to branch on which action the user chose. A source message can be
tapped many times — each tap is its own inbound row (keyed on the callback query
id), so a count of taps is a count of rows.
fromIsBot flags a tap that came from another bot — a loop guard for bot-to-bot
flows. A url button never produces a tap event — it only opens its link.
Editing a message
Edit a message you already sent with manage_sent_messages { action: "edit" } (REST:
POST /v1/messages/{id}/edit). Pass text or html to replace the body and/or a buttons
grid to change the keyboard; supplying both text and html is a 422. This is the
canonical "acknowledge, then finish" pattern — send with buttons, then edit the text to a
result.
How buttons behaves on an edit:
| You pass | Result |
|---|---|
omit buttons | The existing keyboard is preserved (Dairo re-sends it for you). |
buttons: [[…]] | The keyboard is replaced with the new grid. |
buttons: [] | The keyboard is cleared. |
Omitting buttons on a body edit preserves the keyboard even on Telegram Desktop,
where a raw editMessageText without a keyboard would otherwise drop the buttons.
To retire the buttons once an action is taken, pass buttons: [] explicitly.
Only Telegram messages you sent are editable. A non-Telegram id, an inbound id, or another tenant's id returns 404 (existence is never leaked).
Reacting to a message
Set the bot's emoji reaction on a message you sent or received with
manage_sent_messages { action: "react" } (REST: POST /v1/messages/{id}/react). Clear it
with action: "unreact".
emoji must be one of Telegram's allowed reaction emoji (👍 👎 ❤ 🔥 🥰 👏 😁 🤔
🎉 🙏 …) — a normal bot cannot use a custom or premium emoji, and one that isn't on
the list returns 400. big (optional) plays the big-reaction animation. Reacting
to an inbound message is the lightweight way to acknowledge it without sending a reply.
Bot-to-bot & by-username messaging
A send normally goes to the chat that connected the bot. To message a specific bot,
channel, or user by username instead, put an @username (or telegram:@username)
in to:
Because a by-username send needs no bound chat, it works even before the inbox is verified — handy for a bot that only ever talks to other bots or posts to a channel.
For bot-to-bot messaging, both bots must enable Bot-to-Bot Communication Mode in @BotFather; for a channel or user the bot must be permitted to post there. If Telegram rejects the send, Dairo surfaces a 422 with that exact guidance rather than a generic error.
Put it together: an approval loop
- Send a message with
Approve/Rejectcallback buttons. - Wait for the
message.receivedwebhook (or poll inbound messages). - Branch on
buttonCallback, matchingsourceMessageIdto your send. - Edit the original message to the outcome and pass
buttons: []to retire the buttons — so the same message can't be actioned twice. - Optionally react 👍 on the tap to acknowledge it.
Every step is one call on the surface you already use — no Telegram SDK, no webhook plumbing of your own.