Skip to content

Reference

REST API

SurrealDB Agent Memory 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:

agent-memory scopes create org/acme
agent-memory scopes create org/acme/user/alice

See Contexts and scope.

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

Every list surface pages the same way. Three optional query parameters, uniform on each listing:

ParameterDefaultMeaning
limit100Page size. A value above the cap of 500 is clamped rather than rejected. An explicit limit=0 is a 400, because it asks for no rows and clamping it up would answer a different request.
cursorfirst pageOpaque continuation token from the previous response.
countfalseAlso compute totalSize.

The collection comes back beside a page block:

{
  "entities": [ ... ],
  "page": {
    "nextCursor": "eyJ2IjoxLCJrIjp7ImsiOiJ0eXBlX25hbWUi…",
    "hasMore": true,
    "totalSize": 40312
  }
}

nextCursor and totalSize are omitted when absent, so the last page is {"hasMore": false}.

Important

Walk a listing by following nextCursor until it is absent. Never terminate on a short page. A listing that is bounded in the database and then filtered for visibility, such as /scopes or /keys, can return fewer rows than limit while further pages remain.

Paging is keyset rather than offset: a cursor names the last returned row's position in the listing's sort order, so any page costs the same as the first and the walk stays stable under concurrent writes. The record id is the last component of every ordering, which is what makes a position unambiguous within a run of equal sort values.

A cursor is unsigned and carries no identity. The scope predicate is resolved from the caller's credentials on every request, independently of the cursor, so replaying another caller's cursor grants nothing. What a cursor does carry is a fingerprint of the request's filters, so reusing one under changed filters returns 400 rather than silently resuming inside a different result set.

count=true costs a full count of the filtered set, which is the unbounded read that pagination exists to avoid, so it never rides along with a default page fetch. totalSize counts the whole listing, not the rows remaining after the cursor.

Over gRPC the same three inputs are page_size, page_token and want_total, and responses carry next_page_token and an optional total_size. There is no has_more, because an absent token already means the last page.

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}/chatSurrealDB Agent Memory acting as the 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), near-duplicate document passages and stored conversation text are excluded from fused recall so the same wording 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), optional contextHits (same-section siblings from section expansion - not counted against k), queryMs, and inline trace (with traceId for GET .../traces/{id}). See Recalling memories.

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.

The response (and the SSE done frame) includes a citations list when the model cites retrieved sources inline with [S1]-style markers. Each citation resolves a marker to its source row (id, kind, snippet, score, optional occurredAt, optional role). For document passages it also includes documentTitle and positionPercent - an approximate percentage through the document, not a raw chunk id. When the source is a prior assistant reply, role is "assistant". Citations index both ranked hits and section-expansion contextHits. See Chat sessions for transcript-window and ranking behaviour.

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 /consolidate is caller-scoped when invoked on the data plane: facts are pooled only from the caller's memory:read region, and persistence requires memory:write over each scope group. A readable-but-not-writable group returns a dry-run preview in the response but is not committed. The background scheduler pass is unchanged and pools every scope. An empty read region denies all pooling (fail-closed).

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 (mimeType). Page with limit and cursor as described under Pagination; the older zero-indexed page and pageSize parameters are still accepted here for existing callers.

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.

GET /keys returns only the caller's authenticating key, unless the caller is a context administrator (grant:manage over /), in which case all keys are listed. DELETE /keys/{name} and POST /keys/{name}/rotate apply the same rule: ordinary keys may manage only themselves; a mismatched or sibling key returns 404 (no cross-key enumeration).

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 Key policy.

/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 Permissions and delegation.

GET /api/v1/health

Unauthenticated liveness check.

Not available in the current release.

All errors follow RFC 7807 problem details.

Was this page helpful?