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

# The complete lifecycle

> Every call in the order you use it. For each call, the request, the success response, every error, and what to do.

This page walks through the whole API in the order a product uses it, from your organization to the deletion of an agent. Read it once from top to bottom. After that, use it as a reference: each section has the request, the success response, and a table of errors with what to do.

Every example uses the base URL `https://app.sapientpriors.com/api`, and carries your API key. See [Authentication](/docs/authentication).

Two errors can happen on any call, so the tables below do not repeat them:

* `401 unauthorized`: your key is missing, unknown, revoked, or expired. Check the key.
* `503 service_unavailable`: a part of Priors did not respond, or the agent is restarting. Wait a few seconds, then send the same request again.

<Note>
  Add `X-SP-End-User` to any call inside a thread to act for one named person on your side. It decides which threads you reach and which rate-limit window the call spends from. See [Call Priors from your backend](/docs/walkthroughs/call-from-your-backend).
</Note>

## Stage 0: your key

Your organization exists before your first API call, and an administrator of it mints your key in the Priors application under **Settings → API keys**. There is no endpoint for that. See [Authentication](/docs/authentication).

`GET /v1/session` confirms which organization your key resolves to, and returns the upload limits your organization is held to. Call it once when your client starts.

## Stage 1: create an agent

Send `POST /v1/agents`. You become the owner. Only the owner can change the agent, delete it, or share it.

```bash theme={null}
curl -X POST https://app.sapientpriors.com/api/v1/agents \
  -H "Authorization: Bearer $SP_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "agent_config": {
      "role": "Support assistant for Acme",
      "objective": "Remember Acme'"'"'s setup and past issues. Answer their team from that."
    }
  }'
```

**Success:** status `202`. The `agent_id` is final. The agent is not ready yet.

```json theme={null}
{"agent_id": "agent_01J9X4K6M2P8Q1R3S5T7V9W0YZ", "status": "initializing", "access": "owner", "threads": []}
```

**Then:** call `GET /v1/agents/{agent_id}` every second until `status` is `ready`. This usually takes a few seconds. An agent that is still `initializing` after 10 minutes becomes `failed`. A `failed` agent does not recover: delete it and create a new one.

**Errors:**

| Status | `code` | Cause | What to do |
| - | - | - | - |
| `400` or `422` | `invalid_request` | `role` (1 to 200 characters) or `objective` (1 to 2,000 characters) is missing or too long, or `metadata` has more than 50 keys. | Correct the request body. |
| `409` | `agent_quota_exceeded` | You already own 20 agents. | Delete one. |

You can also send `agent_config.metadata` (your own labels, returned unchanged) and `ttl_seconds`. Agents do not expire today. `ttl_seconds` is stored for a future feature, and `expires_at` is always `null`. An agent exists until you delete it.

`visibility` is always `external` for an agent your key creates, so you can leave it out. Your organization owns the agent rather than any person, and it appears in no directory.

**Keep your own record of which `agent_id` belongs to which of your customers.** Every later call puts it in the path. `metadata` is a label, not an index: Priors stores it and hands it back unchanged, but no call finds an agent by it. Priors cannot check that mapping for you, so if your lookup returns the wrong `agent_id` the call still succeeds, and one customer reads another customer's memory.

### Change an agent later

`PATCH /v1/agents/{agent_id}` changes `agent_config`. The memory is not touched. `visibility` cannot change. While the agent is initializing, the call returns `409 agent_not_ready`.

Two rules apply:

* `agent_config` replaces the whole object. Send `role`, `objective`, and `metadata` together. A field you leave out is cleared.

### An agent id never changes

To move a user to another agent, open a thread on the other agent. Nothing moves between agents. If two groups of people must not share a memory, give them two agents.

## Stage 2: open a thread

Send `POST /v1/agents/{agent_id}/threads`. Use one thread for each conversation, task, or user session.

```bash theme={null}
curl -X POST https://app.sapientpriors.com/api/v1/agents/agent_01J.../threads \
  -H "Authorization: Bearer $SP_API_KEY" \
  -H "Content-Type: application/json" -d '{"title": "Ticket 4821"}'
```

**Success:** status `201` with a `thread_id`.

**Errors:**

* `409 agent_not_ready`: poll the agent until it is ready.
* `404 agent_not_found`: the id is wrong, or the agent is not yours.
* `429 rate_limit_exceeded`: more than 5 threads in 10 seconds.

`GET .../threads/{thread_id}` returns the thread with up to its 200 most recent messages. Each message has `speaker`, `text`, `timestamp`, and `source_ids`. `PATCH` renames the thread. `DELETE` removes it and returns `202`. Deleting a thread does not remove what the agent learned from it.

