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

# Post apiv2conversations messages

> `GET`/`POST /api/v2/conversations/{id}/messages` (§10, §11).

**Required scope:** `conversations:write`. A key without it is a 403 naming the scope; the endpoint is never silently skipped.

**Idempotency.** Send `Idempotency-Key` to make a retry safe.

- *Same key, same body* → the stored response, byte for byte, including its status. Some resources therefore replay **200** where the first call answered **201** or **202**: the second call created nothing, and saying otherwise would be a lie a client acts on. `Idempotency-Replayed: true` is on the replay, which is how a client answers "did my retry actually run?" from the response alone. **The header is not spelled the same on every endpoint** — this one uses `Idempotency-Replayed`. Read each operation rather than assuming one name across the surface.
- *Same key, different body* → **409** `idempotency_conflict`. Never a replay: returning the earlier answer for a request that does not match it turns a client-side key collision into a silently wrong result.

A key is scoped by endpoint, environment and caller identity — never by tenant alone, so two end users of one embedded application both sending `Idempotency-Key: 1` cannot meet.



## OpenAPI

````yaml /openapi/v2.yaml post /api/v2/conversations/{conversation_id}/messages
openapi: 3.0.3
info:
  description: >
    The Impel Labs public API, version 2.


    Every operation here is reachable with a machine credential and demands a

    scope, named on the operation as `x-required-scope`. Scopes are not implied
    by

    one another: a key that may read conversations cannot run an agent, and a
    key

    that may edit an assistant cannot preview its drafts.


    ## Streaming


    Set `"stream": true` on any execution endpoint and the body is

    `text/event-stream` instead of JSON. One contract for all three

    endpoints — `/responses`, `/conversations/{id}/messages` and

    `/agents/{id}/runs` — so an event means the same thing wherever it arrives.


    Event types (contract `2026-09-07`):


    - `response.started`

    - `response.output_text.delta`

    - `response.output_json.delta`

    - `response.tool.started`

    - `response.tool.completed`

    - `response.tool.failed`

    - `response.completed`

    - `response.failed`

    - `response.cancelled`


    Exactly one terminal event is emitted per stream, ever:
    `response.cancelled`, `response.completed`, `response.failed`. A stream

    that ends without one is a bug on this side, not a state to handle.


    A line beginning `:` is a comment, sent about every

    15 seconds so that proxies do not close an idle

    connection. Comments are not events and must be skipped, not parsed.


    **Failures before the first event are not streamed.** No credential, no

    credits, an unpublished agent, a schema this runtime cannot enforce: each is

    the ordinary JSON error with its own status code, because nothing has been

    written to the wire yet and §36 says not to answer 200 for a failed
    execution.

    Once `response.started` is on the wire the response is already a 200, and a

    later failure arrives as `response.failed` carrying the same

    `{"error": {code, message, request_id}}` payload.


    A client that disconnects is not billed differently and does not leave a run

    running forever: the run is cancelled and settles once, like any other.


    ## Structured output


    Ask for structured output (§31). `json_schema` validates the model's output
    against `schema` and retries a mismatch once before failing with **422**
    `structured_output_failed`, which names the paths that did not match. The
    bound is read before the run starts, so the number of model calls a
    structured request can cost is knowable in advance.


    The schema is checked when the request is parsed — before authorization,
    before the idempotency claim, and before any spend — so a schema this
    runtime cannot enforce is a 400 rather than a charge. `strict` is accepted
    only as `true`: there is no mode in which a schema is requested and not
    enforced.


    `POST /api/v2/reports` accepts `format: "json"` with a `schema`.


    ## Preview and drafts


    Run the agent's unpublished configuration (§94). **Asking is free; being
    allowed is not.** A machine caller needs the `agent:preview` scope, which is
    issued on its own and is not implied by any other — a key that may edit an
    assistant does not thereby preview its drafts.


    Without permission the answer is **404, not 403**: confirming that a draft
    exists but may not be run is the confirmation the not-found rule exists to
    withhold.


    Statuses a preview run may execute: `active`, `draft`, `paused`, `preview`.
    Without preview, only `active`.


    ## Errors


    Every failure is `{"error": {"code", "message", "request_id"}}`. Switch on

    `code`; `message` is prose. A failure that ends a run that had already
    started

    also carries `id`, `status` and `trace_id` (see `RunError`).


    | code | HTTP | run status |

    | --- | --- | --- |

    | `agent_not_available` | 409 | `failed` |

    | `agent_not_found` | 404 | `failed` |

    | `authentication_error` | 401 | `failed` |

    | `conversation_not_found` | 404 | `failed` |

    | `idempotency_conflict` | 409 | `failed` |

    | `internal_error` | 500 | `failed` |

    | `invalid_context` | 403 | `failed` |

    | `invalid_request` | 400 | `failed` |

    | `knowledge_unavailable` | 503 | `failed` |

    | `not_implemented` | 501 | `failed` |

    | `permission_denied` | 403 | `failed` |

    | `provider_unavailable` | 502 | `failed` |

    | `rate_limited` | 429 | `blocked` |

    | `run_not_found` | 404 | `failed` |

    | `structured_output_failed` | 422 | `failed` |

    | `tool_execution_failed` | 502 | `failed` |

    | `usage_blocked` | 402 | `blocked` |


    Two of those are decisions rather than faults, and both end a run as
    `blocked`:

    `usage_blocked` (**402** — the workspace cannot spend) and `rate_limited`

    (**429**). Filing either as `failed` is what makes an error-rate dashboard

    useless and a support answer wrong.


    A resource in another workspace and a resource that does not exist answer

    **404** identically, byte for byte. There is no 403 that confirms existence.


    ## Paging


    Cursor-based, `?limit=&after=` where the endpoint says so. Cursors are
    opaque

    base64url — do not build one. A raw ISO timestamp is not a cursor: `+00:00`

    decodes to a space in a query string, which silently breaks every second
    page.


    The v2 surface does **not** yet use one cursor name everywhere. `/logs`
    returns

    `next`; `/documents`, `/extractions`, `/reports` and `/knowledge/ingestions`

    return `next_cursor` and read it back as `cursor`;
    `/conversations/{id}/messages`

    returns `next_cursor` and reads it back as `after`, and its cursor is a
    message

    id rather than an encoded pair; `/jobs` pages by `offset` and returns no
    cursor

    at all. Each operation documents what it actually takes.


    ## Idempotency


    Send `Idempotency-Key` on any operation that lists it. Same key and same
    body

    replays the stored response including its status — so some resources replay

    **200** where they first answered **201** or **202**. Same key and a
    different

    body is **409**, never a replay.
  title: Impel Labs API v2
  version: 2.0.0
