Skip to content

Send a message ​

Send a manual message from the authenticated user into a conversation. The message is persisted, delivered through the conversation's connected channel (Meta / Waha / Unipile), and broadcast to any subscribed socket clients.

You can send text, attachments, or both — at least one is required.

POST/api/user/{conversation}/messages
Required scope: messages:send
Path parameters
integer
Request body (JSON)
curl -X POST "https://loopx-console.test/api/user/89328/messages" \
  -H "Authorization: Bearer $TOKEN" \
  -H "Accept: application/json" \
  -H "Content-Type: application/json" \
  -d '{"message":"Hi Ali, following up on your pricing question — happy to jump on a quick call this week."}'
<?php

use GuzzleHttp\Client;

$client = new Client();
$response = $client->request('POST', 'https://loopx-console.test/api/user/89328/messages', [
    'headers' => [
        'Authorization' => 'Bearer $TOKEN',
        'Accept'        => 'application/json',
    ],
    'json' => [
        'message' => 'Hi Ali, following up on your pricing question — happy to jump on a quick call this week.'
    ]
]);

$data = json_decode($response->getBody(), true);
const res = await fetch('https://loopx-console.test/api/user/89328/messages', {
  method: 'POST',
  headers: {
    Authorization: 'Bearer $TOKEN',
    Accept: 'application/json',
    'Content-Type': 'application/json'
  },
  body: JSON.stringify({
    "message": "Hi Ali, following up on your pricing question — happy to jump on a quick call this week."
  })
});

const data = await res.json();

Example response

{
  "status": true,
  "code": 200,
  "validation": [],
  "message": "Message Sent",
  "data": {
    "_id": "68e0f4a3c2b1a9f0d3e5a7b2",
    "role": "assistant",
    "status": "sent_to_user",
    "message": "Hi Ali, following up on your pricing question — happy to jump on a quick call this week.",
    "cost_for_user": 0,
    "external_chat_id": "120248819452900116",
    "conversation_id": 89328,
    "opportunity_id": 98314,
    "contact_id": 44016,
    "channel_id": 512,
    "stage_id": 189,
    "pipeline_id": 17,
    "call_type": "chat",
    "sender_id": 731,
    "files": [],
    "created_at": "2026-07-15T10:22:41.000000Z",
    "updated_at": "2026-07-15T10:22:43.000000Z"
  }
}

The runnable widget above sends JSON only

Attachments require a multipart/form-data request — use the curl / PHP / Node examples in the Sending attachments section below.

Path parameters ​

NameTypeRequiredDescription
conversationintegeryesConversation id — must belong to your workspace (or a workspace you manage)

Request body ​

Provide message, files, or both. Sending neither returns a 422.

FieldTypeDescription
messagestring | nullMessage text. Max 10,000 characters.
filesfile[] | nullUp to 10 files, each ≤ 25 MB. Requires multipart/form-data.

Sending attachments ​

Attachments must be uploaded as multipart/form-data — one form field per file, repeating the files[] key. message becomes a regular form field alongside them (and stays optional as long as at least one file is present).

Accepted content: any file type. The server infers a bucket from the MIME type — image/* → image, video/* → video, audio/* → audio, application/* → document, everything else → file. Uploaded files are stored on private S3 and served through signed URLs; you never need to host them yourself.

curl ​

bash
curl -X POST "https://api.genudo.ai/api/user/89328/messages" \
  -H "Authorization: Bearer $TOKEN" \
  -H "Accept: application/json" \
  -F "message=Here are the specs you asked for" \
  -F "files[]=@./pricing.pdf" \
  -F "files[]=@./product-shot.png"

PHP (Guzzle) ​

php
<?php

use GuzzleHttp\Client;

$client = new Client();
$response = $client->request('POST', 'https://api.genudo.ai/api/user/89328/messages', [
    'headers' => [
        'Authorization' => 'Bearer '.$token,
        'Accept'        => 'application/json',
    ],
    'multipart' => [
        ['name' => 'message',  'contents' => 'Here are the specs you asked for'],
        ['name' => 'files[]',  'contents' => fopen('./pricing.pdf', 'r'),      'filename' => 'pricing.pdf'],
        ['name' => 'files[]',  'contents' => fopen('./product-shot.png', 'r'), 'filename' => 'product-shot.png'],
    ],
]);

Node.js ​

js
import fs from 'node:fs'

const form = new FormData()
form.append('message', 'Here are the specs you asked for')
form.append('files[]', new Blob([fs.readFileSync('./pricing.pdf')]),      'pricing.pdf')
form.append('files[]', new Blob([fs.readFileSync('./product-shot.png')]), 'product-shot.png')

const res = await fetch('https://api.genudo.ai/api/user/89328/messages', {
  method: 'POST',
  headers: {
    Authorization: `Bearer ${token}`,
    Accept: 'application/json',
    // Do NOT set Content-Type — fetch sets the multipart boundary for you.
  },
  body: form,
})

Response shape with attachments ​

Every uploaded file becomes an entry under data.files:

json
{
  "status": true,
  "code": 200,
  "message": "Message Sent",
  "data": {
    "_id": "68e0f4a3c2b1a9f0d3e5a7b2",
    "message": "Here are the specs you asked for",
    "status": "sent_to_user",
    "files": [
      {
        "path": "media/512/68e0f4a3c2b1a9f0d3e5a7b2.pdf",
        "mimetype": "application/pdf",
        "type": "document",
        "size": 184320,
        "name": "pricing.pdf"
      },
      {
        "path": "media/512/68e0f4a3c2b1a9f0d3e5a7b2_1.png",
        "mimetype": "image/png",
        "type": "image",
        "size": 42117,
        "name": "product-shot.png"
      }
    ]
  }
}

Response ​

The full persisted message record (Mongo document) is returned. Notable fields:

FieldTypeDescription
_idstringMongo message id
rolestringAlways assistant for messages sent through this endpoint
statusenumsent_to_user on success, failed if delivery through the channel raised (see error field), created_manually briefly during processing
messagestring | nullThe text you sent
filesobject[]Uploaded attachments — path (private S3 key), mimetype, type (image / video / audio / document / file), size, name
external_chat_idstringThe thread id on the origin channel (Meta thread id, WhatsApp phone, etc.)
conversation_id, opportunity_id, contact_id, channel_id, stage_id, pipeline_idintegerRelational context resolved from the conversation
sender_idintegerThe authenticated user id (or the acted-as user when using super-user impersonation)

Side effects ​

  • conversations.last_message_at and conversations.last_message_content are updated (text falls back to [Attachment] when only files are sent).
  • A new_message event is fired on the conversation.{id} socket channel — subscribed socket clients receive the new message in real time.
  • Delivery to the underlying provider is synchronous — the request only returns after the message has been handed off to Meta / Waha / Unipile.

Errors ​

CodeWhen
403Token missing messages:send scope, or the conversation belongs to a different workspace
404Conversation not found
422Neither message nor files provided, message over 10k chars, a file over 25 MB, or more than 10 files
500The channel provider (Meta / Waha / Unipile) rejected the message — the persisted record's status becomes failed and its error field contains the provider error