## Stage 3: teach the agent

Four calls send observations to an agent. They differ in what the response contains. These four calls are the center of the API.

| Call | Learns | What the response contains | Use it when |
| - | - | - | - |
| `observe` | yes | An id to check later. | You want the agent to remember, and you do not need an answer. |
| `observe-and-enrich` | yes | The related memories, as text for your own model. | You have your own language model. |
| `observe-and-respond` | yes | An answer. | You want Priors to write the answer. |
| `chat` / `chat/stream` | yes | An answer, saved in the thread, with links to the files it used. | You build a chat screen. |

### The observation: the format the first three calls share

```json theme={null}
{
  "content": [{"type": "text", "text": "Their last outage was caused by a full disk."}],
  "timestamp": "2026-09-17T10:07:00Z",
  "speaker": "Priya",
  "tags": ["email"],
  "img_url": "https://files.acme.example/error.png",
  "blip_caption": "Screenshot of a disk-full error"
}
```

| Field | Required | Meaning |
| - | - | - |
| `content` | yes | 1 to 20 text blocks. Each up to 100,000 characters. |
| `timestamp` | no | When the thing happened. This becomes the time of the memory. If you leave it out, Priors uses the time of the request. |
| `speaker` | no | **Who wrote or said this**, for example a person's name. Up to 200 characters. Use a readable name, not an id: the agent reads it when it reasons about the conversation. In a shared thread, it keeps each person's messages apart. |
| `tags` | no | Free-form labels for the source, channel, or topic, for example `email`. Kept with the message when you send `speaker`. Without `speaker`, the first tag is read as the speaker instead (older behavior), and `user` is recorded when there are no tags. Up to 20 entries, 1 to 64 characters each. |
| `img_url` | no | One image, as a URL that Priors downloads. Priors writes a description of the image and learns the description with your text. |
| `blip_caption` | no | Your own description of that image. Learned with the downloaded description, or instead of it. |

### Images: the three ways

Priors learns images as text. At some point, an image becomes a description, and the agent remembers the description. There are three ways to send an image. They are not interchangeable.

| Way | Call | How the image gets in | Limits |
| - | - | - | - |
| A. By URL | `observe`, `observe-and-enrich`, `observe-and-respond` | Put a URL in `img_url`. Priors downloads the image, describes it, and learns the description with your text. | One image per observation. URL up to 2,048 characters. Download within 10 seconds. JPEG, PNG, GIF, or WebP. Up to 10 MB. |
| B. By your own description | the same three calls | Put your description in `blip_caption`, or in the `content` text. No image is sent. | Text only. |
| C. By upload | `chat`, `chat/stream` | Upload the bytes with `PUT .../sources/images`, then put the returned id in `image_source_ids`. The model looks at the image for that one answer. Priors keeps the file, and answers can point to it. | 10 MB per image. 10 images per message. 32 MB per message. |

**When the image is a file on a computer or a phone:** the observe calls do not accept bytes. You have two options:

1. Put the file where Priors can download it, for example your own storage with a signed URL that expires soon. Send that URL in `img_url`. Priors cannot download a URL that needs a login, a URL on a private network, or a URL on `localhost`.
2. Describe the image yourself and send the words. Use your own image model, or use the caption your user wrote. Send the words in `blip_caption` or in `content`.

If the image belongs to a conversation, use way C.

**What the response does not tell you:** with way A, Priors returns `202` before it downloads the image. If the download fails, or the image cannot be described, the observation is still learned from your text and `blip_caption` only. The status does not report the failure. Always send a `blip_caption` when you have one.

**Images in `observe-file`:** the uploaded file can itself be an image. `observe-file` accepts one file per call. A PNG, JPEG, GIF, or WebP file is read as a one-page document: Priors extracts the text in the image, for example the text in a photographed receipt or a screenshot, and learns it. Pictures inside a PDF are extracted and described in place. To send several files, send several calls, one after another.

### `observe`: learn only

```bash theme={null}
curl -X POST https://app.sapientpriors.com/api/v1/agents/agent_01J.../threads/thread_01J.../observe \
  -H "Authorization: Bearer $SP_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "observations": [
      {
        "content": [{"type": "text", "text": "Their last outage was caused by a full disk."}],
        "timestamp": "2026-09-17T10:07:00Z"
      }
    ]
  }'
```

**Success:** status `202` with `{"observation_id": "obs_01J...", "accepted": true}`. Nothing is learned yet.

**Then:** call `GET .../observe/{observation_id}/status` every second until `status` is `committed`. The table below lists every status value.

