Skip to main content
POST
/
v1
/
api
/
conversations
/
{id}
/
messages
Post a message to a conversation
curl --request POST \
  --url https://api.hq.zone/v1/api/conversations/{id}/messages \
  --header 'Authorization: Bearer <token>' \
  --header 'Content-Type: application/json' \
  --data '
{
  "text": "<string>",
  "attachments": [
    {
      "content_type": "<string>",
      "data_b64": "<string>",
      "document_id": "3c90c3cc-0d44-4b50-8888-8dd25736052a",
      "filename": "<string>"
    }
  ],
  "browser_session": true,
  "idempotency_key": "<string>",
  "identity": "<unknown>",
  "turn_source": "<string>"
}
'
{
  "accepted": true,
  "idempotent": true,
  "stream_url": "<string>"
}

Asynchronous turn — read the reply from the event stream

This endpoint does not return the agent’s reply. It accepts the message, starts the turn server-side, and returns immediately with 202 Accepted. You observe progress and read the final reply on the conversation’s event stream. Typical flow:
  1. Open GET /v1/api/conversations/{id}/stream (SSE).
  2. POST the message here → 202 Accepted.
  3. Read this turn’s text_deltafinal events off the stream.
Send an Idempotency-Key header to make retries safe — the same key on the same conversation returns the original turn instead of starting a duplicate (deduplicated for ~1 hour).
To stop a turn that’s already running, see Interrupt the active turn.

Authorizations

Authorization
string
header
required

Personal Access Token. Send as Authorization: Bearer hq_pat_....

Path Parameters

id
string<uuid>
required

Conversation id

Body

application/json

Body for the 202-style submit endpoint. A slim subset of [PostMessageBody]: agent / tenant / channel are derived from the conversation row, so the caller never has to know them.

text
string
required
attachments
object[]

File attachments (base64-encoded). Same shape as the inline endpoint; carried through to TurnPayload.attachments.

browser_session
boolean

True iff this is a local-browser turn (the extension's "Use my browser" mode). Fails fast if the user's browser isn't connected, then scopes the turn to computer_* + injects the browser prompt via the per-turn browser_session Forge flag - exactly like POST /v1/agent-browser/ask, but on the streaming path so the computer_* tool calls render live.

idempotency_key
string | null

Optional dedup token. Same semantics as the inline endpoint: re-POST with the same key inside the 24h TTL is a no-op and returns idempotent: true. Caller should generate a UUID per logical submit (e.g. per Studio chat-send button click).

identity
null | any

Free-form identity claims from the caller. PAT auth (#729) will replace this with auth-derived chain; for now the trust boundary is network-level reach to controlplane.

turn_source
string | null

Reply-to-source channel for this turn. Defaults to "web"; the browser extension (#729) sets "extension" so artifacts/replies aren't pushed back into a Slack thread the conv may have been born in. Drives conversations.last_turn_channel via turn.rs.

Response

Turn accepted (spawned in the background); attach to stream_url for events

accepted
boolean
required
idempotent
boolean
required

true when the Idempotency-Key collided with a prior submit inside the 24h dedup window. The caller should attach to stream_url to read the original turn's events from the journal; no new turn was spawned.

stream_url
string
required