servers: []
security: []
paths:
  /api/v2/conversations/{conversation_id}/messages:
    post:
      tags:
        - conversations
      description: >-
        `GET`/`POST /api/v2/conversations/{id}/messages` (§10, §11).


        **Required scope:** `conversations:write`. A key without it is a 403
        naming the scope; the endpoint is never silently skipped.


        **Idempotency.** Send `Idempotency-Key` to make a retry safe.


        - *Same key, same body* → the stored response, byte for byte, including
        its status. Some resources therefore replay **200** where the first call
        answered **201** or **202**: the second call created nothing, and saying
        otherwise would be a lie a client acts on. `Idempotency-Replayed: true`
        is on the replay, which is how a client answers "did my retry actually
        run?" from the response alone. **The header is not spelled the same on
        every endpoint** — this one uses `Idempotency-Replayed`. Read each
        operation rather than assuming one name across the surface.

        - *Same key, different body* → **409** `idempotency_conflict`. Never a
        replay: returning the earlier answer for a request that does not match
        it turns a client-side key collision into a silently wrong result.


        A key is scoped by endpoint, environment and caller identity — never by
        tenant alone, so two end users of one embedded application both sending
        `Idempotency-Key: 1` cannot meet.
      operationId: post_conversations_by_conversation_id_messages
      parameters:
        - in: path
          name: conversation_id
          required: true
          schema:
            type: string
        - description: >-
            A key of your own choosing, so a retry cannot create a second of
            anything. Replayable for 24 hours. An unfinished claim is released
            after 15 minutes, so a request that died mid-flight does not block
            its own retry.
          in: header
          name: Idempotency-Key
          required: false
          schema:
            maxLength: 255
            type: string
      requestBody:
        content:
          application/json:
            schema:
              allOf:
                - $ref: '#/components/schemas/RuntimeInput'
                - $ref: '#/components/schemas/RuntimeOptions'
                - properties:
                    agent:
                      description: >-
                        The assistant to run. Required by `/responses`; taken
                        from the URL by `/agents/{agent_id}/runs`, and from the
                        conversation when posting a message — a body that named
                        a different one there would be a request to run somebody
                        else's assistant on this transcript.
                      type: string
                    external_ref:
                      description: >-
                        Your own id for a conversation (§46). Resolving the same
                        one twice returns the same conversation rather than a
                        second beside it. Printable, no newlines.
                      maxLength: 200
                      type: string
                    options:
                      $ref: '#/components/schemas/RuntimeOptions'
                  type: object
        required: true
      responses:
        '200':
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/RuntimeResponse'
            text/event-stream:
              schema:
                description: >-
                  Server-Sent Events. One `event:` line naming a type from the
                  contract, one `data:` line of JSON, a blank line between
                  frames. Exactly one of `response.cancelled`,
                  `response.completed`, `response.failed` ends the stream. Lines
                  beginning `:` are keep-alive comments and must be skipped
                  rather than parsed.
                type: string
          description: >-
            The run completed, or ended in a status the response object names.
            With `"stream": true` the body is Server-Sent Events instead; see
            the event contract in this document's description. A failure that
            happens **before** the first event is an ordinary JSON error with
            its own status — the stream only becomes a 200 once a byte is on the
            wire.
        '400':
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/RunError'
          description: >-
            The request could not be understood. An unknown key inside `options`
            is refused by name rather than ignored — a silently dropped option
            is three support tickets away from being noticed.


            Codes with this status: `invalid_request`
        '401':
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/RunError'
          description: |-
            No credential, or one that does not resolve.

            Codes with this status: `authentication_error`
        '402':
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/RunError'
          description: >-
            **Not in §36's list, and deliberate.** The workspace cannot spend:
            out of credits, or refused by the billing gate. 403 would say the
            caller lacks permission, which is a different fix; 429 would say
            retry, which will never help. The run ends `blocked`, not `failed`.


            Codes with this status: `usage_blocked`
        '403':
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/RunError'
          description: >-
            The credential is valid and does not carry the scope this operation
            demands, or the request context named something the caller does not
            hold. **Never** returned for a resource in another tenant — that is
            404, so that guessing ids cannot become an inventory.


            Codes with this status: `invalid_context`, `permission_denied`
        '404':
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/RunError'
          description: >-
            No such resource **for this caller**. A resource that exists in
            another workspace and a resource that does not exist at all answer
            identically, byte for byte.


            Codes with this status: `agent_not_found`, `conversation_not_found`,
            `run_not_found`
        '409':
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/RunError'
          description: >-
            The request is fine and the state is not: an agent that is not
            published, a conversation with no assistant, or an `Idempotency-Key`
            already used for a *different* body.


            Codes with this status: `agent_not_available`,
            `idempotency_conflict`
        '422':
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/RunError'
          description: >-
            Semantic validation failed — the shape parsed and the content did
            not.


            Codes with this status: `structured_output_failed`
        '429':
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/RunError'
          description: >-
            Rate limited. Like 402 this ends a run as `blocked` rather than
            `failed`. Honour `Retry-After` when it is present.


            Codes with this status: `rate_limited`
        '500':
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/RunError'
          description: >-
            Something failed on this side. The response carries a `request_id`;
            nothing else about the failure is disclosed.


            Codes with this status: `internal_error`
        '502':
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/RunError'
          description: >-
            An upstream the runtime depends on failed — a model provider, or a
            tool this run required.


            Codes with this status: `provider_unavailable`,
            `tool_execution_failed`
        '503':
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/RunError'
          description: >-
            A dependency is temporarily unavailable. Knowledge retrieval answers
            this rather than silently returning an unretrieved answer.


            Codes with this status: `knowledge_unavailable`
      security:
        - CompanyApiKey: []
        - ApplicationCredential: []
        - DashboardSession: []
