> ## 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 apiv2documents

> One endpoint, §6's three modes, chosen by what the body carries.

Multipart with a `file` is A. A JSON `upload_id` is B's confirmation. A
JSON `document_id` is C. They are mutually exclusive and naming two is
a 400 rather than a precedence rule nobody can remember — an ambiguous
request is a request whose author believed something that is not true.

**Required scope:** `documents:create`. 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. This endpoint sets no replay header: the status is the signal, and a **200** where a create answers **201** is the replay.
- *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/documents
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/documents:
    post:
      tags:
        - documents
      description: >-
        One endpoint, §6's three modes, chosen by what the body carries.


        Multipart with a `file` is A. A JSON `upload_id` is B's confirmation. A

        JSON `document_id` is C. They are mutually exclusive and naming two is

        a 400 rather than a precedence rule nobody can remember — an ambiguous

        request is a request whose author believed something that is not true.


        **Required scope:** `documents:create`. 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. This endpoint sets no replay
        header: the status is the signal, and a **200** where a create answers
        **201** is the replay.

        - *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_documents
      parameters:
        - 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:
              additionalProperties: true
              description: >-
                The keys below are the ones this handler reads, taken from its
                source rather than from a serializer — these endpoints validate
                by hand and have no serializer to generate from. **Their types
                and which are required are not pinned by this document**; the
                endpoint answers 400 with a message naming the problem. Treat
                that as a gap to be closed by the owning batch, not as licence
                to send anything.
              properties:
                document_id: {}
                file:
                  description: Sent as `multipart/form-data`.
                  format: binary
                  type: string
                filename: {}
                metadata: {}
                upload_id: {}
              type: object
          application/x-www-form-urlencoded:
            schema:
              additionalProperties: true
              description: >-
                The keys below are the ones this handler reads, taken from its
                source rather than from a serializer — these endpoints validate
                by hand and have no serializer to generate from. **Their types
                and which are required are not pinned by this document**; the
                endpoint answers 400 with a message naming the problem. Treat
                that as a gap to be closed by the owning batch, not as licence
                to send anything.
              properties:
                document_id: {}
                file:
                  description: Sent as `multipart/form-data`.
                  format: binary
                  type: string
                filename: {}
                metadata: {}
                upload_id: {}
              type: object
          multipart/form-data:
            schema:
              additionalProperties: true
              description: >-
                The keys below are the ones this handler reads, taken from its
                source rather than from a serializer — these endpoints validate
                by hand and have no serializer to generate from. **Their types
                and which are required are not pinned by this document**; the
                endpoint answers 400 with a message naming the problem. Treat
                that as a gap to be closed by the owning batch, not as licence
                to send anything.
              properties:
                document_id: {}
                file:
                  description: Sent as `multipart/form-data`.
                  format: binary
                  type: string
                filename: {}
                metadata: {}
                upload_id: {}
              type: object
        required: true
      responses:
        '200':
          description: No response body
        '201':
          description: Created.
        '400':
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
          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/Error'
          description: |-
            No credential, or one that does not resolve.

            Codes with this status: `authentication_error`
        '403':
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
          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`
        '409':
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
          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`
        '413':
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
          description: Error.
        '500':
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
          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`
      security:
        - CompanyApiKey: []
        - ApplicationCredential: []
        - DashboardSession: []
components:
  schemas:
    Error:
      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
      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

````