# Agent sessions
Source: https://docs.file.ai/docs-api/api-agent-sessions

> Hold a conversation over your workspace documents: open a session, ask questions, and poll the async job each turn produces.

## On this page

*   [Overview](#overview)
*   [Authentication](#authentication)
*   [Idempotency](#idempotency)
*   [The async job lifecycle](#the-async-job-lifecycle)
*   [Listing and reading sessions](#listing-and-reading-sessions)
*   [Reading the conversation](#reading-the-conversation)
*   [Stopping a run](#stopping-a-run)
*   [Deleting a session](#deleting-a-session)

Key Functions

# Agent sessions

Copy pageCopy page

Hold a conversation over your workspace documents: open a session, ask questions, and poll the async job each turn produces.

Copy pageCopy page

## 

[​

](#overview)

Overview

The agent surface is a small, stateful layer on top of the rest of the v2 API: a **session** is a conversation, and each question you ask it is a **turn**. Every turn runs asynchronously, the same way as every other v2 operation — you get back a `job_id` immediately and poll `GET /v2/jobs/{job_id}` for the answer.

Examples use the default base URL, `https://api.orion.file.ai/prod/v1`. If your workspace is on an instance-specific host, swap the hostname and change nothing else — see [Switching between instances](/docs-api/api-intro#switching-between-instances).

## Create a session

`POST /v2/agent/sessions`

## List sessions

`GET /v2/agent/sessions`

## Get a session

`GET /v2/agent/sessions/{session_id}`

## Ask a question

`POST /v2/agent/sessions/{session_id}/ask`

## Stop the running turn

`POST /v2/agent/sessions/{session_id}/stop`

## Read the conversation

`GET /v2/agent/sessions/{session_id}/messages`

## Delete a session

`DELETE /v2/agent/sessions/{session_id}`

## Poll a job

`GET /v2/jobs/{job_id}`

## 

[​

](#authentication)

Authentication

Every route on this page uses the same `x-api-key` header as the rest of the API — see [About the fileAI API](/docs-api/api-intro#how-to-authorize-an-api-token).

The agent additionally requires the calling key to be associated with a user. A key with no user behind it gets a `403 FORBIDDEN` on session creation and on every `ask` — re-issue the key with a user rather than retrying.

## 

[​

](#idempotency)

Idempotency

Three of these routes are **write** operations that carry a required `Idempotency-Key` header, not merely an optional one:

Endpoint

Idempotency-Key

`POST /v2/agent/sessions`

**Required.** Omitting it is a `400`.

`POST /v2/agent/sessions/{session_id}/ask`

**Required.** Omitting it is a `400`.

`POST /v2/agent/sessions/{session_id}/stop`

**Required.** Omitting it is a `400`.

`DELETE /v2/agent/sessions/{session_id}`

Optional, standard 24h replay.

`GET /v2/agent/sessions`, `GET .../{session_id}`, `GET .../messages`

Accepted but ignored — these are reads.

The header being required here (rather than optional, as on most of the API) is deliberate: a session opened twice because a `POST /v2/agent/sessions` retry timed out, or a turn asked twice because an `ask` retry raced the original, are both mistakes an idempotency key exists to prevent. See [Idempotent requests](/docs-api/api-idempotency) for the general replay mechanics — the 24h window, the `Idempotent-Replayed` header, and what counts as “the same body” all apply here unchanged.

`ask` and `stop` are also serialized by a separate rule that has nothing to do with idempotency: a session runs **one turn at a time**. Asking again while a run is live is a `409 AGENT_RUN_IN_PROGRESS`, independent of whether you sent a fresh `Idempotency-Key`.

## 

[​

](#the-async-job-lifecycle)

The async job lifecycle

Every turn follows the same three steps as the rest of the v2 API’s async operations:

1

ask

`POST /v2/agent/sessions/{session_id}/ask` adds the turn to the session and returns `202 Accepted` immediately — never an answer.

```
curl -X POST "https://api.orion.file.ai/prod/v2/agent/sessions/665f1c2e8a1b4c0012ab34cd/ask" \
  -H "x-api-key: YOUR_API_KEY" \
  -H "Idempotency-Key: 0f1c8e03-978e-40d5-bc93-6894a57f9324" \
  -H "Content-Type: application/json" \
  -d '{
    "user_prompt": "Which invoices from Acme are still unpaid?",
    "document_ids": ["665f1c2e8a1b4c0012ab9911"]
  }'
```

```
{
  "job_id": "sajob_9f2c1a4b7e8d4c1fa0b3",
  "status": "QUEUED",
  "session_id": "665f1c2e8a1b4c0012ab34cd",
  "created_at": "2026-09-15T02:14:00.000Z",
  "poll_url": "https://api.orion.file.ai/prod/v2/jobs/sajob_9f2c1a4b7e8d4c1fa0b3"
}
```

2

poll job\_id

Poll the returned `poll_url` — `GET /v2/jobs/{job_id}` — until `status` reaches a settled value.

```
curl -X GET "https://api.orion.file.ai/prod/v2/jobs/sajob_9f2c1a4b7e8d4c1fa0b3" \
  -H "x-api-key: YOUR_API_KEY"
```

```
{
  "job_id": "sajob_9f2c1a4b7e8d4c1fa0b3",
  "kind": "agent",
  "status": "COMPLETED",
  "session_id": "665f1c2e8a1b4c0012ab34cd",
  "created_at": "2026-09-15T02:14:00.000Z",
  "updated_at": "2026-09-15T02:14:11.000Z",
  "settled_at": "2026-09-15T02:14:11.000Z",
  "client_ref": null,
  "result": {
    "answer": "Two Acme invoices are unpaid: INV-2291 ($4,200) and INV-2309 ($1,150).",
    "execution_time": 8.7,
    "execution_cost": 0.021
  },
  "error": null
}
```

`status`

Meaning

`QUEUED`, `RUNNING`

Not ready — poll again.

`COMPLETED`

Final. `result.answer` carries the response.

`STOPPED`

Final. A success that ended early — `result.answer` carries the partial answer produced before the stop.

`FAILED`

Final, with one exception below. `error` carries the failure.

`FAILED` with `error.code: "SUPERAGENT_RUN_STALE"` is **provisional**: it means the run went quiet longer than we wait and was abandoned, not that the agent reported a failure. A run that was only slow can still finish afterwards and resolve to `COMPLETED` or `STOPPED` — including a second, corrected callback. Treat this one status as retryable-but-not-final if you keep your own records per `job_id`.

3

or receive the callback

If the API key has a callback URL configured, the settled job is also POSTed there — you don’t have to poll if you’d rather be notified. Poll `poll_url` as a fallback regardless: a webhook delivery can be delayed or lost, while the job itself is durable.

`GET /v2/jobs/{job_id}` is shared with other v2 operations. Branch on `kind` before reading the rest of the body — an agent job (`kind: "agent"`, id prefixed `sajob_`) reports the statuses above and carries `result.answer`; other kinds report their own statuses and results.

## 

[​

](#listing-and-reading-sessions)

Listing and reading sessions

`GET /v2/agent/sessions` is cursor-paginated like the rest of the API’s list endpoints — see [Pagination](/docs-api/api-pagination) for the general cursor mechanics. Rows are newest first and deliberately **omit** `run_status`: resolving it costs a query per session, so read a single session (`GET /v2/agent/sessions/{session_id}`) when you actually need to know whether it can take a turn.

```
{
  "session_id": "665f1c2e8a1b4c0012ab34cd",
  "title": "Q3 supplier invoices",
  "created_at": "2026-09-14T09:00:00.000Z",
  "updated_at": "2026-09-15T02:14:11.000Z",
  "last_active_at": "2026-09-15T02:14:00.000Z",
  "run_status": "COMPLETED",
  "job_id": "sajob_9f2c1a4b7e8d4c1fa0b3"
}
```

`run_status` is how you tell whether the session is free:

`run_status`

Session state

`null`

Never run. Free.

`QUEUED`, `RUNNING`

A turn is in flight — `ask` is a `409`.

`COMPLETED`, `FAILED`, `STOPPED`

Free.

`STALE`

Free. The last run’s worker went quiet past its staleness threshold and was reconciled on read.

A run whose worker died sends no terminal event, so a run that stopped reporting is only reconciled to `STALE` when you **read** the session (or its messages) — reading is what frees a session stuck behind a dead run.

## 

[​

](#reading-the-conversation)

Reading the conversation

`GET /v2/agent/sessions/{session_id}/messages` returns turns oldest first, one row per turn, cursor-paginated with a smaller default page (20, vs. 50 elsewhere) because a turn carries a whole answer. An answer longer than 256KB is returned truncated, with `content_truncated: true` and `content_length` giving its real size — fetch the turn’s `job_id` via `GET /v2/jobs/{job_id}` to read it in full.

## 

[​

](#stopping-a-run)

Stopping a run

`POST /v2/agent/sessions/{session_id}/stop` asks the agent to end the live run and returns immediately with `stop_requested`. It is a **request**, not a completion:

*   The agent keeps working for about a second while it checkpoints its partial work, then ends the run itself — the answer produced so far is kept, and the time and cost already incurred are still billed.
*   A stop always races the run it is ending: if the run had already finished, the response reports `stop_requested: false` and nothing was forwarded. This is a normal outcome, not an error.
*   The session stays open — the next turn continues from the checkpoint the agent wrote.

Poll the returned `job_id` to see the run reach `STOPPED`.

## 

[​

](#deleting-a-session)

Deleting a session

`DELETE /v2/agent/sessions/{session_id}` deletes the session, stopping a live run first rather than refusing the delete. `stopped_run` in the response tells you whether that happened. Nothing is lost: the agent still ends the run properly, and its job lives outside the session — the partial answer, the real cost, and any callback all still arrive via `GET /v2/jobs/{job_id}`. The turns themselves are retained but become unreachable; every route on a deleted session, including `DELETE` again, returns `404`.

[

Error Responses

](/docs-api/api-errors)[

Get all files

](/api-reference/endpoint/get-all-files)

[linkedin](https://www.linkedin.com/company/file-ai)[x](https://x.com/fileAI_)[youtube](https://www.youtube.com/@file_AI)

[Powered byThis documentation is built and hosted on Mintlify, a developer documentation platform](https://www.mintlify.com?utm_campaign=poweredBy&utm_medium=referral&utm_source=undefined)
