# REST API

End-user HTTP endpoints on the unified SurrealDB Agent Memory substrate.

SurrealDB Agent Memory exposes two HTTP surfaces from the **api** role:

| Surface | Prefix | Authentication |
| --- | --- | --- |
| **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](/docs/agent-memory/reference/management-api.md) for Context and key lifecycle.

## Authentication

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

```http
Authorization: Bearer <your-key>
```

Optional idempotent writes accept:

```http
Idempotency-Key: <opaque-string>
```

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

```http
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.

## Base URL

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

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

## Scope selectors on the wire

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).

| Direction | Field | Shape |
| --- | --- | --- |
| Write (facts, sessions, uploads) | **`scopes`** | `[[\"org/acme/user/alice\"]]` - legacy alias **`scope`** still accepted |
| Read (`/query`, `/context`) | **`lens`** | Same 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:

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

See [Contexts and scope](/docs/agent-memory/mental-model/contexts-and-scope.md).

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

## Unified verbs

| Verb | Method and path | Purpose |
| --- | --- | --- |
| **Remember** | `POST /api/v1/{ctx}/facts` | Single fact or turn; `infer` controls extraction |
| **Remember (bulk)** | `POST /api/v1/{ctx}/facts/batch` | Whole conversation in one request |
| **Upload** | `POST /api/v1/{ctx}/documents` | Byte ingest; async parse → chunk → extract → embed |
| **Recall** | `POST /api/v1/{ctx}/query` | Unified four-tier router over facts and passages |
| **Chat** | `POST /api/v1/{ctx}/chat` | SurrealDB 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`.

---

## Facts (remember)

### Single fact

```http
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](/docs/agent-memory/reasoning/temporal-validity.md#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.

### Batch conversation

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

```http
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 (episodic introspection)

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

```http
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:

```json
{
  "scopes": [["org/acme/user/alice"]],
  "metadata": { "source": "chat-ui" }
}
```

---

## Recall and context

### Ranked hits (`/query`)

```http
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"
}
```

| Field | Purpose |
| --- | --- |
| `lens` | DNF scope selector narrowing the read region (clamped to the key’s grant). |
| `labels` | Descriptive `key=value` tags that **filter** hits within the allowed region - labels never widen access on their own. |
| `scope_view` | How 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. |
| `include` | Which 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. |
| `mode` | Closed set: `hybrid`, `vector`, `bm25`, or `graph`. Invalid values return `400`. |
| `asOf` | Known-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. |
| `includeDuplicates` | When `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`). |
| `source` | Free-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](/docs/agent-memory/reference/configuration.md#request-and-list-limits)). 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](/docs/agent-memory/retrieve/recall.md#section-expansion).

### Formatted context block (`/context`)

```http
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.

### Chat (SSE optional)

```http
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](/docs/agent-memory/sessions/chat-sessions.md) for transcript-window and ranking behaviour.

---

## State, profile, entities

```http
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.

---

## Reflection, lifecycle, maintenance

```http
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](/docs/agent-memory/operations/forget.md)).

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

---

## Documents (upload)

```http
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:

| Field | Purpose |
| --- | --- |
| `title`, `source`, `mimeType`, `filename` | Display and provenance |
| `scopes` | DNF 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`**. |
| `labels` | Descriptive `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`**. |
| `observedAt` | Optional RFC 3339 known time for derived facts - stamp each page or episode on a narrative timeline so `asOf` queries stay spoiler-safe ([cookbook](/docs/agent-memory/cookbooks/patterns/spoiler-safe-narrative-memory.md)). Omitted means facts date to ingest time. |

```http
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"}
```

### List documents

```http
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 query (`/documents/query`)

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](/docs/agent-memory/reference/configuration.md#reranker-optional)). 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`:

| Value | Signal |
| --- | --- |
| `knowledge_has_keyword` | Keyword overlap between documents |
| `section_match` | Section-heading similarity |
| `document_link` | Cross-document link density |
| `document_summary` | Document-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.

---

## Traces and audit

```http
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.

---

## Self-service keys and `/me`

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

```http
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](/docs/agent-memory/reference/configuration.md)):

- **`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](/docs/agent-memory/reference/configuration.md#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`.

---

## Scopes and principals

```http
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](/docs/agent-memory/mental-model/contexts-and-scope.md#permissions-and-delegation).

---

## Health

```http
GET /api/v1/health
```

Unauthenticated liveness check.

---

## Connectors

Not available in the current release.

---

## Errors

All errors follow [RFC 7807 problem details](/docs/agent-memory/reference/errors.md).
