Access Keys

An access token is a short-lived, opaque credential you hand to clients you don't fully trust — most importantly, code running in a browser.

An access token is a short-lived, opaque credential you hand to clients you don't fully
trust — most importantly, code running in a browser. It carries a mandatory scope, so
it can only do what you explicitly allow, and it expires quickly.

Sent as a header on /v1/ requests:

X-Access-Token: 8Jf3kQ2p...

Why access tokens exist

Your RSA private key must never reach a browser or a mobile app — anyone who has it can
impersonate your whole organization. But frontends still need to call the API (to run
queries, upload files, power the chatbot widget…).

The answer is a two-tier model:

Backend (holds private key, mints RSA JWT)
        │  POST /v1/auth/access-token   (authenticated with the JWT)
        ▼
Access token  (short-lived, scoped, opaque)
        │  X-Access-Token: <token>
        ▼
Browser / widget / untrusted client  → calls /v1/ endpoints

The untrusted client only ever sees a token that is short-lived, narrowly scoped, and
revocable.


Creating an access token

POST /v1/auth/access-token — authenticated with an RSA JWT (call it from your backend).

Request body

FieldRequiredDefaultDescription
scopeyes—Non-empty list of DOMAIN:ACTION entries. See scopes.
ttlno3600Lifetime in seconds. At least 10, and at most the platform ceiling — 86400 (24 hours) unless the platform is configured otherwise. A longer ttl is refused with 400, not shortened.
issuerno—Free-text label for the system requesting the token (e.g. widget-frontend).
emailno—Email of the end-user the token is for.
userIdno—Your identifier for that end-user.

Scope is mandatory. Unlike an RSA JWT (where an empty scope means "unrestricted"), an
access token with no scope is rejected at creation. This keeps browser-facing tokens
least-privilege by design.

Example

curl -X POST https://staging-api.verbatim-ai.com/v1/auth/access-token \
  -H "Authorization: Bearer $JWT" \
  -H "Content-Type: application/json" \
  -d '{
        "ttl": 3600,
        "issuer": "widget-frontend",
        "email": "[email protected]",
        "scope": ["corpus:read", "session:read", "session:create", "post:read", "post:create"]
      }'

Response

{
  "token": "8Jf3kQ2p...",
  "scope": ["corpus:read", "session:read", "session:create", "post:read", "post:create"],
  "createdAt": "2026-07-07T10:00:00Z",
  "expiresAt": "2026-07-07T11:00:00Z"
}

Return token to the frontend. It is opaque — there is nothing to decode client-side.

The token value is returned once, here, and never again. The listing below shows only its
first characters. If you lose it, mint a new one and revoke the old.

Access tokens cannot be updated: to change a token's scope or lifetime, mint a new one and revoke
the old.

Errors

StatusWhen
400scope missing, empty, or with an entry that is not a valid DOMAIN:ACTION; ttl below 10 or above the platform ceiling.
403No JWT — or the call was made with an access token.

Choosing scopes

Grant the minimum the client needs.

ClientTypical scope
Read-only Q&A widget["corpus:read", "session:read", "session:create", "post:read", "post:create"]
Upload-only integration["doc:create", "doc:read"]
Dashboard reading usage["usage:read", "corpus:read"]

Valid domains: config, auth, session, thread, doc, chunk, corpus, post, usage, agent. Valid actions:
create, read, update, delete. Each entry must match exactly, e.g. doc:create.

Listing the available scopes

GET /v1/auth/access-token/scopes returns every scope an access token can be created with, so a
console can build its scope picker from the API instead of hard-coding the list above. It answers
the same for every organization; it needs a JWT, or an access token carrying auth:read.

curl -H "Authorization: Bearer $JWT" \
     https://staging-api.verbatim-ai.com/v1/auth/access-token/scopes
{
  "domains": [
    {
      "name": "doc",
      "path": "/v1/doc",
      "description": "Upload, list, download, convert and delete documents.",
      "scopes": ["doc:create", "doc:read", "doc:update", "doc:delete"]
    }
  ],
  "actions": [
    { "name": "create", "methods": ["POST"] },
    { "name": "read", "methods": ["GET"] },
    { "name": "update", "methods": ["PUT", "PATCH"] },
    { "name": "delete", "methods": ["DELETE"] }
  ],
  "scopes": ["config:create", "config:read", "...", "agent:delete"]
}
FieldContent
domainsEach domain with the API path it covers, what it gives access to, and its four scope entries.
actionsEach action with the HTTP methods it opens — read is GET, so a RAG query (GET /v1/post/q) needs post:read.
scopesThe flat list of every valid entry, all accepted by POST /v1/auth/access-token.

