Skip to main content
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 new send from the inbox that received the message, back to the inbound from.address. 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.
Threading is not first-class. send_message has no threadId or inReplyTo parameter, and the inbound payload does not expose an RFC Message-Id. Dairo starts a new thread for the outbound send; it does not auto-join the inbound thread. For email you may set In-Reply-To and References yourself through the passthrough headers field (both are allowed) so the recipient’s mail client threads it — the A2A path ignores those headers. Treat idempotency, not threading, as the safety primitive here.

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) and fires a message.button.tapped webhook. Read it with the mailbox tool, or subscribe to the event. 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):
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, or template. Two or more is a 400"Provide exactly one body source: one of text, html, react, or template". text + html together 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, not templateId. Send from a stored template with template: { 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, requires filename, ≤ 8 MiB each and ≤ 8 MiB total across all inline attachments) or objectId (≤ 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 subject or to still returns the original, plus a warnings[] entry reason: "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_inboxes create/delete/setSchema, manage_contacts create/update/addHandle, replay_event, delete_messages, manage_sent_messages cancelScheduled, all manage_storage writes, manage_budgets set/delete, and the rest). The Telegram edits are the exception: manage_sent_messages edit, react, and unreact take no confirm. send_message does not take confirm either — it is gated by the messages:send scope.
  • Suppression safety. Complained recipients are blocked by default; override only with ignoreComplaints: true for a deliberate contact. A listComplaints hit means do not contact again. A bounce means verify the address before retrying.
  • Scope is per action, not per tool. Reads use …:read, writes use …:write; send_message needs messages:send. A key that lacks the scope gets a 403.
  • 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 with sendAt is a 400 — omit one. When channel is 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_message advertises contactId, channel (email or a2a), replyTo, headers {}, and tags {}, and its top-level schema is additionalProperties: true, so unknown extras are tolerated. headers is screened by a server-side denylist — From, To, Subject, Reply-To, Message-Id, DKIM, Content-*, and X-Dairo-* are rejected, while any other custom header (including In-Reply-To and References) passes through. replyTo is validated as a single email address.
  • Telegram buttons and edits. buttons is a 2-D grid (≤ 8 rows × ≤ 8 per row); each button is exactly one of url (http, https, or tg only) or callback (≤ 200 characters — Dairo stores it and hands it back on the tap, so it never hits Telegram’s 64-byte callback_data cap). manage_sent_messages { action: "edit" } edits only Telegram messages you sent (else 404); on an edit, omitting buttons preserves the keyboard, [] clears it. react needs a Telegram-allowed emoji (a custom or premium one is a 400). Each tap is its own inbound row, plus a message.button.tapped event.
  • 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.