API reference

REST routes & MCP tools

Every route below is copied from crates/verity-server/src/main.rs; every tool from crates/verity-mcp/src/main.rs. Default listen address is http://127.0.0.1:7717. The MCP server is a thin stdio proxy over these same REST routes — no enforcement logic lives in it.

Auth & conventions

There are two authorization surfaces:

The inbound webhook lane (POST /wh/{token}) is authorized by the capability token in the URL path itself.

Scopes

POST /v1/scopes
Mint an HMAC scope handle. Body: tenant_id, and either principals (int tokens, dev/no-ReBAC) or subject ("user:...", ReBAC-resolved — mutually exclusive with principals); optional entity_scope, max_confidentiality (default internal), actor_sub, actor_azp, ttl_seconds (default 3600), purpose (clamps the ceiling; unknown → 422). Returns { scope_handle, expires_at }.
http
POST /v1/scopes
{ "tenant_id":"c1e6...", "principals":[7,11], "entity_scope":["account:acme"],
  "max_confidentiality":"confidential", "actor_azp":"agent:sales-bot" }
// 200 → { "scope_handle":"vs_9f2...", "expires_at":"2026-07-10T18:00:00Z" }

Recall & get

POST /v1/recall
Scoped hybrid search. Body: scope_handle, text (embedded locally for the dense leg) and/or embedding, k (default 8, capped 100). Returns an array of hits with content, entity tags, trust tier, timestamps, and ACL provenance. Restricted-class hits pass a live recheck before returning.
GET /v1/records/{source}/{entity}/{field}
Point read of one structured L1 field. Query: scope_handle, optional as_of (RFC 3339 — bi-temporal read; absent = current). 404 if there is no value for that key/time.
http
POST /v1/recall
{ "scope_handle":"vs_9f2...", "text":"renewal risks and last pricing discussion", "k":8 }
// 200 → [ { "chunk_id":"...", "content":"...", "entity_tags":["account:acme"],
//          "confidentiality":"internal", "acl_provenance":"mirrored", ... }, ... ]

GET /v1/records/postgresql:crm.opportunities/opp-1/amount?scope_handle=vs_9f2...&as_of=2026-07-10T11:59:00Z
// 200 → { "value":50000, "valid_from":"...", "valid_to":"...", ... }  (superseded value, labeled)

Episodes / actions / activity / briefs

