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
  }
}
ArgumentTypeDescription
messageStringText content
recipientUserIdUUIDSend to a specific user (mutually exclusive with conversationId)
conversationIdUUIDSend to an existing conversation (recipient inferred from participants)
senderUserIdUUIDOverride the sender (defaults to the connection’s system user)
platformPlatformNameTarget platform (e.g. WHATSAPP, TELEGRAM, SLACK)
platformConnectionIdUUIDA specific platform connection, when several exist
fileIds[UUID!]File attachments (see below)
templateJSONTemplate message; WhatsApp only (see below)

Two rules are enforced:

  1. Content: at least one of message, template, or fileIds is required.
  2. Target: exactly one of recipientUserId or conversationId is 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 senderUserId if 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:

PriorityYou provideBehavior
1platformConnectionIdThat exact connection is used
2platformThe 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:

  1. One identity on an enabled connection → use it.
  2. 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.
  3. 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.

  • senderUserId overrides 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.inbound event 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 a data:<mime>;base64,... URI), stored in your configured storage backend
  • url: an already-hosted http(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
  }
}
FieldRequiredDescription
nameyesTemplate name as registered with the platform
languageyesLanguage code ("en", "en_US", …)
componentsnoComponent 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
  }
}
FieldSet when
sent_atThe platform accepted the message
delivered_atThe platform confirmed delivery to the device
read_atThe recipient read the message
failed_atDelivery failed (with error and error_type)

Two things to know about failures reported after the send:

  • The original event’s delivery_status gains failed_at, error, and error_type.
  • A separate system.message_delivery_failed event is emitted so webhook subscribers are notified. Subscribe to it alongside message.* 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:

ErrorCause / fix
Must provide at least one of message, template, or file_idsEmpty message; supply content
Must provide either recipient_user_id or conversation_id / Cannot provide bothFix your targeting arguments
Recipient user not found or not in current applicationWrong ID or wrong app scope
Recipient user does not have a <platform> accountThe user has no identity on the resolved platform; create one with createIdentity or choose another platform
Platform connection ... is disabled and cannot send messagesRe-enable the connection in the console or pick another via platformConnectionId
No <platform> connection found for this applicationYou passed platform but no connection of that type exists
No system user (bot) found for this <platform> connectionThe connection has no system-user identity; reconnect the platform or pass senderUserId
Cannot determine senderConversation targeting with no system user participant; pass senderUserId
Template messages can only be sent by system usersDon’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#