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
Two routes are unauthenticated: GET /healthz and GET /readyz. Every other route
declares a required scope and authenticates first.
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 is a plain write. Retrying POST /v1/leases or
POST /v1/channel-releases without a key does what the underlying rule says: a
second lease acquire by the same holder replaces its own lease, a second release
creates a new version.
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. |
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. |
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. |
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. |
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 |
|---|---|---|---|
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. |
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. |
DELETE |
/v1/leases/:id |
leases:write |
Release a lease before its TTL. |
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/initialized, initialized, ping, tools/list, tools/call. Seven tools. |
Protocol versions 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. lens is evidence_trace, neighborhood, conflict_freshness or scope_preview; limit 1–200, default 50. |
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.
Governance and channel releases
| Method | Path | Scope | What it does |
|---|---|---|---|
POST |
/v1/policies |
policies:write |
Record a retention, approval_required, visibility_default or trust_ceiling policy with a JSON config. |
GET |
/v1/policies |
policies:read |
List the organization’s policies. |
POST |
/v1/channel-releases |
releases:write |
Draft a release for one channel and audience. Only active claims with verified or policy_approved trust are included; up to 200 claim_ids. |
POST |
/v1/channel-releases/:id/publish |
releases:write |
draft → active. Publishing from any other state is 409. |
POST |
/v1/channel-releases/:id/revoke |
releases:write |
active → revoked, keeping the release record and its audit trail. |
GET |
/v1/channel-releases/:id |
releases:read |
Read a release with its released items. |
POST |
/v1/channel-context |
channel:read |
Serve only the content of active releases matching a channel and audience, packed under max_tokens (128–32,000). |
verified trust describes evidence authority; it does not by itself permit
disclosure. A public-facing chatbot holds a credential pinned to its channel and reads
/v1/channel-context, 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. |
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. |
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.
Not implemented
Named here because earlier drafts documented them as if they existed: batch
observation append, channel CRUD at /v1/channels, a separate approve/activate
release workflow, /v1/webhook-subscriptions, /v1/webhook-deliveries as a
top-level collection, /v1/audit/events, and
/v1/channels/:id/context/compile.
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.