POST /v1/episodes
Append an observation (immutable episode + searchable chunk). Body: scope_handle, observation, optional entities (subset of the scope). Returns { episode_id }.
POST /v1/actions
Record an action on the activity timeline (idempotent on action_id). Body: scope_handle, action_id, action_type (dotted, e.g. quote.issued), optional entities, summary, optional payload, outcome (succeeded|failed|pending), occurred_at. Actor identity is stamped from the scope, not the body.
GET /v1/activity
Scoped activity timeline for an entity. Query: scope_handle, entity, optional since, action_types (comma-separated exact or prefix.*), limit (default 50).
GET /v1/briefs/{entity}
One-call entity state: recent_memory + recent_activity (always re-derived under the caller's scope) + is_stale/last_synced_at/source_version + a derived-scope-gated summary (null unless the caller intersects every contributing source).
POST /v1/forget
Invalidate a memory. Body: scope_handle, ref = {kind: "chunk"|"episode", id}, reason. An episode cascade retires its derived chunks/facts and triggers the knowledge retraction recount. Tenant comes from the handle.
GET /v1/subscribe
Server-sent-events change stream (capped; 429 beyond). The MCP layer uses a pull tool instead of forwarding this over stdio.
GET /v1/slo/freshness
Freshness SLO percentiles from ingest samples (event-time → queryable).
http
POST /v1/actions
{ "scope_handle":"vs_9f2...", "action_id":"q1", "action_type":"quote.issued",
  "summary":"Issued renewal quote at $84,000 (12mo, net-30).",
  "outcome":"succeeded", "occurred_at":"2026-07-10T17:41:02Z" }
// 200 → { "recorded": true }

Ingest (admin plane)

POST /v1/ingest/debezium
Debezium change envelope(s) → deterministic keyed L1 upserts. Query: tenant_id, optional pk (default id). Body: one envelope or an array. Returns { facts_inserted, facts_superseded, facts_unchanged, facts_retired }. Provenance mirrored.
POST /v1/ingest/documents
One document version → one episode + paragraph chunks. Body: tenant_id, source, document_id, content, optional entities, visibility (int tokens), acl_provenance (mirrored|approximated|admin-assignedquarantined is rejected), optional valid_from. The contract the Google Drive connector codes against.

Webhooks & files

POST /v1/webhooks admin
Mint a scoped ingest URL. Body: tenant_id, name, visibility (non-empty int tokens), optional entity_scope, confidentiality, manifest_id. Returns { webhook_id, url }; the raw token is shown once.
DELETE /v1/webhooks/{id} admin
Revoke — the URL stops resolving. Returns { revoked }.
POST /wh/{token}
Inbound lane. Native shape: content/observation (→ chunk), facts[], entities[], optional visibility[] (may narrow, never widen → 403). Unknown shapes → 202 {quarantined:true}. Manifest-bound URLs route through the manifest runtime.
POST /v1/files
Multipart drop: fields scope_handle, optional entities (comma-separated), file. Splits to paragraph chunks + embeds. Returns { episode_id, chunks_indexed }.
GET /v1/media/{id}
Fetch stored media by id (scope-checked).
POST /v1/media/{id}/sign
Mint an HMAC-signed, scope-checked media URL.

Knowledge

POST /v1/knowledge
Propose a cross-customer generalization (a proposal, never a publish). Body: scope_handle, statement (about categories, never entities), categories, evidence (episode ids). Runs the de-identification gate; failures are stored quarantined, auditable.
GET /v1/knowledge admin
Review queue. Query: tenant_id, optional status. Returns items with admin-exact distinct_entities, bucketed support_tier, merge reason, and evidence lineage.
POST /v1/knowledge/{id}/publish admin
Publish. Body: tenant_id, visibility (broad tokens), k_min (clamped to ≥ 3). Grants broad visibility; embeds the statement for dense recall.
GET /v1/admin/knowledge/{id} admin
Full review detail incl. the de-identification gate result and complete evidence list.
POST /v1/admin/knowledge/{id}/reject admin
Reject a candidate; the same canonical statement will not resurrect. Rejecting a published item is refused — retraction is forget's job.

Manifests

POST /v1/manifests admin
Upload a manifest as a schema-validated draft. Body: {tenant_id, yaml}. Re-uploading the same source.name demotes it to draft.
GET /v1/manifests admin
List with status/tier/acl summary. Query: tenant_id.
POST /v1/manifests/{id}/activate admin
The human gate. Body: {tenant_id, approved_by}. Refuses (422) when acl_policy is absent or violates the tier contract; approval is recorded in the audit log.

Admin

POST /v1/admin/tenants
Create a tenant. Body: {name}{tenant_id}.
POST /v1/admin/principals
Upsert principal strings → int tokens (idempotent). Body: {tenant_id, principals[]}.
POST / DELETE /v1/admin/groups
Write/remove a membership tuple (requires ReBAC). DELETE writes revocation tombstones first (fail-closed ordering).
GET /v1/admin/audit
Audit log of scoped reads. Access-controlled and itself audited.
GET /v1/admin/quarantine
Recent quarantined webhook payloads, newest first. Query: tenant_id, limit.
POST /v1/admin/erasure
Compliance erasure — crypto-shred + lineage hard-purge. Admin/compliance verb, never reachable from a scope handle.
GET /v1/admin/dsar/export
DSAR export of everything attributable to a subject.
POST /v1/admin/briefs/refresh
Recompute stale briefs for a tenant (the sleep-time path). Query: tenant.
POST /v1/admin/reembed/batch, /v1/admin/reembed/cutover
Embedding-migration: dual-vector backfill, then coverage-gated route cutover (refuses V2 below 100% coverage unless force).
POST /v1/admin/connector-status GET too
Connector status/staleness for the scope inspector.
Consolidation & tags
/v1/admin/consolidation/lease, /complete, /merge-candidates; /v1/admin/tag-suggestions (+ /{id}/approve) — the L2/knowledge worker plane and probabilistic-tag approval.
GET /v1/admin/media
List stored media for a tenant.
GET /healthz, GET /ui
Health check; the read-only scope-inspector UI (handle decode + live probes + quarantine/audit/freshness panels).

MCP tools (14)

Connect any MCP-capable agent with verity-cli mcp install (prints the exact claude mcp add verity ...). Identity (tenant, principals, actor sub/azp) is process configuration and is never a tool argument. Workflow: memory_open_scope once, pass the handle to everything else.

ToolArgumentsWhen to use
memory_open_scopeentity_scope?, purpose?, ttl_seconds?FIRST, once per task — get a scope_handle.
memory_recallscope_handle, text?, embedding?, k?Open-ended questions ("what do we know about X?").
memory_getscope_handle, source, entity, fieldExact current value of one structured field.
memory_briefscope_handle, entityStart of work on an entity — current state in one call.
memory_rememberscope_handle, observation, entities?Persist durable knowledge learned during the task.
memory_record_actionscope_handle, action_id, action_type, entities?, summary, payload?, outcome, occurred_atAFTER a side-effecting action, so other agents don't repeat it.
memory_activityscope_handle, entity, since?, action_types?, limit?BEFORE acting — what other agents did on this entity.
memory_poll_changesscope_handle, entities, sinceBetween turns — pull changes since a cursor (pass back next_since verbatim).
memory_ingest_textscope_handle, content, entities?, document_id?Ingest a document-sized piece of text held in-context.
memory_ingest_filescope_handle, path, entities?Ingest a local UTF-8 text file (≤ 512 KB).
memory_ingest_urlscope_handle, url, entities?Fetch + remember a public http(s) page.
memory_propose_learningscope_handle, statement, categories?, evidence?Propose a cross-customer generalization (never names a customer).
memory_forgetscope_handle, ref_kind (chunk|episode), id, reasonRetract a wrong/sensitive memory, audited.
memory_whoamiDiagnostic: the server's configured identity defaults.
note memory_ingest_text currently proxies POST /v1/episodes (a document_id arg is reserved but not yet sent); memory_ingest_file/_url proxy the multipart POST /v1/files. memory_poll_changes combines GET /v1/activity and GET /v1/briefs/{entity} client-side — MCP delivers no pushes into a running turn, so this pull tool is the honest delivery model.