Skip to main content
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/html body becomes the caption (≤1024 chars); 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: Files route by content type too: an image/gif attachment becomes an animation (sendAnimation), an image/webp becomes a sticker.
MCP — send_message (a poll)

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:
MCP — send_message (a voice note)
  • text — required, up to 4000 characters (~4 minutes of speech).
  • voiceId — optional: a catalog slug (browse them with GET /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:
cURL — browse voices
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: 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/tg only).
  • callback — an action button. Tapping it surfaces the tap back to your agent (see Handling a tap). The callback string 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: 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 — set true to show a modal the user must dismiss, instead of a toast.
Buttons with feedback
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:
  1. Answers the tap immediately, so the tapper’s client stops its loading spinner.
  2. Persists an inbound message into your mailbox — your agent’s normal read loop sees it, no special casing. Its subject is Button tapped: <label> and its textBody is your original callback value.
  3. Emits a message.button.tapped webhook on the durable event ledger.
The inbound message carries the tap details in channelMetadata:
The message.button.tapped webhook event 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:
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

  1. Send a message with Approve / Reject callback buttons.
  2. Wait for the message.button.tapped webhook (or poll inbound messages).
  3. Branch on buttonCallback, matching sourceMessageId to your send.
  4. Edit the original message to the outcome and pass buttons: [] to retire the buttons — so the same message can’t be actioned twice.
  5. 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.

Next steps

Rich messages

Headings, tables, media, and code blocks in an html body.

MCP recipes

Copy-pasteable tool-call JSON for the canonical flows.

Webhooks

Receive message.button.tapped and other events in real time.