HTTP API
Every route the service declares, with the scope its credential must hold, the two response envelopes, and the rules that apply to all of them.
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 — which is the mechanism that keeps this page honest
rather than a promise to keep it updated.
Authentication
Two routes are unauthenticated: GET /healthz and GET /readyz. Every other route
declares a required scope, and a route with a scope always 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. A request body never grants tenant authority, and 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, and the same value is repeated in an x-request-id response header
so you can correlate a log line without parsing the body. 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 on purpose. GET /v1/export and
GET /v1/audit/export stream application/x-ndjson, and POST /mcp returns a bare
JSON-RPC body because an MCP client expects the protocol’s own shape.
Request limits
MAX_BODY_BYTES 1,048,576. Checked against the declared content-length first, then against the body that actually arrived, so a lying header gets no free pass. 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 three endpoints — see 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 instead of producing a second copy.
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 — nothing was created this time.
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. It exists because a
feedback call is the one write an agent is most likely to retry from a queue it does
not control.
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 — rather than replaying a stored response.
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, because 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. 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 — 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. This is not a transcript or a raw memory feed.
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, because 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 however well it is configured. This route is that consumer.
It is 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. The tables above are authoritative.
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.