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.
What it does
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.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.”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.Fires webhooks
message.created fires with the new message. If the status changed, conversation.updated also fires with the diff.Allowed conversation states
The conversation must be inopen or agent_requested. Any other status returns 400.
| Current status | What POST /messages does |
|---|---|
open | Posts the reply on the active thread. |
agent_requested | Performs human takeover, then posts the reply. |
bot_active | Rejected. Use PATCH to move to agent_requested or open first. |
resolved / closed / archived | Rejected. Reopen by PATCHing status back to open first. |
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
The reply text. May be empty if you’re sending only attachments. Trimmed of leading/trailing whitespace.
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 resultingmessage_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/httpsURL required. Theurlmust use thehttporhttpsscheme; 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
| Status | Code | When |
|---|---|---|
422 | invalid_payload | URL couldn’t be fetched, wasn’t an image, was too large, timed out, or pointed at an unsafe host. The url of the offending attachment is included in details. |
File attachments (PDFs, video, etc.) will be supported in a future release. v1 only accepts images.
Response
Idempotency
Pass anIdempotency-Key header to make retries safe:
Common errors
| Status | Code | When |
|---|---|---|
400 | validation_error | Body missing both text and attachments. |
400 | invalid_state | Conversation is in a status that doesn’t accept agent replies. |
404 | not_found | Conversation ID doesn’t exist or isn’t in your org. |
429 | rate_limit_exceeded | Your organization’s shared rate window was exhausted. Honor the Retry-After header. See Rate limits. |

