What's new
Notable changes to the public REST API of the Verbatim AI platform.
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.
2026-10
Added
-
2026-10-08 —
GET /v1/doc/{id}/md— read a document as Markdown. Returns a time-limited presigned
URL to the Markdown conversion produced by ingestion —{url, timestamp, expiresAt}— toGETstraight from
storage without a token. The URL answers404until the document isREADY; a document still
AWAITING_UPLOADis a409. Requiresdoc:read. -
2026-10-08 — MCP — new tool
get_document_markdown(doc:read): the full Markdown content of a
document, by its id — e.g. adocIdcited in the sources of arag_queryanswer — returned as an embedded
resource of typetext/markdown. See MCP server. -
2026-10-08 —
POST /mcp— an MCP server. AI assistants connect over Streamable HTTP (stateless) and
ask questions with therag_querytool — the same RAG query asGET /v1/post/q, over every corpus of the
organization or thecorpusIdsgiven, answered with its sources. Each call opens a new thread
(metadata.source = "mcp") that records the exchange. Only access tokens open it, asAuthorization: Bearer <access-token>orX-Access-Token;
rag_queryrequirespost:read. A second tool,list_corpora(corpus:read), lists the organization's
corpora so the assistant can pickcorpusIds. A missing or invalid token is a401. See MCP server. -
2026-10-07 —
GET /v1/doc/q— search documents by metadata. Repeatmeta=key:valuefor each
condition on the document'smetadata(split at the first:; the value compares as text, soyear:2026
matches a string or a number).metaMatch=ALL(default) requires every condition,metaMatch=ANYat least one.
Combines with every other filter of the search.GET /v1/doc/q?corpusId=<uuid>&meta=team:legal&meta=year:2026 GET /v1/doc/q?corpusId=<uuid>&meta=team:legal&meta=team:hr&metaMatch=ANY -
2026-10-07 —
POST /v1/doc/url— import a web page into a corpus. Send the page'shttpsURL and
the server prints it to PDF (headless Chromium, scripts included) and commits it: the answer is the document,
alreadyPENDING. It is named after the page's<title>and takes its language from<html lang>;
provideriswebandmetadata.urlkeeps the source URL. Optionalscale(0.1–2) andheaders— sent to the
page's own origin only, never stored — for pages behind a login. The URL is checked first: it must
answer200with HTML after redirects, or the call is a400(415for a non-HTML answer) and nothing is printed.curl -X POST "https://api.verbatim-ai.com/v1/doc/url" \ -H "Authorization: Bearer $TOKEN" \ -H "Content-Type: application/json" \ -d '{"corpusId": "550e8400-e29b-41d4-a716-446655440001", "url": "https://example.com/"}' -
2026-10-02 —
GET /v1/auth/access-token/scopes— list the scopes an access token can be created
with. Each domain comes with the API path it covers, a description and itsDOMAIN:ACTIONentries; each
action with the HTTP methods it opens; andscopesis the flat list of every valid entry. Build scope
pickers from it rather than hard-coding the list. -
2026-10-02 —
GET /v1/auth/access-token— list your organization's access tokens, newest
first, paginated withpageSize/pageIndex. Every stored attribute is returned except the token
value, which is cut down to its first characters ("token": "8Jf3kQ2p..."). The full value is only
ever returned by the create call. Expired tokens stay listed until revoked. -
2026-10-02 —
DELETE /v1/auth/access-token/id/{id}— revoke an access token by id, theid
coming from the listing.404when the id names no token of your organization. Revocation by value,
DELETE /v1/auth/access-token/{token}, is unchanged. -
2026-10-01 —
POST /v1/doc/convert— convert a file to Markdown on the fly. Send the file itself
as the body (application/octet-stream) and get its Markdown back in the response, ready to be put in an LLM's
context. Nothing is stored and no corpus is involved.curl -X POST "https://api.verbatim-ai.com/v1/doc/convert?filename=report.pdf" \ -H "Authorization: Bearer $TOKEN" \ -H "Content-Type: application/octet-stream" \ --data-binary @report.pdf{ "contentType": "application/pdf", "filename": "report.pdf", "size": 48213, "pages": 3, "markdown": "# Annual report 2025\n\nRevenue grew **12%** over the year.", "warnings": [] }The format is detected from the content — no need to declare it;
filenameis optional. PDF, Word, Excel and
PowerPoint (current and legacy), OpenDocument, RTF, EPUB, HTML, Markdown, plain text, CSV and e-mail are read; an
unrecognised file answers415, an unreadable one (corrupt, password-protected)400. A scanned PDF converts to no
text, with a warning. Bodies are limited to 25 MB — above,413, a status this endpoint introduces. An access token
needs thedoc:createscope. See Converting a file to Markdown. -
2026-10-01 — four more supported models, listed by
GET /v1/config/modelfrom this
release and usable straight away as an agent'sbaseModelorrerankModel:idName Notes deepseek-v4-flashDeepSeek V4 Flash Reasoning close to V4-Pro, with a smaller footprint and faster answers. qwen36QWen 3.6 Mixture-of-experts with a vision encoder — 35B parameters, 3B active. llama33Llama 3.3 Meta's 70B multilingual instruction model, tuned for dialogue. openai-gpt-oss-120bGPT OSS 120b OpenAI's open-weight model, for reasoning and agentic tasks. They come after
gemma4andmistralinmodels(and in the deprecateditems), so a picker
that preselects the first entry still lands ongemma4: the platform default is unchanged.Non-breaking: the catalog grows, and existing agents keep the model they were set to.
Changed
- ⚠️ 2026-10-02 —
POST /v1/auth/access-token—ttlis capped by the platform, at 86400 seconds
(24 hours) unless configured otherwise. A longerttlused to be accepted; it is now refused with400.
The same ceiling applies toPUT /root/v1/impersonate. - 2026-10-02 —
POST /v1/auth/access-token— an invalidscopeor attlbelow 10 seconds is now a
400with the reason inmessage, instead of a500. - 2026-10-02 — Access-token management (create, list, revoke) refuses any access token with
403,
whatever its scope. It used to rely on scope alone, so a token carryingauth:createcould mint another.
Use a JWT.
2026-09
Added
-
2026-09-30 —
GET /v1/config/model— the models now describe themselves. Alongside the
identifiers, the response carriesmodels— one entry per supported LLM with everything needed to
present it — andtotal, how many that is:{ "total": 2, "models": [ { "id": "gemma4", "name": "Gemma 4", "description": "Google's open lightweight model. The platform default: quick to answer and inexpensive, a good fit for everyday questions over a corpus.", "iconUrl": "https://cdn.simpleicons.org/google" }, { "id": "mistral", "name": "Mistral Small 3.2", "description": "Mistral AI's 24B instruction-tuned model. Stronger on European languages and on long, nuanced answers, for a higher cost per query.", "iconUrl": "https://cdn.simpleicons.org/mistralai" } ], "items": ["gemma4", "mistral"] }idis still the only value the server reads back — it is what an agent'sbaseModeland
rerankModelare set to. The other three exist to be rendered: a model picker is now this
endpoint's response and nothing else, where before every client carried its own table of labels
keyed by identifier and drifted from the platform each time the list moved.nameand
descriptionare editorial and may be reworded at any time, so do not match on them;iconUrlis
an absolute URL to an SVG hosted off-platform — render it as a remote image and keep a fallback.
modelscomes in the order the models are meant to be offered, so preselect the first entry. The
list is not paginated: it holds the whole catalog, andtotalis its length.itemsis deprecated and unchanged — the same identifiers in the same order, derived from the
same catalog asmodels, so the two cannot disagree for as long as both are served. Clients
reading it keep working; move tomodels[].id, as it will be removed in a future release.Non-breaking: two new fields on an existing payload.
-
2026-09-07 —
storageandnbChunks— 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 two more counters filled in by ingestion.storageis the space the document actually occupies on the platform, in bytes: the source file
plus everything ingestion derived from it — rendered page previews, the markdown conversion,
the summary, the embedding payloads.sizehas always been the uploaded file alone, which
under-reports the footprint of anything that renders or chunks, sometimes by an order of
magnitude. Both are served: displaysize, bill onstorage.nbChunksis how many chunks the document was split into — the number of passages
GET /v1/chunk/q?documentId=…returns for it, available without paging through them. It follows
DELETE /v1/chunk/{chunkId}down: deleting a chunk takes one off the count, so the two never
disagree. A document still reporting0stays at0, since there the value means not computed
yet rather than no chunks.Both are always present, and
0means not computed yet — the processing pipeline reports them
during ingestion, so they stay0until it does, exactly likenbPages. Replacing a document's
content withPUT /v1/doc/{id}/initresets both to0along with the other ingestion counters.Non-breaking: two new fields on an existing payload.
Changed
-
2026-09-23 — Tuning Vector search. New duplicate content algorithm.
Retrieval now skips chunks an out-of-band analysis has identified as redundant copies of text
already embedded elsewhere — a boilerplate header repeated on every page, a mail thread quoting
itself at each reply, a template signed many times. Every copy embeds to nearly the same vector,
so they used to arrive together and fill the context window with one passage while the other
things worth reading were pushed out of it.Nothing in the request or the response changes, and no new field is exposed. What changes is the
quality of whatGET /v1/post/qretrieves: the sametopKnow buys distinct
passages. Answers over corpora holding repeated text should cite more documents than before.GET /v1/chunk/andGET /v1/chunk/qare unaffected and keep listing every chunk, suppressed
ones included — the flag governs retrieval, not visibility. -
2026-09-23 —
docCreateanddocUpdateare always present on 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}). They were nullable
and are not any more.The two fields are the source document's own dates — when the file was written and when it
was last modified — as opposed tocreatedAt/updatedAt, which describe the platform row. A
contract signed in 2019 and uploaded yesterday has adocCreateof 2019 and acreatedAtof
yesterday.Both stay optional on the request:
POST /v1/doc/initstill accepts neither, one, or both.
Nothing about what you send changes — there is no new required field. What changes is what comes
back when you send nothing: the upload instant, on whichever of the two you left out, rather than
null. The two fall back independently, so sendingdocCreatealone does not filldocUpdate
in from it.Replacing a document's content with
PUT /v1/doc/{id}/initnow re-stampsdocUpdatewith the
moment of that call, since the stored date described the bytes being replaced.docCreateis kept,
along with the rest of the document's identity. If you know the new file's real modification date,
PATCH /v1/doc/{id}it after the re-init — patching it before is overwritten.Neither field can be set back to
null: onPATCH /v1/doc/{id}, omitting one means leave it
alone, and there is no value meaning unset. Use the patch to replace a date the platform had to
guess at with the real one.Existing documents are backfilled with their upload date, so nothing stays null. Non-breaking for
any client that reads these fields; a client that wrote logic around them being absent —
"nodocCreatemeans unknown" — now sees a date where it saw nothing, and should key off whether
it supplied one instead. -
2026-09-07 — Domain name
Sessionrenamed toThread. All endpoints/v1/sessionare deprecated and replaced by/v1/thread. -
2026-09-07 —
GET /v1/usage/all,GET /v1/usage/user/USERID,GET /v1/usage/corpus/{corpusId}—
thestoragedimension now sums the documents'storagerather than theirsize.The reported totals therefore grow for every scope holding documents that have been ingested
since this release: what is counted is now the whole footprint — page previews, markdown, summary
and embedding payloads included — where before it was only the bytes that were uploaded. The
shape of the response is unchanged, and the values are still bytes.Documents ingested before this release keep reporting their uploaded size until they are
ingested again: the derived artefacts they produced were never measured, and nothing records
them retroactively.
2026-08
Added
-
2026-09-02 —
GET /v1/post/— anorderparameter,DESC(the default) orASC.DESCreads the conversation backwards, most recent first, so page0is 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.ASCreads 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 timeThe 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 oncreatedAtand the ordering is closed by
the post id, so walkingpageIndexnever 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 — readowner
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/qSearch 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=9e107d9d372bb6826bd81d3542a419d6pagesis a span, not a page: a chunk crossing pages 3 to 5 answers topage=3,page=4and
page=5alike, and a chunk covering no page in particular — the document summary — carries an
empty array.hashis 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
scalarpagecolumn.bodycosts a storage read, so the two listings omit it unless you passbody=true, where the
page size is capped at25;GET /v1/chunk/{chunkId}always carries it. An emptybodyon 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
PATCHandDELETE:- Rewriting
bodydoes 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.hashis not recomputed either — a chunk whosehashno longer matches itsbody
is one that has been patched. metadatareplaces rather than merges. Send the whole object;{}clears it.DELETEtakes 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 matchingchunk:*scope. - Rewriting
-
2026-08-28 —
GET /v1/session/andGET /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/qnarrows the same set by
userId,corpusIdand metadata (key/value, orjsonfor a nested fragment), every filter
optional and combining:GET /v1/session/q?userId=user_42&corpusId=<uuid>&key=customer_id&value=42That 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 asGET /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 walkingpageIndexnever
repeats nor skips one.totalcounts 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 acorpusIdoutside your organization answers403on the request that named
it, rather than an empty page.keywithoutvalue(or the reverse) answers400rather than
quietly returning everything. -
2026-08-28 — sessions now report
updatedAtalongsidecreatedAt, 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 asagentIdonGET /v1/post/q:Agent Use it for Verbatim HelpdeskEveryday end-user questions — answer first, then the steps in the order they are performed, in plain language. Verbatim LegalContracts, 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 MarketingPositioning and product questions — the point that matters first, then the evidence; never invents a figure or a customer name. Verbatim DailyDay-to-day use — the answer in the first sentence, direct and friendly. Verbatim R&DTechnical 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 sincethinkingMode,temperatureandbaseModelare still
the onesVerbatim Defaultcarries, your session's own settings keep precedence exactly as before.Non-breaking, and nothing changes unless you ask for it:
Verbatim Defaultremains the only agent
carryingdefault: 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"}answers409. -
2026-08-25 —
GET /v1/post/q(and the deprecatedPOST /v1/post/) — optionalagentId
query parameter, selecting the agent 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
agentIdfield, and only on the answer —
the user's question is not something an agent produced. A missingagentIdon 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}answers404; re-attributing old answers to the default agent when one
is deleted would misreport what actually ran.An
agentIdyour organization cannot see answers404and 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}andDELETE /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
answers400. Custom agents (lock: false) belong to your organization;POSTalways
creates one of those. Exactly one core agent carriesdefault: 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.GETtherefore reports them as
stored, and read-onlysystemInstructionshows the resolved text the model receives. OnPATCH,
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 answer404—
indistinguishable from an id that never existed. Answers already produced under it keep naming it
in theiragentId, 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|deletescope.Non-breaking: a new domain, and existing queries are unaffected. The seeded default agent carries
the exact valuesapp.search.*held before, andthinkingMode/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/USERIDand
GET /v1/usage/corpus/{corpusId}—series, a per-bucket breakdown of the same metrics the
report already carried at the top level.timeframeno longer selects a single rolling window;
it selects the bucket size, and with it how far back the report reaches:Dayreturns 30
daily buckets (~1 month),Week12 ISO weeks (~3 months),Month12 months (1 year) andYear
5 years. Each entry carries its ownfrom/to, atokenscount, andcreated/removedfor
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 ofpageson
GET /v1/doc/{id}/preview-urls, whose valid indices are0..nbPages-1.0means not counted
yet: the rendering pipeline fills it in during ingestion, and it stays0until then and for
formats that have no pages. Unlikesize,tokensandnbWords, which are omitted from the
response while unset,nbPagesis 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.corpusIdis required; every other parameter is an optional filter and they all
narrow together.qmatches the filename, case-insensitively and anchored at the start
of the name:?q=annualfindsAnnual-Report-2025.pdf,?q=reportdoes not. Put a*
anywhere to match elsewhere —?q=*report*for a substring,?q=2025-*.pdffor a name that
starts with2025-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.tagsrepeats as onGET /v1/doc/and
now takes a companiontagsMatch:ANY(the default, at least one of them) orALL(every one
of them, extra tags allowed).statusrepeats too and matches any of the listed states
(?status=PENDING&status=FAILED).contentType,langandprovidermatch exactly.
createdAfterandcreatedBeforebound 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 (createdAfterat or aftercreatedBefore) is refused with400. Order the result with
sort(CREATED_AT,UPDATED_AT,FILENAME,SIZE) andorder(ASC,DESC), newest
first by default; the ordering is stable, so walkingpageIndexnever repeats nor skips a
document. Paging uses the samepageSize(1-100, default 25) andpageIndexas everywhere
else, and the response carriespageSizeandtotal— the number of matches across all
pages — alongside the usualcorpusId,pageIndexanditems. 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/initand change them withPATCH /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 (400otherwise).GET /v1/doc/gains a matchingtags
filter returning documents that carry at least one of the requested tags — repeat the
parameter for several (?tags=legal&tags=2026), and combine it withstatusto narrow on both.
Non-breaking: the field is optional andnullon 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_titleorbasic),max_characters,new_after_n_chars,overlap,
overlap_all,combine_text_under_n_charsandmultipage_sections. All are optional — send
only what you want to change. Set it atPOST /v1/doc/initand 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 andnullon 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 inREADYorFAILEDstatus (409
otherwise); it moves back toAWAITING_UPLOADand the response carries a fresh presigned PUT
URL — same payload asPOST /v1/doc/init, so the usualPUT+POST /v1/doc/{id}/commitflow
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 thesize/tokens/nbWordscounters. Two consequences: posts that cited this
document lose their attachments to it, and the previously uploaded file stays in storage
until yourPUToverwrites it, so committing without uploading re-ingests the old content.
Requires thedoc:updatescope. -
2026-08-05 —
PATCH /v1/doc/{id}— update the editable attributes of a document:filename,
docCreate,docUpdateandmetadata. Only the fields present in the body are applied;
metadatareplaces the stored map when provided.docCreate/docUpdatedescribe the
source document —createdAt/updatedAtstay server-managed. Renaming does not move the
stored file nor re-trigger ingestion. Requires thedoc:updatescope. -
2026-08-05 —
GET /v1/doc/{docId}/download-urland
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
thedocdomain, so a scoped token needsdoc:readhere where the/v1/postpath needs
post:read. -
2026-08-05 —
PATCH /v1/corpus/{corpusId}— replacesPUT /v1/corpus/{corpusId}. Same
request body, same response, samecorpus:updatescope: the verb changes, the behaviour does
not.PATCHis 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, andDELETE /v1/doc/{docId}
cascades to the document's chunks. Nothing observable changes about either endpoint — a deleted
chunk still disappears fromGET /v1/chunk/,/qandGET /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 — ⚠️
agentIdis 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 absentagentIdmeant "ran on the default": every post says
what produced it. If your code treats a missingagentIdas "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/andPATCH /v1/session/{sessionId}no longer takemodel,
system,temperatureorthinking. How a query is answered has been the agent'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.metadatais 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/andPATCH /v1/agent/{agentId}— an agent'snamenow 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), answers409and writes nothing.The edges of the rule. Names are compared exactly, so
Supportandsupportare two names
andVerbatim Default v2is 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
PATCHis 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/USERIDand
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=Daycovers 25 July → 24 August
00:00 where it covered 26 July → 25 August 00:00.tois therefore the instant the current
bucket starts at — a timestamp in the past, always earlier thantimestamp, 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 ofseriesis 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.inPeriodandseriesuntil the bucket it falls in closes, which
ontimeframe=Yearis up to a year. Lifetimetotalvalues are unaffected and count it
immediately, sototaland 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/USERIDand
GET /v1/usage/corpus/{corpusId}— two consequences ofseriesthat change numbers an existing
client reads, without changing the shape of the response.The top-level
created,removedandtokens.inPeriodnow span the whole reported range
rather than a fixed rolling window:timeframe=Daycovers 30 days where it covered 24 hours,
Week12 weeks where it covered 7 days,Month12 months where it covered 30 days, andYear
5 years where it covered 365 days. Lifetimetotalvalues are unaffected. A client that wants
the old single-period figure should read the last entry ofseriesinstead.And
tois now the exclusive end of the in-progress bucket, so it is a timestamp in the
future and is no longer equal totimestamp.timestampremains the server time the report
was computed at, and now always falls inside the last bucket. Code that used either field as
"now" should readtimestamp. -
⚠️ 2026-08-20 —
GET /v1/doc/{id}/preview-urlsand
GET /v1/post/attachment/{docId}/preview-urls—pagesis now required and bounded:
between 1 and 10 zero-based page indices per request,400otherwise. 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 = 20presigned 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 (nbPagesis0, meaning not counted yet, until the rendering pipeline
reports it; the upper bound is not applied then). Calling either endpoint withoutpagesused
to return every page of the document and now returns400: request the pages you are about
to display, several calls if needed —nbPagesfromGET /v1/doc/{id}tells you how many
there are. Everything else is unchanged: same response shape, same per-tile404fallback. -
⚠️ 2026-08-20 —
GET /v1/doc/— the paging parameters are now validated:pageSizemust be
between1and100, andpageIndexzero or greater. Values outside those bounds are refused
with400, where they previously failed with500. Clients requesting more than 100 documents
per page must lowerpageSizeand iterate overpageIndex.
Removed
-
⚠️ 2026-08-28 — the
attachmentsfield is gone from every post. It had been deprecated
since the dedicated endpoint shipped, carrying the note "use/post/attachmentto get an accurate
list", and it is now removed fromGET /v1/post/q,POST /v1/post/,GET /v1/post/{postId}and
both listings.Replace a read of
post.attachmentswith a call toGET /v1/post/attachment/{postId}, which
returns the same document-level citations — grouped by document, with the pages used and the
document summary. Theattachmentfield 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/byUserandGET /v1/session/byMetadata, superseded by
GET /v1/session/q. Both are still served and unchanged. Migrating is a rename:
byUser?userId=…&corpusId=…becomesq?userId=…&corpusId=…, andbyMetadata?key=…&value=…
becomesq?key=…&value=…. One difference is worth knowing: on/qa request with no metadata
parameter is legal and means "do not filter on metadata", wherebyMetadataanswers400. -
2026-08-05 —
PUT /v1/corpus/{corpusId}→ usePATCH /v1/corpus/{corpusId}. Still served
and strictly equivalent; migrate at your convenience.
Fixed
-
⚠️ 2026-08-31 —
GET /v1/post/— the paging parameters now work.pageSizeand
pageIndexwere 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, andpageIndex=1returned the same
posts aspageIndex=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, walkpageIndex— or ask for a larger page, up to100. - 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, passorder=ASC. pageSizeis validated. It has to be between1and100, andpageIndexzero or
greater. Values outside those bounds are refused with400instead of being ignored.
Two fields are new on the response.
pageSizeechoes the page you asked for, andtotalis the
number of posts in the session across every page — divide bypageSizeto know how far you have
to walk. Soft-deleted posts are excluded from both the page and the count. - A default call returns 25 posts, not 200. If you were relying on one request bringing back a
-
2026-08-20 —
GET /v1/doc/— thestatusfilter answered500whenever it was used on its
own.?status=PENDINGnow returns the documents in that lifecycle state as documented, for every
status value. Only the filter used alone was affected: combiningstatuswithtags
(?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. Pages0–3in sizesSMALLandMEDIUMby default;
pass the repeatablepagesquery parameter to restrict the result (?pages=0&pages=2).
Individual URLs may return404while a preview is still being generated — fall back per tile. - 2026-07-30 —
scopeonPOST /v1/auth/access-token— restrict what a token may do with a
list ofDOMAIN:ACTIONentries, whereDOMAINis one of
config,auth,session,doc,corpus,post,usageandACTIONone 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 theX-Access-Tokenheader as an alternative to the
Authorization: Bearer <jwt>header. Short-lived opaque tokens (default TTL3600s, 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, optionallang), expressed as a read. - 2026-07-07 —
GET /v1/post/attachment/{postId}— sources used to build an answer, grouped by
document:docId,summary,pages[]andmetadata. Posts also expose anattachmentcount,
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}/summaryis now produced asynchronously. Same
text/markdownpayload; the request completes when the summary is ready.
Deprecated
- 2026-07-30 —
GET /v1/doc/{id}/download-urlandGET /v1/doc/{id}/preview-urls→
useGET /v1/post/attachment/{docId}/download-urland
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/→ useGET /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 (previouslyGET /ping). - 2026-06-03 —
GET /v1/session/byUser,GET /v1/session/byOrganizationand
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/USERIDand
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-urlandGET /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
orgIdrequest parameter was removed from every endpoint that accepted it
(corpus, usage, user, widget). The organization is resolved from the caller's token; sending
orgIdis 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/initreturns a document in
AWAITING_UPLOADstatus together with a single-use presigneduploadUrland itsexpiresAt;
the clientPUTs the file with the exactcontentTypedeclared at init, then calls
POST /v1/doc/{id}/committo 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, astext/markdown. - 2026-05-17 — document payloads accept
metadata(free-form JSON),provider,userIdand
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 samplingtemperatureand athinkingflag. 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 —
embeddingModelandllmSummaryModelwere removed from the corpus payloads.
A corpus now carriesname,descriptionandmetadataonly; 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.
Updated about 1 hour ago