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.
Current statusWhat POST /messages does
openPosts the reply on the active thread.
agent_requestedPerforms human takeover, then posts the reply.
bot_activeRejected. Use PATCH to move to agent_requested or open first.
resolved / closed / archivedRejected. 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

curl -X POST https://api.awardee.dev/v1/conversations/8f3a9d2e-1b4c-4f5d-9e8a-7c3b2a1d0f9e/messages \
  -H "Authorization: Bearer aw_live_..." \
  -H "Content-Type: application/json" \
  -d '{
    "text": "Hi June — sorry about the damaged item. I just emailed you a prepaid return label."
  }'

Body

text
string
required
The reply text. May be empty if you’re sending only attachments. Trimmed of leading/trailing whitespace.
attachments
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.
curl -X POST https://api.awardee.dev/v1/conversations/{id}/messages \
  -H "Authorization: Bearer aw_live_..." \
  -H "Content-Type: application/json" \
  -d '{
    "text": "Here's a screenshot of the dashboard view you were asking about:",
    "attachments": [
      { "url": "https://your-cdn.example.com/screenshots/dashboard.png" }
    ]
  }'

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

StatusCodeWhen
422invalid_payloadURL 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

{
  "object": "message",
  "id": "b7e9f1a2-3c4d-4e5f-9a8b-7c6d5e4f3a2b",
  "conversation_id": "8f3a9d2e-1b4c-4f5d-9e8a-7c3b2a1d0f9e",
  "role": "assistant",
  "metadata": {
    "source": "api",
    "api_key_id": "9d8c7b6a-5e4f-4d3c-9b8a-7c6e5d4f3a2b",
    "agent_name": null
  },
  "parts": [
    {
      "object": "message_part",
      "id": "1a2b3c4d-5e6f-4a7b-9c8d-0e1f2a3b4c5d",
      "type": "text",
      "order": 0,
      "text": "Hi June — sorry about the damaged item. I just emailed you a prepaid return label.",
      "tool_name": null,
      "tool_call_id": null,
      "tool_input": null,
      "tool_output": null,
      "tool_error_text": null,
      "file_url": null,
      "file_media_type": null,
      "file_filename": null,
      "created_at": "2026-05-22T14:11:08+00:00"
    }
  ],
  "created_at": "2026-05-22T14:11:08+00:00"
}

Idempotency

Pass an Idempotency-Key header to make retries safe:
-H "Idempotency-Key: reply-orders-4421-attempt-1"
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

StatusCodeWhen
400validation_errorBody missing both text and attachments.
400invalid_stateConversation is in a status that doesn’t accept agent replies.
404not_foundConversation ID doesn’t exist or isn’t in your org.
429rate_limit_exceededYour organization’s shared rate window was exhausted. Honor the Retry-After header. See Rate limits.