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

# Runtime API overview

> What /api/v2 is, what it contains, and why the reference beside it can be believed.

`/api/v2/` is the surface external developers are meant to call. Where `/api/v1/`
exposes the platform's own resources — leads, widgets, knowledge cards — v2
exposes **executions**: you ask an agent for something, and you get a run with an
id, a status, a cost and a trace.

```bash theme={null}
https://api.impellabs.tech/api/v2/
```

It is additive. `/api/v1/` is unchanged and is not deprecated by anything here.

## Four products, one contract

<CardGroup cols={2}>
  <Card title="Runtime" icon="bolt" href="/v2/responses">
    `/responses`, `/conversations`, `/agents/{id}/runs`. Stateless replies,
    durable transcripts, and non-conversational agent runs. All three take the
    same options and stream with the same events.
  </Card>

  <Card title="Documents" icon="file" href="/v2/documents">
    `/documents`. Upload a file, parse it once, read the parsed representation,
    hand out expiring download links.
  </Card>

  <Card title="Intelligence" icon="table" href="/v2/extractions">
    `/extractions` and `/reports`. A document plus a JSON Schema becomes
    validated fields with evidence; an agent plus a task becomes a report.
  </Card>

  <Card title="Operations" icon="gear" href="/v2/jobs">
    `/jobs`, `/logs`, `/webhooks`. Poll long work, read what an execution
    actually did, and get told when something finishes.
  </Card>
</CardGroup>

Knowledge sits across the line: [`/knowledge/ingestions`](/v2/knowledge-ingestion)
is the one place a stored document becomes something an agent can retrieve.

## One shape, everywhere

Learn these four things once and every endpoint on the surface behaves the same
way.

|                 |                                                                                                                                        |
| --------------- | -------------------------------------------------------------------------------------------------------------------------------------- |
| **Scopes**      | Every operation names the one scope it demands, and scopes never imply one another. [Authentication](/v2/authentication)               |
| **Errors**      | Always `{"error": {"code", "message", "request_id"}}`. Switch on `code`. [Errors](/v2/errors)                                          |
| **Idempotency** | `Idempotency-Key` on any write. Same key and same body replays; same key and a different body is a 409. [Idempotency](/v2/idempotency) |
| **Streaming**   | `"stream": true` on any execution endpoint, one event vocabulary. [Streaming](/v2/streaming)                                           |

Two conventions that surprise people, stated here so they surprise nobody:

<AccordionGroup>
  <Accordion title="A resource in another workspace and a resource that does not exist answer identically">
    Both are **404**, byte for byte. There is no 403 that confirms existence,
    because a 403 turns id-guessing into an inventory.

    The one place this reads oddly is `preview`: a caller without the
    `agent:preview` scope asking for a draft gets 404, not 403. Confirming that
    a draft exists but may not be run is exactly the confirmation the rule
    exists to withhold.
  </Accordion>

  <Accordion title="`blocked` is not `failed`">
    A workspace out of credits has not had a model failure — nothing went wrong
    upstream, the platform declined to spend. `usage_blocked` (**402**) and
    `rate_limited` (**429**) both end a run as `blocked`. Every other error code
    ends it as `failed`.

    Filing the two together is what makes an error-rate dashboard useless.
  </Accordion>
</AccordionGroup>

## What the caller does not control

The runtime chooses the model, the provider, the sampling parameters and the
prompt. A request that tries to set one of them is a **400 that names the field
and says why** — not a silently ignored key:

```json theme={null}
{
  "error": {
    "code": "invalid_request",
    "message": "model is not a settable option. Model selection is decided by runtime routing.",
    "request_id": "req_..."
  }
}
```

The complete list of what a caller *may* set is seven fields — `stream`,
`response_format`, `tool_policy`, `memory_policy`, `knowledge_policy`,
`timeout_ms`, `preview` — and the three policy fields can only ever subtract.
There is no value meaning "retrieve more than this agent is configured for".

<Note>
  Identity is never in the body. Who you are comes from the credential. The
  runtime input refuses about thirty reserved keys — `tenant_id`, `scopes`,
  `billed_to`, `principal`, and the rest — **by name** rather than dropping them
  quietly, so a caller who believed one of them worked finds out immediately.
</Note>

## Why the endpoint reference can be believed

The endpoint reference under **Endpoint reference** in this tab is not written by
hand. It is generated from the running code by `manage.py openapi` and gated in CI by
`manage.py openapi --check`, which regenerates the document in memory and
refuses a build where the checked-in file and the code disagree.

Nothing non-trivial in it is typed by a person:

| What the reference says                       | Where it reads it                  |
| --------------------------------------------- | ---------------------------------- |
| The error envelope and its HTTP mapping       | `runtime.errors._CODES`            |
| The run status vocabulary                     | `runtime.status.ALL`               |
| The options a caller may set, and their enums | `runtime.options.PUBLIC_FIELDS`    |
| The options a caller may **not** set, and why | `runtime.options.FORBIDDEN_FIELDS` |
| The scope each operation demands              | the view's `required_scope`        |
| The query and body fields each endpoint reads | the handler's own source           |
| The HTTP statuses each endpoint can answer    | the handler's own source           |
| The idempotency window                        | `runtime.idempotency.RETENTION`    |
| The streaming event contract                  | `runtime.streaming.EVENTS`         |

`--check` also refuses a document that documents a scope no key could ever be
issued, that describes a route that is not wired, or that leaves a routed scoped
operation undescribed.

## What it is honest about not being finished

A generated document is only worth the gaps it admits to. Two are worth knowing
before you write a client.

<AccordionGroup>
  <Accordion title="Paging is not uniform across the surface">
    There are four mechanisms, and each operation documents the one it actually
    takes:

    * `/logs` returns `next`.
    * `/documents`, `/extractions`, `/reports` and `/knowledge/ingestions`
      return `next_cursor` and read it back as `cursor`.
    * `/conversations/{id}/messages` returns `next_cursor`, 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.

    Cursors are opaque base64url. Do not build one: a raw ISO timestamp is not a
    cursor, because `+00:00` decodes to a space in a query string and silently
    breaks every second page.
  </Accordion>

  <Accordion title="The idempotent-replay header is spelled two ways">
    `/responses`, `/conversations` and `/agents/{id}/runs` set
    `Idempotency-Replayed`. `/jobs`, `/extractions`, `/reports` and
    `/knowledge/ingestions` set `Idempotent-Replay`. `POST /documents` sets
    neither and signals a replay by answering **200** where a create answers
    **201**.

    Read each operation rather than assuming one name. A client checking for the
    wrong header reads every replay as a fresh execution — which for a create is
    the one conclusion idempotency exists to prevent.
  </Accordion>
</AccordionGroup>

Some Phase 4 request bodies list the field names their handler reads and pin no
types. That is a stated gap, not licence to send anything: the endpoint answers
400 with a message naming the problem.

## Where to start

<Steps>
  <Step title="Get a scoped key">
    [Authentication](/v2/authentication) — and grant the minimum scopes the
    caller needs, because that is what decides how bad an exposure is.
  </Step>

  <Step title="Make one call">
    [Quickstart](/v2/quickstart) — a single response, then the same call
    streamed.
  </Step>

  <Step title="Read the endpoint you need">
    The generated reference in this tab has a request playground for every one
    of the 52 operations.
  </Step>
</Steps>
