REST API

End-user HTTP endpoints on the unified Spectron substrate.

Spectron exposes two HTTP surfaces from the api role:

SurfacePrefixAuthentication
End-user API/api/v1/{context_id}/...Context API key as Authorization: Bearer
Management API/api/v1/... (no context id in path)Management key as Authorization: Bearer

Both surfaces share the same host and port (default http://localhost:9090). MCP is served at /mcp on the same listener.

See Management API for Context and key lifecycle.

Every request must include your API key as a Bearer token:

Authorization: Bearer <your-key>

Optional idempotent writes accept:

Idempotency-Key: <opaque-string>

Optional delegated identity on a single request (depth 1, intersected with the target principal’s grants):

X-Spectron-On-Behalf-Of: <principal-id>

Use this when an agent acts for another principal — for example a supervisor tool calling on behalf of a user. The effective authority is always the intersection of the caller’s key, the caller’s grants, and the target’s grants; delegation cannot widen access.

The same Idempotency-Key (scoped to context + principal) within 24 hours returns the stored response. A different body with the same key, or a duplicate request while the first is still in flight, returns 409 Conflict. Failed writes release the reservation so a retry can re-run.

https://<host>/api/v1/{context_id}

Replace {context_id} with the identifier you chose at bootstrap (for example dev or docs_test).

Scope on requests is a DNF selector (ScopeSets): an OR of conjunctive clauses, each clause an AND of hierarchical slash paths (for example org/acme/user/alice; a trailing / is optional).

DirectionFieldShape
Write (facts, sessions, uploads)scopes[[\"org/acme/user/alice\"]] — legacy alias scope still accepted
Read (/query, /context)lensSame DNF shape; filters by involvement within the caller’s grant

A flat ["a", "b"] means a OR b. For a AND b in one clause, nest: [["a", "b"]]. Single-path tagging is unchanged in practice — [["org/acme/user/alice"]] or a bare string.

Register paths before first use:

spectron scopes create org/acme
spectron scopes create org/acme/user/alice

See Contexts and scope.

JSON responses use camelCase field names (queryMs, sessionId, traceId, …).

VerbMethod and pathPurpose
RememberPOST /api/v1/{ctx}/factsSingle fact or turn; infer controls extraction
Remember (bulk)POST /api/v1/{ctx}/facts/batchWhole conversation in one request
UploadPOST /api/v1/{ctx}/documentsByte ingest; async parse → chunk → extract → embed
RecallPOST /api/v1/{ctx}/queryUnified four-tier router over facts and passages
ChatPOST /api/v1/{ctx}/chatSpectron-as-agent (composed over facts + query + LLM)

Document-only retrieval remains under POST /api/v1/{ctx}/documents/query. Formatted prompt blocks use POST /api/v1/{ctx}/context.

POST /api/v1/{context_id}/facts
Authorization: Bearer <key>
Content-Type: application/json

{
"text": "King Charles III became head of state of the United Kingdom in September 2022.",
"infer": "full",
"scopes": [["org/acme/user/alice"]],
"observed_at": "2022-09-08T12:00:00Z"
}

infer values: full (LLM extraction, default), triples (caller-supplied structured triples), preview (dry run), none (literal store).

Optional observed_at (RFC 3339) sets the known time (created_at) of derived facts. Use when ingesting serial narrative — novels, episodes, franchise films — so asOf recall matches how far the user has read or watched, not when you bulk-imported the corpus. In the example above, this would be useful to replay a narrative in which a user or character is unaware of the fact until the date indicated. See Narrative playback and spoiler safety. Omitted means wall-clock ingest time.

Response (infer: full): nested extraction (entities, attributes, relations, …), plus sessionId, turnId, and mode. infer: none returns chunkId, sessionId, and turnId for the literal memory chunk.

When supplying triples or structured entities, memory_category on entities, attributes, and relations must be one of identity, knowledge, or context. Invalid values are rejected with 400 Bad Request. Instructions and uncertainties use their own tables; episodic material lives on sessions and turns.

Preferred path for multi-turn ingest (replaces per-turn POST .../sessions/{id}/turns for new integrations):

POST /api/v1/{context_id}/facts/batch
Authorization: Bearer <key>
Content-Type: application/json
Idempotency-Key: conv-01HF...

{
"messages": [
{ "role": "user", "content": "I was just promoted to CTO.", "ts": "2026-05-12T10:00:00Z" },
{ "role": "assistant", "content": "Congratulations!", "ts": "2026-05-12T10:00:05Z" }
],
"scopes": [["org/acme/user/alice"]],
"session_id": "sess_01HF...",
"extract": "whole_conversation"
}

extract controls batch extraction strategy: whole_conversation (default) runs one LLM pass over the full transcript; per_message runs extraction once per message.

Returns extractions (one extraction result per message or one for the whole conversation), sessionId, and turnIds.

Sessions remain for browsing transcripts and session-scoped state. Writes should use /facts or /facts/batch.

POST   /api/v1/{context_id}/sessions
GET /api/v1/{context_id}/sessions/{session_id}
DELETE /api/v1/{context_id}/sessions/{session_id}
GET /api/v1/{context_id}/sessions/{session_id}/turns?limit=100&offset=0
GET /api/v1/{context_id}/sessions/{session_id}/context

list_turns paginates: limit defaults to 100 (max 500); offset skips rows. The CLI sessions show / transcript commands page through turns automatically.

Create session body:

{
"scopes": [["org/acme/user/alice"]],
"metadata": { "source": "chat-ui" }
}
POST /api/v1/{context_id}/query
Content-Type: application/json

{
"query": "What is Alice's role?",
"k": 10,
"lens": [["org/acme/user/alice"]],
"labels": ["subject=alice"],
"scope_view": "strict",
"asOf": "2025-02-01T00:00:00Z",
"source": "ingest-cli"
}
FieldPurpose
lensDNF scope selector narrowing the read region (clamped to the key’s grant).
labelsDescriptive key=value tags that filter hits within the allowed region — labels never widen access on their own.
scope_viewHow broadly to fold results within the grant: strict (default — caller’s region only), crossTeam (cross-principal shared reads), or merged (same-fact records at narrower scopes). crossTeam and merged resolve like strict. None widen past the caller’s grant.
includeWhich result families to return: facts (entities and attributes) and/or passages (document chunks and sections). Default: both. Omitted or empty means no narrowing. Filters the response only — retrieval and trace reinforcement still see the full candidate set.
modeClosed set: hybrid, vector, bm25, or graph. Invalid values return 400.
asOfKnown-time filter — recall what the system would have believed at this instant (supersession walk by created_at for attributes; relations are gated by created_at and open validity). Omitted means current state.
includeDuplicatesWhen false (default), passage chunks flagged as near-duplicates (duplicate_of set) are excluded from fused chunk recall so the same text does not occupy multiple ranks. Set true to include them (parity with /documents/query).
sourceFree-form label recorded on the retrieval trace for audit replay; does not affect ranking.

k on /query and document query default to 10 and are capped at 50 (operator-tunable downward only — see Configuration). The internal retrieval pool is fixed separately from k.

Response includes tier (direct, cache, hybrid, or full_context), hits (each with source as a ResultKind: entity, attribute, memory_chunk, chunk, or section; optional occurredAt — known time of the underlying row for temporal resolution), queryMs, and inline trace (with traceId for GET .../traces/{id}).

POST /api/v1/{context_id}/context
Content-Type: application/json

{
"query": "What is Alice's role?",
"k": 10,
"lens": [["org/acme/user/alice"]]
}

Returns a string suitable for LLM system-prompt injection.

POST /api/v1/{context_id}/chat
Content-Type: application/json

{
"message": "Summarise what you know about me",
"scopes": [["org/acme/user/alice"]],
"session_id": "sess_01HF..."
}

Use Accept: text/event-stream for streaming replies.

POST /api/v1/{context_id}/state
GET /api/v1/{context_id}/profile
GET /api/v1/{context_id}/entities
GET /api/v1/{context_id}/entities/{entity_type}/{entity_name}
GET /api/v1/{context_id}/entities/{entity_type}/{entity_name}/history/{key}
DELETE /api/v1/{context_id}/entities/{entity_type}/{entity_name}

Tri-temporal reads accept asOf, atInstant, validFrom, and validUntil query parameters on entity GETs.

POST /api/v1/{context_id}/reflect
POST /api/v1/{context_id}/forget
POST /api/v1/{context_id}/lifecycle/expire
POST /api/v1/{context_id}/lifecycle/decay
POST /api/v1/{context_id}/elaborate
POST /api/v1/{context_id}/consolidate
POST /api/v1/{context_id}/fsck

reflect runs on-demand synthesis and returns traceId (correlate with GET .../traces/{id}); persist on the body stores lower-trust derived facts when permitted by key type.

POST /forget accepts a natural-language query, optional purge: true for permanent erasure, and optional dryRun: true to return a match count without expiring anything (see Forgetting memories).

consolidate responses reference per-record DecisionKind values: create, update, or supersede.

POST   /api/v1/{context_id}/documents
GET /api/v1/{context_id}/documents
GET /api/v1/{context_id}/documents/{id}
GET /api/v1/{context_id}/documents/{id}/raw
GET /api/v1/{context_id}/documents/{id}/chunks
DELETE /api/v1/{context_id}/documents/{id}
POST /api/v1/{context_id}/documents/query
GET /api/v1/{context_id}/documents/keywords
POST /api/v1/{context_id}/documents/recompute-links

Upload uses multipart/form-data; processing is asynchronous via the job queue.

The metadata part is JSON. Optional fields:

FieldPurpose
title, source, mimeType, filenameDisplay and provenance
scopesDNF scope selector narrower than the caller's memory:write region — same semantics as remember. Out-of-region scope returns 403. Omit to tag the document with the caller's full write region. Legacy alias scope.
labelsDescriptive key=value tags stamped on the document and its chunks (not on reconciled graph rows). Keys must not start with _ (400); count caps return 409.
observedAtOptional RFC 3339 known time for derived facts — stamp each page or episode on a narrative timeline so asOf queries stay spoiler-safe (cookbook). Omitted means facts date to ingest time.
POST /api/v1/{context_id}/documents
Content-Type: multipart/form-data

file=<binary>
metadata={"title":"Returns Policy","scopes":[["org/acme/team/eng"]],"labels":["team=eng"],"observedAt":"2024-01-15T00:00:00Z"}
GET /api/v1/{context_id}/documents?pageSize=20&mimeType=application/pdf

Query parameters use camelCase (pageSize, mimeType). Pagination is zero-indexed (page=0 is the first page).

Document-only ranked retrieval accepts the same mode values as unified recall (vector, bm25, hybrid, hybrid_graph). Optional flags useHyde, decomposeQuery, and useReranker are honoured when configured: HyDE embeds a hypothetical answer for the vector leg; decomposition fans out sub-questions; reranking rescores top candidates with a cross-encoder when SPECTRON_RERANKER_URL is set (see Configuration). All degrade gracefully when the backing provider is absent.

When using graph-density reranking, optional graph_edges selects which structural signals contribute. Each value must be a recognised edge kind — unknown values return 400 Bad Request:

ValueSignal
knowledge_has_keywordKeyword overlap between documents
section_matchSection-heading similarity
document_linkCross-document link density
document_summaryDocument-level summary similarity

Responses may include hybrid_graph on individual hits when several signals combine; that label describes evidence in the result, not an input filter.

GET /api/v1/{context_id}/traces
GET /api/v1/{context_id}/traces/stats
GET /api/v1/{context_id}/traces/{trace_id}
GET /api/v1/{context_id}/inspect
GET /api/v1/{context_id}/audit

/traces/stats accepts scope[…], since, and windowHours (rolling window for operational counters). The response includes aggregate counts and health signals for retrieval, decision, and response traces in the window.

Scope gate: callers with grant:manage see Context-wide aggregates. Scope-enforcing keys receive a zeroed stats view for regions outside their grant, mirroring trace listing so that cross-principal usage metadata does not leak.

Members who hold a brokered or admin-minted key can manage their own keys on the data-plane API without a management credential:

GET    /api/v1/{context_id}/me
POST /api/v1/{context_id}/keys
GET /api/v1/{context_id}/keys
DELETE /api/v1/{context_id}/keys/{name}
POST /api/v1/{context_id}/keys/{name}/rotate

GET /me returns caller introspection: principal identity, principal grants, the key’s attenuating grants, effective grants, and delegatedPrincipalId when delegating. Ungated — members deliberately lack grant:manage, so admin-side principal endpoints are not sufficient.

POST /keys mints a key for the caller’s own principal. Optional grants only attenuate (narrow) per verb; widening returns 400. Returns the plaintext secret once.

Per-Context policy (see Configuration):

  • allow_self_service_keys — set false to require Cloud-brokered keys only.

  • max_token_ttl_seconds — server-side TTL clamp on every mint path (management, broker, self-service).

See API keys and delegation.

/audit returns rows with a kind filter (decision, retrieval, or response) and traceId linking back to graph-resident traces. Query with ?kind=decision&limit=50.

GET  /api/v1/{context_id}/scopes
POST /api/v1/{context_id}/scopes
POST /api/v1/{context_id}/scopes/forget
POST /api/v1/{context_id}/scope-grants # returns 501 — cross-principal sharing surface locked

GET /api/v1/verbs # management API — grant verb catalog

GET /api/v1/{context_id}/principals
POST /api/v1/{context_id}/principals
GET /api/v1/{context_id}/principals/{principal_id}/effective

See Scope as security boundary.

GET /api/v1/health

Unauthenticated liveness check.

Not available in the current release.

All errors follow RFC 7807 problem details.

Was this page helpful?