Skip to main content
POST /conversations/{id}/messages is the API equivalent of an agent typing into the dashboard’s reply box. It posts a message from your team to the visitor — not from the bot.
This endpoint does not run the chatbot. It does not generate a response, consult the knowledge base, or call any tools. If you want bot output, you don’t want this endpoint — the bot only runs in response to visitor input through the widget.

What it does

1

Inserts an assistant-role message

The message lands on the thread with role: "assistant", attributed to your API key in metadata.source = "api". It renders in the visitor’s chat window immediately.
2

Performs human takeover

If the conversation was in agent_requested, status flips to open. The bot stops handling the slot — every subsequent visitor turn waits for an agent. A system marker message is inserted: “A team member has joined the conversation.”
3

Broadcasts in realtime

Connected clients (open browser tabs on the visitor’s side, other dashboard sessions) receive a realtime new_message event so they don’t have to poll.
4

Fires webhooks

message.created fires with the new message. If the status changed, conversation.updated also fires with the diff.
5

Schedules an away-email

If the visitor isn’t actively watching the tab and contact_email is set, an email notification is scheduled. The job rechecks presence before sending — replying many times in quick succession won’t fan out duplicate emails.

Allowed conversation states

The conversation must be in open or agent_requested. Any other status returns 400.
Unlike the dashboard UI, the API does not require the conversation to be assigned to a specific user. API keys operate at the org level. The conversation will remain unassigned after the reply unless a human picks it up.

Request

Body

string
required
The reply text. May be empty if you’re sending only attachments. Trimmed of leading/trailing whitespace.
array
Image attachments to include. Each entry is { "url": "https://..." }. See Attachments below for the full contract.

Attachments

You can include image attachments by passing a publicly-accessible URL. Awardee fetches the image, stores it in your organization’s asset library (content-addressed and de-duplicated), and references the stored copy on the resulting message_part row. The URL on the persisted message is ours, not yours — so the visitor’s chat tab keeps rendering the attachment even if your source link rots. Asset bytes are de-duplicated within your organization: posting the same screenshot to ten different conversations stores it once, and you’re billed for storage once. The file_url returned for the same image in different conversations will be identical.

Limits

  • v1 is image-only. Accepted MIME types: image/png, image/jpeg, image/webp, image/gif.
  • 15 MiB max per attachment.
  • Public http/https URL required. The url must use the http or https scheme; any other scheme (data:, ftp:, file:, …) is rejected. Awardee fetches the bytes server-side from your URL — no auth headers are forwarded. Private / loopback / link-local hosts are blocked (SSRF protection).
  • 15s fetch timeout. Slow or unresponsive sources return 422.

Error responses

File attachments (PDFs, video, etc.) will be supported in a future release. v1 only accepts images.

Response

Idempotency

Pass an Idempotency-Key header to make retries safe:
The first call with a given key is processed; subsequent calls within 24 hours return the cached response without re-posting. See Idempotency for the full contract.
Build your idempotency key from a stable identifier your system already has — an outbound message ID from your CRM, a queue job ID — not a random UUID generated at call time. That way an automatic retry of the whole job stays idempotent, not just an HTTP-level retry.

Common errors