> For the complete documentation index, see [llms.txt](https://verbatim-ai.gitbook.io/docs/llms.txt). Markdown versions of documentation pages are available by appending `.md` to page URLs; this page is available as [Markdown](https://verbatim-ai.gitbook.io/docs/integration/changelog.md).

# What's new

Notable changes to the **public REST API** of the Verbatim AI platform.

Only the API surface is documented here — endpoints, request parameters, payloads, authentication and deprecations. Internal changes (services, storage, pipeline, infrastructure) are omitted unless they change what a client sees.

**Conventions**

* ⚠️ marks a **breaking change**: existing clients must be updated.
* Deprecated operations are still served, but are hidden from the published spec and may be removed in a future release. Migrate as soon as the replacement is available.
* Endpoints under `/_/v1/**` are private back-office operations and are not part of the public contract — they are not listed here.
* The reference specification is served at [api-docs](https://www.verbatim-ai.com/api-docs/).

***

## 2026-08

### Added

* **2026-09-02** — `GET /v1/post/` — **an `order` parameter**, `DESC` (the default) or `ASC`.

  `DESC` reads the conversation backwards, most recent first, so page `0` is always the latest exchange whatever the session has grown to — what a client polling for what just happened wants, and one that never has to compute an index. `ASC` reads it forwards, from the first question: transcript order, and the one to walk when rendering a whole conversation from the beginning.

  ```
  GET /v1/post/?sessionId=<uuid>                            # 25 most recent, newest first
  GET /v1/post/?sessionId=<uuid>&order=ASC&pageSize=50      # from the first post, 50 at a time
  ```

  The applied ordering comes back on the response as `order`, so a client that passed nothing still learns which way its page reads. Posts are ordered on `createdAt` and the ordering is closed by the post id, so walking `pageIndex` never shows the same post twice nor skips one. One consequence of that tiebreaker is worth knowing: the two posts of a single exchange are written microseconds apart and can share a timestamp, and when they do their relative order is arbitrary — read `owner` rather than position to tell a question from its answer.
* **2026-09-02** — `/v1/chunk` — **a new domain: the chunks a document was split into.** A chunk is one embeddable piece of a document — the text that was vectorised, the pages it came from, the metadata ingestion attached to it — and it is the unit retrieval actually returns, since a post's attachments point at chunks rather than at documents. Until now nothing exposed them.

  Five endpoints, and **no create**: chunks come out of ingestion, and what this domain adds is seeing what came out and fixing a chunk that came out wrong.

  | Method & path                | Summary                                                                            |
  | ---------------------------- | ---------------------------------------------------------------------------------- |
  | `GET /v1/chunk/`             | List every chunk of your organization, in reading order.                           |
  | `GET /v1/chunk/q`            | Search by corpus, document, hash, page and metadata — all optional, all combining. |
  | `GET /v1/chunk/{chunkId}`    | Get one chunk **with its text**.                                                   |
  | `PATCH /v1/chunk/{chunkId}`  | Patch its page span, metadata or text.                                             |
  | `DELETE /v1/chunk/{chunkId}` | Remove the chunk and its stored text.                                              |

  ```
  GET /v1/chunk/q?documentId=<uuid>&page=4
  GET /v1/chunk/q?hash=9e107d9d372bb6826bd81d3542a419d6
  ```

  `pages` is a **span**, not a page: a chunk crossing pages 3 to 5 answers to `page=3`, `page=4` and `page=5` alike, and a chunk covering no page in particular — the document summary — carries an empty array. `hash` is the MD5 of the stored text, so searching it finds every copy of a passage across your corpora. The 1024-dimension vector is not published, and neither is the deprecated scalar `page` column.

  **`body` costs a storage read**, so the two listings omit it unless you pass `body=true`, where the page size is capped at `25`; `GET /v1/chunk/{chunkId}` always carries it. An **empty** `body` on a chunk that exists is not an error and is worth acting on — the row is there and the stored object is not, so the chunk still matches vector searches and then contributes nothing to the answer.

  Three things worth knowing before using `PATCH` and `DELETE`:

  * **Rewriting `body` does not re-embed the chunk.** The vector is not recomputed, so the chunk is still retrieved for the text it used to hold and handed to the model as the text it holds now. Right for a mangled character or a name to redact; wrong for a rewrite, which needs the document re-ingested. `hash` is not recomputed either — a chunk whose `hash` no longer matches its `body` is one that has been patched.
  * **`metadata` replaces rather than merges.** Send the whole object; `{}` clears it.
  * **`DELETE` takes the chunk out of the index**, and answers that cited it keep their text and lose the citation. The document, its file, its summary and its other chunks are untouched. *(Superseded on 2026-09-02: this shipped as a hard delete and is a soft delete now — see the entry above.)*

  A chunk is visible exactly as long as its document: deleting a document takes its chunks out of this API too. Access tokens accept the matching `chunk:*` scope.
* **2026-08-28** — `GET /v1/session/` and `GET /v1/session/q` — **listing and searching sessions**. `GET /v1/session/` paginates every session of your organization, newest first; the organization comes from your token, so there is nothing to pass. `GET /v1/session/q` narrows the same set by `userId`, `corpusId` and metadata (`key`/`value`, or `json` for a nested fragment), **every filter optional and combining**:

  ```
  GET /v1/session/q?userId=user_42&corpusId=<uuid>&key=customer_id&value=42
  ```

  That combination is the point: the `by…` listings each answered one fixed set of filters, so "this user's sessions on this corpus carrying this metadata" could not be asked for at all. A request carrying no filter returns the same page as `GET /v1/session/`.

  Metadata is matched by containment — a session matches when its metadata *carries* the fragment, extra keys being fine — and the response echoes the filters that were actually applied. Sessions come back newest first with the ordering closed by the session id, so walking `pageIndex` never repeats nor skips one. `total` counts every match across all pages.

  The organization is never a parameter: it is taken from your token and always applied, so no combination of filters reaches another tenant — including when two tenants share a user identifier. Naming a `corpusId` outside your organization answers `403` on the request that named it, rather than an empty page. `key` without `value` (or the reverse) answers `400` rather than quietly returning everything.
* **2026-08-28** — sessions now report **`updatedAt`** alongside `createdAt`, on every endpoint that returns one. The two are equal until the session is patched; `PATCH /v1/session/{sessionId}` moves it. Additive — existing clients that ignore the field are unaffected.
* **2026-08-26** — five more **core agents**, visible to every organization on `GET /v1/agent/` from this release and usable straight away as `agentId` on `GET /v1/post/q`:

  | Agent                | Use it for                                                                                                                                                                               |
  | -------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
  | `Verbatim Helpdesk`  | Everyday end-user questions — answer first, then the steps in the order they are performed, in plain language.                                                                           |
  | `Verbatim Legal`     | Contracts, regulations and policies — names the clause each part of the answer rests on, separates what the text states from what it implies, and says what the documents do not settle. |
  | `Verbatim Marketing` | Positioning and product questions — the point that matters first, then the evidence; never invents a figure or a customer name.                                                          |
  | `Verbatim Daily`     | Day-to-day use — the answer in the first sentence, direct and friendly.                                                                                                                  |
  | `Verbatim R&D`       | Technical questions — identifiers, signatures, units and versions reproduced exactly as the sources write them, code shown as code.                                                      |

  They are the same pipeline as `Verbatim Default`: identical retrieval width, re-ranking and models, with only the system instruction differing. Picking one picks a register and a set of habits, not a different search — and since `thinkingMode`, `temperature` and `baseModel` are still the ones `Verbatim Default` carries, your session's own settings keep precedence exactly as before.

  Non-breaking, and nothing changes unless you ask for it: `Verbatim Default` remains the only agent carrying `default: true`, so a query naming no agent behaves as it always has. All five are `lock: true` — usable and readable, not editable. Their names are reserved, like every core agent's: `POST /v1/agent/` with `{"name": "Verbatim Legal"}` answers `409`.
* **2026-08-25** — `GET /v1/post/q` (and the deprecated `POST /v1/post/`) — optional **`agentId`** query parameter, selecting the [agent](/docs/integration/changelog.md) a single query runs under. Omitted, the query runs on the platform default agent, exactly as before this release — so nothing changes for existing clients. The choice is per query, not per session: the next query on the same session is independent.

  The agent is recorded on the answer post as a new **`agentId`** field, and only on the answer — the user's question is not something an agent produced. A missing `agentId` on an answer means "ran on the default agent", not "unknown". Deleting an agent does not rewrite the answers it produced, so this still names an agent you have since deleted, and resolving that id through `GET /v1/agent/{agentId}` answers `404`; re-attributing old answers to the default agent when one is deleted would misreport what actually ran.

  An `agentId` your organization cannot see answers `404` and **writes no post** — the agent is resolved before the question is persisted, so a rejected query leaves the conversation untouched.
* **2026-08-25** — `GET /v1/agent/`, `GET /v1/agent/{agentId}`, `POST /v1/agent/`, `PATCH /v1/agent/{agentId}` and `DELETE /v1/agent/{agentId}` — **agents**, the setup a RAG query runs on: retrieval width (`topK`), re-ranking (`rerank`, `rerankTopK`, `rerankModel`), the system instruction (`context`, `behaviour`, `spirit`), how much conversation is replayed (`useHistory`, `historySize`) and which models answer (`baseModel`, `thinkingMode`, `temperature`). These were platform-wide constants until now; they are a per-agent choice from this release.

  Two kinds of agent share the endpoints, told apart by `lock`. **Core** agents (`lock: true`, no `orgId`) are shipped by Verbatim, visible to every organization and read-only — writing one answers `400`. **Custom** agents (`lock: false`) belong to your organization; `POST` always creates one of those. Exactly one core agent carries `default: true`, and it is what a query that names no agent runs on.

  Nullable fields are **overrides, not copies**: leaving one out ties it to the platform default, so a default Verbatim retunes later moves your agent with it. `GET` therefore reports them as stored, and read-only `systemInstruction` shows the resolved text the model receives. On `PATCH`, an omitted field means "leave alone", so un-setting one back to the default is spelled `"reset": ["spirit", "temperature"]`.

  `DELETE /v1/agent/{agentId}` takes an agent out of circulation rather than erasing it: it disappears from the listing, and get, update, delete and any query naming it all answer `404` — indistinguishable from an id that never existed. Answers already produced under it keep naming it in their `agentId`, so a conversation stays readable exactly as it happened. Deleting an agent changes what you can use from now on, not what already ran. Sessions are unaffected: an agent is resolved per query, so a conversation that used the deleted one carries on under the default.

  Access tokens accept a matching `agent:create|read|update|delete` scope.

  Non-breaking: a new domain, and existing queries are unaffected. The seeded default agent carries the exact values `app.search.*` held before, and `thinkingMode` / `temperature` / `baseModel` apply only where the session left the corresponding setting unset — so sessions created before this release behave as they did.
* **2026-08-24** — `GET /v1/usage/all`, `GET /v1/usage/user/{userId}` and `GET /v1/usage/corpus/{corpusId}` — `series`, a per-bucket breakdown of the same metrics the report already carried at the top level. `timeframe` no longer selects a single rolling window; it selects the **bucket size**, and with it how far back the report reaches: `Day` returns 30 daily buckets (\~1 month), `Week` 12 ISO weeks (\~3 months), `Month` 12 months (1 year) and `Year` 5 years. Each entry carries its own `from`/`to`, a `tokens` count, and `created`/`removed` for sessions, posts, storage and — at organization scope only — corpora.

  Buckets are aligned to **UTC calendar boundaries** (midnight, Monday, the 1st of the month, the 1st of January) rather than measured backwards from the moment of the call, so two requests minutes apart return the same boundaries and two reports line up on the same chart. The series is contiguous and gapless — a bucket in which nothing happened is present with zeros, not omitted — and the newest entry is the bucket currently **in progress**. Non-breaking: a new field on an existing payload; every field that was there before is still there and still means what it did, subject to the range change noted under *Changed*.
* **2026-08-20** — `nbPages` — every response carrying a document (`GET /v1/doc/{id}`, `GET /v1/doc/`, `GET /v1/doc/q`, `POST /v1/doc/init`, `POST /v1/doc/{id}/commit`, `PUT /v1/doc/{id}/init`, `PATCH /v1/doc/{id}`) now reports the number of pages of the source document. It was already the value the preview endpoints validate against, but no endpoint exposed it — clients had to guess the upper bound of `pages` on `GET /v1/doc/{id}/preview-urls`, whose valid indices are `0..nbPages-1`. `0` means *not counted yet*: the rendering pipeline fills it in during ingestion, and it stays `0` until then and for formats that have no pages. Unlike `size`, `tokens` and `nbWords`, which are omitted from the response while unset, `nbPages` is always present. Non-breaking: a new field on an existing payload.
* **2026-08-20** — `GET /v1/doc/q` — search the documents of a corpus instead of paging through the whole list. `corpusId` is required; every other parameter is an optional filter and they all narrow **together**. `q` matches the filename, case-insensitively and **anchored at the start** of the name: `?q=annual` finds `Annual-Report-2025.pdf`, `?q=report` does not. Put a `*` anywhere to match elsewhere — `?q=*report*` for a substring, `?q=2025-*.pdf` for a name that starts with `2025-` and ends in `.pdf`. Anchored is the default because it is the only shape the index can serve; a leading `*` is just as correct, it only filters over the corpus rather than probing the index. `%` and `_` match themselves. `tags` repeats as on `GET /v1/doc/` and now takes a companion `tagsMatch`: `ANY` (the default, at least one of them) or `ALL` (every one of them, extra tags allowed). `status` repeats too and matches any of the listed states (`?status=PENDING&status=FAILED`). `contentType`, `lang` and `provider` match exactly. `createdAfter` and `createdBefore` bound the ingestion date as a half-open window — the first inclusive, the second exclusive, so consecutive windows never return a document twice; an empty window (`createdAfter` at or after `createdBefore`) is refused with `400`. Order the result with `sort` (`CREATED_AT`, `UPDATED_AT`, `FILENAME`, `SIZE`) and `order` (`ASC`, `DESC`), newest first by default; the ordering is stable, so walking `pageIndex` never repeats nor skips a document. Paging uses the same `pageSize` (1-100, default 25) and `pageIndex` as everywhere else, and the response carries `pageSize` and `total` — the number of matches across **all** pages — alongside the usual `corpusId`, `pageIndex` and `items`. Non-breaking, and purely additive: `GET /v1/doc/` is unchanged and remains the simplest way to list a corpus.
* **2026-08-17** — `tags` — documents now carry a list of free-form labels. Set them at `POST /v1/doc/init` and change them with `PATCH /v1/doc/{id}`, where the list **replaces** the stored one; send `"tags": []` to clear every tag. Blanks are dropped and duplicates collapsed; at most 32 tags of 64 characters each (`400` otherwise). `GET /v1/doc/` gains a matching `tags` filter returning documents that carry **at least one** of the requested tags — repeat the parameter for several (`?tags=legal&tags=2026`), and combine it with `status` to narrow on both. Non-breaking: the field is optional and `null` on existing documents.
* **2026-08-17** — `chunk` — documents now carry an optional per-document chunking configuration, applied by the ingestion pipeline. Its keys map one-to-one onto : `strategy` (`by_title` or `basic`), `max_characters`, `new_after_n_chars`, `overlap`, `overlap_all`, `combine_text_under_n_chars` and `multipage_sections`. All are optional — send only what you want to change. Set it at `POST /v1/doc/init` and change it with `PATCH /v1/doc/{id}`, where it **replaces** the stored object wholesale; send `"chunk": {}` to drop it and fall back to the platform default (`by_title`, `max_characters: 10000`, `combine_text_under_n_chars: 1000`). Keys are not validated at the API — the object is stored verbatim and handed to the chunker, so a bad key surfaces as a failed ingestion rather than a `400`. Changing it affects the **next** ingestion; it does not re-chunk an already ingested document. See *Document → Chunking configuration* in the API guide for the full reference and worked examples. Non-breaking: the field is optional and `null` on existing documents.
* **2026-08-05** — `PUT /v1/doc/{id}/init` — replace the **content** of an already ingested document without changing its id. The document must be in `READY` or `FAILED` status (`409` otherwise); it moves back to `AWAITING_UPLOAD` and the response carries a fresh presigned PUT URL — same payload as `POST /v1/doc/init`, so the usual `PUT` + `POST /v1/doc/{id}/commit` flow follows unchanged. Descriptive attributes are kept (`filename`, `userId`, `provider`, `lang`, `metadata`, `tags`, `chunk`, source dates); everything derived from the previous content is dropped — embeddings, summary, and the `size`/`tokens`/`nbWords` counters. Two consequences: posts that cited this document **lose their attachments to it**, and the previously uploaded file stays in storage until your `PUT` overwrites it, so committing without uploading re-ingests the old content. Requires the `doc:update` scope.
* **2026-08-05** — `PATCH /v1/doc/{id}` — update the editable attributes of a document: `filename`, `docCreate`, `docUpdate` and `metadata`. Only the fields present in the body are applied; `metadata` **replaces** the stored map when provided. `docCreate`/`docUpdate` describe the source document — `createdAt`/`updatedAt` stay server-managed. Renaming does not move the stored file nor re-trigger ingestion. Requires the `doc:update` scope.
* **2026-08-05** — `GET /v1/doc/{docId}/download-url` and `GET /v1/doc/{docId}/preview-urls` — aliases of the `/v1/post/attachment/{docId}/…` endpoints, for clients working in the document domain. Identical responses; authorized under the `doc` domain, so a scoped token needs `doc:read` here where the `/v1/post` path needs `post:read`.
* **2026-08-05** — `PATCH /v1/corpus/{corpusId}` — replaces `PUT /v1/corpus/{corpusId}`. Same request body, same response, same `corpus:update` scope: the verb changes, the behaviour does not. `PATCH` is the accurate one, since omitted fields have always kept their current value rather than being reset.

### Changed

* **2026-09-02** — **`DELETE /v1/chunk/{chunkId}` is now a soft delete, and `DELETE /v1/doc/{docId}` cascades to the document's chunks.** Nothing observable changes about either endpoint — a deleted chunk still disappears from `GET /v1/chunk/`, `/q` and `GET /v1/chunk/{chunkId}`, still stops being retrievable as context, and there is still no endpoint that brings it back. What changed is what happens underneath: the chunk and the text it was vectorised from are both kept, exactly as deleting a document keeps its archived file.

  **Read this if you were using a chunk deletion to erase content.** It no longer destroys the stored text. If you need a passage gone from storage and not merely out of the index — an erasure request, or content that must not be retained — deleting the chunk is no longer sufficient on its own. It remains the right way to take a passage out of retrieval.

  Deleting a document now takes its chunks with it by the same mechanism, rather than leaving them live and merely unreachable behind their document; deleting a corpus cascades the same way, through every document it holds. Answers that cited a deleted chunk keep their text and lose the citation, as before.
* **2026-08-28** — ⚠️ **`agentId` is now always present on a post**, and is carried by *both* posts of an exchange — the question and the answer — where before it sat on the answer alone and was omitted whenever the query ran on the platform default.

  A query naming no agent is stamped with the default agent, resolved at the moment it runs. So a client no longer has to know that an absent `agentId` meant "ran on the default": every post says what produced it. **If your code treats a missing `agentId` as "the default", that branch is now dead** — read the field instead. Grouping posts by agent will also count both halves of an exchange rather than the answer only.

  Posts written before this release carried no agent and were backfilled with the platform default, which is what their absent value meant. One caveat, since it cannot be undone: the backfill names the agent that is default *today*, so if the default has been re-seeded since, an old post names the current default rather than the one that actually answered it. Posts written from now on record the id at query time and are exact.
* **2026-08-28** — `POST /v1/session/` and `PATCH /v1/session/{sessionId}` no longer take `model`, `system`, `temperature` or `thinking`. How a query is answered has been the [agent](/docs/integration/changelog.md)'s business since agents shipped, and a session naming a second, competing setup was a contradiction — the agent named on each query decides. Sending the fields is **not** an error: they are ignored, so existing clients keep working without a change. `metadata` is what remains patchable.

  Sessions opened before this release keep the values they recorded, and still report them, so a past conversation stays readable as it happened. New sessions carry none, and — nulls being excluded from responses — the fields are simply absent from them.
* **2026-08-26** — `POST /v1/agent/` and `PATCH /v1/agent/{agentId}` — an agent's `name` now has to be **unmistakable in your listing**. `GET /v1/agent/` merges your agents with Verbatim's core ones, and a name identifies an agent to whoever picks one out of that list, so it must be free on both sides: creating or renaming an agent onto a name **one of your agents** already uses, or one a **core agent** carries (`Verbatim Default`), answers `409` and writes nothing.

  The edges of the rule. Names are compared **exactly**, so `Support` and `support` are two names and `Verbatim Default v2` is free — core agents reserve the names they carry, not a namespace. Other organizations do not enter into it: names are per-organization, not global. **Deleting an agent frees its name** immediately, yours or Verbatim's. And a core agent Verbatim ships later never renames your agents — only writes are checked, so an existing agent keeps a name that has since become a core one, though it cannot take it back once it changes it.

  Sending an agent's own current name back on `PATCH` is not a rename and is never a conflict — a client that re-sends the object it just read is unaffected.
* **2026-08-25** — `GET /v1/usage/all`, `GET /v1/usage/user/{userId}` and `GET /v1/usage/corpus/{corpusId}` — the report now stops at the last **completed** bucket. The bucket in progress (today, this week, this month, this year) is no longer reported, and the range moves back one bucket with it: asked on 24 August, `timeframe=Day` covers 25 July → 24 August 00:00 where it covered 26 July → 25 August 00:00. `to` is therefore the instant the current bucket starts at — a timestamp in the **past**, always earlier than `timestamp`, where it used to be in the future.

  The number of buckets (30 / 12 / 12 / 5), the field set and the alignment to UTC calendar boundaries are unchanged. What changes is that every entry of `series` is now final: two calls return the same numbers for the same buckets, so a chart can be cached and two reports compared without a partial point moving between them. The cost is latency — activity is absent from `created`, `removed`, `tokens.inPeriod` and `series` until the bucket it falls in closes, which on `timeframe=Year` is up to a year. Lifetime `total` values are unaffected and count it immediately, so `total` and the sum of the series legitimately differ by whatever happened since the current bucket opened. Clients that charted the last, still-growing point should stop special-casing it; there is no longer a field carrying "today so far".
* **2026-08-24** — `GET /v1/usage/all`, `GET /v1/usage/user/{userId}` and `GET /v1/usage/corpus/{corpusId}` — two consequences of `series` that change numbers an existing client reads, without changing the shape of the response.

  The top-level `created`, `removed` and `tokens.inPeriod` now span the **whole reported range** rather than a fixed rolling window: `timeframe=Day` covers 30 days where it covered 24 hours, `Week` 12 weeks where it covered 7 days, `Month` 12 months where it covered 30 days, and `Year` 5 years where it covered 365 days. Lifetime `total` values are unaffected. A client that wants the old single-period figure should read the last entry of `series` instead.

  And `to` is now the exclusive end of the in-progress bucket, so it is a timestamp in the **future** and is no longer equal to `timestamp`. `timestamp` remains the server time the report was computed at, and now always falls inside the last bucket. Code that used either field as "now" should read `timestamp`.
* ⚠️ **2026-08-20** — `GET /v1/doc/{id}/preview-urls` and `GET /v1/post/attachment/{docId}/preview-urls` — `pages` is now **required** and bounded: between 1 and 10 zero-based page indices per request, `400` otherwise. Repeat the parameter (`?pages=0&pages=2`) or send it comma-separated (`?pages=0,2`); duplicates are preserved as supplied and count towards the limit. A call therefore issues at most `10 pages × 2 sizes = 20` presigned URLs. Each index must also address a page of that document — negatives are rejected, and so is anything at or past its page count once that count is known (`nbPages` is `0`, meaning *not counted yet*, until the rendering pipeline reports it; the upper bound is not applied then). Calling either endpoint **without** `pages` used to return every page of the document and now returns `400`: request the pages you are about to display, several calls if needed — `nbPages` from `GET /v1/doc/{id}` tells you how many there are. Everything else is unchanged: same response shape, same per-tile `404` fallback.
* ⚠️ **2026-08-20** — `GET /v1/doc/` — the paging parameters are now validated: `pageSize` must be between `1` and `100`, and `pageIndex` zero or greater. Values outside those bounds are refused with `400`, where they previously failed with `500`. Clients requesting more than 100 documents per page must lower `pageSize` and iterate over `pageIndex`.

### Removed

* ⚠️ **2026-08-28** — the **`attachments`** field is gone from every post. It had been deprecated since the dedicated endpoint shipped, carrying the note "use `/post/attachment` to get an accurate list", and it is now removed from `GET /v1/post/q`, `POST /v1/post/`, `GET /v1/post/{postId}` and both listings.

  Replace a read of `post.attachments` with a call to `GET /v1/post/attachment/{postId}`, which returns the same document-level citations — grouped by document, with the pages used and the document summary. The `attachment` field stays and is the cheap way to know whether that call is worth making: it counts the **chunks** behind the answer, so it is zero when there is nothing to fetch. Note it does not equal the length of the list you get back — several chunks of one document collapse into a single citation there.

  This is why the read paths got faster: returning a page of posts used to cost an attachment query per post, an embedding query per post carrying sources, and a document summary per citation, to fill a field clients had already been told to stop reading.

### Deprecated

* **2026-08-28** — `GET /v1/session/byUser` and `GET /v1/session/byMetadata`, superseded by `GET /v1/session/q`. Both are still served and unchanged. Migrating is a rename: `byUser?userId=…&corpusId=…` becomes `q?userId=…&corpusId=…`, and `byMetadata?key=…&value=…` becomes `q?key=…&value=…`. One difference is worth knowing: on `/q` a request with no metadata parameter is legal and means "do not filter on metadata", where `byMetadata` answers `400`.
* **2026-08-05** — `PUT /v1/corpus/{corpusId}` → use `PATCH /v1/corpus/{corpusId}`. Still served and strictly equivalent; migrate at your convenience.

### Fixed

* ⚠️ **2026-08-31** — `GET /v1/post/` — **the paging parameters now work.** `pageSize` and `pageIndex` were declared, documented and then discarded: every call ran a fixed "last 200 posts of the session" query, so asking for 25 returned up to 200, and `pageIndex=1` returned the same posts as `pageIndex=0`. A long conversation could not be walked at all.

  The endpoint now pages properly, and that changes what an existing call returns:

  * **A default call returns 25 posts, not 200.** If you were relying on one request bringing back a whole conversation, walk `pageIndex` — or ask for a larger page, up to `100`.
  * **The page is newest first.** That is what the endpoint always claimed to do; the old query returned its 200 posts oldest first. To keep reading a conversation forwards, pass `order=ASC`.
  * **`pageSize` is validated.** It has to be between `1` and `100`, and `pageIndex` zero or greater. Values outside those bounds are refused with `400` instead of being ignored.

  Two fields are new on the response. `pageSize` echoes the page you asked for, and `total` is the number of posts in the session across every page — divide by `pageSize` to know how far you have to walk. Soft-deleted posts are excluded from both the page and the count.
* **2026-08-20** — `GET /v1/doc/` — the `status` filter answered `500` whenever it was used on its own. `?status=PENDING` now returns the documents in that lifecycle state as documented, for every status value. Only the filter used alone was affected: combining `status` with `tags` (`?status=READY&tags=legal`) already worked and is unchanged.

***

## 2026-07

### Added

* **2026-07-30** — `GET /v1/post/attachment/{docId}/download-url` — presigned URL to download a source document cited by an answer, reachable with the same token used to read the session.
* **2026-07-30** — `GET /v1/post/attachment/{docId}/preview-urls` — presigned URLs for the rendered page previews of a source document. Pages `0–3` in sizes `SMALL` and `MEDIUM` by default; pass the repeatable `pages` query parameter to restrict the result (`?pages=0&pages=2`). Individual URLs may return `404` while a preview is still being generated — fall back per tile.
* **2026-07-30** — `scope` on `POST /v1/auth/access-token` — restrict what a token may do with a list of `DOMAIN:ACTION` entries, where `DOMAIN` is one of `config`, `auth`, `session`, `doc`, `corpus`, `post`, `usage` and `ACTION` one of `create`, `read`, `update`, `delete` (e.g. `["corpus:read","doc:create"]`). Omit the field to issue a token with full privileges over the organization.
* **2026-07-08** — **Access tokens** — `POST /v1/access-token/` and `DELETE /v1/access-token/{token}`, plus the `X-Access-Token` header as an alternative to the `Authorization: Bearer <jwt>` header. Short-lived opaque tokens (default TTL `3600` s, optional `issuer`, `email`, `userId`) meant for browser and widget clients that must not hold your RSA key.
* **2026-07-08** — `GET /v1/post/q` — send a query to a session. Same contract as the former `POST /v1/post/` (`sessionId`, `body`, optional `lang`), expressed as a read.
* **2026-07-07** — `GET /v1/post/attachment/{postId}` — sources used to build an answer, grouped by document: `docId`, `summary`, `pages[]` and `metadata`. Posts also expose an `attachment` count, so a client can tell at a glance whether an answer cites anything.

### Changed

* ⚠️ **2026-07-30** — `POST /v1/access-token/` → `POST /v1/auth/access-token`, and `DELETE /v1/access-token/{token}` → `DELETE /v1/auth/access-token/{token}`.
* ⚠️ **2026-07-30** — `GET /v1/whoami/` → `GET /v1/auth/whoami`.
* ⚠️ **2026-07-30** — `GET /v1/model/` → `GET /v1/config/model`.
* ⚠️ **2026-07-30** — widget webhook responses reworked: posts and attachments are returned with their own schemas, attachments being grouped by document with the list of pages used.
* **2026-07-02** — `GET /v1/doc/{id}/summary` is now produced asynchronously. Same `text/markdown` payload; the request completes when the summary is ready.

### Deprecated

* **2026-07-30** — `GET /v1/doc/{id}/download-url` and `GET /v1/doc/{id}/preview-urls` → use `GET /v1/post/attachment/{docId}/download-url` and `GET /v1/post/attachment/{docId}/preview-urls`, which work with session-scoped access tokens.
* **2026-07-30** — the whole widget webhook family `/v1/webhook/widget/**` (`/init`, `/q`, `/`, `/attachment/{postId}`) → build directly on `/v1/session/**` and `/v1/post/**` with a scoped access token.
* **2026-07-08** — `POST /v1/post/` → use `GET /v1/post/q`.

***

## 2026-06

### Added

* **2026-06-08** — `PATCH /v1/session/{sessionId}` — update a live session: `model`, `system`, `temperature`, `thinking`, `metadata`. Only the fields you send are applied; `metadata` **replaces** the stored map when present.
* **2026-06-05** — `GET /pub/ping` — unauthenticated health check (previously `GET /ping`).
* **2026-06-03** — `GET /v1/session/byUser`, `GET /v1/session/byOrganization` and `GET /v1/session/byMetadata` — list sessions by owner, across the organization, or by matching a metadata fragment.
* **2026-06-03** — `GET /v1/usage/all`, `GET /v1/usage/user/{userId}` and `GET /v1/usage/corpus/{corpusId}` — aggregated counters (tokens, corpora, documents, sessions, posts, storage).
* **2026-06-03** — `GET /v1/doc/{id}/status` — ingestion status of a document, so a client can poll instead of fetching the whole resource.
* **2026-06-03** — `GET /v1/doc/{id}/download-url` and `GET /v1/doc/{id}/preview-urls` — presigned URLs served directly by the storage backend; no binary content flows through the API. Previews are JPEG images, one per page and size.
* **2026-06-02** — `GET /v1/whoami/` — identity and organization carried by the caller's token.

### Changed

* ⚠️ **2026-06-08** — `GET /v1/session/` → `GET /v1/session/byCorpus`.
* ⚠️ **2026-06-03** — the `orgId` request parameter was removed from every endpoint that accepted it (corpus, usage, user, widget). The organization is resolved from the caller's token; sending `orgId` is no longer necessary and no longer honoured.
* ⚠️ **2026-06-03** — operations that returned an empty body now return an acknowledgement object (`timestamp`, `message`) — deletes on corpus, document, session and post.
* **2026-06-03** — every resource reached through the API is checked against the organization carried by the token. A resource belonging to another organization is refused, whether or not it exists.
* **2026-06-03** — deleting a document is a soft delete: the document stops being returned by the API and stops being searched, and the identifier is not reused.
* **2026-06-03** — widget webhook base path moved to `/v1/widget/webhook`, then to `/v1/webhook/widget/` on 2026-07-08.

### Removed

* ⚠️ **2026-06-03** — `GET /v1/doc/{id}/download` (binary stream) — use the presigned download URL endpoint instead.
* ⚠️ **2026-06-03** — `/v1/admin/**`.
* ⚠️ **2026-06-02** — `/v1/org/**` — organizations are provisioned during onboarding and are no longer managed through the public API.

### Deprecated

* **2026-06-10** — the first-generation widget endpoints `GET|POST /webhook/v1/widget/{lang}` are kept available for embeds already in production, but should not be used for new integrations.

***

## 2026-05

### Added

* **2026-05-22** — **Direct-to-storage upload**: `POST /v1/doc/init` returns a document in `AWAITING_UPLOAD` status together with a single-use presigned `uploadUrl` and its `expiresAt`; the client `PUT`s the file with the exact `contentType` declared at init, then calls `POST /v1/doc/{id}/commit` to start ingestion. Content types must be one of those listed by `GET /v1/doc/accept`.
* **2026-05-17** — `GET /v1/doc/{id}/summary` — LLM summary of a document, as `text/markdown`.
* **2026-05-17** — document payloads accept `metadata` (free-form JSON), `provider`, `userId` and the original document dates (`docCreate`, `docUpdate`).
* **2026-05-16** — first widget endpoints, `GET|POST /webhook/v1/widget/{lang}`.
* **2026-05-13** — `GET /ping`.
* **2026-05-11** — `lang` (ISO-639) on documents and posts: drives the language of the summary and of the answer.
* **2026-05-10** — sessions accept several corpora (`corpusIds`), a custom system prompt (`system`), a sampling `temperature` and a `thinking` flag. These are locked at creation time and apply to every post of the session.

### Changed

* ⚠️ **2026-05-22** — `POST /v1/doc/upload` (multipart) was replaced by the init/commit flow above.
* ⚠️ **2026-05-18** — resource identifiers moved from a query-parameter style to path variables: `/v1/corpus/{corpusId}`, `/v1/doc/{id}`, `/v1/session/{sessionId}`, `/v1/post/{postId}`.
* ⚠️ **2026-05-18** — `embeddingModel` and `llmSummaryModel` were removed from the corpus payloads. A corpus now carries `name`, `description` and `metadata` only; the answering model is chosen per session (`model`) and the embedding model is managed by the platform.

***

## 2026-04

### Added

* **2026-04-30** — first version of the API: `/v1/corpus`, `/v1/doc`, `/v1/session`, `/v1/post`, with JWT Bearer authentication (RSA-signed tokens), asynchronous document ingestion with a status per document, and RAG queries answered from the corpora bound to a session.


---

# Agent Instructions
This documentation is published with GitBook. GitBook is the documentation platform designed so that both humans and AI agents can read, navigate, and reason over technical content effectively. Learn more at gitbook.com.

## Querying This Documentation
If you need additional information that is not directly available in this page, you can query the documentation dynamically by asking a question.

Perform an HTTP GET request on the current page URL with the `ask` query parameter, and the optional `goal` query parameter:

```
GET https://verbatim-ai.gitbook.io/docs/integration/changelog.md?ask=<question>&goal=<endgoal>
```

`ask` is the immediate question: it should be specific, self-contained, and written in natural language.
`goal` is optional and describes the broader end goal you are ultimately trying to accomplish on behalf of the user. GitBook uses it to tailor the answer towards what is most useful for that goal.

The response will contain a direct answer to the question and relevant excerpts and sources from the documentation.

Use this mechanism when the answer is not explicitly present in the current page, you need clarification or additional context, or you want to retrieve related documentation sections.