| `status` | Meaning | What to do |
| - | - | - |
| `queued` | Earlier observations in this thread are still being learned. `queue_depth` says how many. | Poll again. |
| `processing` | Memories are being written. | Poll again. |
| `committed` | Done. | Ask the agent. |
| `failed` | Learning stopped. `error` says why. `learning` lists the pieces that were not learned. | Correct the input. Send those pieces again. |
| `cancelled` | You cancelled it. | Send it again if you still need it. |
| `interrupted` | A Priors restart paused the learning. It resumes by itself, and nothing is charged again. | Keep polling. |

`POST .../observe/{observation_id}/cancel` stops learning that has not finished. Pieces that were already learned stay learned.

**Errors:**

* `400` or `422 invalid_request`: empty content, a block that is not text, or more than 100 observations.
* `402 payment_required`: no credits.
* `404 thread_not_found`: the thread or the agent does not exist, or you cannot open it.
* `409 agent_not_ready`: poll the agent until it is ready.
* `429 rate_limit_exceeded`: more than 60 calls in one minute, or Priors is accepting another `observe` for this agent at this moment (`Retry-After: 1`).

**Cost:** the words you send.

### `observe-and-enrich`: memory for your own model

The request body is the same as for `observe`. You can add `detail` (`brief`, `standard`, or `detailed`) to set how much the agent writes, and `max_context_chars` for a hard limit on size. See [Answer length](/docs/concepts/answering#answer-length-detail).

**Success:** status `200`.

```json theme={null}
{
  "response_context": "The customer runs version 4.2. Their last outage was caused by a full disk.",
  "preference": "",
  "in_context_memory_ids": ["mem_01J...", "mem_01J..."],
  "used_memory_ids": ["mem_01J..."],
  "in_context_source_ids": [],
  "used_source_ids": []
}
```

`response_context` is memory text for your prompt. It is not an answer. `used_memory_ids` and `used_source_ids` tell you which memories and files were used. An empty `response_context` means the agent has no related memory yet. That is a success, not an error.

**Errors:** `400` or `422`, `402 payment_required` (no credits), `404`, `429` (more than 20 calls in one minute, or 2 answers already running), `503 answer_model_unavailable` (wait, then try again).

**Cost:** the words you send plus the words in `response_context`.

### `observe-and-respond`: an answer from Priors

The request body is the same, plus `learn_reply`. **Success:** status `200` with the same fields. Here `response_context` **is the answer**. Show it to the user. Do not send it to another model.

The agent learns the answer it writes, as `chat` does. Send `"learn_reply": false` if you also send the answer back as an observation, or if answers must not become memory.

**Errors and cost:** the same as `observe-and-enrich`.

### `chat` and `chat/stream`: one message in a chat

```bash theme={null}
curl -X POST https://app.sapientpriors.com/api/v1/agents/agent_01J.../threads/thread_01J.../chat \
  -H "Authorization: Bearer $SP_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "turn_id": "c6a9f2e0-1d2b-4c3a-9e8f-7a6b5c4d3e2f",
    "message": "Why did it go down last time?",
    "image_source_ids": []
  }'
```

`turn_id` is an id you create. It must be unique for each message and at most 200 characters. If you send the same `turn_id` again, Priors charges it only once. `message` is 1 to 2,000 characters. Longer text belongs in `observe`. `detail` (`brief`, `standard`, or `detailed`) sets how long the answer is.

**Success:** status `200` with `message` (the answer), `memory_context`, `citations` (a list with `source_id`, `label`, `href`, `verified`), `duration_ms`, `stages` (`memory_context`, `final_generation`, `turn_learning`, each with `duration_ms`), and `learning` (earlier messages of the thread that were not learned; usually empty). `verified` is `true` only for an image attached to this message. It is `false` for a document learned earlier.

`chat/stream` returns server-sent events: `stage` (`{"stage": "memory_context"}`), `delta` (`{"delta": "..."}`, add it to the text on the screen), then one `complete` event (the full response) or one `error` event (`{"error", "code", "request_id"}`).

**Errors:**

* `400` or `422 invalid_request`: `message` is longer than 2,000 characters, or `turn_id` is missing.
* `402 payment_required`: no credits.
* `404 thread_not_found`: the thread or the agent does not exist, or you cannot open it.
* `404 not_found`: an id in `image_source_ids` is not an image this agent holds.
* `409 agent_not_ready`: poll the agent until it is ready.
* `413 payload_too_large`: more than 10 images, or more than 32 MB of images.
* `429 rate_limit_exceeded`: more than 20 messages in one minute, or 2 answers already running for this subject.
* `503 answer_model_unavailable`: wait, then send the same request again.

After a stream has started, the same codes arrive in the `error` event.

**Cost:** the words in the `message` you send plus the words in the `message` you receive.

### There are no webhooks

Priors never calls your server. To know when learning is done, poll the status. Answers are returned in the response. Send your own `request_id` on answering calls to match them with your logs.

### The order in which observations are learned

* Observations in one thread are learned in the order Priors received them. Inside one call, the order of the array is kept.
* Two calls that arrive at the same time for the same thread have no defined order. To keep the order across calls, wait for the `202` of one call before you send the next. To send old messages, send them in one call, in order.
* `timestamp` sets the time of the memory. It does not change the order of learning.
* A question sent before an observation is `committed` can be answered without that observation. Poll first when the answer depends on what you just sent.

## Stage 4: teach from a file

Send `POST /v1/agents/{agent_id}/threads/{thread_id}/observe-file` as `multipart/form-data`. One file per call.

```bash theme={null}
curl -X POST https://app.sapientpriors.com/api/v1/agents/agent_01J.../threads/thread_01J.../observe-file \
  -H "Authorization: Bearer $SP_API_KEY" \
  -F "file=@handbook.pdf" -F "strategy=structure"
```

| Field | Required | Meaning |
| - | - | - |
| `file` | yes | PDF, Word (`.doc`, `.docx`), PowerPoint (`.ppt`, `.pptx`, `.pps`, `.ppsx`), Excel (`.xls`, `.xlsx`, `.xlsb`), OpenDocument (`.odt`, `.ods`, `.odp`), CSV, RTF, EPUB, plain text and source files (Markdown, JSON, YAML, HTML, and the common code extensions), or an image (PNG, JPEG, GIF, WebP) that is read as a one-page document. Priors reads the format from the bytes, not from the file name. The name is used only for formats with no signature, such as CSV and plain text. SVG is not accepted. Up to 10 MB. |
| `strategy` | no | How to split the text: `auto`, `length`, `sentence`, `code`, or `structure`. |
| `max_chars` | no | The largest piece, from 200 to 4,000 characters. The default is 2,000. |
| `overlap_chars` | no | Characters repeated between two pieces. The default is 200. |

**Success:** status `202`.

```json theme={null}
{
  "observation_id": "obs_01J...",
  "accepted": true,
  "turns_total": 64,
  "figures": 3,
  "words_total": 14200,
  "duplicate": false,
  "filename": "handbook.pdf",
  "doc_format": "pdf",
  "source_id": "src_01J..."
}
```

The file became 64 pieces. Poll the status in the same way as for `observe`. Save the `source_id`: answers point to the file with it.

**Limits:** one file read at a time per agent, three at a time per organization, 2,000 pieces per file, and one upload every 10 seconds per subject. To load ten files into one agent, upload them one after another. You do not need to wait for `committed`: a file sent while the agent reads another is accepted with `queued: true` and starts by itself.

**If an upload stops:** a Priors restart only pauses it (`interrupted`), and it resumes by itself. An upload that stopped partway or failed can be finished with `POST .../sources/{source_id}/finish-learning`, which reads and charges only what is left. See [Sources](/docs/concepts/sources#finish-an-upload-that-stopped).

**Errors:** Priors converts and splits the file before it returns `202`. So every problem with the file itself is an error on this call.

| Status | `code` | Cause | What to do |
| - | - | - | - |
| `400` | `unsupported_document`, `document_needs_ocr`, `encrypted_document`, `malformed_document`, `empty_document`, `ingestion_failed` | The file itself. `error` says which problem. | Convert the file, remove the password, or export it again. Then upload it again. |
| `413` | `document_too_large`, `document_too_complex`, `document_too_many_turns`, `document_too_many_pages`, `document_too_large_for_ocr` | The file is larger than 10 MB; or it is smaller but too heavy to process (the converter's resource limit, or a scan with too many pages, or a scan too large to send for text recognition); or it splits into more than 2,000 pieces. `error` says which. | Split the file, send a version with text instead of a scan, or raise `max_chars`. |
| `409` | `document_uploaded_elsewhere` | An upload of this file in another thread stopped partway or failed. | Finish it with `finish-learning`, or send `allow_duplicate=true` to read it again here, charged in full. |
| `409` | `conflict` | Your `Idempotency-Key` belongs to an upload that is still running, or was used for a different file. | Wait, or use a new key. |
| `429` | `rate_limit_exceeded` | Less than 10 seconds since your last upload, or three uploads already running in your organization. | Wait the number of seconds in `Retry-After`. |
| `502` | `document_service_unavailable`, `ocr_failed`, `document_pages_unexpected` | The document converter or text recognition failed. The file is fine. | Wait, then try again. |
| `503` | `ocr_busy` | Text recognition is busy. | Try again in a few minutes. |

Learning can still stop after the `202`. The status then reads `failed`, and `error` says why. Correct what it names and upload the file again. The full list of causes is on [Learning](/docs/concepts/learning).

A second upload of exactly the same bytes **into the same thread** returns `202` with `"duplicate": true` and costs nothing. The same bytes into **another** thread of this agent return `409 document_already_ingested`, because what the first read learned is already searchable from every thread. Send `allow_duplicate=true` to read it again and store a second copy.

**To retry an upload safely:** send an `Idempotency-Key` header, any string up to 255 characters, unique for this upload. If you send the same key with the same file again, Priors returns the original `202` and does not read or charge the file again. The same key with a different file or different split settings returns `409`. Priors keeps a key for 24 hours.

**Cost:** the words of every piece. A refused file or a duplicate costs nothing.

## Stage 5: images and sources

### Upload an image for a chat message

Send the bytes with `PUT /v1/agents/{agent_id}/threads/{thread_id}/sources/images`. Set `Content-Type` to `image/png`, `image/jpeg`, or `image/webp`. Up to 10 MB.

**Success:** status `200` with `source_id`, `label`, `media_type`, `href`, and `verified`. Send the `source_id` in `image_source_ids` on a `chat` message. Up to 10 images per message.

**Errors:**

* `400 invalid_request`: the `Content-Length` header is missing or is not a number.
* `404 thread_not_found`: the thread or the agent does not exist, or you cannot open it.
* `413 payload_too_large`: the image is larger than 10 MB.
* `422 invalid_request`: the bytes are not an image that can be decoded.
* `429 rate_limit_exceeded`: more than 20 uploads in one minute.
* `503 service_unavailable`: image storage is not available.

### List and download the files an agent holds

| Call | What it returns |
| - | - |
| `GET /v1/agents/{agent_id}/sources` | Every file and image the agent holds. Use `limit` and `cursor` to read page by page. |
| `GET /v1/agents/{agent_id}/sources/{source_id}` | The original file. |
| `GET .../threads/{thread_id}/sources/{source_id}` | The same file, as an answer in this thread pointed to it. A citation's `href` is this path. |

**Errors:** the list returns `400 invalid_request` for an old `cursor` (start again without one) and `503` when storage does not respond. A download returns `404 not_found` when the agent has no source with that id, and `503` when storage does not respond.

### What you cannot list

You cannot list memories. There is no call that returns them. Memories reach you only inside answers, as `used_memory_ids` and as the text that was written from them. To learn what an agent knows, ask it. To see what the agent was given, read the thread and the list of sources.

## Stage 6: see who used the agent

`GET /v1/agents/{agent_id}/logs` returns which thread was used, by which subject, and when. It never returns the text of a message. Send `X-SP-End-User` on your calls and each turn is attributed to the person it was taken for.

## Stage 7: credits

`GET /v1/billing` returns your organization's balance and every charge. The calls that cost credits are the four calls in Stage 3 and `observe-file`. Every other call is free. When the balance is zero, the calls that cost credits return `402 payment_required`, and the free calls keep working. See [Credits](/docs/credits).

## Stage 8: delete

| Call | Effect |
| - | - |
| `DELETE .../threads/{thread_id}` | The conversation is gone. The memories stay. |
| `DELETE /v1/agents/{agent_id}` | The agent, its threads, its memory, its files, and its history can no longer be read, by anyone. Status `202`. This cannot be undone. |

## The whole lifecycle in one table

| Step | Call | Success | Then |
| - | - | - | - |
| 1 | `POST /v1/agents` | `202`, `initializing` | Poll until `ready` |
| 2 | `POST .../threads` | `201` | |
| 3 | `POST .../observe` | `202`, `observation_id` | Poll until `committed` |
| 4 | `POST .../observe-file` | `202`, `observation_id` | Poll until `committed` |
| 5 | `POST .../observe-and-enrich` | `200`, memory text | Your model answers |
| 6 | `POST .../observe-and-respond` | `200`, an answer | Show the answer |
| 7 | `POST .../chat/stream` | events, then `complete` | Show the answer |
| 8 | `GET /v1/billing` | `200` | |
| 9 | `DELETE /v1/agents/{agent_id}` | `202` | |


This documentation is built and hosted on [Mintlify](https://mintlify.com), a developer documentation platform.