Copy-paste tool-call JSON for the flows an agent runs on Dairo — send, reply, resolve a contact, attach a file, drive Telegram.
Copy-paste tools/call arguments for the flows an autonomous agent actually runs.
Dairo's MCP surface is split per domain into read and write tools (list_inboxes
and manage_inboxes, read_mailbox and delete_messages); a few primitives stay
standalone (send_message, verify_message, search_docs).
Each block below is { "name": <tool>, "arguments": {...} } — the shape a
tools/call takes.
Recipe 1 — Send from an inbox, then confirm delivery
1a. List inboxes and pick one. Each inbox carries an id (pass it as
inboxId when you send), an address, and a status.
1b. Send. An immediate send is synchronous: you get status: "sent" with a
providerMessageId, or a precise error at submit time — never a false "queued"
that fails later.
Response: { "id": "msg_…", "status": "sent", "providerMessageId": "…", "channel": "email", "warnings": [] }.
A send where every recipient is a Dairo inbox is delivered status: "sent" over
channel: "a2a" with a provenance receipt instead.
1c. Diagnose — the one call to reach for when a send misbehaves. Read-only,
messages:read, and A2A-aware.
Returns renderStatus, providerStatus, domainVerification,
recipientEligibility, deliveryEvents, and a precise nextAction.
1d. Fetch delivery events. Two surfaces — reach for the right one.
Per-message timeline (messages:read):
Durable, gap-detectable ledger (events:read) — filter by the same
idempotencyKey you sent with to correlate the send to its events:
The ledger defaults to oldest-first — it is a forward catch-up and replay stream.
Pass order: "newest" to see the most recent event on page 1. On an unfiltered
page, gaps: [] is the healthy signal (no lost events in the window), not
"detection skipped".
Recipe 2 — Receive a message, fetch the thread, reply idempotently
2a. Find new inbound messages.
2b. Fetch the full thread — the thread object plus messages[], each with
from.address, subject, direction, and textBody.
2c. Reply. A reply is a send from the inbox that received the message, back to
the inbound from.address, with replyToMessageId set to the message you answer:
Dairo sets the reply headers from it and files the reply in the same thread. Make it
safe against retries with a deterministic idempotencyKey derived from the inbound
message id — re-running the call returns the original send verbatim instead of
double-replying.
A replyToMessageId you cannot see is a 404. To answer mail that is not in Dairo,
pass its raw inReplyTo Message-ID (and optionally references) instead.
Recipe 3 — Resolve a contact and send to it
Contacts are a channel-agnostic address book: one identity, many handles, plus a
free-text info note.
3a. Create a contact (mutation, so confirm: true, scope contacts:write).
3b. Resolve (use contactId: "me" for the project self-contact):
3c. Send to it. In to[] use "@alias", "contact:<uuid>", or "@me" — Dairo
resolves the contact's primary handle for the sending inbox's channel
automatically. send_message also takes a top-level contactId field that does
the same resolution.
Send to yourself:
3d. Read a contact's full history (messages action, contacts:read) — every
inbound and outbound message with this contact across all channels, merged
newest-first, so an agent can catch up on a relationship before it replies. Each
item carries its channel and direction; page with limit and cursor.
Recipe 4 — Send with an attachment
Each attachment is an object, and each item is exactly one of contentBase64
(inline) or objectId (storage reference) — never both, never neither. Up to 10
attachments.
4a. Inline — small files only (≤ 8 MiB decoded per attachment and ≤ 8 MiB total
inline). filename is required.
4b. By objectId — the way to attach anything larger (native-attached up to
24 MiB, fetched server-side, ownership-checked). First upload:
The response gives { "objectId": "obj_…", "uploadUrl": "…", "method": "PUT", "headers": {…}, "expiresInSeconds": … }.
PUT the raw bytes to uploadUrl with exactly those headers yourself. To confirm
the object finalized before you attach it, poll the read tool list_storage with
{ "action": "getUploadStatus", "bucketId": "buk_…", "objectId": "obj_…" }. Then
attach by id — filename and contentType are derived from the object unless you
override them:
Keep an A2A send on A2A. A send to Dairo-inbox recipients stays on the internal
A2A channel (delivered "sent", with a receipt) only if every attachment is an
objectId reference. A single inline contentBase64 attachment forces the whole
send over email. Upload first and attach by objectId to preserve A2A.
Recipe 5 — Interactive Telegram: buttons, tap, edit
On a Telegram-channel inbox, send_message takes a buttons
grid, taps come back as inbound events, and manage_sent_messages edits or reacts
in place.
5a. Send with an inline keyboard. buttons is a 2-D array (rows of buttons);
each button is exactly one of url (opens a link) or callback (surfaces the
tap).
5b. Receive the tap. A callback tap lands as a normal inbound message
(subject: "Button tapped: Approve", textBody set to your callback value),
fires the message.received webhook, and is recorded in the event stream as
message.button.tapped. Read it with the mailbox tool, or react to the webhook. Match sourceMessageId to your 5a send and branch on
buttonCallback:
5c. Edit the message to its outcome. The body is exactly one of text or
html. Omit buttons to keep the current keyboard, pass [] to clear it, or pass
a new grid to replace it. Clearing the keyboard on completion stops a second tap.
5d. React (optional). Acknowledge a message you sent or received without a
reply. emoji must be a Telegram-allowed reaction; unreact clears it.
5e. Message another bot or channel. Put an @username in to to send by
username instead of the bound chat.
See the Telegram interactivity guide for the full tap-event shape and grid limits.
Recipe 6 — Send a compliant physical letter (no postal knowledge needed)
Physical mail has a layout contract (address window, keep-out zones) that the platform teaches you — never guess it.
6a. Get the spec + starter templates. One call returns the full layout contract in mm plus compliant, ready-to-store letter HTML:
6b. Store a starter template. Take a starterTemplates[].html from the
response verbatim (or your own HTML that keeps the documented zones clear):
The response is a compact summary: id, name, status, variables,
updatedAt and htmlBytes. It does not echo the HTML back, and neither does
updateTemplate. Use getTemplate when you need the HTML. previewTemplate
returns failedChecks and checks next to valid, so a failed preview names
the check to fix.
6c. Verify the exact letter you are about to send. The templateId source
renders with your real recipient and checks the result — pass to so the
printed address is matched against the intended recipient:
Response: { "valid": true, "checks": [...] }. Fix every fail before
sending; a bring-your-own PDF (pdfBase64) verifies the same way.
6d. Price, then send. Same source shapes; dryRun: true on send is a
full no-send rehearsal (validated + priced, never mailed, never charged):
Track it with list_letters { "action": "get", "letterId": "let_…" } — the
status runs queued → submitted → in_transit → delivered.
Sharp edges
Things an agent gets wrong without being told:
- Body is a strict one-of. Provide exactly one of
text,html,react, ortemplate. Two or more is a400—"Provide exactly one body source: one of text, html, react, or template".text+htmltogether is rejected; Dairo does not build a multipart alternative. A send with no body is allowed only when it carries an attachment or a Telegram payload (a caption-less file or a poll). template, nottemplateId. Send from a stored template withtemplate: { id, version?, variables }. A template that renders to an empty body fails synchronously (422 template_render_empty, naming the id, version, and variables) before any handoff — not as a silent blank email.- Attachment caps and shape. One object per item; exactly one of
contentBase64(inline, requiresfilename, ≤ 8 MiB each and ≤ 8 MiB total across all inline attachments) orobjectId(≤ 24 MiB). Two 8 MiB inline attachments fail the total cap even though each is within the per-item cap. Over cap →413. Both or neither set →400.delivery: "link"→400; Dairo never auto-edits your body to insert a share link. - Inline attachment breaks A2A. See Recipe 4 — any inline bytes force the send over email.
- Idempotency returns the original, never re-sends. Reusing a key returns the
first send for that key verbatim. Reusing it with a different
subjectortostill returns the original, plus awarnings[]entryreason: "idempotency_key_reused_with_different_params"— use a fresh key for different content. Keys are ≤ 128 characters. - Confirmation gate on mutations. Most mutating write-tool actions require
confirm: true(manage_inboxescreate/delete/setSchema,manage_contactscreate/update/addHandle,replay_event,delete_messages,manage_sent_messagescancelScheduled, allmanage_storagewrites,manage_budgetsset/delete, and the rest). The Telegram edits are the exception:manage_sent_messagesedit,react, andunreacttake noconfirm.send_messagedoes not takeconfirmeither — it is gated by themessages:sendscope. - Suppression safety. Recipients who complained or hard-bounced are blocked by
default; override only with
ignoreComplaints: truefor a deliberate contact. AlistComplaintshit means do not contact again; a permanent bounce means the address is dead. - Scope is per action, not per tool. Reads use
…:read, writes use…:write;send_messageneedsmessages:send. A key that lacks the scope gets a403. - Recipient limits and resolution.
to + cc + bcc≤ 50. A UUID-shaped recipient resolves to that inbox's address, org-scoped — another tenant's UUID will not resolve. Contact refs (@alias,contact:<uuid>,@me) resolve to the primary handle for the sending inbox's channel. channel: "a2a"cannot be scheduled. A2A delivers synchronously, so pairing it withsendAtis a400— omit one. Whenchannelis omitted, Dairo auto-classifies: A2A when every recipient is a Dairo inbox, otherwise email. A Telegram-channel inbox always delivers over Telegram regardless.- Extra send fields. Beyond the body and recipients,
send_messagetakescontactId,channel(emailora2a),replyToMessageId(or the rawinReplyTo/referencesfor mail outside Dairo),replyTo,headers {}, andtags {}.headersis screened server-side — protected names such asFrom,To,Subject,Reply-To,Message-Id, DKIM,Content-*, andX-Dairo-*are rejected.replyTois validated as a single email address. - Telegram buttons and edits.
buttonsis a 2-D grid (≤ 8 rows × ≤ 8 per row); each button is exactly one ofurl(http,https, ortgonly) orcallback(≤ 200 characters — Dairo stores it and hands it back on the tap, so it never hits Telegram's 64-bytecallback_datacap).manage_sent_messages { action: "edit" }edits only Telegram messages you sent (else404); on an edit, omittingbuttonspreserves the keyboard,[]clears it.reactneeds a Telegram-allowed emoji (a custom or premium one is a400). Each tap is its own inbound row, plus amessage.button.tappedevent. - MCP never streams raw bytes. Attachment actions return metadata and URLs
only. To read an inbound file, call
read_mailbox { "action": "getAttachmentUrl" }(or"downloadAttachment") and fetch the returned URL yourself.
Next steps
Connect a client and see the full tool surface in the MCP server. The design patterns these recipes rest on are in Why Dairo is agent-native. The per-action scope each tool call needs is covered in Permissions & scopes. For why a deterministic key is the reply-safety primitive, read Retries & idempotency.