components:
  schemas:
    RuntimeInput:
      description: >-
        The runtime input (§14). It carries no identity field of any kind — who
        the caller is comes from the credential and never from the body.
        `request_context` refuses the reserved keys below by name rather than
        dropping them silently:


        `api_key`, `application`, `application_id`, `billed_to`, `billing`,
        `company`, `company_id`, `credential`, `credential_id`, `entitlements`,
        `environment`, `environment_id`, `partner`, `partner_id`, `permissions`,
        `plan`, `principal`, `principal_id`, `principal_type`, `role`, `roles`,
        `scope`, `scopes`, `subject`, `subject_id`, `tenant`, `tenant_id`,
        `token`, `usage_category`
      properties:
        attachments:
          items:
            properties:
              mime_type:
                maxLength: 128
                type: string
              name:
                maxLength: 255
                type: string
              ref:
                description: A reference to something already stored. Never bytes.
                maxLength: 512
                type: string
              type:
                enum:
                  - file
                  - image
                  - audio
                  - video
                  - document
                type: string
            required:
              - ref
            type: object
          type: array
        channel:
          description: >-
            How the request arrived. `""` means "not stated" and falls through
            to the conversation's own channel — it is not a synonym for `api`,
            because the channel decides both rendering and which ledger line the
            turn bills to.
          enum:
            - ''
            - api
            - web
            - website
            - whatsapp
            - phone
            - voice
          type: string
        input:
          oneOf:
            - type: string
            - description: >-
                A structured payload. It reaches the model fenced and labelled
                inside the user message, never in the system prompt.
              type: object
        locale:
          type: string
        metadata:
          additionalProperties: true
          type: object
        request_context:
          additionalProperties: true
          description: >-
            Caller-supplied context. The one key the runtime acts on is
            `resource_id`, which narrows the execution context to that resource
            — and raises rather than narrowing to nothing when the caller could
            not reach it.
          type: object
      type: object
    RuntimeOptions:
      additionalProperties: false
      description: >-
        Options may be sent flat in the body or nested under `options`; both are
        read and the nested value wins. An unrecognised key inside `options` is
        a 400.


        The fields below are the complete set a caller may set. These are
        refused by name with a 400 explaining why, because the caller must not
        be able to override the runtime's security or billing behaviour:


        - `billing_account` — Billing is derived from the authenticated tenant.

        - `idempotency_key` — Send an Idempotency-Key header instead.

        - `instructions` — Prompt assembly is the runtime's, not the caller's.

        - `knowledge_groups` — Knowledge scope comes from the agent and the
        caller's own authorization.

        - `max_tokens` — Token limits are not caller-controlled.

        - `memory_namespace` — Memory namespacing is derived from the execution
        context.

        - `model` — Model selection is decided by runtime routing.

        - `prompt` — Prompt assembly is the runtime's, not the caller's.

        - `provider` — Provider selection is decided by runtime routing.

        - `request_id` — Request ids are minted by the runtime.

        - `system_prompt` — Prompt assembly is the runtime's, not the caller's.

        - `temperature` — Sampling parameters are not caller-controlled.

        - `tools` — Tool availability comes from the agent configuration.

        - `top_p` — Sampling parameters are not caller-controlled.

        - `trace_id` — Trace ids are minted by the runtime.

        - `usage_category` — Billing is derived from the authenticated tenant.
      properties:
        knowledge_policy:
          default: auto
          description: >-
            These three can only ever subtract. There is no value meaning
            "retrieve more than this agent is configured for".
          enum:
            - auto
            - none
            - required
          type: string
        memory_policy:
          default: auto
          enum:
            - auto
            - none
          type: string
        preview:
          default: false
          description: >-
            Run the agent's unpublished configuration (§94). **Asking is free;
            being allowed is not.** A machine caller needs the `agent:preview`
            scope, which is issued on its own and is not implied by any other —
            a key that may edit an assistant does not thereby preview its
            drafts.


            Without permission the answer is **404, not 403**: confirming that a
            draft exists but may not be run is the confirmation the not-found
            rule exists to withhold.


            Statuses a preview run may execute: `active`, `draft`, `paused`,
            `preview`. Without preview, only `active`.
          type: boolean
        response_format:
          description: >-
            Ask for structured output (§31). `json_schema` validates the model's
            output against `schema` and retries a mismatch once before failing
            with **422** `structured_output_failed`, which names the paths that
            did not match. The bound is read before the run starts, so the
            number of model calls a structured request can cost is knowable in
            advance.


            The schema is checked when the request is parsed — before
            authorization, before the idempotency claim, and before any spend —
            so a schema this runtime cannot enforce is a 400 rather than a
            charge. `strict` is accepted only as `true`: there is no mode in
            which a schema is requested and not enforced.
          oneOf:
            - enum:
                - text
                - json_object
                - json_schema
              type: string
            - properties:
                name:
                  maxLength: 64
                  type: string
                schema:
                  description: Required when `type` is `json_schema`.
                  type: object
                strict:
                  default: true
                  type: boolean
                type:
                  enum:
                    - text
                    - json_object
                    - json_schema
                  type: string
              type: object
        stream:
          default: false
          description: >-
            Answer with Server-Sent Events rather than one JSON body. The event
            contract is in this document's description; a failure that happens
            before the first event is still an ordinary JSON error with its own
            status code.
          type: boolean
        timeout_ms:
          default: 60000
          description: >-
            Out of range is refused, not clamped: a caller who asked for ten
            minutes and silently got sixty seconds reads the resulting timeout
            as a platform fault.
          maximum: 120000
          minimum: 1000
          type: integer
        tool_policy:
          default: auto
          enum:
            - auto
            - none
            - required
          type: string
      type: object
    RuntimeResponse:
      description: >-
        §33's response object. `output` is an array because a run can produce
        more than one thing — prose plus a structured object, or a refusal — and
        a contract that starts as a string has nowhere to put the second one.
      properties:
        agent:
          type: string
        agent_version:
          type: integer
        conversation:
          description: >-
            Omitted by `POST /api/v2/responses`, which is stateless: it opens an
            internal conversation so the turn has somewhere to live, writes no
            messages to it, and returns no id.
          type: string
        id:
          description: The public run id, `run_…`. Database primary keys are never exposed.
          type: string
        metadata:
          additionalProperties: true
          type: object
        output:
          items:
            properties:
              json:
                description: Present only when `type` is `json`.
              text:
                type: string
              type:
                enum:
                  - text
                  - json
                  - refusal
                type: string
            type: object
          type: array
        request_id:
          type: string
        status:
          enum:
            - queued
            - running
            - completed
            - failed
            - cancelled
            - blocked
          type: string
        trace_id:
          type: string
        usage:
          properties:
            cached_tokens:
              type: integer
            input_tokens:
              type: integer
            output_tokens:
              type: integer
            total_tokens:
              type: integer
          type: object
      type: object
    RunError:
      additionalProperties: false
      properties:
        error:
          description: >-
            Some endpoints add one extra key beside these three, carrying the
            resource the failure was about. Treat this object as open.
          properties:
            code:
              description: >-
                A stable machine-readable code. Switch on this, never on
                `message`, which is prose and may be reworded without a version
                bump.
              enum:
                - agent_not_available
                - agent_not_found
                - authentication_error
                - conversation_not_found
                - idempotency_conflict
                - internal_error
                - invalid_context
                - invalid_request
                - knowledge_unavailable
                - not_implemented
                - permission_denied
                - provider_unavailable
                - rate_limited
                - run_not_found
                - structured_output_failed
                - tool_execution_failed
                - usage_blocked
              type: string
            message:
              description: >-
                A sentence safe to show a person. It never contains a stack
                trace, a provider name, a database id or any prompt text — the
                diagnostic detail is logged against `request_id` and is not
                serialised.
              type: string
            request_id:
              description: >-
                Quote this in a support ticket. It is also on the `X-Request-Id`
                response header, including on responses that carry no body.
              type: string
          required:
            - code
            - message
          type: object
        id:
          description: >-
            The run id, `run_…`. Present only when the failure ended a run that
            had already started.
          type: string
        status:
          description: >-
            The run's terminal status. `usage_blocked` and `rate_limited` end a
            run as `blocked`, not `failed`: nothing went wrong upstream, the
            platform declined to spend.
          enum:
            - queued
            - running
            - completed
            - failed
            - cancelled
            - blocked
          type: string
        trace_id:
          type: string
      required:
        - error
      type: object
  securitySchemes:
    CompanyApiKey:
      description: >-
        A managed API key, sent as `Authorization: Bearer <key>`. A workspace
        key is prefixed `tgcc_`; a reseller key is prefixed `tgpk_`. Keys carry
        scopes; the scope each operation demands is on the operation as
        `x-required-scope`.
      scheme: bearer
      type: http
    ApplicationCredential:
      description: >-
        An embedded application credential, sent as `Authorization: Bearer
        impa_<environment>_<id>.<secret>`. The environment is part of the
        credential, so a development credential can never read production data.
      scheme: bearer
      type: http
    DashboardSession:
      description: >-
        The dashboard's own session. Listed because the same endpoints serve the
        console; an integration uses a key.
      in: cookie
      name: tg_access
      type: apiKey

````