Message retrieval and sending
List messages in a conversation
Cursor-paginated. Returns newest first.
path Parameters
idquery Parameters
cursorMongoDB ObjectId hex string for pagination
limitList messages in a conversation › Responses
Paginated messages
has_morenext_cursorMongoDB ObjectId hex for next page
Send an outbound message
Produces an outbound job to Kafka. The message-sender service delivers it via Meta Graph API (WhatsApp/Instagram) or SMTP (Email).
path Parameters
idSend an outbound message › Request Body
typeMirrors MessageType in crates/omni-common/src/models/message.rs. contacts (a shared contact card) and postback (a tapped button) are emitted by the channel engines; unknown covers anything the parsers did not recognise.
contentFree-form JSON. For text: {"text": "Hello"} or {"body": "Hello"} — the WhatsApp sender reads text first and falls back to body. For image: {"url": "...", "caption": "..."}.
Whichever key you send is stored verbatim and read back on Message.content, so sending body keeps writes and reads on the same key that every inbound message already uses.
Send an outbound message › Responses
Message created and queued for delivery
conversation_iddirectiontypeMirrors MessageType in crates/omni-common/src/models/message.rs. contacts (a shared contact card) and postback (a tapped button) are emitted by the channel engines; unknown covers anything the parsers did not recognise.
contentFree-form JSON whose shape depends on type.
READING a stored message: text lives under body. Every channel engine normalises inbound content to {"body": "..."} before storing it (see the parser modules in crates/chat-engine), and outbound text stored through this API arrives the same way from the first-party clients. The server's own reader, extract_content_text in crates/api-gateway/src/routes/messages/handlers.rs, tries body, then text, then caption, then subject — a consumer should do the same rather than reading one key. Email messages carry body, subject, html and text together.
WRITING via SendMessageRequest: either key works. The WhatsApp sender reads text first and falls back to body (crates/message-sender/src/whatsapp.rs), and whichever you send is stored verbatim — so send body if you want reads and writes to agree.
statuscreated_atMongoDB ObjectId in extended JSON — {"$oid": "<24-hex>"}, NOT a bare string. The server field is a bson::oid::ObjectId, which serializes to this wrapper object under serde_json.
Note the asymmetry with MessageSearchResult._id, which IS a plain hex string: the search handler flattens it with oid.to_hex() while building the result. The same logical value therefore arrives in two different shapes depending on the endpoint. MessageListResponse.next_cursor and the cursor query parameter both use the plain hex form, so the value read from here must be unwrapped before it can be used as a cursor.
Absent on a message whose id was never assigned (the field is skipped when null).
external_idsender_phonesent_by_agent_idAgent who sent this outbound message. Absent on inbound and system-generated messages.
sent_by_agent_nameDenormalized display name of the sending agent
CRM-internal agent reactions. Never forwarded to the external channel. Absent when nobody has reacted.
error_messageWhy delivery failed — human-readable, only set when status is failed
error_codeProvider error code for the failure (e.g. "132001" from Meta)
error_sourceOrigin of the failure ("meta", "smtp", or "system")
error_detailsRaw provider error object (sanitized), for support/debugging
Send a WhatsApp template message in one call
Send a business-initiated WhatsApp template to a phone number without an
existing conversation_id. Auto-creates the contact and conversation, sends
the template, and logs it under a campaign (shown as "Sent via API") with
delivery tracking. Intended for third-party integrations.
Authenticate with X-API-Key (recommended) or a bearer JWT. Requires the
campaigns.broadcast permission.
Send a WhatsApp template message in one call › Request Body
toDestination phone in international format, no +.
templateApproved template name as registered with Meta.
languageTemplate language code. Optional if the name is unambiguous.
integration_account_idWhatsApp account to send from. Defaults to the active default.
campaign_nameLabel shown in the broadcast history. Defaults to "API:
body_paramsPositional values for body variables {{1}}..{{N}}.
button_url_paramValue substituted into a dynamic URL button's {{1}}.
Escape hatch — raw Meta Cloud API components array. When present it is
used verbatim and body_params/button_url_param are ignored.
Send a WhatsApp template message in one call › Responses
Template accepted and queued for delivery.
message_idMongoDB message id (hex).
conversation_idcampaign_idCampaign this send is logged under.
statusSearch messages across conversations
query Parameters
qSearch query (text content). Trimmed; a blank query returns an empty page.
conversation_idScope the search to a single conversation
channeldirectionfromStart of a created-at range filter
toEnd of a created-at range filter
pageper_pageSearch messages across conversations › Responses
Paginated search hits. Note this endpoint returns an envelope whose items are MessageSearchResult, NOT a bare array of Message.
totalpageper_pagePage size actually applied, clamped to 50
total_pages