The API lives under /api/v1. Interactive docs with every request and response schema are at /docs (Swagger UI) and /redoc on the backend; the schema at /openapi.json generates the frontend's typed client (one class per tag, e.g. Recordings.getRecording).
Authenticate with Authorization: Bearer <token>: an API token (la_…, made in the app or with POST /tokens), the access token of an app someone gave access to through OAuth (lo_…), or a session's access token. People sign in with a passkey (POST /auth/passkey answers a one-time ticket, which POST /auth/ticket swaps for a token pair); password sign-in (POST /auth/login) is off unless an admin turns on auth.passwords. See Authentication. Errors are JSON {"detail": …}; validation errors answer 422. Namespaces you can't read answer 404.
Outside /api/v1: IIIF resources under /iiif/… (IIIF), the embeddable player at /embed/<id> (and at a share link's short address, /s/<code>) and stored reports at /reports/<namespace>/….
GET /api/v1/auth/status
POST /api/v1/auth/setup
POST /api/v1/auth/login
POST /api/v1/auth/refresh
POST /api/v1/auth/logout
GET /api/v1/auth/me
PATCH /api/v1/auth/me
POST /api/v1/auth/me/tour
POST /api/v1/auth/password
POST /api/v1/auth/password/forgot
POST /api/v1/auth/password/reset
POST /api/v1/auth/passkey/setup/options
POST /api/v1/auth/passkey/setup
POST /api/v1/auth/setup/no-passkey
POST /api/v1/auth/passkey/options
POST /api/v1/auth/passkey
POST /api/v1/auth/ticket
GET /api/v1/auth/passkeys
POST /api/v1/auth/passkeys/options
POST /api/v1/auth/passkeys
PATCH /api/v1/auth/passkeys/{pid}
DELETE /api/v1/auth/passkeys/{pid}
POST /api/v1/auth/signin-link/info
POST /api/v1/auth/signin-link/options
POST /api/v1/auth/signin-link
POST /api/v1/auth/signin-link/use
POST /api/v1/auth/signin-link/lost
GET /api/v1/auth/external
POST /api/v1/auth/external/{key}/start
POST /api/v1/auth/external/{key}/connect
GET /api/v1/auth/identities
DELETE /api/v1/auth/identities/{iid}
GET /api/v1/auth/providers
POST /api/v1/auth/providers
PATCH /api/v1/auth/providers/{key}
DELETE /api/v1/auth/providers/{key}
GET /auth/me gives your account, your roles (namespace → role) and partial: the namespaces you have no role in
but see some collections of (Access), and toured_at, when you finished or skipped the
welcome tour (POST /auth/me/tour sets it; repeating it keeps the first time).
Passkey sign-in, sign-in links (emailed by an admin, or by signin-link/lost for a lost passkey) and outside accounts
(/auth/external, configured by admins under /auth/providers) all answer a one-time ticket; POST /auth/ticket
swaps it for a token pair. /auth/passkeys manages your own passkeys and /auth/identities your linked outside
accounts. See Authentication.
GET /api/v1/tokens
GET /api/v1/tokens/limits
POST /api/v1/tokens
PATCH /api/v1/tokens/{token_id}
DELETE /api/v1/tokens/{token_id}
GET /api/v1/admin/tokens
DELETE /api/v1/admin/tokens/{token_id}
POST /tokens {name, scope, days?, namespace?} makes an API key (while signed in); with namespace (one you can read)
it opens that namespace's own MCP server and chat completions alone, and the list shows it as namespace
(API tokens). days defaults to tokens.default_days; more
than tokens.max_days, or 0 (never expires) when tokens.never_expire is off, is a 400. GET /tokens/limits says
{default_days, max_days, never_expire}. PATCH /tokens/{token_id} {name} renames one of your keys (audited as
token.rename). /admin/tokens lists everyone's keys with their owner's email (admins), and
DELETE /admin/tokens/{token_id} revokes any of them; revoking is audited as token.revoke.
GET /.well-known/oauth-authorization-server
GET /.well-known/oauth-protected-resource
POST /api/v1/oauth/register
GET /api/v1/oauth/authorize
POST /api/v1/oauth/authorize
POST /api/v1/oauth/token
POST /api/v1/oauth/revoke
GET /api/v1/oauth/grants
DELETE /api/v1/oauth/grants/{grant_id}
Lens is an OAuth 2.1 authorization server for API and MCP clients (Authentication). The MCP
server itself is at /mcp, outside /api/v1 (MCP server).
POST /oauth/register {redirect_uris, client_name?, client_uri?, token_endpoint_auth_method?}(RFC 7591, no sign-in, 201) registers an app:client_id(lc_…), and aclient_secret(ls_…, shown once) when it asked forclient_secret_postorclient_secret_basicinstead ofnone. Redirect addresses are https,http://localhostor127.0.0.1(any port), or the app's own scheme; other metadata is ignored. Errors are{error, error_description}; more than 8 apps from one address in 15 minutes is a 429.GET /oauth/authorize?client_id=&redirect_uri=&code_challenge=&code_challenge_method=S256&scope=(signed in, as a person) answers what the consent page shows:{client: {id, name, uri}, redirect_uri, scope, granted},scopebeingreadorread writeandgrantedwhat this app was given before. A request that can't be answered (an unknown app, an address it didn't register, no S256 challenge) is a 400 with the reason.POST /oauth/authorize {client_id, redirect_uri, code_challenge, code_challenge_method, scope?, state?, resource?, approve, grant?}answers it:{redirect_to}, the app's address withcodeandstate, or witherror=access_denied.grant: "read"gives less than was asked for.POST /oauth/token(form-encoded) withgrant_type=authorization_code,code,redirect_uri,code_verifierandclient_id(andclient_secret, or HTTP Basic, for apps that have one), or withgrant_type=refresh_tokenandrefresh_token:{access_token, token_type: "Bearer", expires_in, refresh_token, scope}. Errors are{error, error_description}(invalid_grant,invalid_clientwith 401,unsupported_grant_type).POST /oauth/revoke(form-encoded:token,client_id) ends the access the token belongs to; it answers{ok: true}whether or not the token was known.GET /oauth/grantslists the apps you gave access to (id,client,name,uri,scope,created_at,last_used_at,expires_at);DELETE /oauth/grants/{grant_id}takes one's access away.
GET /auth/me says via: "oauth" for an app's token, with scope read or write.
GET /api/v1/users
POST /api/v1/users
PATCH /api/v1/users/{uid}
DELETE /api/v1/users/{uid} remove for good (authentication.md#removing-someone)
POST /api/v1/users/{uid}/signin-link ?email=true also emails it
GET /api/v1/namespaces/{name}/members
PUT /api/v1/namespaces/{name}/members {email | account, role}: built-in or custom; null removes
GET /api/v1/roles the permissions and every role (access.md#roles-and-permissions)
POST /api/v1/roles admins: {name, label, description, permissions}
PATCH /api/v1/roles/{name} admins: a custom role's label, description, permissions
DELETE /api/v1/roles/{name} admins: a custom role nobody holds
GET /api/v1/settings
PUT /api/v1/settings/{section}
POST /api/v1/settings/llm/test
POST /api/v1/settings/embeddings/test
POST /api/v1/settings/mail/test
POST /api/v1/settings/files/test
POST /api/v1/settings/speech/test
GET /api/v1/settings/decisions/status
POST /api/v1/settings/decisions/test
GET /api/v1/settings/telemetry/status
POST /api/v1/settings/telemetry/test
GET /api/v1/settings/bridge
POST /api/v1/settings/bridge/test
GET /api/v1/settings/tunnel/status
GET /api/v1/settings/local-llm/status
DELETE /api/v1/settings/local-llm/models
GET /api/v1/audit
GET /api/v1/admin/health
POST /api/v1/admin/reindex
GET /api/v1/admin/semantic
POST /api/v1/admin/semantic/index
POST /settings/embeddings/test embeds one sentence with the configured embedding model (dimension, ms, or the
error). GET /admin/semantic says whether search by meaning is set up, with which model, and how many assets
and passages are indexed with it; POST /admin/semantic/index?limit=500 queues the embed step for assets not yet
indexed with it (remaining when more are waiting). The other test endpoints try the saved settings of their
section (mail, file store, speech provider, decision model, telemetry, bridge) and answer what happened or the
error; the status endpoints say how that part is doing. All are for admins.
GET /api/v1/activity
GET /api/v1/activity/totals
GET /api/v1/activity/costs
GET /api/v1/activity/top
GET /activity?resource=<table>:<id> is a resource's history, newest first (Activity and costs).
/activity/totals is what one resource (or, for admins, everything) cost this period (day, week, month,
all). GET /activity/costs?resource=…&resource=… (up to 500) answers each resource's cost at once for lists,
leaving out the ones you can't see; estimate marks a figure as a floor. /activity/top lists the costliest
resources (admins).
GET /api/v1/activities
GET /api/v1/activities/kinds
GET /api/v1/activities/{activity_id}
POST /api/v1/activities/mark
POST /api/v1/activities/{activity_id}/mark
GET /activities is what happened in Lens, newest first, from every source: Activity Streams 2.0 activities with a
Lens kind (the shape is in Automations). Filters, each optional: kinds (repeat it,
or separate with commas; the kinds under each count too), actor (an actor's id, like lens:account/1),
namespace (by name), tag (an entity's id or name), since (published after) and until (at or before),
q (words in the summary) and where (a match tree on the activity, as JSON).
limit is 1 to 200 (50). The answer is {items, next, namespaces}: give next as before for the page after (it is
null at the end); namespaces names the namespaces in the page by id. Noisy kinds are left out unless named. People
see the activities of the namespaces they can read, their own (lens.audience owner), and admins also those for
admins alone. /activities/kinds lists the kinds of activity with their schemas and samples; /activities/{id}
reads one activity, like create-recording-7.
Each person marks activities reviewed, important or hidden for themselves alone (Timeline):
POST /activities/{id}/mark with any of the three (true or false; one left out stays) answers the marks on it now,
and POST /activities/mark with ids (up to 500) answers {marks, unknown}. GET /activities has your marks on
the page in marks and filters by them with reviewed, important and hidden (true or false).
POST /api/v1/automations/hooks/{token}
An automation's webhook (Webhook trigger): calling the address of a trigger
{"on": "webhook"} sets that automation off. No session or API token: the token in the address (lah_..., shown
once when the automation is saved) is the credential. Send JSON, a form (text fields) or text, up to 64 KB; the answer
(202) is {"id"}, the webhook.received activity the call made, before the automation runs. Sign the call the
Standard Webhooks way (webhook-id, webhook-timestamp, webhook-signature, with the secret shown with the
address) and the signature is checked; a webhook-id or Idempotency-Key makes a retried call count once. 401 for a
token that isn't a webhook's or a signature that doesn't match, 403 while the automation is switched off, 413 over 64
KB, 415 for other bodies, 429 past 60 calls a minute.
curl -H "Content-Type: application/json" -d '{"event": "doorbell"}' https://lens.example/api/v1/automations/hooks/lah_...
GET /api/v1/automations/{id}/vars
PUT /api/v1/automations/{id}/vars
GET /api/v1/namespaces/{name}/automation-vars
PUT /api/v1/namespaces/{name}/automation-vars
The values automations keep between runs (Saved variables): an automation's own,
or a namespace's, which every automation working there reads. GET answers {"vars": [{name, value, untrusted, updated_at, updated_by}]} by name; untrusted says a run set it from text from outside Lens. PUT {"vars": {name: value}} sets several at once (null clears one) and answers the same list. An automation's are for admins and
whoever may change it (it works only in namespaces they edit); a namespace's are read by its readers and changed by
its editors. 404 for an automation you can't see, 400 for a bad name (lowercase letters, digits and _, up to 40),
a value over 16 KB, or past 100 variables.
curl -X PUT -H "Authorization: Bearer $LENS_TOKEN" -H "Content-Type: application/json" \
-d '{"vars": {"alerts": 0, "last_alert": null}}' https://lens.example/api/v1/automations/7/vars
GET /api/v1/namespaces/{name}/vault
POST /api/v1/namespaces/{name}/vault/options
POST /api/v1/namespaces/{name}/vault
POST /api/v1/namespaces/{name}/vault/unlock
POST /api/v1/namespaces/{name}/vault/lock
POST /api/v1/namespaces/{name}/vault/passkeys
DELETE /api/v1/namespaces/{name}/vault/passkeys/{pid}
DELETE /api/v1/namespaces/{name}/vault
A vault is a namespace whose files only its owners' passkeys open. options starts the passkey prompt for sealing,
unlocking or adding a passkey; POST …/vault seals the namespace, unlock opens it for encryption.vault_minutes,
lock closes it now, and DELETE …/vault (while open) makes it an ordinary namespace again. Each is audited as
vault.<action>. See Encryption at rest.
GET /api/v1/content-types
POST /api/v1/content-types
PATCH /api/v1/content-types/{key}
DELETE /api/v1/content-types/{key}
GET /api/v1/recordings/{rid}/content-type
PUT /api/v1/recordings/{rid}/content-type
GET /api/v1/custom-nodes
POST /api/v1/custom-nodes
GET /api/v1/custom-nodes/{nid}
POST /api/v1/custom-nodes/{nid}/versions
PATCH /api/v1/custom-nodes/{nid}
DELETE /api/v1/custom-nodes/{nid}
Content types (admins change them; everyone reads them) say what kind of thing an asset is, and an asset's
content-type sets it. Custom nodes are versioned steps people write for workflows (?scope= filters the list,
?version= reads an older one).
GET /api/v1/routines
GET /api/v1/routines/schedule
POST /api/v1/routines
GET /api/v1/routines/{rid}
PATCH /api/v1/routines/{rid}
DELETE /api/v1/routines/{rid}
POST /api/v1/routines/{rid}/run
GET /api/v1/routines/{rid}/runs
POST /api/v1/routines/runs/{run_id}/decide
GET /api/v1/routine-runs/{run_id}
POST /api/v1/routine-runs/{run_id}/undo
GET /api/v1/graph-changes
POST /api/v1/graph-changes/{cid}/accept
POST /api/v1/graph-changes/{cid}/dismiss
POST /api/v1/graph-changes/{cid}/undo
Routines are for admins. GET /routines/schedule?schedule=&timezone= previews when a schedule would next run; run
starts one now and runs lists its last runs. A run held over a budget is run or skipped with decide, and undo
takes back every graph change a run made. GET /graph-changes lists the changes to the entities of namespaces you can
read, newest first; editors accept, dismiss or undo one (Graph history).
GET /api/v1/namespaces
POST /api/v1/namespaces
PATCH /api/v1/namespaces/{name}
GET /api/v1/namespaces/{name}/wordcloud.svg
GET /api/v1/namespaces/{name}/stats
GET /api/v1/namespaces/{name}/ip-groups
POST /api/v1/namespaces/{name}/ip-groups
PATCH /api/v1/namespaces/{name}/ip-groups/{gid}
DELETE /api/v1/namespaces/{name}/ip-groups/{gid}
GET /api/v1/namespaces/{name}/notifications
POST /api/v1/namespaces/{name}/notifications
PATCH /api/v1/namespaces/{name}/notifications/{tid}
DELETE /api/v1/namespaces/{name}/notifications/{tid}
POST /api/v1/namespaces/{name}/notifications/{tid}/test
POST /api/v1/namespaces/{name}/notifications/{tid}/secret
GET /api/v1/namespaces/{name}/notifications/{tid}/deliveries
GET /api/v1/namespaces/{name}/nsfw
PUT /api/v1/namespaces/{name}/nsfw
POST /api/v1/namespaces/{name}/nsfw/classify
GET /api/v1/namespaces/{name}/collections
POST /api/v1/namespaces/{name}/collections
GET /api/v1/namespaces/{name}/collections/{cid}
PATCH /api/v1/namespaces/{name}/collections/{cid}
DELETE /api/v1/namespaces/{name}/collections/{cid}
GET /api/v1/namespaces/{name}/collections/{cid}/members
PUT /api/v1/namespaces/{name}/collections/{cid}/members
GET /api/v1/namespaces/{name}/integrations
PATCH /api/v1/namespaces/{name}/integrations
GET /api/v1/namespaces/{name}/models
POST /api/v1/namespaces/{name}/chat/completions
GET /namespaces lists the namespaces you have a role in, with their counts and your role, and those you see only
some collections of: partial: true, no role, counted over those collections, without the namespace-wide word
cloud and people.
/namespaces/{name}/stats?from=&to=&top= gives the Reports overview its numbers (viewers of the namespace): the
assets made from from to to (days, YYYY-MM-DD, both included; leave either open), how many and how long
(recordings, ms), how many people are heard in them and the top ones by talk time in them (top_speakers,
default 5, at most 50, each with talk_ms and recordings), the days of the first and last, and months: every
calendar month from from (else the first asset) to to (else the later of today and the last asset),
empty ones included, at most the latest 240. Without a range it counts every asset; undated says how many
have no date, and those are never in a month or a range. Months and days are the asset dates as stored (the
time where it was recorded). 400 when to comes before from.
/namespaces/{name}/ip-groups lists a namespace's IP groups (Access, owners), with address:
your address as the server sees it (null when it can't tell; see server.trusted_proxies), and on each group here
(your address is in it) and chosen (how many assets it opens when it doesn't open everything). POST with
{"name", "ranges", "everything"} adds one: ranges are addresses or CIDR ranges, at most 100, none wider than /8
(IPv4) or /16 (IPv6); names are unique in the namespace. PATCH changes any of them and DELETE removes the group.
All three answer with the list and are audited as namespace.ip_group.create, .update and .delete.
/namespaces/{name}/notifications lists a namespace's notification targets (Notifications,
owners), the events they can get, and whether notifications are enabled for the server. POST with
{"name", "kind", "url", "events"} adds one: kind is webhook, matterbridge, slack or discord, and a
Matterbridge target also takes gateway (required), username and token. A webhook comes back with its signing
secret, this once; POST …/{tid}/secret makes a new one. A target's url comes back as its scheme and host only.
PATCH changes the name, events, enabled, the address (in full) and Matterbridge's gateway, name and token (""
removes it). POST …/{tid}/test sends a test message now and answers {ok, code, error}; GET …/{tid}/deliveries
lists the latest 50 messages with their status (pending, sending, sent, failed, dropped), attempts,
answer code and error. Changes are audited as namespace.notification.create, .update, .delete and .secret.
/namespaces/{name}/nsfw says how NSFW classification works in a namespace (viewers): whether it's
enabled, the threshold and display that apply, what the namespace chose itself (own, null where it follows
the archive) and the archive's settings (archive), and how many resources are classified, flagged and
unclassified. PUT with {enabled?, threshold?, display?, classify?} changes the namespace's own choices (owners;
null follows the archive again, a new threshold flags or unflags what was classified at once), and classify: true
also queues the nsfw step for its videos, documents and images not classified yet, answering how many runs were
queued. Audited as namespace.nsfw. POST …/nsfw/classify queues them on its own (editors) and answers the jobs.
Both are a 409 where classification is off in the namespace.
/namespaces/{name}/integrations says whether the namespace's own MCP server
(mcp, at mcp_url) and its chat completions (chat, at chat_url) are on, the
read tools the model may use there (tools) and the ones it may be offered (tool_choices), for anyone who can read
the namespace. PATCH with any of {mcp, chat, tools} changes them (owners); what isn't sent stays. A tool that
isn't one of tool_choices is a 400. Audited as namespace.integrations.
A namespace can answer OpenAI's chat completions API, so any app that speaks it (a chat app, an automation tool, a library's OpenAI client) gets answers from the configured model, grounded in that namespace and citing it. Give the app:
- Base URL:
https://lens.example.org/api/v1/namespaces/<namespace>(the app adds/chat/completions); - API key: a Lens API token (
la_…; read-only is enough), best one made for this namespace alone (Connect apps links to it), or an app's OAuth token; - Model:
lens, the configured model. One of the models people may pick in Lens's chat works too (GET …/modelslists them); any other name answers with the configured model.
curl https://lens.example.org/api/v1/namespaces/family/chat/completions \
-H "Authorization: Bearer la_…" -H "Content-Type: application/json" \
-d '{"model": "lens", "messages": [{"role": "user", "content": "When is the dentist?"}]}'Each request is answered like a question in Lens's chat, from that namespace alone, with what the token's person can read there:
- the last message is the question (a
usermessage), theuserandassistantmessages before it the conversation, andsystem(ordeveloper) messages the app's own instructions, which the model reads after Lens's rules. Pictures and files in a message are left out; - the namespace's excerpts that best answer it are found and the model answers from them, citing each claim as
[n]. With no model set up, the best excerpts come back on their own; - when the namespace's owners picked tools, the model may first look things up with them, in the namespace. They are
the assistant's tools that only read (
tool_choices). A model that can't call tools answers from the excerpts and says so inlens.notice; - when the namespace's own assistant is on, its name and instructions answer.
What it cites comes back three ways, so any app can show it: [n] in the text with a Sources list of links after
it (left out when the request has a response_format), citations (the link for [n] is the n-th), and
lens.sources (n, recording_id, title, time, speaker, url, cited). lens.steps lists the tools the
model used. With "stream": true the answer comes as OpenAI's chat.completion.chunk events, ending with
data: [DONE]; the last chunk before it carries finish_reason: "stop", citations and lens. While the tools run,
the stream sends SSE comments, so nothing in between closes it.
It is off until an owner of the namespace turns it on: Admin → Namespaces → the namespace → Connect apps,
which also copies the base URL and picks the tools, or PATCH /namespaces/{name}/integrations with {"chat": true},
or an agent's set_namespace_integrations. Errors come in OpenAI's shape ({"error": {"message", "type", "code"}}):
401 invalid_api_key without a token, 422 for a body that isn't a request (FastAPI's shape), 404 not_found for a
namespace the token's person can't read (or that doesn't exist), 403 endpoint_off while it's off, 400
invalid_messages with nothing to answer, 429 insufficient_quota when the namespace's budget or
Lens's overall one is used up, 502 upstream_error when the model server fails.
It only reads. Model calls go through the activity ledger and count for the namespace (space:<id>), so its budget
covers them. Not yet: the app's own tools (they are ignored), max_tokens, enforcing a response_format, token
counts in usage, streaming the tools' answer piece by piece (it comes in one chunk), and what the namespace's
assistant remembers.
Every asset lives in exactly one collection of its namespace, and collections nest (at most 8 deep, 5000 per namespace). Each namespace has a default collection, "General" when it's made, where assets go when nobody says where: imports, uploads, scans, watched folders and assets moved in from another namespace. Existing assets were put in their namespace's General when collections came in. (Saved collections, under collections, are something else: lists of assets from anywhere.)
GET /namespaces/{name}/collections lists them depth first, by name (viewers): each with its parent (null at the
top), depth, path (names from the top down), how many assets it holds (recordings) and holds with the
collections inside it (total), how many collections are directly inside it (children), and default.
POST {name, parent?, description?} makes one (editors); a name is unique among the collections next to it, ignoring
case and repeated spaces. PATCH renames it, describes it, moves it inside another collection of the namespace
(parent; null: to the top; not inside itself), with the assets and collections in it, or makes it the default
(default: true; false on the default is a 400: make another one the default instead). DELETE removes an empty
collection: 409 while it holds assets or collections, or is the default, so deleting one never takes an
asset with it. Audited as collection.create, collection.update (with what changed) and collection.delete.
In IIIF each collection is a Collection inside its namespace's (IIIF).
People can be given a role on a collection, which holds for the ones inside it too (Access).
Each collection in the list says what you may do with it: your role on it (viewer, editor or admin; an owner
of the namespace is an admin of every collection), can_change (rename, describe, move or delete it and make
collections inside it: editors of the namespace, and admins of the collection) and can_grant (give roles on it:
owners of the namespace, and admins of the collection). Someone who sees only some collections of a namespace gets
those, starting from the ones they were given (their path and depth start there). An admin of a collection can't
make collections at the top of the namespace, move one there or into a collection they aren't an admin of, or change
the default. GET …/{cid}/members lists who was given a role on the collection and then who has one through a
collection it's inside (inherited_from); PUT …/{cid}/members {email | account, role} gives, changes or (role: null) takes away a role, answers with the members, and is audited as collection.member. People with a role in the
namespace aren't listed: theirs holds everywhere in it.
A resource is what the archive holds: a recording (audio, video, or text without media), a document (a PDF) or
an image (Documents and images). The API calls them resources: /api/v1/resources/… is the canonical path for everything below, and the schema and the generated
client use it (the Resources class). /api/v1/recordings/…, their address before, keeps working for existing
clients and reaches the same routes; links the server writes (signed media links, for one) may still use it. Fields
keep their names (recording, RecordingSummary). The web app's pages moved too: /resources/<id>, with
/recordings/<id> redirecting there.
GET /api/v1/resources
GET /api/v1/resources/tags
GET /api/v1/resources/origins
GET /api/v1/resources/languages
GET /api/v1/resources/objects
GET /api/v1/resources/nsfw
POST /api/v1/resources/tags
POST /api/v1/resources/collection
GET /api/v1/resources/{rid}
PATCH /api/v1/resources/{rid}
DELETE /api/v1/resources/{rid}
POST /api/v1/resources/{rid}/move
GET /api/v1/resources/{rid}/access
PUT /api/v1/resources/{rid}/access
GET /api/v1/resources/{rid}/permissions
POST /api/v1/resources/{rid}/permissions
DELETE /api/v1/resources/{rid}/permissions/{account}
GET /api/v1/resources/{rid}/requests
POST /api/v1/resources/{rid}/requests/{account}/approve
POST /api/v1/resources/{rid}/requests/{account}/decline
GET /api/v1/resources/{rid}/ip-groups
PUT /api/v1/resources/{rid}/ip-groups/{gid}
DELETE /api/v1/resources/{rid}/ip-groups/{gid}
GET /api/v1/resources/{rid}/player
PUT /api/v1/resources/{rid}/nsfw
GET /api/v1/resources/{rid}/embed-link
GET /api/v1/resources/{rid}/audio
GET /api/v1/resources/{rid}/wordcloud.svg
POST /api/v1/resources/{rid}/reprocess
POST /api/v1/resources/{rid}/share
DELETE /api/v1/resources/{rid}/share
GET /api/v1/resources/{rid}/shares
DELETE /api/v1/resources/{rid}/shares/{id}
GET /api/v1/resources/{rid}/export.{fmt}
GET /api/v1/resources/{rid}/evidence.pdf
PATCH /api/v1/resources/{rid}/segments/{idx}
POST /api/v1/resources/{rid}/segments/{idx}/split
POST /api/v1/resources/{rid}/segments/{idx}/merge
GET /api/v1/resources/{rid}/edits
GET /api/v1/resources/{rid}/outputs
GET /resources filters, sorts and pages on the server, over every asset you can read (in the namespaces you have
a role in, and in the collections you were given a role on), each with your role on it. Filters combine with AND;
repeat a parameter that takes several values (?status=new&status=error) to match any of them.
| Parameter | |
|---|---|
ns |
one namespace (default: every namespace you can read) |
collection |
a collection's id: the assets in it and in the collections inside it |
q |
words that must all appear in the title, the namespace's name or a person's name |
status |
new, transcribed, diarized, analyzed, error, and two job states: processing (a job is queued or running) and failed (the latest job failed) |
attention |
true: only assets that need a person (errored, latest job failed, or a voice match waiting for review) |
processing |
true: only assets with a job queued or running |
speaker |
person ids |
from, to |
the asset date, YYYY-MM-DD, both days included; assets without a date don't match |
min_duration, max_duration |
seconds: at least min_duration, shorter than max_duration |
media |
audio, video, transcript (text without media), document or image |
access |
public, restricted or private: the asset's own setting, else its namespace's default |
featured |
true: only featured assets; false: only the others |
tag |
tags, ignoring case |
origin |
where they came from: source:<id> (a connected source), upload, paste, iiif, folder (the archive's own folders) or file (another file imported by path) |
language |
language codes, ignoring case; none for assets whose language isn't known |
object |
kinds of object seen in them (person, car …; Objects), ignoring case |
edited_by |
me: assets you edited (corrected a line of the text, changed the catalogue record, or renamed) |
sensitive |
where NSFW classification is on: only the ones flagged NSFW, exclude them, or include them. Without it, flagged ones are left out where their namespace hides them (display: hide) |
sort |
date, title, duration, speakers, status or importance; - in front for descending (default -date). Assets without the value come last either way |
limit, offset |
one page (default 500 rows, at most 1000) |
The body is the page's rows; the X-Total-Count header says how many assets match on all pages.
Rows also say where each asset came from (origin, and origin_name: the source's name, or e.g. "Uploaded"),
its language when known and the collection it lives in (collection, collection_name). GET /resources/{rid}
adds collection_path: the collections from the top of the namespace down to its own, [{id, name}].
POST /resources/collection {recordings, collection} moves assets into a collection of their namespace (editors
of each); an asset of another namespace is a 400 (move it to that namespace first). It answers how many moved;
their IIIF Manifests change (partOf), so harvesters see an Update. Audited as recording.collection.
GET /resources/origins, GET /resources/languages and GET /resources/objects (ns for one namespace) list the
origins, languages and kinds of object of the assets you can read with how many have each, most first, for the
Library's Source, Language and Objects filters.
PATCH /resources/{rid}/segments/{idx} corrects a line of text: its text, its speaker (a person id in the
namespace, or null), or both (editors). POST …/segments/{idx}/split {at, t?, speaker?} splits a line in two at
at, a position in its text moved back to the start of the word it's in. The second part starts at t (ms, inside
the line) or else when its first timed word was said (or as far into the line's time as at is into its text), and
keeps the line's speaker unless speaker is sent. POST …/segments/{idx}/merge joins a line with the next one: both
texts (with a space between them, none in scripts written without spaces) and their words, from the first's start to
the second's end, with the first's speaker. A split or merge renumbers the lines after it, and what points at line
numbers follows: the edit history, people's entity corrections and the chapters. Each change re-analyses the
asset (job), is audited (transcript.edit, transcript.split, transcript.merge) and is kept in GET …/edits,
the latest first, with kind (split, merge, or null for a correction); a merge keeps where its second line
started (after.at, after.t) to split it there again.
An asset's summary (the Summarize step's, also in GET …/player) has summary, key_points and
action_items, topics, people, sentiment and importance (1–5). Key points and action items are
{text, who?, t0?}: who will do it, and t0 is where the line they come from starts (ms), when the model gave it;
summaries made before they had times hold plain strings.
In GET …/player, a line whose words have timings from transcription carries them as w: [c0, c1, t0, t1] for
each word, a character range of its text and when it was said (ms). A corrected line keeps the timings of the words
it still has.
Assets carry tags. PATCH /resources/{rid} with {"tags": [...]} replaces an asset's tags (editors; at most
20, 40 characters each; whitespace is collapsed and repeats are dropped, ignoring case). POST /resources/tags with
{"recordings", "add", "remove"} changes the tags of several at once (editors of each one's namespace) and says how
many changed. GET /resources/tags (ns for one namespace) lists the tags in use with how many assets have each,
most used first.
PATCH /resources/{rid} with {"title": …} renames an asset (editors; whitespace is collapsed, at most 200
characters). It is audited as recording.rename; the asset's report page follows the new title and is rebuilt,
and a published asset shows up as an Update in the IIIF change feed.
DELETE /resources/{rid} deletes an asset (owners). Everything Lens made from it goes: its text and analysis
(segments, speaker turns, chapters, entity mentions, terms, edits), video shots, text on screen, face tracks and
frames, outputs, reports and exports, shares, permissions, requests for access, and its place in IP groups, fixed
collections, chat scopes and batch runs that haven't started it. People and faces only it had are removed unless
someone named them. Its waiting jobs are cancelled; while a job is running on it the answer is 409, and a worker that
finds its asset gone stops. The media file stays where it is: scans skip the same path and the same file elsewhere,
and watched folders skip the same remote file, even when it changes; importing it on purpose (the Import dialog,
lens import, a IIIF manifest) brings it back. A public asset shows up as a Delete in the IIIF change feed. It is
audited as recording.delete with its title, namespace and path.
POST /resources/{rid}/move with {"namespace", "collection", "rediarize", "revoke_shares"} moves an asset to
another namespace (owners of its namespace, editors of the new one), into collection there (default: the new
namespace's default collection; 404 for a collection of another namespace). It keeps its text, media, frames, outputs, the people given
permission on it and its share links (revoke_shares: they stop working). Its IIIF manifest stays as it was: what it
had from its old namespace (default access and open parts, the metadata profile's defaults) is pinned on it wherever
the new namespace would change it, kept in its metadata history and listed in pinned. People and faces are matched
by name in the new namespace, or start there with this asset's voice and face (rediarize, audio only: identified
again from their voices instead); unnamed ones left with nothing in the old namespace are removed. Its entity mentions
and per-mention corrections start over: analysis runs again in the new namespace (job). Its report pages and exports
move to the new namespace's folders. The old namespace's scans and watched folders don't import the file again, and
its IP groups no longer open the asset. 409 when the new namespace already has the same file or a job is running
on it. Audited as recording.move.
Share links (editors): POST /resources/{rid}/share with {"days"} (1–3650, default 30) makes one and returns its
id, token, embed (/embed/<id>?s=<token>) and short (/s/<code>, ten characters, the same player); only their
hashes are kept, so the addresses are shown this once. GET /resources/{rid}/shares lists the links, newest first:
active (neither revoked nor expired), revoked_by/revoked_at, short (links from before short links have
none), plays (times its player started playing, once per page load; previews in Lens itself don't count) with
played_at, and embedded_on: the sites whose pages framed its player (origin, opens, last_at, most recent
first, up to 50 per link), from the browser's Referer when it reports a frame. DELETE /resources/{rid}/shares/{id}
revokes one link (404 if the asset has no such link), DELETE /resources/{rid}/share every one that still
works; both are audited as share.revoke. A link that doesn't work (expired, revoked, mistyped, or its asset is
gone) opens a neutral page with status 410 that says nothing about the asset.
GET /resources/{rid}/access says who may see an asset (members): access (public, restricted or
private), open (the parts a public asset opens to everyone: media, transcript, index), featured,
inherited (the access comes from the namespace) and the namespace's default. PUT changes any of them (owners);
null follows the namespace again. Changes are audited as recording.access, kept in the metadata history, and
announced in the IIIF change feed. Access explains what each level lets people do.
/resources/{rid}/permissions lists the people given permission on the asset (owners). POST with {"email": …} gives it to the account with that address (404 when there is none, 400 for members of the namespace, who already
see all of it); DELETE …/{account} takes it away. Both answer with the list and are audited as
recording.permission.give and recording.permission.take. /resources/{rid}/requests lists the requests for access
(owners); approving one gives permission, declining lets the person ask again (audited as
recording.request.approve and recording.request.decline).
/resources/{rid}/ip-groups lists the namespace's IP groups with opens: whether visitors from their addresses see
all of the asset (owners). PUT …/{gid} opens the asset to a group that opens chosen assets (400 for one
that opens everything already), DELETE …/{gid} closes it again (404 when it wasn't open). Both answer with the list
and are audited as recording.ip_group.open and recording.ip_group.close.
A document (a PDF; a Word, PowerPoint or spreadsheet file, OpenDocument or RTF; text or Markdown; a saved web page; an
email, .eml or .msg) or an image (JPEG, PNG, TIFF, WebP, GIF or BMP) is a resource of its own: its source is
document or image, and so is media_kind in the list, with its pages. A document that isn't a PDF is made into
one first (Configuration): the resource keeps its own file
(GET /{rid}/media), and the PDF it's read from is GET /{rid}/pdf (primary.pdf in its files, and rendition:
{from, by} on the resource says how it was made). Nothing a document refers to is fetched while converting it. An
email's email is its subject, from, to, cc and date; its subject becomes its title and its date when it was
made, unless someone named or dated it. Its attachments are kept as its files (role attachment), and those Lens can
read become resources of their own beside it, each with attached_to: {resource, file, title} (the email, and its
title when the caller may see it); an attachment file's resource is the resource it became, or the one it was already
in the namespace (documents.attachment_resources). Transcribing an email again keeps and makes nothing twice.
GET /uploads/limits says what the server can convert (convert: {office, pages, msg, tika}); with an
Apache Tika server set, what nothing else can convert is read by Tika as text without
pages (engine text:tika), and every document has described, what it says about itself as Dublin Core terms
(POST /settings/documents/tika/test checks the server). An upload it can't read is
refused, saying what it needs, and the files that can be read as plain text too (PDFs, Word, text and Markdown) can be imported
as text instead (POST /import). Its pipeline's read step (its transcribe step, for a document or an image:
no speech model is fetched for one) draws its pages and reads their text: a PDF's own text, block by block with where each block is on its page (poppler's
pdftotext; pypdf without poppler, when there are no pages to look at), and by OCR (video.ocr_engine) the pages with
fewer than documents.ocr_below_chars characters of text, drawn sharper for it. An image is one page (a TIFF one per
frame), read by OCR. Diarize is left out for them, shots and text on screen skip them; analyze, summarize and report run as for any text.
Configuration has the settings.
The text becomes the resource's segments, one per block, so search, chat, entities, notes and corrections
(PATCH /segments/{idx}) work on it as on a transcript. A block's times are only a reading pace; what places it is its
page: GET /{rid}/player gives each segment p (its page, from 0) and b (where it is on the page, [x, y, w, h] as
fractions, when known), and pages: each page's idx, width and height (pixels of its image), signed links to its
image and its thumbnail (none when it couldn't be drawn), how its text was read (pdf or ocr), its chars and
its label (the PDF's own name for it, when it isn't its number). media says its kind, its pages and the first
page's size. GET /{rid}/media is the file itself, as a download that browsers don't run. A summary's key points and
action items cite pages: each has the page it comes from. The web app opens a document at a page with
/resources/<id>?page=<n> (from 1).
Storage sources bring documents and images in too (sources, imports): their files stay on the
source, like audio and video. Where a namespace looks for faces (Video), the faces step looks at each page
drawn: a track's spans, first_ms and boxes then count pages (from 0) rather than milliseconds, and screen_ms is
how many pages it's on (the namespace's faces don't count those as time on screen). Published, a document is a IIIF
Manifest with a Canvas per page (IIIF), and its public page shows its pages and its text page by page
(public).
GET /resources/{rid}/evidence.pdf and GET /collections/{cid}/evidence.pdf are a resource, or the ones in a
collection you can read (a list in its own order, a filter's oldest first; 50 at most), as one PDF to hand on, made by
anytopdf. Each resource goes in as its own file: a document or an image as it is, a
captured web page as captured, audio or video with Lens's transcript of it (its speakers' names on each line), and a
text-only asset as its text. After it come the pages of what Lens found about it, a text file of its own
named <title> - what Lens found.txt. The PDF is searchable PDF/A-3 with its text and chunks embedded, and its last
page is a provenance page giving every file's SHA-256 and size, so whoever gets it can check each part against the
file it came from. It holds no paths of the server (anytopdf's share profile), is made when asked for, and nothing
of it is kept. Anyone who can read the resources can have it (share links and signed links can't).
include picks its parts (repeat it for several, ?include=file&include=summary); without it, every part but
notes goes in. In the web app, a resource's Export menu has Evidence PDF (the default parts) and Choose what goes in
it…, and a collection's page has the same beside its Evidence PDF button; the picks are remembered in the browser.
| Part | What goes in |
|---|---|
file |
its own file, as above |
details |
what it is, when it was recorded and added, its length or pages, size, language, words, content type, where it came from, its status and NSFW classification (where that is on); an email's subject, sender, recipients and date; a web page's address and when it was captured; what a document says about itself (Tika) and how its PDF was made |
metadata |
its description as saved over what it says about itself: title, description, date, language, creators, contributors, subjects, identifiers, rights, attribution, provider, related links, access, Dublin Core terms and other statements, and its custom fields |
tags |
its tags |
topics |
the topics it's about: accepted ones, then suggested ones, each saying how it came |
summary |
the summary a model wrote, its key points and follow-ups (each with where it comes from), the people and topics it names, its overall tone and importance |
analysis |
what its pipeline's model steps saved, by key, with the model |
sections |
its sections, with where each starts |
speakers |
who spoke, for how long and what share of the speech, their turns, words and pace, and how each was recognised |
emotion |
the share of the speech time of each emotion, overall and by speaker, and the sounds heard; each transcript line is marked with its emotion and sound, such as "(Happy, Laughter)" (neutral speech isn't marked) |
entities |
the names it mentions by type (people, organisations, places, dates …), each with how often |
keywords |
its keywords |
visual |
its shots, the faces found (and who, when recognised), the objects seen, what a model said each page or shot shows, and the text on screen, with where each is |
places |
the places and dates anytopdf found in it and where a photo was taken (members of its namespace only, as on its page) |
notes |
the highlights, shared notes and comments on it, with who wrote them (names only); never anyone's private notes |
What a model wrote (the summary, analysis and descriptions) says so on its page. A list longer than 1,000 lines (text on screen in a long video, say) shows the first 1,000 and says how many more there are. Making these pages calls no model: they're read from what Lens already stored.
anytopdf runs as it does for conversions (no network, plugins or config file of its own); if it isn't here it's fetched on first use, unless Settings › Documents says never to use it (503). A conversion node takes one file per job and its own options: a single document goes as it is, and more go as one zip, with audio and video as their transcripts only.
GET /api/v1/resources/{rid}/notes
POST /api/v1/resources/{rid}/notes
PATCH /api/v1/resources/{rid}/notes/{nid}
DELETE /api/v1/resources/{rid}/notes/{nid}
Notes on an asset, for people with a role in its namespace (share links and signed links don't reach them). POST
with text (up to 5,000 characters) and, for a note about a moment, t0 and t1 (ms from the start; t1 defaults
to t0 and stops at the end of the asset) and the quote picked in the text (up to 1,000 characters);
without t0 the note is about the whole asset. Anyone who can read the asset can write notes, up to 500 each
on an asset. A note is its writer's: only they see it, unless they share it (shared: true, which needs editor
access) with everyone who can read the asset. GET lists yours and the shared ones: notes about the whole
asset first, then by moment, each with its writer (created_by, created_by_name), mine, can_delete and
edited_at (when its text last changed). Only its writer changes a note (PATCH with text or shared); its writer,
or an owner of the namespace for a shared one, deletes it. Sharing, unsharing and deleting a shared note are audited
(note.share, note.unshare, note.delete, on the asset). Notes move with their asset and go when it's
deleted.
GET /api/v1/notes
POST /api/v1/notes
POST /api/v1/notes/today
GET /api/v1/notes/search
GET /api/v1/notes/deleted
POST /api/v1/notes/deleted/{pid}/restore
DELETE /api/v1/notes/deleted/{pid}
GET /api/v1/notes/{pid}
PATCH /api/v1/notes/{pid}
DELETE /api/v1/notes/{pid}
POST /api/v1/notes/{pid}/move
PUT /api/v1/notes/{pid}/pin
DELETE /api/v1/notes/{pid}/pin
GET /api/v1/notes/{pid}/history
GET /api/v1/notes/{pid}/history/{vid}
POST /api/v1/notes/{pid}/history/{vid}/restore
GET /api/v1/notes/{pid}/files
PUT /api/v1/notes/{pid}/blobs
GET /api/v1/notes/{pid}/blobs
DELETE /api/v1/notes/{pid}/files
Note pages (Notes), apart from the notes on an asset above. GET /notes?ns= is a namespace's page tree
(all=true adds the pages of things). today gives the namespace's note for date, made the first time it's asked
for (Today's note). search?q= finds pages by the words in their title, summary or text, title
matches first, each with the words around the match (Finding notes). DELETE /notes/{pid}
moves a page to recently deleted for 30 days: deleted lists them, restore puts one back where it was and
DELETE /notes/deleted/{pid} deletes one for good (Deleting). move puts a
free note inside parent before before. PUT …/pin pins a page for you (pinned on each page says whether you
did) and DELETE …/pin unpins it (Pinned notes). history lists what the page was before
each change, and restore puts an earlier version back (what it was becomes a version too). files
lists the files and images attached to a page; blobs uploads (in chunks) and reads them, and DELETE …/files?key=
removes one.
GET /api/v1/tree?path=
GET /api/v1/tree/file?path=
GET /api/v1/shares/s3
POST /api/v1/shares/s3/keys
DELETE /api/v1/shares/s3/keys/{key_id}
GET /api/v1/shares/s3/keys
The archive as folders and files (The graph as files): tree lists a folder (an empty path lists your
namespaces; in each, Collections, Topics and Entities), tree/file reads a file with byte ranges. Read-only; everyone sees what they can read in the web app.
shares/s3 is the S3 and WebDAV servers' state and your access keys; keys are
made only while signed in.
GET /api/v1/resources/{rid}/comments
POST /api/v1/resources/{rid}/comments
PATCH /api/v1/resources/{rid}/comments/{cid}
DELETE /api/v1/resources/{rid}/comments/{cid}
Comments on a resource: a conversation everyone with a role on the resource (in its namespace, or on its collection)
reads and joins; share links and signed links don't reach them. POST with text (up to 5,000 characters) and, for
a comment about a moment or a passage, t0 and t1 (ms from the start; a document's pages have a reading-pace
clock, so t0 is a place on a page; t1 defaults to t0 and stops at the end of the resource) and the quote
picked in the text (up to 1,000 characters); without t0 the comment is about the whole resource. A comment starts
a thread; POST with parent replies on that comment's thread (a reply to a reply goes on the thread too, and a
reply is about what its thread is about, so its moment is ignored). Anyone who can read the resource can comment, up
to 1,000 each on a resource. GET lists them threaded: each thread (the ones about the whole resource first, then by
moment) followed by its replies, the earliest first, each with its writer (created_by, created_by_name), mine,
can_resolve, can_delete and edited_at. Only its writer changes a comment's text (PATCH with text). A thread
is resolved and reopened on its first comment (PATCH with resolved), by its writer or an editor of the resource;
the thread says who resolved it (resolved_by, resolved_by_name, resolved_at). Its writer, or an owner of the
resource (of its namespace, or an admin of its collection), deletes a comment; a thread's first comment takes its
replies with it. Resolving, reopening and deleting are audited (comment.resolve, comment.reopen, comment.delete
with whether its writer did it and how many replies went, on the resource). Comments move with their resource and go
when it's deleted.
GET /api/v1/resources/{rid}/highlights
POST /api/v1/resources/{rid}/highlights
PATCH /api/v1/resources/{rid}/highlights/{hid}
DELETE /api/v1/resources/{rid}/highlights/{hid}
Passages of a resource its editors mark in colour, for everyone with a role on it. POST (editors) with the passage
(t0 and t1, as for comments; t1 defaults to t0), the quote picked in the text (up to 1,000 characters), a
colour (yellow, the default, green, blue or red: the web app has a token for each) and a label of what it
marks (up to 200 characters). Up to 1,000 on a resource. GET lists them by passage, each with who made it
(created_by, created_by_name), mine and can_edit. Any editor of the resource changes one (PATCH with
colour or label; an empty label clears it) or deletes it; deleting is audited (highlight.delete, on the
resource). Highlights move with their resource and go when it's deleted.
GET /api/v1/resources/{rid}/files
POST /api/v1/resources/{rid}/files?role=&name=&language=&label=
PATCH /api/v1/resources/{rid}/files/{fid}
DELETE /api/v1/resources/{rid}/files/{fid}
GET /api/v1/resources/{rid}/files/{fid}/download
GET /api/v1/resources/{rid}/files/{fid}/lines
A resource's files: its primary file (primary: the audio or video its pipeline runs on, none for text
without media) and any number of supplementary files beside it, each with a role, and a language, label and
description of its own:
role |
Types | What becomes of it |
|---|---|---|
transcript |
.txt .md .json .jsonl .srt .vtt .docx .doc .pdf | another transcript, read into lines search finds |
captions |
.vtt .srt | lines, and WebVTT captions in IIIF |
translation |
as transcripts | lines in another language |
index |
WebVTT or SRT chapters, JSON, OHMS XML, text whose entries start with a time | entries with a title, synopsis and keywords, and a table of contents in IIIF |
thumbnail |
.jpg .jpeg .png .webp .gif | the Manifest's thumbnail in IIIF |
attachment |
anything | a file to download |
People who can read the resource list its files (GET, each with a signed download link that works on its own, as
media links do) and read the lines of transcripts, captions, translations and indexes (…/lines, in order, with
offset and limit up to 1,000). A line has t0/t1 in ms, which are null when its file doesn't say when its lines
are (timed: false, such as prose in a PDF); an index entry also has its title, synopsis and keywords.
Editors add a file as the raw request body (application/octet-stream), naming it and its role in the query, up to
server.max_upload_mb. 400 when its type doesn't fit the role, when it can't be read as its role (no text, more than
20,000 lines, a PDF without a text layer), or when the resource has 100 files already; 413 past the limit. PATCH
changes its role (reading it again), language, label or description (null clears the last three); DELETE deletes it
and its lines. Adding, changing and deleting are audited (file.add, file.update, file.delete, on the resource).
Downloads come as attachments and are never run by a browser: pages, scripts and SVG are sent as plain bytes. Files
stay with a resource that moves to another namespace and go when it's deleted.
public says whether everyone may download a file. A public resource opens its files with its parts
(Access): transcripts, captions and translations with its text, indexes with its index,
thumbnails with its media. Attachments always need permission.
POST /api/v1/import
POST /api/v1/import/preview
POST /api/v1/import/web
POST /api/v1/import/web/links
POST /import queues the namespace's pipeline after the import, or the saved pipeline named by pipeline (any of
GET /pipelines; 400 for one that doesn't exist, before anything is saved). Every import takes a collection of the
namespace to put the assets in (default: its default collection; 404 for one of another namespace): POST /import,
POST /import/source, POST /uploads and POST /import/iiif. A transcript imported without its audio has nothing to
transcribe and its speakers came with it, so its pipeline's transcribe and diarize steps are left out and no speech or
voice model is fetched for it.
POST /api/v1/import/source {source, paths, namespace, pipeline?, collection?, documents_as?} imports chosen files of
a storage source now, rather than watching their folder (admins, like sources; up to 500 paths, as
GET /sources/{sid}/browse lists them). Audio, video, documents and images stay on the source and run the namespace's
pipeline, or pipeline; transcripts are imported. A PDF, Word, text or Markdown file is a document unless
documents_as is transcript (its text only), or the server can't make a PDF of it. Each path gets a result: queued (with recording and job), already (the namespace has it from this
source), skipped (a folder, or not audio, video, a document, an image or a transcript) or error. Choosing a file
whose asset was deleted brings it back. Audited as import.source.
POST /api/v1/import/web {url, namespace, title?, collection?, pipeline?} captures a web page as a document (editors
of the namespace; admins may name a new one). Its pipeline keeps the page as it is then, as a PDF, and reads it like
any document (Documents and images): a link to a PDF is kept as it is, any other page is
printed by headless Chromium with its scripts run. Its web is {url, final, captured_at, how}: the address given,
the one it ended at after redirects, when it was captured, and printed or pdf. Its title is the page's own once it's
captured, unless title gave one. Capturing happens once: reading it again keeps the page it captured.
Only public addresses are reached, on ports 80 and 443: the address is checked when it's given (400, saying why: not
http(s), a user name or password in it, another port, or a host with an address that isn't public), and every request
the page makes is checked again by a proxy inside Lens, which resolves each host itself and connects only to the
address it checked; redirects are followed to http and https addresses only, each checked. Chromium has no other way
out: it looks up no names itself, reaches no D-Bus, and WebRTC, which a page's script can start, may only go through
the proxy (never UDP straight to an address). documents.web_networks (Settings → Documents) adds networks that may be reached too, for an
intranet. Without Chromium on the server it answers 400 (GET /uploads/limits → convert.web). Audited as
import.web. The PDF is the resource's file (GET /resources/{rid}/media), named after the link
(annual-report.pdf) or the page's title (harbour-news.pdf).
POST /api/v1/import/web/links {namespace, text | urls | filename + data, folders_as_tags?, collection?, pipeline?}
captures one or many pages at once: pasted addresses (text, one per line; a line's other words become its title, #
lines are skipped), a JSON list of addresses (urls), or a file (base64 data): a list of addresses, a browser's bookmark export (the HTML file every browser
writes) or Chrome's own Bookmarks file (JSON). Each link becomes a page captured as above, titled with its bookmark's
name until it's captured, and tagged with the bookmark folders it was in (the innermost five, without the browser's
own: Bookmarks bar, Other bookmarks), unless folders_as_tags is false. At most 500 links at once (400 beyond); links
are taken in order, without repeats. It answers with each link's status: queued (with its recording and job),
already (a page captured from that address is in the namespace: its recording) or skipped (detail says why,
as the checks above would). Audited as import.web once, with how many were queued.
POST /api/v1/import/video {url, namespace, title?, collection?, pipeline?} imports a video or a podcast episode by its
address (editors of the namespace; admins may name a new one): a page on a video site, a podcast's episode page, or a
media file. It's off by default: documents.video_urls (Settings › Documents) turns it on, and until then it answers
400. Its transcribe step downloads the media with yt-dlp (Unlicense), which Lens
fetches on first use (release 2026.08.19, checked against its SHA-256, into data_dir/models/) unless one is on PATH,
then transcribes it like an upload. One video is downloaded, never a playlist: the best single file with both sound and
picture, at most 720 lines high, so nothing needs joining, and at most uploads.max_mb. yt-dlp runs with no config
file, plugins or cache, and the same proxy as capturing a web page as its only way out, so only public addresses on
ports 80 and 443 (and documents.web_networks) are reached; the address is checked when it's given, as above.
The resource is audio until it's downloaded (the namespace's pipeline for audio runs, unless pipeline names one);
the video steps then see its picture. Its web is {url, media: true, final, site, by, captured_at}, and its title
the video's own unless title gave one. Downloading happens once: transcribing it again keeps the file, which is the
resource's media (GET /resources/{rid}/media). Audited as import.video.
An import webhook is an address other services push files, web addresses or text to (a scanner, a phone shortcut, an automation tool, a conversion node). What arrives lands in one namespace as if someone had uploaded or imported it there, and runs the hook's pipeline, else the namespace's.
GET /api/v1/namespaces/{name}/import-hooks
POST /api/v1/namespaces/{name}/import-hooks
PATCH /api/v1/namespaces/{name}/import-hooks/{hid}
POST /api/v1/namespaces/{name}/import-hooks/{hid}/token
DELETE /api/v1/namespaces/{name}/import-hooks/{hid}
POST /api/v1/hooks/import
POST /api/v1/hooks/import/{token}
A namespace's owners manage its hooks. POST {name, collection?, pipeline?} makes one and answers with its token
(lih_...), shown this once; only a hash is kept, and lists show its last four characters. PATCH renames it, pauses
or resumes it (enabled), or changes its collection or pipeline (null for the namespace's own). POST .../token gives
it a new token and the old one stops at once; DELETE stops it altogether. What a hook imported stays either way.
Audited as import_hook.create, .update, .token and .delete.
Pushing needs only the token, as Authorization: Bearer <token> (or in the address, for services that can only be given
a URL; the address form ends up in proxy logs, so prefer the header). Three ways to send:
- A file as the raw body, named by
?filename=,Content-DispositionorX-Filename. Audio, video, documents and images become assets exactly as uploads do, duplicates included;.srt,.vtt,.json,.jsonland.icsare imported as text. - Multipart form files (field
file, up to 20), with optionaltitle,urlandtextfields. - JSON
{url?, text?, title?}: a web 8 page or 10BC8 PDF kept as a document, as web pages are (a page needs Chromium on the server; a link to a PDF doesn't), and text imported as pasted text is.
The answer (202) is {namespace, items: [{kind, name, recording, job, duplicate}]}. 401 for a token that isn't a hook's,
403 while the hook is paused, 413 over uploads.max_mb, 507 when the disk can't hold the file. Each item is audited as
import.hook, by import_hook:<id> rather than a person.
curl -H "Authorization: Bearer $TOKEN" --data-binary @minutes.pdf "https://lens.example/api/v1/hooks/import?filename=minutes.pdf"
curl -H "Authorization: Bearer $TOKEN" -F file=@call.m4a -F title="Call with Sam" https://lens.example/api/v1/hooks/import
curl -H "Authorization: Bearer $TOKEN" -H "Content-Type: application/json" -d '{"url": "https://example.org/report.pdf"}' https://lens.example/api/v1/hooks/import
Audio, video, documents (PDF) and images go up in pieces, so a dropped connection costs one piece, not the file. A PDF
uploaded this way is a document; to import only a PDF's text, send it to
POST /import instead (the web app's Import asks which).
GET /api/v1/uploads/limits
POST /api/v1/uploads
GET /api/v1/uploads
GET /api/v1/uploads/{uid}
PUT /api/v1/uploads/{uid}?offset=
DELETE /api/v1/uploads/{uid}
GET /limits tells anyone signed in what can be uploaded: the types (uploads.extensions), the largest file
(uploads.max_mb), the piece size the web app sends (uploads.chunk_mb) and the largest text or transcript file for
POST /import (server.max_upload_mb).
POST /uploads {namespace, filename, size, title?, modified?, pipeline?, collection?, recording?} starts one (editors of the namespace; admins
may name a new namespace, created when the upload finishes). With recording, a text-only asset, the file
becomes that asset's audio instead of an asset of its own (editors of its namespace; namespace can then be left
out; 409 when it has audio already; 400 for anything but audio or video). Processing says what runs then. pipeline
runs instead of the namespace's once a new asset is made, and collection is where it goes (neither with
recording, which stays in its collection). The name loses any folders and its extension is lowercased; modified
(the file's last-modified time in milliseconds) dates the asset when its name doesn't. 400 for a type not in
uploads.extensions, 413 over uploads.max_mb, 507 when the server's disk can't hold it with 512 MB to spare.
PUT /uploads/{uid}?offset= sends the next piece as the raw request body (application/octet-stream), starting at
offset: the upload's offset is how many bytes have arrived. Pieces stream to a partial file under
data_dir/uploads/.partial; a piece that breaks off is dropped whole, so after a dropped connection the client gets the
upload and sends from its offset. 409 when offset isn't where the upload has got to, or while another piece of it
is arriving. The piece with the last byte moves the file to data_dir/uploads/<namespace>/<uid>/<name>, makes it an
asset and queues the namespace's pipeline (or attaches it, adding the steps that need media to the asset's job):
the answer has state: "done", recording and job. When the namespace
already has the same file (uploaded, scanned or imported before), no second asset is made: duplicate is true and
recording is that one, which gets its media back if it had lost it. Audited as upload (with attached in the
detail). Sending the last piece again
answers the same.
GET /uploads lists your unfinished uploads, newest first: choosing the same file for the same namespace again carries
on from its offset. Only the person uploading (and admins) can see, send to or DELETE an upload; DELETE throws away
what arrived. An upload nobody has sent a piece to for uploads.expire_hours is removed with its partial file. Deleting an
asset later leaves its uploaded file in place, like any other media file.
GET /api/v1/search
GET /api/v1/search/terms
GET /api/v1/similar
GET /api/v1/graph
GET /api/v1/mentions
GET /search?q= finds the moments where the words appear (in an asset's text or transcript, on screen in a video, in a resource's
supplementary transcripts, captions, translations and indexes, or in what a model says its shots and pages show) in the
namespaces you can read, best first: every word
(English stemming), "quoted phrases" as written, OR between alternatives; ns, speaker, emotion and recording
narrow it, and object keeps to the assets a kind of object is seen in (Objects);
limit/offset page through it. total counts the moments ranked so far (capped when there may be more).
With facets=true it also counts all the matching moments, whatever the page, by namespace, person, emotion and
asset (facets: up to 50 values each, most first, and moments), and lists the kinds of object seen in the
assets they're in, with how many of those assets each is in (objects); past 20,000 moments the counts cover
20,000 of them (partial).
Each hit's source says where it was found: said (a line of the asset's text or transcript), screen (text on screen), page (a document's or an
image's text: its page, from 0, and its box on it), object (a kind of object, where it's first seen: its time, or
on a document's pages its page, with the frame and the box it's best seen in), described (what a shot or a page
shows, Descriptions: the shot's time, or the page, and its frame) or file (a line of a
supplementary file: its file, file_role and file_label, and which line). A file's lines have no t0 when the
file doesn't say when they are; the web app opens those in the resource's Files tab. A speaker or emotion filter
keeps to said hits.
mode says how the words are matched (search by meaning): keyword (BM25),
semantic (passages an embedding model finds alike), hybrid (both, fused by rank) or auto (the default: hybrid
when search by meaning is set up and the query has no "phrases" or OR, else keyword). The reply's mode is how it was
matched, semantic whether search by meaning is set up, and meaning why it wasn't used when asked for. Each hit's
match is words, meaning (with its similarity, cosine, and shown at the passage's best line) or both.
GET /similar lists the resources most like one thing, best first, in the namespaces you can read and the
collections you were given a role on (similar content): a text (what was selected,
up to 2,000 characters; exclude leaves out the resource it was selected in), a recording, a topic or an
entity; give exactly one (422 otherwise, 404 for one you can't read). limit (default 10, at most 50) and offset
page through them. Each item says why it's alike in match (meaning, words, topics, entities), with its
similarity by meaning, up to three cites (moments to open: source, idx, t0 or page, and a snippet with the
key words marked), the key words it says, and the topics and entities in common. The reply's mode is meaning
when an embedding model compared them too, else words; terms are the key words looked for and meaning says why
it wasn't by meaning when search by meaning is set up. An asset is never listed as like itself.
Search has no prefix search (interp* looks for the word "interp"). GET /search/terms?prefix=interp lists whole
words found in the namespaces you can read (ns for one) that start with it, the most frequent first, with how often and
in how many assets (limit, default 8, at most 20); the web app offers them as "Try …".
GET /api/v1/speakers
GET /api/v1/speakers/{sid}/recordings
POST /api/v1/speakers/{sid}
POST /api/v1/speakers/{sid}/merge
POST /api/v1/merges/{mid}/undo
POST /api/v1/speakers/{sid}/link
POST /api/v1/speakers/{sid}/unlink
POST /api/v1/speakers/{sid}/not-same
People (everyone recognised by voice or face) live under /speakers.
GET /speakers?ns= lists the namespace's people (each with its suggested merges), its last merges (with by, the
recordings and segments that moved, and undone_by/undone_at), its links to other namespaces, and cross:
voices in other shared namespaces you can read that are likely the same person (score, best first). not-same {with} says two entries aren't the same person (one namespace or two): the suggestion goes and isn't made again.
unlink {with} removes a link (404 when there's none). Linking, unlinking, "not the same" and undoing a merge need
editor access to both people's namespaces and are audited (speaker.link, speaker.unlink, speaker.not_same,
speaker.merge.undo).
GET /api/v1/entities
GET /api/v1/entities/types
GET /api/v1/entities/suggestions
GET /api/v1/entities/merges
GET /api/v1/entities/timeline
POST /api/v1/entities/retype
POST /api/v1/entities/merge
POST /api/v1/entities/merges/{mid}/undo
POST /api/v1/entities/not-same
GET /api/v1/entities/{eid}
GET /api/v1/wiki/{ns}/{address}
GET /api/v1/entities/{eid}/mentions
GET /api/v1/entities/{eid}/connections
POST /api/v1/entities/{eid}/rename
POST /api/v1/entities/{eid}/hide
POST /api/v1/entities/{eid}/link
DELETE /api/v1/entities/{eid}/link/{other}
POST /api/v1/mentions/{mention}/move
GET /api/v1/graph/explore
GET /api/v1/graph/path
An entity's page is its page in the web app, by name: /wiki/<namespace>/<Name_As_Written>.
GET /wiki/{ns}/{address} says which entity an address names: by its name in any case and spacing (underscores for
spaces), or a name it had before a rename or a merge. It answers {kind, id, namespace, title, path, canonical};
canonical is false when the address is another way of writing path (Notes).
GET /api/v1/resources/{rid}/metadata
PUT /api/v1/resources/{rid}/metadata
GET /api/v1/resources/{rid}/metadata/history
POST /api/v1/metadata/edits/{eid}/revert
GET /api/v1/namespaces/{name}/metadata
PUT /api/v1/namespaces/{name}/metadata
POST /api/v1/metadata/bulk
GET /api/v1/namespaces/{name}/fields
POST /api/v1/namespaces/{name}/fields
GET /api/v1/namespaces/{name}/fields/{fid}
PATCH /api/v1/namespaces/{name}/fields/{fid}
DELETE /api/v1/namespaces/{name}/fields/{fid}
GET /api/v1/resources/{rid}/fields
PUT /api/v1/resources/{rid}/fields
GET /api/v1/namespaces/{name}/collections/{cid}/fields
PUT /api/v1/namespaces/{name}/collections/{cid}/fields
GET /api/v1/resources/{rid}/files/{fid}/fields
PUT /api/v1/resources/{rid}/files/{fid}/fields
Custom metadata fields, defined by editors on a namespace or on one of its collections (collection). A field describes
the resources, the collections or the files inside where it's defined (target): one defined on a namespace applies
everywhere in it, one defined on a collection to what is inside that collection. Each has a label (unique where it's
defined, for its target), a type (text, longtext, number, date, boolean, choice, choices or link),
options for the two choice types, help, and whether it's published: published fields appear on public pages and
in IIIF metadata (Manifests for resources, Collections for collections), internal ones only in the workspace. New
fields are internal. Up to 100 fields per namespace.
Editors of the namespace define fields on it and on any of its collections; editors of a collection, on it. PATCH
renames a field and changes its options (400 for an option that items have chosen), help, publishing or place in the
order (ord); its type and target stay. DELETE deletes it with every value it has (uses, which GET of one field
also reports). These are audited as field.create, field.update and field.delete. A collection's fields go when it
is deleted.
Values: GET …/fields on a resource, a collection or a file lists the fields that describe it with its values;
PUT with {"values": {"<field id>": value}} sets them (null clears one; fields not named keep theirs). Values are
checked by type: text up to 500 characters (long text 5,000), a number, a date as YYYY, YYYY-MM or YYYY-MM-DD, true or
false, an option (or a list of options), an http(s) link; 400 says what doesn't fit, or names a field that doesn't
describe the item. Editors set a resource's and its files' values, those who arrange a collection (editors of the
namespace, admins of the collection) its own. A resource's values are part of its metadata history, so reverting an
edit puts them back too; saving values is audited as fields.save. A resource that moves keeps its values, and shows
those of the fields that describe it in its new place.
GET /resources?field=<id> lists the resources with a value for a field; with value, those whose value contains
that text (text and links, ignoring case), equals that number, starts with that date (1998, 1998-05), is true or
false, or is (or includes) that option. Saved views keep field and value.
GET /api/v1/resources/{rid}/media
GET /api/v1/resources/{rid}/frames/{name}
PATCH /api/v1/resources/{rid}/ocr/{span}
DELETE /api/v1/resources/{rid}/faces/{track}
GET /api/v1/namespaces/{name}/faces
DELETE /api/v1/namespaces/{name}/faces
PUT /api/v1/namespaces/{name}/faces/mode
POST /api/v1/faces/merges/{mid}/undo
POST /api/v1/faces/{fid}
DELETE /api/v1/faces/{fid}
POST /api/v1/faces/{fid}/merge
POST /api/v1/faces/{fid}/speaker
POST /api/v1/faces/{fid}/dismiss
PUT /namespaces/{name}/faces/mode {mode?, purpose?, pixelate?, reprocess?} (owners) sets the namespace's face mode
and whether the faces found are pixelated in the pictures visitors see (pixelate; needs faces detected; see
Video). GET /namespaces/{name}/faces says both, and the player's faces_mode and faces_pixelate
say them for a resource. GET /resources/{rid}/frames/{name} (frames, keyframes, face crops, a document's or an
image's pages and thumbnails) serves a request without a role in the namespace the faces found on it pixelated, where
the namespace says so; a role, or a link the API signed for a member (full=1), gets the picture as it is.
The objects step finds the people, vehicles, animals and everyday things (the 80 kinds of the COCO dataset) on a
video's sampled frames and on a document's or an image's pages; it comes after faces in the standard pipeline. With
no detector on the server it's skipped, and its job says why (Configuration). Each kind
found is kept once per resource, and the player's objects lists them, the most seen first:
{label, spans, screen_ms, first_ms, count, score, frame, box, boxes, paged, engine}: where it's seen ([from, to)
in ms, or on a document's pages page numbers from 0, with paged), how long or on how many pages, how often it was
found, the detector's average confidence, a signed link to the frame or page it's best seen on and where it is there,
and each place it was found ([t, x, y, w, h, score], at most 500). The kinds a resource has are its objects, which
GET /resources?object= filters on; search finds them by name (source: object), and its object filter and facet
use them (search). Moving or deleting a resource moves or deletes its objects.
The nsfw step scores a video's sampled frames, a document's pages or an image for NSFW pictures with a model on the
server, where NSFW classification is on in the resource's namespace (off by default); it comes after
objects in the standard pipeline, and is skipped, saying why, where it's off. A resource, its player and its Library
row carry nsfw: {flagged, score, manual, blur, display, threshold, checked, of, at}, and on the resource and its
player hits, its highest-scoring pictures ({t, score, frame}, a signed link); null where it's off in the
namespace or the resource wasn't classified. PUT /resources/{rid}/nsfw {flagged} (editors) marks it NSFW (true)
or not (false) by hand, over the model's score, or hands it back to the score (null); a 409 where it's off.
Audited as recording.nsfw. GET /resources/nsfw (ns for one namespace) says whether it's on in any namespace
you read, how many resources are flagged, and how many of those the Library leaves out (hidden). Search hits and
photo search results carry nsfw: true for the ones the web app blurs.
The describe step has a model that can see images, the one an admin chose as llm.vision_model, say in a few
sentences what each page of a document or an image shows, and each shot of a video (from its keyframe): the setting,
the people (by what they do and wear, never who they might be), the things and any text in it. It comes after
nsfw in the standard pipeline, and describes at most llm.describe_max pages or shots of a resource. Without an
LLM, or with no llm.vision_model, it's skipped, saying why; a model that can't see makes it fail with what the server
said (Configuration). The player's descriptions lists them in order:
{idx, t0, t1, text, model, frame, paged}: the shot or page, where it starts and ends (ms; on pages, the page from 0
and the next), what the model said, which model, and a signed link to the picture it was shown. Search finds them
(source: described, search). Moving or deleting a resource moves or deletes them; the step run again
replaces them.
GET /api/v1/resources/{rid}/iiif
GET /api/v1/resources/{rid}/content-state
POST /api/v1/import/iiif/preview
POST /api/v1/import/iiif
Importing from IIIF (admins) reads a Presentation 3 Manifest, or a Collection's Manifests, also those of the
Collections inside it (at most 8 deep and 50 Collections read). The preview of a Collection lists its Manifests in
order (items, the first 200, each with path: the Collections it's in below this one), how many it found
(total), how many Collections it read (collections) and whether it stopped early (more). POST /import/iiif {url, namespace, collection?, keep_transcripts, limit, wait} imports the first limit Manifests, all into
collection (default: the namespace's default collection).
GET /api/v1/jobs
POST /api/v1/jobs
GET /api/v1/jobs/{jid}
GET /api/v1/jobs/{jid}/log
POST /api/v1/jobs/steps/{step}
POST /api/v1/jobs/{jid}/cancel
POST /api/v1/jobs/{jid}/retry
GET /api/v1/workers
POST /api/v1/workers/{name}/pause
POST /api/v1/workers/{name}/drain
POST /api/v1/workers/{name}/resume
GET /api/v1/events
GET /workers (admins) lists every worker that has checked in: the steps it runs, its heartbeat_at (every 30 s,
every 15 s while it runs a job), current job, paused/draining (with paused_by, paused_at), its machine's
load (1-minute load average per CPU) and cpus, and steps_last_hour (steps it finished, done or skipped). pause
stops it taking new jobs; drain also hands the job it has back to the queue after the step it's on (it stays paused);
resume undoes either. Each returns the worker, is for admins, and is audited as worker.<action>.
GET /jobs lists the newest jobs (limit, up to 2,000) in the namespaces you can read, filtered by status
(comma separated), recording, batch (a batch run's jobs) and namespace (404 when you can't read it). counts
gives the jobs of each status and namespaces the jobs in each namespace, both over namespace and batch but any
status, so a client can show its filters' counts while one is applied.
Every job names its asset (title) and the pipeline it runs with the version it pinned (pipeline:
{id, version, name}; {name: "Standard"} for the standard steps, null when the steps were chosen directly).
GET /jobs/{jid} adds how each step went, step_runs (one per step, null until it runs): started_at, finished_at,
seconds, outcome (running, done, skipped, failed), note (its last message, why it skipped, or its error),
worker, log_from/log_to (its lines of the whole log) and outputs ([{key, template, version, model}], saved as
outputs.<key>). It also adds estimates, how long each step usually takes on this asset in seconds (from the
last 25 runs of that step or template; null with none), and while the job is queued or running eta_seconds.
A run keeps its whole log (up to 100,000 lines); the job itself carries its last 200 lines (log) and how many there
are (log_total). GET /jobs/{jid}/log?after=&limit= pages through it: {start, lines, total, more}, up to 5,000
lines a page (runs from before whole logs were kept have their last 200 lines).
GET /events streams job events: one per job change in the namespaces you can read, from since. With
logs=<jid> it follows that one job, and each batch of its new lines comes as a log event
{job, start, lines}: start numbers the first line (the first event carries up to the last 200), so a client can
tell an overlap from a gap and fetch the gap with GET /jobs/{jid}/log. Lines go out about every 2 seconds.
GET /api/v1/sources/backends
GET /api/v1/sources
POST /api/v1/sources
PATCH /api/v1/sources/{sid}
DELETE /api/v1/sources/{sid}
POST /api/v1/sources/{sid}/test
GET /api/v1/sources/{sid}/browse
GET /api/v1/watches
POST /api/v1/watches
POST /api/v1/watches/preview
PATCH /api/v1/watches/{wid}
DELETE /api/v1/watches/{wid}
POST /api/v1/watches/{wid}/scan
GET /sources/{sid}/browse?path= lists a folder of a source; each file says which assets it is already
(imported: [{recording, namespace}]).
A watched folder's kinds says what it picks up: audio (and video), transcripts, both of those, documents
(documents and images) or all of them, the default. The three from before documents (audio, transcripts, both)
read PDFs, Word and text files as plain text, as they always did; documents and all take them as documents
(subtitles and JSON stay transcripts). POST /watches/preview {source, path, kinds?, include?, exclude?} counts what a folder would pick up: files, audio, transcripts,
documents and images.
GET /api/v1/templates
POST /api/v1/templates
POST /api/v1/templates/preview
GET /api/v1/templates/{tid}
PATCH /api/v1/templates/{tid}
POST /api/v1/templates/{tid}/versions
GET /api/v1/templates/{tid}/diff
PATCH {name?, description?} renames a template or changes its description (admins); its text changes by saving a
version.
GET /api/v1/pipelines
POST /api/v1/pipelines
GET /api/v1/pipelines/{pid}
PATCH /api/v1/pipelines/{pid}
POST /api/v1/pipelines/{pid}/versions
POST /api/v1/pipelines/{pid}/run
PATCH {name?, description?} renames a pipeline or changes its description (admins); its steps change by saving a
version.
GET /api/v1/chats
GET /api/v1/chats/capabilities
POST /api/v1/chats
GET /api/v1/chats/{cid}
PATCH /api/v1/chats/{cid}
DELETE /api/v1/chats/{cid}
POST /api/v1/chats/{cid}/messages
POST /api/v1/chats/{cid}/stop
POST /api/v1/chats/{cid}/messages/{mid}/check
GET /api/v1/approvals
GET /api/v1/approvals/count
GET /api/v1/approvals/{aid}
POST /api/v1/approvals/{aid}
GET /approvals lists what waits for your pick and what became of it, newest first: the assistant's proposals and
automations' actions (Approvals); filter by status, origin (automation or chat),
automation or chat_id, and page with before (the last id of the page before) and limit. /approvals/count
gives {waiting, automations}. POST /approvals/{aid} {decision, params?} decides one: approve, sample (a batch
run on 3 assets first), decline, edit (an automation's action with params changed) or always (approved, and
that node runs without asking from now on). Approving, editing and always on an automation's action need a signed-in
session (403 for an API key or an app); each approval's can says what you may do with it.
GET /chats/capabilities tells anyone signed in whether a language model is set up (configured), which (model),
whether it uses tools (tools, max_steps), whether answers can be checked (check) and the models people may pick
(the configured one first; llm.chat_models, else the server's list); never the model server's address or key. A
conversation's model (POST /chats, PATCH /chats/{cid}; null for the configured one) answers in it, and a
question can name another (POST /chats/{cid}/messages {content, model}, e.g. to retry); a model that isn't offered is
a 400. Each answer records the model that wrote it. POST /chats/{cid}/stop stops the answer being written in your conversation after the piece or tool
step it's on: the stream sends stopped, then done with the saved message, whose stopped is true and whose
content is what came before ((stopped) when nothing had). {stopping: false} when nothing was being written.
POST /chats/{cid}/messages {content, edit} edits one of your earlier questions (edit is its message id): it and
everything after it are replaced by this question and its new answer; 404 when edit isn't a question in that
conversation. A conversation titled after the first question is retitled when that question is edited.
A conversation's scope narrows what it draws on: namespaces, recordings, collections, speakers, from and
to; every key narrows it further, and an empty scope is everything you can read. collections are ids of
collections you can see (yours or shared); the conversation draws on their assets as they are each time it
answers, so a filter collection's new assets count, and a deleted collection adds none.
GET /chats/{cid} returns each answer with the steps the assistant took ({tool, args, summary}), its notice and
error, and its latest source check ({claims, supported, verdicts, uncited}), as they were when it was written
or checked.
POST /api/v1/browser/sessions
GET /api/v1/browser/sessions
DELETE /api/v1/browser/sessions/{sid}
GET /api/v1/browser/sessions/{sid}/actions
POST /api/v1/browser/actions/{aid}
POST /api/v1/browser/sessions/{sid}/run
A browser's Lens side panel connects a session and carries out the actions the assistant asks of it; a question sent
with browser (POST /chats/{cid}/messages {content, browser}) can use that browser while it's connected. See
The assistant in your browser.
Saved collections: lists of assets from anywhere, to chat with or run batches on. A namespace's own collections, which assets live in, are under namespaces.
GET /api/v1/collections
POST /api/v1/collections
GET /api/v1/collections/{cid}
PATCH /api/v1/collections/{cid}
DELETE /api/v1/collections/{cid}
GET /api/v1/collections/{cid}/evidence.pdf
GET /{cid}/evidence.pdf is the collection as one evidence PDF.
GET /api/v1/views
POST /api/v1/views
PATCH /api/v1/views/{vid}
DELETE /api/v1/views/{vid}
Saved views of the Library: a name (unique among your views, ignoring case), the namespace it shows (null: every
namespace you can read) and its state: the tab (all, attention, processing), the filter box q, statuses,
a speaker (a person, by name; each namespace has its own person ids), the Library's date and duration ranges (kept as
ranges, so 30d stays the last 30 days), media, tags, a collection of its namespace and sort (as
GET /resources takes it). A view is its
maker's; shared: true shows it to everyone with a role in its namespace (sharing needs editor access there, and a
view of every namespace can't be shared). GET lists yours, then the shared ones of namespaces you can read, each
with mine and can_delete; a view of a namespace you can no longer read is left out. Only its maker changes a view
(PATCH with name, shared or state); its maker, or an owner of its namespace for a shared one, deletes it. Up to
100 views each. Sharing, unsharing and deleting a shared view are audited (view.share, view.unshare,
view.delete).
GET /api/v1/searches
POST /api/v1/searches
PATCH /api/v1/searches/{sid}
DELETE /api/v1/searches/{sid}
Saved searches: POST with a name (unique among yours), the search's q, and any of namespace, speaker,
emotion and recording, as GET /search takes them. They follow the rules of saved views (above): yours, or
shared with the namespace they search (editors there; a search of every namespace can't be shared); only their maker
renames or shares them (PATCH with name or shared); their maker, or an owner of the namespace for a shared one,
deletes them. GET lists yours, then the shared ones, with speaker_name and recording_title where you can read
them. Sharing, unsharing and deleting a shared one are audited (search.share, search.unshare, search.delete).
Saved searches are not collections: to chat with a search or run things on it, keep it as a collection too.
POST /api/v1/batches/estimate
GET /api/v1/batches
POST /api/v1/batches
GET /api/v1/batches/{bid}
POST /api/v1/batches/{bid}/combine
POST /api/v1/batches/{bid}/{action}
GET /api/v1/batches/{bid}/results
GET /api/v1/batches/{bid}/results.{fmt}
What visitors see (Access). No sign-in is needed; send a token and people with permission see more, and so do visitors whose address is in an IP group.
GET /api/v1/public/home
GET /api/v1/public/collections/{name}
GET /api/v1/public/search
GET /api/v1/public/recordings/{rid}
POST /api/v1/public/recordings/{rid}/request
GET /api/v1/access-requests
GET /public/home lists the featured public assets (for everyone, members too) and the collections the caller
sees anything in, with how many of their assets they see (network names the IP group that opens all of one to
the caller's address). GET /public/collections/{name} is a collection's page:
the namespace's description and the assets the caller sees there, newest first (limit, offset, and total on
all pages). Each asset is a card with the caller's view of it; locked cards carry the title only. A poster
frame comes only with media the caller may play. A collection with nothing for the caller answers 404.
GET /public/search?q= (limit, offset) finds the assets the caller sees by their title, and by the lines of
text they may read (the same query syntax as /search: words, "phrases", OR). Title matches come first;
each result is a card with up to three matching hits (t0 and an HTML-escaped snippet with <mark>, and the
page of a document's text). Restricted assets and closed text match on the title only.
GET /public/recordings/{rid} is an asset's public page as the caller may see it. view says how:
full: the caller has permission, so all of it: a role in its namespace (admins have every role;memberis true), permission given on the asset (granted), or an address in an IP group that opens it (network, the group's name).public: a public asset, for everyone else: its description (the metadata IIIF publishes) and only its open parts:media(a signed link to the audio or video, with its waveform),transcript(people and lines, and the transcript files to download when the text is open to everyone) andchapters(the index).locked: a restricted asset, for someone signed in without permission: its title and namespace only.
A document's or an image's media are its pages and its file: media.kind is document or image, media.pages
lists each page (idx, width, height, label, and signed links to its image and thumb), media.url is the
file to save and media.poster the first page. Its transcript is its text: each segment has its page (p), and the
downloads are its text (txt, md, json) without subtitles. A public document's page opens at ?page=<n> (from 1).
Parts the caller can't use are null and listed in closed. files are the supplementary files the caller may
download (with signed links: those that follow an open part, or all of them with permission), and files_closed how
many more there are. For someone signed in without permission,
can_request says whether they may ask for access (something is closed to them) and request is their latest request.
POST …/request with an optional message asks the owners (400 when the caller already sees all of it, or when all of
it is open to everyone); asking again replaces the request. GET /access-requests lists the requests waiting for an
answer in the namespaces the caller owns. Assets the caller may not see at all (restricted ones
to visitors who aren't signed in, private ones to anyone without permission) answer 404, as missing ones do.