active, you’re ready.
Sending media (photos, PDFs, files)
Attach files to a Telegram send exactly as you would on email — the sameattachments
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 (≤1024 chars); a send with an attachment needs no body, so a caption-less voice note or photo is valid.
{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 amessageId 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 atelegram 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 withGET /v1/telegram/voices), a raw Dairo TTS voice id, or omit it for the default voice (adrian, a clean narrator).
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 afeatured
flag. Browse it:
cURL — browse voices
{ 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 abuttons 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.
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 acallback 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.
Buttons with feedback
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 — your agent’s normal read loop
sees it, no special casing. Its
subjectisButton tapped: <label>and itstextBodyis your originalcallbackvalue. - Emits a
message.button.tappedwebhook on the durable event ledger.
channelMetadata:
message.button.tapped webhook event carries the same facts in its data:
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 withmanage_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.
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.Reacting to a message
Set the bot’s emoji reaction on a message you sent or received withmanage_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:
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.button.tappedwebhook (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.
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.