> ## Documentation Index
> Fetch the complete documentation index at: https://docs.hq.zone/llms.txt
> Use this file to discover all available pages before exploring further.

# Post a message to a conversation

> `POST /v1/api/conversations/{id}/messages` -- Phase A.4 (#726).

Decoupled from the SSE stream: returns `202 Accepted` after
idempotency check, spawns the turn driver in the background,
caller observes events via `GET /v1/api/conversations/{id}/stream`
(already attached or about to attach).

Idempotency is decided IN THIS HANDLER, before spawning. The
spawn passes `idempotency_key: None` so the inner pipeline's
`prepare_turn` skips its own re-check -- avoids a double
SETNX that would log a spurious "duplicate" on the fresh
path.

## 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](/api-reference/conversations/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_delta` … `final` events off the stream.

<Tip>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).</Tip>

To stop a turn that's already running, see [Interrupt the active turn](/api-reference/conversations/interrupt).


## OpenAPI

````yaml POST /v1/api/conversations/{id}/messages
openapi: 3.1.0
info:
  title: HQ API
  description: >-
    Public HTTP API for HQ. Authenticate with a Personal Access Token
    (`Authorization: Bearer hq_pat_...`) for server-side integrations, or an
    OAuth 2.1 authorization-code + PKCE flow for browser apps acting on a user's
    behalf. Both grant from the same resource:action scope vocabulary; an
    endpoint's required scope is listed under its `security`.
  license:
    name: Apache-2.0
    identifier: Apache-2.0
  version: 1.0.0
servers:
  - url: https://api.hq.zone
    description: HQ API (production)
security: []
tags:
  - name: me
    description: The signed-in user's own account
  - name: conversations
    description: Conversations and their messages
  - name: documents
    description: The content-addressed documents library
  - name: schedules
    description: Scheduled prompts and recurring tasks
  - name: agents
    description: Agents, their skills and integrations
  - name: memory
    description: What the assistant remembers (L5 governance)
  - name: tokens
    description: Personal Access Token management
  - name: billing
    description: Usage and billing
  - name: notifications
    description: In-app notification center
  - name: admin
    description: Workspace administration
  - name: integrations
    description: Workspace integrations (Slack, MCP, skills)
  - name: onboarding
    description: New-workspace onboarding wizard
  - name: auth
    description: Sign-in, sessions, and OAuth
paths:
  /v1/api/conversations/{id}/messages:
    post:
      tags:
        - conversations
      summary: Post a message to a conversation
      description: |-
        `POST /v1/api/conversations/{id}/messages` -- Phase A.4 (#726).

        Decoupled from the SSE stream: returns `202 Accepted` after
        idempotency check, spawns the turn driver in the background,
        caller observes events via `GET /v1/api/conversations/{id}/stream`
        (already attached or about to attach).

        Idempotency is decided IN THIS HANDLER, before spawning. The
        spawn passes `idempotency_key: None` so the inner pipeline's
        `prepare_turn` skips its own re-check -- avoids a double
        SETNX that would log a spurious "duplicate" on the fresh
        path.
      operationId: post_message
      parameters:
        - name: id
          in: path
          description: Conversation id
          required: true
          schema:
            type: string
            format: uuid
      requestBody:
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/SubmitMessageBody'
        required: true
      responses:
        '202':
          description: >-
            Turn accepted (spawned in the background); attach to `stream_url`
            for events
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/SubmitResponse'
        '404':
          description: No such conversation for the caller
      security:
        - bearer_pat: []
        - oauth2: []
components:
  schemas:
    SubmitMessageBody:
      type: object
      description: |-
        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.
      required:
        - text
      properties:
        attachments:
          type: array
          items:
            $ref: '#/components/schemas/AttachmentInput'
          description: |-
            File attachments (base64-encoded). Same shape as the inline
            endpoint; carried through to TurnPayload.attachments.
        browser_session:
          type: boolean
          description: >-
            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:
          type:
            - string
            - 'null'
          description: |-
            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:
          oneOf:
            - type: 'null'
            - $ref: '#/components/schemas/Value'
              description: |-
                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.
        text:
          type: string
        turn_source:
          type:
            - string
            - 'null'
          description: >-
            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`.
    SubmitResponse:
      type: object
      required:
        - accepted
        - stream_url
        - idempotent
      properties:
        accepted:
          type: boolean
        idempotent:
          type: boolean
          description: |-
            `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:
          type: string
    AttachmentInput:
      type: object
      description: |-
        One file attached to an inbound user message. Two delivery
        modes, mutually exclusive:

          * **Inline bytes** (`data_b64`) - legacy path. The caller already
            has the bytes in hand (e.g. slack-transport just downloaded
            them from Slack) and ships them directly in the request body.
            Bounded by the JSON body limit.

          * **Document reference** (`document_id`) - SOTA path. The file
            is already in Trove behind a stable `documents` row (Studio
            drag-drop, an earlier `documents.save`, an OAuth-shared file).
            The turn driver resolves the id, loads bytes from Trove, and
            builds the same downstream `Attachment` shape -- the agent
            doesn't see the distinction.

        `filename` is optional only on the `document_id` path (the row
        already carries one); inline callers must supply it. `content_type`
        follows the same rule.
      properties:
        content_type:
          type:
            - string
            - 'null'
        data_b64:
          type:
            - string
            - 'null'
          description: Inline base64 bytes. Provide this OR `document_id`, not both.
        document_id:
          type:
            - string
            - 'null'
          format: uuid
          description: |-
            Reference to an existing `documents` row in the caller's tenant.
            The turn driver loads bytes from Trove server-side.
        filename:
          type:
            - string
            - 'null'
    Value: {}
  securitySchemes:
    bearer_pat:
      type: http
      scheme: bearer
      bearerFormat: hq_pat
      description: 'Personal Access Token. Send as `Authorization: Bearer hq_pat_...`.'
    oauth2:
      type: oauth2
      flows:
        authorizationCode:
          authorizationUrl: https://app.hq.zone/v1/oauth/authorize
          tokenUrl: https://api.hq.zone/v1/oauth/token
          refreshUrl: https://api.hq.zone/v1/oauth/token
          scopes:
            admin: Administer the workspace (users, settings, integrations)
            agents:read: View the agents in your workspace
            agents:write: Create and configure agents
            billing:read: View usage and billing information
            conversations:read: Read your conversations and their messages
            conversations:write: Start conversations and send messages on your behalf
            documents:read: Read your documents library
            documents:write: Upload and manage documents in your library
            memory:read: Read what the assistant remembers about you
            memory:write: Correct or delete what the assistant remembers
            schedules:read: View your scheduled tasks
            schedules:write: Create and manage scheduled tasks
            tables:read: Read your tables and their rows
            tables:write: Create tables and add, edit, or delete rows

````