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} — to GET straight from
    storage without a token. The URL answers 404 until the document is READY; a document still
    AWAITING_UPLOAD is a 409. Requires doc:read.

  • 2026-10-08 — MCP — new tool get_document_markdown (doc:read): the full Markdown content of a
    document, by its id — e.g. a docId cited in the sources of a rag_query answer — returned as an embedded
    resource of type text/markdown. See MCP server.

  • 2026-10-08 — POST /mcp — an MCP server. AI assistants connect over Streamable HTTP (stateless) and
    ask questions with the rag_query tool — the same RAG query as GET /v1/post/q, over every corpus of the
    organization or the corpusIds given, answered with its sources. Each call opens a new thread
    (metadata.source = "mcp") that records the exchange. Only access tokens open it, as Authorization: Bearer <access-token> or X-Access-Token;
    rag_query requires post:read. A second tool, list_corpora (corpus:read), lists the organization's
    corpora so the assistant can pick corpusIds. A missing or invalid token is a 401. See MCP server.

  • 2026-10-07 — GET /v1/doc/q — search documents by metadata. Repeat meta=key:value for each
    condition on the document's metadata (split at the first :; the value compares as text, so year:2026
    matches a string or a number). metaMatch=ALL (default) requires every condition, metaMatch=ANY at 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's https URL and
    the server prints it to PDF (headless Chromium, scripts included) and commits it: the answer is the document,
    already PENDING. It is named after the page's <title> and takes its language from <html lang>;
    provider is web and metadata.url keeps the source URL. Optional scale (0.1–2) and headers — sent to the
    page's own origin only, never stored — for pages behind a login. The URL is checked first: it must
    answer 200 with HTML after redirects, or the call is a 400 (415 for 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 its DOMAIN:ACTION entries; each
    action with the HTTP methods it opens; and scopes is 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 with pageSize/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, the id
    coming from the listing. 404 when 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; filename is 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 answers 415, 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 the doc:create scope. See Converting a file to Markdown.

  • 2026-10-01 — four more supported models, listed by GET /v1/config/model from this
    release and usable straight away as an agent's baseModel or rerankModel:

    idNameNotes
    deepseek-v4-flashDeepSeek V4 FlashReasoning close to V4-Pro, with a smaller footprint and faster answers.
    qwen36QWen 3.6Mixture-of-experts with a vision encoder — 35B parameters, 3B active.
    llama33Llama 3.3Meta's 70B multilingual instruction model, tuned for dialogue.
    openai-gpt-oss-120bGPT OSS 120bOpenAI's open-weight model, for reasoning and agentic tasks.

    They come after gemma4 and mistral in models (and in the deprecated items), so a picker
    that preselects the first entry still lands on gemma4: 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 — ttl is capped by the platform, at 86400 seconds
    (24 hours) unless configured otherwise. A longer ttl used to be accepted; it is now refused with 400.
    The same ceiling applies to PUT /root/v1/impersonate.
  • 2026-10-02 — POST /v1/auth/access-token — an invalid scope or a ttl below 10 seconds is now a
    400 with the reason in message, instead of a 500.
  • 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 carrying auth:create could 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 carries models — one entry per supported LLM with everything needed to
    present it — and total, 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"]
    }

    id is still the only value the server reads back — it is what an agent's baseModel and
    rerankModel are 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. name and
    description are editorial and may be reworded at any time, so do not match on them; iconUrl is
    an absolute URL to an SVG hosted off-platform — render it as a remote image and keep a fallback.
    models comes 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, and total is its length.

    items is deprecated and unchanged — the same identifiers in the same order, derived from the
    same catalog as models, so the two cannot disagree for as long as both are served. Clients
    reading it keep working; move to models[].id, as it will be removed in a future release.

    Non-breaking: two new fields on an existing payload.

  • 2026-09-07 — storage and nbChunks — 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.

    storage is 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. size has 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: display size, bill on storage.

    nbChunks is 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 reporting 0 stays at 0, since there the value means not computed
    yet
    rather than no chunks.

    Both are always present, and 0 means not computed yet — the processing pipeline reports them
    during ingestion, so they stay 0 until it does, exactly like nbPages. Replacing a document's
    content with PUT /v1/doc/{id}/init resets both to 0 along 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 what GET /v1/post/q retrieves: the same topK now buys distinct
    passages. Answers over corpora holding repeated text should cite more documents than before.

    GET /v1/chunk/ and GET /v1/chunk/q are unaffected and keep listing every chunk, suppressed
    ones included — the flag governs retrieval, not visibility.

  • 2026-09-23 — docCreate and docUpdate are 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 to createdAt/updatedAt, which describe the platform row. A
    contract signed in 2019 and uploaded yesterday has a docCreate of 2019 and a createdAt of
    yesterday.

    Both stay optional on the request: POST /v1/doc/init still 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 sending docCreate alone does not fill docUpdate
    in from it.

    Replacing a document's content with PUT /v1/doc/{id}/init now re-stamps docUpdate with the
    moment of that call, since the stored date described the bytes being replaced. docCreate is 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: on PATCH /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 —
    "no docCreate means unknown" — now sees a date where it saw nothing, and should key off whether
    it supplied one instead.

  • 2026-09-07 — Domain name Session renamed to Thread. All endpoints /v1/session are deprecated and replaced by /v1/thread.

  • 2026-09-07 — GET /v1/usage/all, GET /v1/usage/user/USERID, GET /v1/usage/corpus/{corpusId} —
    the storage dimension now sums the documents' storage rather than their size.

    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/ — 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 & pathSummary
    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=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:

    AgentUse 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 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 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'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 PUTs 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.

Did this page help you?