Sending
Text, templates, media, broadcasts
One send_message tool covers text, templates and rich media to a single recipient; send_broadcast fans the same content out to an audience. Policy decides — a blocked send explains itself instead of throwing.
Text messages
Target a stored contact by contact_id, or address a channel directly with { channel, address } (direct addressing supports whatsapp, sms, mail, telegram and discord). When a contact has identifiers on several channels, the optional top-level channel field picks the preferred one.
// send_message — free-form text inside an open session window
{
"to": { "contact_id": "c_123" },
"content": { "text": "Your order shipped this morning." }
}
// or address a channel directly
{
"to": { "channel": "whatsapp", "address": "+33612345678" },
"content": { "text": "Your order shipped this morning." }
}Templates
Templates are pre-approved message formats (Meta-approved on WhatsApp) that can be sent outside session windows. content.text and content.templateName are mutually exclusive; locale selects the template language (e.g. fr, ar-MA).
// outside the session window: use a pre-approved template
{
"to": { "contact_id": "c_123" },
"content": { "templateName": "order_shipped_v2", "locale": "fr" }
}list_templatesreturns the available templates per locale with their approval status — onlyAPPROVEDtemplates go through;PENDING/REJECTEDones are blocked at send time.
Media
Attach images, video, audio or documents via content.media. Every media item must carry either a url or a providerMediaId — an item with neither is rejected at validation.
| Field | Notes |
|---|---|
type | image · video · audio · document |
url | Publicly reachable HTTPS URL — the provider downloads it server-side. |
providerMediaId | Id of a file already uploaded to the provider (e.g. a WhatsApp media id from upload_media). Exclusive with url. |
caption | Shown under images, videos and documents. |
filename | Display name — mainly for documents. |
mimeType | E.g. video/mp4; inferred from the URL when omitted. |
// media by public HTTPS url — the provider downloads it server-side
{
"to": { "channel": "whatsapp", "address": "+33612345678" },
"content": {
"media": [
{
"type": "video",
"url": "https://cdn.example.com/demo.mp4",
"caption": "Here's the walkthrough",
"mimeType": "video/mp4"
}
]
}
}Local files: upload_media
When the file isn't publicly hosted (local bytes), upload it first with upload_media — pass url (fetched server-side) or data (base64) — then reference the returned media_id as providerMediaId:
// 1. upload local bytes (base64) to the channel's provider
POST /tools/upload_media
{
"channel": "whatsapp",
"type": "document",
"data": "<base64>",
"filename": "invoice-2026-041.pdf",
"mimeType": "application/pdf"
}
// → { "media_id": "mid.abc123…" }
// 2. reference it in send_message
POST /tools/send_message
{
"to": { "contact_id": "c_123" },
"content": {
"media": [
{ "type": "document", "providerMediaId": "mid.abc123…", "filename": "invoice-2026-041.pdf" }
]
}
}Broadcasts
send_broadcast sends one piece of content to every member of an audience. Always run preview_segment first — it returns the match count and a sample so an oversized or empty segment gets caught before anything goes out.
// 1. preview the segment first — always
POST /tools/preview_segment → { "count": 412, "sample": [ … ] }
// 2. send to the audience
POST /tools/send_broadcast
{
"audience_id": "aud_black-friday-optins",
"content": { "templateName": "bf_teaser", "locale": "fr" }
}
// → { "broadcast_id": "b_…", results per recipient: sent / blocked / failed }
// 3. track progress
POST /tools/check_delivery
{ "broadcast_id": "b_…" }
// → aggregate counts: sent, delivered, read, failed- Per-recipient results come back as
sent/blocked/failed; eachblockedentry carries its ownblocked_reasonandsuggestion. - Free-form text or media in a broadcast requires an open session window per recipient — most cold audiences will block in bulk. Prefer
templateName. - REST equivalents exist for audience management:
GET /audiences,POST /audiences,POST /audiences/previewandPOST /broadcasts.
Draft → approve (email & review flows)
For flows where a human (or supervising agent) reviews outbound content before it ships, the messaging_* family splits composing from sending:
messaging_draft → creates a pending draft, returns draft_id + preview
messaging_preview → renders it (e.g. markdown→HTML for email) without sending
messaging_approve → { "draft_id": "…", "confirm": true } ← the ONLY call that sends
messaging_list → inspect pending/approved/sent draftsmessaging_approverequiresconfirm: true— it is the only path in this family that actually sends.- Drafts live in memory and are ephemeral — they do not survive a process restart.
- Inbound email attachments are fetched lazily with
messaging_attachment_fetch(bymessage_id+attachment_id) — polling only captures metadata, never the bytes.
Every send is journaled — follow up with check_delivery, get_stats, get_spend or get_ttr, all described in MCP tools.