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
| Field | Required | Default | Description |
|---|---|---|---|
scope | yes | — | Non-empty list of DOMAIN:ACTION entries. See scopes. |
ttl | no | 3600 | Lifetime 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. |
issuer | no | — | Free-text label for the system requesting the token (e.g. widget-frontend). |
email | no | — | Email of the end-user the token is for. |
userId | no | — | 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
| Status | When |
|---|---|
400 | scope missing, empty, or with an entry that is not a valid DOMAIN:ACTION; ttl below 10 or above the platform ceiling. |
403 | No JWT — or the call was made with an access token. |
Choosing scopes
Grant the minimum the client needs.
| Client | Typical 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"]
}| Field | Content |
|---|---|
domains | Each domain with the API path it covers, what it gives access to, and its four scope entries. |
actions | Each action with the HTTP methods it opens — read is GET, so a RAG query (GET /v1/post/q) needs post:read. |
scopes | The 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
AuthorizationandX-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 parameter | Default | Description |
|---|---|---|
pageSize | 25 | Items per page, at least 1. |
pageIndex | 0 | Zero-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
whoamiGET /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/whoamiUsing 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:
- On your backend, mint an access token scoped for the widget (read corpora + run queries),
as shown above. - Pass it to
mountChatbotWidget({ accessToken, corpusIds, ... }). - 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 JWT | Access token | |
|---|---|---|
| Header | Authorization: Bearer <jwt> | X-Access-Token: <token> |
| Signed by | You (private RSA key) | Verbatim (server-side) |
| Where it may live | Backend only | Backend and untrusted clients |
| Scope | Optional (empty = unrestricted) | Mandatory, non-empty |
| Lifetime | Set by you in exp | ttl at creation, 1 hour by default, 24 hours at most |
| Revoke | Deactivate/delete the key | DELETE /v1/auth/access-token/id/{id} or /{token} |
| Create it with | scripts/build_keys.py or a JWT lib | POST /v1/auth/access-token (needs a JWT) |
Next: Security best practices → ·
API domains & conventions → ·
Widget installation →
Updated about 1 hour ago