HTTP API
Every route the service declares, with the scope its credential must hold and the two response envelopes.
The route table below is the router’s own contents. Anything absent from it does not
exist yet, and scripts/verify-live.ts fails if the router declares a route the
verification script never called.
Authentication
Three routes accept no bearer credential: GET /healthz, GET /readyz, and the
username/password exchange at POST /v1/dashboard-sessions. Every other route
authenticates first; routes without a named scope still require a valid credential.
Send Authorization: Bearer titen_sk_…. A token that does not start with titen_sk_ is rejected without a database lookup.
Verification is one indexed lookup on the SHA-256 of the key. Nothing about the principal is cached, so a revoked key stops working on the next request.
Organization comes from the credential. No route accepts an org_id field.
A key may hold * in place of a scope list. A key can never mint another key with scopes or a trust ceiling it does not itself hold.
Scope failures are distinguishable from identity failures, but resource ownership is
not: see Errors for why a foreign id returns 404 and never 403.
The full scope list and trust ceilings are on Keys & scopes.
Envelopes
Success is data plus meta. Failure is error plus meta. Both always carry
meta.request_id, repeated in an x-request-id response header. Envelope responses
are content-type: application/json; charset=utf-8, and every response is
cache-control: no-store.
{ "data": { "…": "…" },
"meta": { "request_id": "req_930dd110186f42ad959858d8745fc0b6" } }
{ "error": { "code": "VALIDATION_ERROR", "message": "Field \"kind\" must be one of: …" },
"meta": { "request_id": "req_71b6d2e2eba54b0298b41de8544dedc6" } }
Request ids are req_ + 32 lowercase hex. Record ids follow the same shape with their
own prefix: org_, key_, obs_, claim_, ctx_, fb_.
Three responses sit outside the JSON envelope. GET /v1/export and
GET /v1/audit/export stream application/x-ndjson; POST /mcp returns a bare
JSON-RPC body, the shape an MCP client expects.
Request limits
MAX_BODY_BYTES 1,048,576. Checked against the declared content-length first, then against the body that actually arrived. Over the limit is 413 PAYLOAD_TOO_LARGE.
content-type JSON requests are parsed as JSON; an empty body or unparseable body is a 400 VALIDATION_ERROR, not a 500.
Idempotency-Key Optional, trimmed to 200 characters. Honoured on the three endpoints below.
Idempotency and meta.replayed
POST /v1/observations, POST /v1/consolidations and POST /v1/context/:id/feedback
accept an Idempotency-Key header. The replay record is inserted inside the same
atomic batch as the canonical rows, so a duplicate key makes the whole write fail and
roll back.
First call: the normal 201, with meta.replayed: false.
Same key, same body: the stored response, with meta.replayed: true and status 200 rather than 201.
Same key, different body: 409 CONFLICT, "Idempotency-Key was reused with a different request body." A key identifies one request, not one endpoint.
POST /v1/context/:id/feedback additionally accepts a client_mutation_id field in
the body, deduplicated per organization by a unique index.
Every other mutating route follows its own transition or uniqueness rule. Retrying without an idempotency key can repeat a write, or return the route’s normal conflict.
Health
| Method | Path | Scope | What it does |
|---|---|---|---|
GET |
/healthz |
— | Liveness, plus the runtime label and build revision. No sensitive detail. |
GET |
/readyz |
— | Storage check, applied-vs-expected migration counts, capability flags. Returns 503 NOT_READY with the same body when not ready. |
The memory loop
| Method | Path | Scope | What it does |
|---|---|---|---|
POST |
/v1/projects/resolve |
projects:resolve |
Normalize a project reference to an opaque project id. create: true additionally requires projects:create. |
POST |
/v1/observations |
observations:write |
Append content-hashed evidence. 201. trust may not exceed the credential’s ceiling. |
DELETE |
/v1/observations/:id |
observations:purge |
Irreversibly tombstone readable evidence while retaining its id, hash and audit provenance. Active legal holds block it. |
POST |
/v1/consolidations |
claims:write |
Derive claims from observations. Deterministic — model_used: false, meta.model: "disabled". |
POST |
/v1/context/compile |
context:compile |
Rank authorized claims into a token budget and record the selection as a ctx_ run. |
GET |
/v1/context/:id |
handoffs:read |
Read an actor-owned context or one delegated to the intended handoff recipient, with current visibility rechecked. |
POST |
/v1/context/:id/feedback |
feedback:write |
Record used, useful, irrelevant, incorrect or harmful against a run or one of its items. |
GET |
/v1/claims/:id/evidence |
evidence:read |
Resolve a claim to its supporting, contradicting and qualifying observations, plus hidden_source_count. |
GET |
/v1/memories |
views:compile |
List authorized canonical claims with optional lexical search, lifecycle/visibility/kind filters, and a stable keyset cursor; no Atlas compile is required. |
Memories list
GET /v1/memories is the dashboard’s canonical inventory. It defaults to current
active and disputed claims and accepts q, subject_id, project_id,
status, visibility, kind, limit (1–100, default 25), and an opaque after
cursor. Authorization, retention, and validity are applied in SQL before lexical
search and pagination. The response exposes has_more and next_cursor instead
of a global count, so large tenants can use keyset pagination without an expensive
count query. A selected memory can be opened in the dashboard’s Atlas graph for
relationship inspection.
A walkthrough with real payloads is in the Quickstart; the ranking is explained on Compile context.
Claim lifecycle
| Method | Path | Scope | What it does |
|---|---|---|---|
POST |
/v1/claims/:id/supersede |
claims:write |
Replace a claim with an active one named in superseded_by. Only active or disputed claims can be superseded. |
POST |
/v1/claims/:id/revoke |
claims:write |
Withdraw a claim. Idempotent, reports already_revoked. A superseded claim cannot be revoked. |
POST |
/v1/claims/:id/expire |
claims:write |
End validity now by setting valid_to. Idempotent, reports already_expired. |
All three stop the claim appearing in compiled context and none of them delete an observation. Details on Claim lifecycle.
Checkpoints
| Method | Path | Scope | What it does |
|---|---|---|---|
POST |
/v1/checkpoints |
checkpoints:write |
Upsert task state on organization + subject + agent + kind. A second write returns 200 with updated: true and the same checkpoint_id. |
GET |
/v1/checkpoints |
checkpoints:read |
Read the current checkpoint. Requires subject_id and kind query parameters; returns 404 once expired. |
GET |
/v1/checkpoints/:id |
checkpoints:read |
Read the exact unexpired checkpoint referenced by an eligible handoff. |
DELETE |
/v1/checkpoints/:id |
checkpoints:write |
Delete a checkpoint row outright. Checkpoints are task state, not evidence. |
kind is one of task_state, conversation, workflow, cursor. ttl_seconds is
between 60 and 2,592,000, and the serialized state is capped at 64,000 characters.
Keys
| Method | Path | Scope | What it does |
|---|---|---|---|
GET |
/v1/principal |
— | Return the caller’s principal, credential scopes, trust ceiling and organization role. Requires a valid API key. |
POST |
/v1/keys |
keys:manage |
Mint a key. The raw titen_sk_… value is in the response once and never again; storage holds only its hash. |
GET |
/v1/keys |
keys:manage |
List key metadata with status. No hashes, no secrets. |
DELETE |
/v1/keys/:id |
keys:manage |
Revoke a key. Effective on the next request. |
Directories and scoped access
| Method | Path | Scope | What it does |
|---|---|---|---|
GET |
/v1/principals |
principals:read |
List the bounded non-secret principal directory. Owner/admin role required. |
GET |
/v1/projects |
projects:read |
List projects backed by canonical records the caller may read. |
GET |
/v1/projects/:id/references |
projects:read |
List bounded canonical references for one readable project; ~ selects explicit unscoped memory. |
GET |
/v1/subjects |
subjects:read |
List subjects backed by canonical records the caller may read. |
GET |
/v1/subjects/:id/references |
subjects:read |
List bounded canonical and namespaced references for one readable subject. |
GET |
/v1/grants |
grants:read |
List active and revoked additive data grants without exposing inaccessible targets. |
POST |
/v1/grants |
grants:write |
Delegate bounded read, write, approve, or admin authority within the caller’s current grants. |
DELETE |
/v1/grants/:id |
grants:write |
Revoke a grant; derived keys narrow on their next request. |
POST |
/v1/access/simulate |
grants:read |
Return visibility, grant, and final authorization gates without impersonation or record disclosure. Owner role required. |
Organization authority still comes from authentication. Grants are additive,
append-and-revoke records over organization, project (including explicit
project:null), or subject targets. A caller must hold every permission it
delegates, and a foreign or inaccessible target is indistinguishable from a
missing one.
Model diagnostics
| Method | Path | Scope | What it does |
|---|---|---|---|
GET |
/v1/models/config |
models:read |
Return the immutable non-secret extraction and embedding startup snapshot, including masked key state and drift. |
POST |
/v1/models/probe |
models:probe |
Run one bounded extraction or embedding provider probe and append a metadata-only audit entry. |
Diagnostics never mutate runtime configuration, reveal a credential, prompt, raw provider response, or embedding, or write canonical memory. Disabled and partially configured model tuples are reported explicitly.
Dashboard accounts
Human dashboard accounts are separate from API keys used by agents and services.
Bootstrap creates username owner with a random temporary password shown once.
Temporary-password login can only replace that password; a fresh login is required
before the private dashboard becomes available. Established accounts with a passkey
must complete a second factor after password verification.
| Method | Path | Scope | What it does |
|---|---|---|---|
POST |
/v1/operator-accounts |
keys:manage |
Atomically create a human account and organization membership, returning its random temporary password once. |
PATCH |
/v1/operator-accounts/current/password |
— | Replace the signed-in account password and revoke its dashboard sessions. Requires a valid credential. |
POST |
/v1/operator-accounts/current/passkeys/options |
— | Create bound WebAuthn registration options for the current full dashboard session. |
POST |
/v1/operator-accounts/current/passkeys |
— | Verify and add a passkey. The first enrollment returns eight recovery codes once. |
GET |
/v1/operator-accounts/current/passkeys |
— | List the current account’s passkeys without credential secrets. |
DELETE |
/v1/operator-accounts/current/passkeys/:id |
— | Revoke one passkey. Removing the last passkey requires the current password. |
POST |
/v1/operator-accounts/current/recovery-codes |
— | Replace all recovery codes and return the new set once. |
POST |
/v1/dashboard-sessions |
— | Exchange username and password for a short-lived dashboard credential. No bearer credential is accepted. |
POST |
/v1/dashboard-sessions/current/passkey-options |
— | Create bound WebAuthn assertion options for a staged second-factor session. |
POST |
/v1/dashboard-sessions/current/passkey |
— | Verify a passkey assertion and replace the staged session with a full session. |
POST |
/v1/dashboard-sessions/current/recovery-code |
— | Consume one recovery code and replace the staged session with a full session. |
DELETE |
/v1/dashboard-sessions/current |
— | Revoke the current dashboard credential. Requires a valid credential. |
Password login uses persistent progressive delays before password verification. Unknown accounts and wrong passwords return the same response. A passkey challenge binds to the account, session, purpose, and expiry. Staged sessions cannot call product routes. Recovery and passkey revocation remain available if relying-party configuration is later disabled.
Portability
| Method | Path | Scope | What it does |
|---|---|---|---|
GET |
/v1/export |
export:read |
Stream NDJSON: one header line, then one record per line. type is projects, observations or claims; limit 1–2000, default 500. |
POST |
/v1/import |
import:write |
Load NDJSON, at most 2000 lines per request, preserving record ids so evidence links survive the move. |
Order matters across requests: an observation references a project, and a claim source
references an observation. Follow import_order from the export header: projects, then
observations, then claims. A reference to a record that does not exist yet is
400 UNRESOLVED_REFERENCE, not a conflict; a conflict means the id already belongs to
another organization. See Backup & export.
Collaboration
| Method | Path | Scope | What it does |
|---|---|---|---|
POST |
/v1/workspaces |
workspaces:write |
Create a workspace. A duplicate name in one organization is 409. |
GET |
/v1/workspaces |
workspaces:read |
List workspaces. |
POST |
/v1/memberships |
memberships:write |
Attach a principal to the organization or a workspace with role owner, admin, member or reader. |
GET |
/v1/memberships |
memberships:read |
List current memberships, optionally filtered by workspace_id. |
DELETE |
/v1/memberships/:id |
memberships:write |
Soft removal: the member stops being listed, prior provenance and audit entries remain. |
POST |
/v1/leases |
leases:write |
Acquire an exclusive lease on resource_type + resource_id. A live lease held by another principal is 409; an expired one or the same holder re-acquires. |
GET |
/v1/leases |
leases:read |
Page unreleased leases in the authenticated organization with an opaque cursor. |
DELETE |
/v1/leases/:id |
leases:write |
Release a lease before its TTL. |
POST |
/v1/leases/:id/force-release |
leases:write |
Owner/admin recovery path for an organization-scoped lease. |
POST |
/v1/handoffs |
handoffs:write |
Offer work to another principal, optionally naming a context_id and checkpoint_id. |
POST |
/v1/handoffs/:id/resolve |
handoffs:write |
The recipient sets status to accepted or rejected. Nobody else can resolve it. |
GET |
/v1/handoffs |
handoffs:read |
Pending handoffs addressed to the caller — the pull path for agents that cannot receive webhooks. |
Lease ttl_seconds is between 10 and 86,400. More on
Leases & handoffs.
MCP
| Method | Path | Scope | What it does |
|---|---|---|---|
POST |
/mcp |
mcp:call |
JSON-RPC 2.0 over Streamable HTTP: initialize, notifications, ping, tools/list, tools/call. Nine tools. |
Protocol versions 2025-11-25, 2025-06-18, 2025-03-26 and 2024-11-05 are supported. A
notification returns 202 with an empty body; a batch returns an array. Key
management, membership, retention, webhook, federation, release and Atlas operations
are deliberately absent from the agent tool profile; publisher, approver, operator and
gateway principals use REST for those. See MCP server.
Events
| Method | Path | Scope | What it does |
|---|---|---|---|
GET |
/v1/events |
events:read |
Authorized, metadata-only domain events after an opaque cursor. after, limit 1–200 default 50, kind. |
GET |
/v1/events/:id |
events:read |
One event by id. |
Event rows are written in the same atomic batch as the canonical change they describe, so an event can never exist for a write that rolled back or be missing for one that committed.
Memory Atlas
| Method | Path | Scope | What it does |
|---|---|---|---|
POST |
/v1/memory-views/compile |
views:compile |
Compile a bounded authorized graph view using one of seven lenses: evidence_trace, neighborhood, conflict_freshness, workspace_graph, review_queue, scope_preview, or knowledge_release. |
Read-only despite the POST; the request needs a bounded body. evidence_trace
requires focus_id, neighborhood requires subject_id. Policy runs before
traversal: both endpoints of every edge must be authorized, and hidden candidates
contribute no edges, labels or counts. An unauthorized focus is a non-disclosing
404. The evidence trace centers that claim and can include readable source
observations, complete readable context packs, and current active releases. It
labels supports, contradicts, qualifies, selected-in, and released-as
relationships; partial contexts and stale releases are omitted rather than
partially disclosed.
workspace_graph accepts an optional workspace_id and max_nodes from 25 to
300. It returns only authorized claim and subject nodes with canonical about,
related, contradicts, and supersedes edges. Truncation and withheld-edge
counts are bounded aggregate state; they never disclose hidden identifiers.
Governance, retention and released knowledge
| Method | Path | Scope | What it does |
|---|---|---|---|
POST |
/v1/policies |
governance:write |
Create a typed, versioned governance or retention policy. Owner/admin role required. |
GET |
/v1/policies |
governance:read |
List authorized policies. |
PATCH |
/v1/policies/:id |
governance:write |
Change policy status with expected-version semantics. |
POST |
/v1/claim-approvals |
approvals:write |
Submit one current claim version and its visible supporting evidence for approval. |
GET |
/v1/claim-approvals |
approvals:read |
List approval records visible to the operator. |
POST |
/v1/claim-approvals/:id/decide |
approvals:approve |
Approve or reject without allowing self-approval where policy requires separation of duty. |
POST |
/v1/channels |
releases:write |
Create a bounded channel with audiences, minimum trust and one gateway service principal. |
GET |
/v1/channels |
releases:read |
List channel metadata without secrets or released content. |
PATCH |
/v1/channels/:id |
releases:write |
Set a channel to active, paused or disabled with expected-version semantics. |
POST |
/v1/knowledge-releases |
releases:write |
Draft an immutable released-content snapshot for one exact claim version. |
GET |
/v1/knowledge-releases |
releases:read |
List authorized release rows; source evidence remains separately authorized. |
POST |
/v1/knowledge-releases/:id/approve |
releases:approve |
Approve an eligible draft snapshot under separation-of-duty policy. |
POST |
/v1/knowledge-releases/:id/activate |
releases:approve |
Activate an approved, still-eligible release. |
POST |
/v1/knowledge-releases/:id/revoke |
releases:approve |
Revoke a release while retaining its history and audit trail. |
POST |
/v1/channels/:id/context/compile |
channel:compile |
Serve only eligible released snapshots to the channel’s bound gateway principal. |
POST |
/v1/legal-holds |
retention:write |
Place an exact claim or observation on legal hold and restore retention eligibility atomically. |
POST |
/v1/legal-holds/:id/release |
retention:write |
Release an active legal hold. |
POST |
/v1/retention/apply |
retention:write |
Apply bounded retrieval exclusions; active direct or dependent holds win. |
POST |
/v1/identity-mappings |
identity:write |
Create a versioned external-to-internal identity mapping. |
GET |
/v1/identity-mappings |
identity:read |
List authorized identity mappings. |
DELETE |
/v1/identity-mappings/:id |
identity:write |
Retire a mapping without erasing its audit history. |
verified trust describes evidence authority; it does not by itself permit
disclosure. A public-facing service uses its bound channel gateway and reads released
snapshots, never canonical memory. Reasoning in
ADR-0002
and on Channel releases.
Audit
| Method | Path | Scope | What it does |
|---|---|---|---|
GET |
/v1/audit |
audit:read |
Metadata-only audit entries with cursor pagination. after, action, limit 1–500 default 50. |
GET |
/v1/audit/export |
audit:export |
Stream the log as NDJSON behind a titen.audit.header line. after, limit 1–5000 default 1000. |
Federation
| Method | Path | Scope | What it does |
|---|---|---|---|
POST |
/v1/federation/peers |
federation:write |
Register a peer with endpoint, shared_secret (16+ characters) and direction push, pull or bidirectional. One endpoint per organization. |
GET |
/v1/federation/peers |
federation:read |
List peers with status and sync cursor. |
POST |
/v1/federation/peers/:id/suspend |
federation:write |
Suspend a peer. Its pull and push then return 403. |
POST |
/v1/federation/peers/:id/filters |
federation:write |
Add a filter on resource_type with optional include_kinds, exclude_subjects, min_trust. Filters bound both directions. |
GET |
/v1/federation/peers/:id/filters |
federation:read |
List a peer’s filters. |
POST |
/v1/federation/pull |
federation:write |
Pull up to 200 filtered events after the peer’s cursor and advance it. |
POST |
/v1/federation/push |
federation:write |
Accept events from a peer. Requires x-titen-peer-signature: sha256=HEX over the raw body; each event reports success, conflict or rejected. |
GET |
/v1/federation/log |
federation:read |
The exchange log for one peer_id. |
Passing include_memory: true on a pull opts into signed canonical claim and evidence
bundles. The destination enforces explicit claim filters, source-organization binding,
trust ceilings, replay identity and complete-graph validation before an atomic import.
Webhooks and delivery
| Method | Path | Scope | What it does |
|---|---|---|---|
POST |
/v1/webhooks |
webhooks:write |
Register a destination with secret (16+ characters) and a non-empty events array (up to 50; * matches every kind). The response states the signing contract and never echoes the secret. |
GET |
/v1/webhooks |
webhooks:read |
List webhooks with status, event filter and failure count. |
DELETE |
/v1/webhooks/:id |
webhooks:write |
Stop future delivery. |
POST |
/v1/webhooks/:id/pause |
webhooks:write |
Suspend delivery without dropping events. |
POST |
/v1/webhooks/:id/resume |
webhooks:write |
Resume delivery and clear the failure counter. |
GET |
/v1/webhooks/:id/deliveries |
webhooks:read |
Metadata-only delivery state: status, attempts, last_attempt_at, next_retry_at, response_status. |
POST |
/v1/webhooks/deliver |
webhooks:write |
Drain the delivery outbox. after, limit 1–200 default 50. |
The destination must be https://, or http://localhost / http://127.0.0.1 for
development; private and link-local ranges are rejected. Deliveries are signed
X-Titen-Signature: sha256=HEX, an HMAC-SHA256 of the raw body, alongside
X-Titen-Event and X-Titen-Delivery. Backoff is 1 minute, 5 minutes, 30 minutes,
2 hours, 12 hours, capped at 5 attempts, and a webhook is disabled after 10
consecutive failures. A unique index on (webhook, event) makes delivery exactly once
per event per webhook even if a drain is replayed. More on
Webhooks & events.
Indexing
| Method | Path | Scope | What it does |
|---|---|---|---|
POST |
/v1/index/drain |
index:write |
Embed queued claims into the vector store. limit 1–100, default 50. Returns drained, indexed, skipped, remaining, model, dimensions, at. |
POST |
/v1/index/verify |
index:write |
Check up to 100 active or disputed claims against the vector store and queue one durable repair for each missing id. Accepts limit and the previous next_after cursor. |
POST |
/v1/enrichment/drain |
enrichment:write |
Drain a bounded optional extraction/reflection ledger; disabled configuration leaves canonical writes available. |
Observation and claim writes queue an outbox row, and until something consumes them the
vector index stays empty. This route is that consumer, pull-driven because an embedding
call crosses the network and a canonical write must not wait on one. With no vector
capability configured it returns 400; if the model or index is unreachable it returns
503 UNAVAILABLE with meta.pending and meta.retryable and leaves the queue
untouched. GET /readyz reports background_repair so an operator can tell whether
anything is calling it. See
Semantic vectors.
/v1/index/verify never reads embedding values. It compares organization-scoped
canonical ids with provider membership and returns checked, present, missing,
repairs_queued, and next_after; the normal drain performs any queued rebuild.
Not implemented
Named here because earlier drafts documented them as if they existed: batch
observation append, the old /v1/channel-releases and /v1/channel-context
paths, /v1/webhook-subscriptions, /v1/webhook-deliveries as a top-level
collection, and /v1/audit/events.
Compatibility
The v1 export format is versioned independently from the HTTP API.
Breaking request or response changes require a new API version or a migration path.
External field names are snake_case. Internal names are not part of the contract.