Sending Messages#
Complete reference for sending outbound messages with the sendMessage mutation: targeting, platform selection, file attachments, templates, delivery tracking, and error handling.
If you haven’t sent a message yet, start with Your First Message.
The sendMessage mutation#
mutation SendMessage(
$message: String
$recipientUserId: UUID
$conversationId: UUID
$senderUserId: UUID
$platform: PlatformName
$platformConnectionId: UUID
$fileIds: [UUID!]
$template: JSON
) {
sendMessage(
message: $message
recipientUserId: $recipientUserId
conversationId: $conversationId
senderUserId: $senderUserId
platform: $platform
platformConnectionId: $platformConnectionId
fileIds: $fileIds
template: $template
) {
id
eventTypeName
payload
createdAt
}
}| Argument | Type | Description |
|---|---|---|
message | String | Text content |
recipientUserId | UUID | Send to a specific user (mutually exclusive with conversationId) |
conversationId | UUID | Send to an existing conversation (recipient inferred from participants) |
senderUserId | UUID | Override the sender (defaults to the connection’s system user) |
platform | PlatformName | Target platform (e.g. WHATSAPP, TELEGRAM, SLACK) |
platformConnectionId | UUID | A specific platform connection, when several exist |
fileIds | [UUID!] | File attachments (see below) |
template | JSON | Template message; WhatsApp only (see below) |
Two rules are enforced:
- Content: at least one of
message,template, orfileIdsis required. - Target: exactly one of
recipientUserIdorconversationIdis required, never both and never neither.
The mutation returns the created message.outbound event.
Targeting: user vs conversation#
recipientUserId#
Send to a user directly. Outeract finds (or later auto-links) the conversation between your system user and the recipient:
mutation {
sendMessage(recipientUserId: "USER_ID", message: "Hi!") { id }
}If exactly one existing conversation matches the sender/recipient pair, the message is linked to it. If multiple conversations exist between the same pair, the message is sent but not auto-linked, so pass conversationId to target a specific thread.
conversationId#
Send into an existing conversation and let Outeract infer the recipient:
mutation {
sendMessage(conversationId: "CONVERSATION_ID", message: "Following up") { id }
}How inference works:
- The conversation must have at least 2 participants.
- The sender is the system user among the participants (or
senderUserIdif you pass one). - The recipient is the other participant.
If the conversation has no system user participant and you don’t pass senderUserId, the call fails with Cannot determine sender.
Choosing the platform and connection#
A recipient may have several identities (WhatsApp number, Telegram ID, Slack ID…). Outeract resolves which connection to send through in this order:
| Priority | You provide | Behavior |
|---|---|---|
| 1 | platformConnectionId | That exact connection is used |
| 2 | platform | The application’s first connection for that platform |
| 3 | (nothing) | Inferred from the recipient’s identities; see below |
When nothing is specified, the inference rules are:
- One identity on an enabled connection → use it.
- Multiple identities → use the platform of the recipient’s most recent
message.inbound(i.e. reply where they last wrote to you). If they’ve never messaged you, the first identity’s connection is used. - No identities → fall back to the application’s first platform connection.
When a user is reachable on more than one platform, relying on inference means replies follow the user's latest channel. Pass `platform` or `platformConnectionId` explicitly if you need deterministic routing, for example always sending notifications via email.
Identities on disabled connections are skipped during inference, and sending through a disabled connection (however selected) is rejected.
The sender#
By default the message is sent from the system user (bot) attached to the resolved platform connection, the business account identity created when the connection was set up.
senderUserIdoverrides this; the sender must have an identity on the resolved connection.- If the sender is a system user, the message goes out through the platform API as
message.outbound. - If the sender is a regular user, no platform API call is made. Outeract records a simulated
message.inboundevent instead (useful for testing inbound pipelines and webhook subscribers without a real device).
Sending files#
Files are first-class records. Create one with createFile (requires the events:write scope), then reference it by ID:
mutation {
createFile(
mimeType: "image/png"
data: "iVBORw0KGgoAAAANSUhEUg..." # base64, or a full data: URI
filename: "receipt.png"
description: "July receipt"
) {
id
url
}
}createFile accepts exactly one of:
data: base64-encoded bytes (or adata:<mime>;base64,...URI), stored in your configured storage backendurl: an already-hostedhttp(s)source, stored by reference and downloaded at send time
Then attach it:
mutation {
sendMessage(
recipientUserId: "USER_ID"
message: "Here's your receipt" # optional; fileIds alone is valid
fileIds: ["FILE_ID"]
) {
id
}
}Each file is linked to the message event with an attachment edge, so you can traverse from the event back to its files:
query {
event(id: "EVENT_ID") {
edges {
edgeType # "attachment"
targetNodeType # "file"
targetNodeId # resolve with files(fileIds:)
}
}
}All referenced files must exist in the current application and have status: "completed", otherwise the mutation fails before anything is sent. See File Attachments for more.
Template messages#
Templates are pre-approved message formats, currently WhatsApp only, required when messaging a user outside the 24-hour service window:
mutation {
sendMessage(
recipientUserId: "USER_ID"
template: {
name: "order_update"
language: "en_US"
components: [
{
type: "body"
parameters: [{ type: "text", text: "ORDER-1234" }]
}
]
}
) {
id
payload
}
}| Field | Required | Description |
|---|---|---|
name | yes | Template name as registered with the platform |
language | yes | Language code ("en", "en_US", …) |
components | no | Component list with parameter substitutions |
Templates can only be sent by system users (the default sender). Passing template together with a regular-user senderUserId is rejected. The stored event records the template under payload.template with a rendered text summary.
Delivery tracking#
sendMessage creates a message.outbound event whose payload carries a delivery_status object that Outeract updates in place as the platform reports receipts:
{
"type": "message",
"message": { "text": "Hi!", "role": "assistant" },
"platform": "whatsapp",
"external_message_id": "wamid.HBgL...",
"delivery_status": {
"sent_at": "2026-07-14T10:30:01.123+00:00",
"delivered_at": "2026-07-14T10:30:02.456+00:00",
"read_at": null,
"failed_at": null,
"error": null,
"error_type": null
}
}| Field | Set when |
|---|---|
sent_at | The platform accepted the message |
delivered_at | The platform confirmed delivery to the device |
read_at | The recipient read the message |
failed_at | Delivery failed (with error and error_type) |
Two things to know about failures reported after the send:
- The original event’s
delivery_statusgainsfailed_at,error, anderror_type. - A separate
system.message_delivery_failedevent is emitted so webhook subscribers are notified. Subscribe to it alongsidemessage.*patterns. See Outbound Webhooks.
If the platform API rejects the message at send time, the message.outbound event is still kept (with delivery_status.failed_at and the error recorded) so failed attempts remain visible in the event stream, and the mutation returns a GraphQL error.
Error cases#
Common sendMessage errors and what they mean:
| Error | Cause / fix |
|---|---|
Must provide at least one of message, template, or file_ids | Empty message; supply content |
Must provide either recipient_user_id or conversation_id / Cannot provide both | Fix your targeting arguments |
Recipient user not found or not in current application | Wrong ID or wrong app scope |
Recipient user does not have a <platform> account | The user has no identity on the resolved platform; create one with createIdentity or choose another platform |
Platform connection ... is disabled and cannot send messages | Re-enable the connection in the console or pick another via platformConnectionId |
No <platform> connection found for this application | You passed platform but no connection of that type exists |
No system user (bot) found for this <platform> connection | The connection has no system-user identity; reconnect the platform or pass senderUserId |
Cannot determine sender | Conversation targeting with no system user participant; pass senderUserId |
Template messages can only be sent by system users | Don’t combine template with a regular-user sender |
Failed to send message via <platform>: ... | The platform API rejected the send; check the error detail and the event’s delivery_status |
A frequent real-world failure is the messaging window: platforms like WhatsApp reject free-form messages sent more than 24 hours after the user’s last message (error_type: "messaging_window_closed"). Use a template in that case.
See also#
- Your First Message - guided walkthrough
- Message Handling - what happens to a message inside Outeract
- Inbound Messages - receiving replies
- Events - the event ledger and edge model
- GraphQL API - full API reference