Using an access token

Send it as X-Access-Token on any /v1/ request the token's scope permits:

const res = await fetch(
  "https://staging-api.verbatim-ai.com/v1/post/q?sessionId=" + sessionId +
    "&body=" + encodeURIComponent("What is the refund policy?"),
  { headers: { "X-Access-Token": accessToken } }
);
const data = await res.json();

If the token is missing, expired, or its scope doesn't cover the request, the API returns
403 Forbidden.

Do not send both Authorization and X-Access-Token. If both are present the bearer
token takes precedence and the access token is ignored.


Listing access tokens

GET /v1/auth/access-token — authenticated with an RSA JWT. Returns the access tokens of
your organization, newest first, with every stored attribute — except the token value, which is
cut down to its first 8 characters followed by .... Expired tokens stay listed (compare
expiresAt with the current time) until revoked.

Query parameterDefaultDescription
pageSize25Items per page, at least 1.
pageIndex0Zero-based page index.
curl -H "Authorization: Bearer $JWT" \
     "https://staging-api.verbatim-ai.com/v1/auth/access-token?pageSize=25&pageIndex=0"
{
  "pageIndex": 0,
  "items": [
    {
      "id": "550e8400-e29b-41d4-a716-446655440000",
      "token": "8Jf3kQ2p...",
      "orgId": "f47ac10b-58cc-4372-a567-0e02b2c3d479",
      "createdAt": "2026-07-07T10:00:00Z",
      "expiresAt": "2026-07-07T11:00:00Z",
      "issuer": "widget-frontend",
      "email": "[email protected]",
      "scope": ["corpus:read", "session:read", "session:create", "post:read", "post:create"]
    }
  ]
}

Revoking an access token

Revocation is immediate: the next request carrying the token is refused. Both calls need an
RSA JWT.

By id — DELETE /v1/auth/access-token/id/{id}, with the id from the listing. This is the
one to use when you no longer hold the token value. An id that names no token of your
organization is a 404.

curl -X DELETE \
  "https://staging-api.verbatim-ai.com/v1/auth/access-token/id/550e8400-e29b-41d4-a716-446655440000" \
  -H "Authorization: Bearer $JWT"

By value — DELETE /v1/auth/access-token/{token}, with the full token value. An unknown
value is acknowledged all the same.

curl -X DELETE \
  "https://staging-api.verbatim-ai.com/v1/auth/access-token/8Jf3kQ2p..." \
  -H "Authorization: Bearer $JWT"

Revoke on sign-out or when you suspect a leak. Even so, always set a short ttl so
tokens expire on their own.


Checking identity — whoami

GET /v1/auth/whoami returns the identity resolved from the caller's credential
(organization, user id, email, name). Use it to bootstrap a UI after sign-in or to confirm a
token is still valid.

curl -H "X-Access-Token: $ACCESS_TOKEN" \
     https://staging-api.verbatim-ai.com/v1/auth/whoami

Using an access token with the chatbot widget

The Verbatim chatbot widget authenticates with an access token, sent as the X-Access-Token
header on every call it makes. To wire it up:

  1. On your backend, mint an access token scoped for the widget (read corpora + run queries),
    as shown above.
  2. Pass it to mountChatbotWidget({ accessToken, corpusIds, ... }).
  3. Because tokens are short-lived, expose a small backend endpoint the page can call to fetch
    a fresh token, and refresh before expiry.

Full widget options, theming and troubleshooting are in the
Chatbot widget installation guide.


Access tokens vs RSA JWT — quick comparison

RSA JWTAccess token
HeaderAuthorization: Bearer <jwt>X-Access-Token: <token>
Signed byYou (private RSA key)Verbatim (server-side)
Where it may liveBackend onlyBackend and untrusted clients
ScopeOptional (empty = unrestricted)Mandatory, non-empty
LifetimeSet by you in expttl at creation, 1 hour by default, 24 hours at most
RevokeDeactivate/delete the keyDELETE /v1/auth/access-token/id/{id} or /{token}
Create it withscripts/build_keys.py or a JWT libPOST /v1/auth/access-token (needs a JWT)

Next: Security best practices → ·
API domains & conventions → ·
Widget installation →


Did this page help you?