> ## Documentation Index
> Fetch the complete documentation index at: https://docs.claw.boo/llms.txt
> Use this file to discover all available pages before exploring further.

# Memory API

> REST reference for the memory resource group: search, save facts and procedures, browse, and inspect the active embedding provider.

REST surface for the shared [memory](/concepts/memory) tier: search the 2-tier store (declarative **facts** + versioned **procedures**), save a fact or a procedure, browse what is stored, and inspect which embedding provider backs vector/hybrid search. This is the UI-facing half of the memory dual surface; the model-facing half is the [Memory MCP server](/reference/rest-api/tools-and-mcp). Both halves share one `SqliteMemoryStore` over the same SQLite file, so a fact saved here is searchable from a runtime's Memory tool and vice versa.

<Note>
  Memory is always on; these routes are not flag-gated. The store is FTS5 (full-text) plus an optional vector index. Vector and hybrid search require a reachable embedding provider; when none resolves, they degrade to FTS automatically. See [`GET /api/memory/provider`](#get-apimemoryprovider) to inspect the active provider.
</Note>

The save route (`POST /api/memory`) reads a JSON body parsed by `express.json({ limit: '2mb' })`. The two GET routes read their inputs from the query string, then validate them against the same zod schemas the save body uses, so an out-of-range `limit` or empty `query` is a **400**.

## Routes

| Method | Path                   | Summary                                              | Stream? |
| ------ | ---------------------- | ---------------------------------------------------- | ------- |
| GET    | `/api/memory`          | Search facts (fts / vector / hybrid), scoped         | No      |
| POST   | `/api/memory`          | Save a fact (default) or a procedure (discriminated) | No      |
| GET    | `/api/memory/browse`   | List recent facts + procedures, scoped               | No      |
| GET    | `/api/memory/provider` | The active embedding provider (or `null` = FTS-only) | No      |

<Info>
  Save scrubs secrets at the write boundary: a fact's `title`/`content` and a procedure's `content` are passed through a secret scrubber before they are embedded and inserted. A credential can never land in a durable, searchable, or auto-injectable fact regardless of who wrote it.
</Info>

***

## `GET /api/memory`

Searches stored facts. The handler reads `query`, `mode`, `limit`, `teamId`, and `agentId` from the query string, assembles a `{ query, mode, limit, scope: { teamId, agentId } }` object, and validates it with the same schema the MCP `memory_search` tool uses. Each result is a fact annotated with a `0..1` `score` and a `matchedVia` field recording how it matched.

* **Query params**

| Param     | Type                            | Required | Notes                                                                                                                             |
| --------- | ------------------------------- | -------- | --------------------------------------------------------------------------------------------------------------------------------- |
| `query`   | string                          | Yes      | 1–2000 chars (a missing/blank query is a 400)                                                                                     |
| `mode`    | `'fts' \| 'vector' \| 'hybrid'` | No       | Default: `hybrid` if an embedding provider resolved, else `fts`. `vector`/`hybrid` degrade to `fts` when no provider is reachable |
| `limit`   | integer                         | No       | 1–100; default `10`                                                                                                               |
| `teamId`  | string                          | No       | Scope filter (see scope note below)                                                                                               |
| `agentId` | string                          | No       | Scope filter (see scope note below)                                                                                               |

* **Request body**: none.

<Note>
  **Scope is inclusive.** A scoped query (a `teamId` and/or `agentId`) also sees globally-scoped facts (rows with a `null` scope), so global memory is always visible to a scoped search. A `tenantId` scope, if supplied, is strict; but `tenantId` is a dormant multi-tenant seam (a single implicit tenant today).
</Note>

### Responses

**`200 OK`**: the (possibly empty) result list:

```ts theme={"theme":{"light":"github-dark","dark":"github-dark"}}
{
  ok: true
  results: Array<{
    id: string
    title: string
    content: string
    tags: string[]
    scopeAgentId: string | null
    scopeTeamId: string | null
    tenantId: string | null
    createdAt: number // epoch ms
    updatedAt: number // epoch ms
    score: number // 0..1, higher = more relevant
    matchedVia: 'fts' | 'vector' | 'hybrid'
  }>
}
```

`matchedVia` reflects the mode that actually ran, not the mode requested: a `vector`/`hybrid` request with no embedding provider runs as `fts` and reports `matchedVia: 'fts'`. In `hybrid` mode the score blends the cosine similarity (60%) and an FTS-hit signal (40%).

**`400 Bad Request`**: the assembled query object failed validation (e.g. blank `query`, `limit` out of `1..100`, or an unknown `mode`):

```json theme={"theme":{"light":"github-dark","dark":"github-dark"}}
{ "error": "invalid query", "details": { "...": "zod flatten output" } }
```

**`500 Internal Server Error`**: any failure constructing the store or running the search:

```json theme={"theme":{"light":"github-dark","dark":"github-dark"}}
{ "error": "<message>" }
```

### Example

```bash theme={"theme":{"light":"github-dark","dark":"github-dark"}}
curl 'http://localhost:18790/api/memory?query=deploy%20checklist&mode=hybrid&teamId=<team-uuid>&limit=5'
```

***

## `POST /api/memory`

Saves a memory entry. The body is a discriminated union: a **fact** (the default, when `kind` is absent or `"fact"`) or a **procedure** (`kind: "procedure"`). A fact is a durable declarative statement ("User prefers concise responses"); a procedure is a versioned, SKILL-style "how" kept out of the fact store. Saving a procedure under a name+scope that already exists creates a new version (the prior max version `+1`) rather than overwriting.

* **Path/query params**: none.
* **Request body**: one of:

**Fact (default):**

```ts theme={"theme":{"light":"github-dark","dark":"github-dark"}}
{
  kind?: 'fact'                     // optional; absence means fact
  title: string                     // 1–500 chars
  content: string                   // 1–50,000 chars
  tags?: string[]                   // up to 50 tags, each ≤100 chars
  scope?: {
    agentId?: string | null
    teamId?: string | null
    tenantId?: string | null        // dormant multi-tenant seam
  }
}
```

**Procedure:**

```ts theme={"theme":{"light":"github-dark","dark":"github-dark"}}
{
  kind: 'procedure'                 // required discriminant
  name: string                      // 1–200 chars
  content: string                   // 1–100,000 chars
  scope?: {
    agentId?: string | null
    teamId?: string | null
    tenantId?: string | null
  }
}
```

### Responses

**`200 OK`**: a fact was saved (it is embedded if a provider was available, but the embedding is best-effort and never blocks the write):

```ts theme={"theme":{"light":"github-dark","dark":"github-dark"}}
{
  ok: true
  fact: {
    id: string
    title: string                  // scrubbed
    content: string                // scrubbed
    tags: string[]
    scopeAgentId: string | null
    scopeTeamId: string | null
    tenantId: string | null
    createdAt: number              // epoch ms
    updatedAt: number              // epoch ms
  }
}
```

**`200 OK`**: a procedure was saved (`kind: 'procedure'`):

```ts theme={"theme":{"light":"github-dark","dark":"github-dark"}}
{
  ok: true
  procedure: {
    id: string
    name: string
    version: number // prior max version + 1
    content: string // scrubbed
    scopeAgentId: string | null
    scopeTeamId: string | null
    tenantId: string | null
    createdAt: number // epoch ms
  }
}
```

**`400 Bad Request`**: the body failed the discriminated-union validation (e.g. an over-length `title`/`content`, too many tags, or a procedure missing `name`):

```json theme={"theme":{"light":"github-dark","dark":"github-dark"}}
{ "error": "invalid body", "details": { "...": "zod flatten output" } }
```

**`500 Internal Server Error`**: any failure constructing the store or writing the row:

```json theme={"theme":{"light":"github-dark","dark":"github-dark"}}
{ "error": "<message>" }
```

### Example

```bash theme={"theme":{"light":"github-dark","dark":"github-dark"}}
# Save a team-scoped fact
curl -X POST http://localhost:18790/api/memory \
  -H 'Content-Type: application/json' \
  -d '{"title":"Release cadence","content":"Ship on Thursdays.","tags":["process"],"scope":{"teamId":"<team-uuid>"}}'

# Save a procedure (auto-versions on a repeat name+scope)
curl -X POST http://localhost:18790/api/memory \
  -H 'Content-Type: application/json' \
  -d '{"kind":"procedure","name":"deploy-runbook","content":"1. run tests …"}'
```

***

## `GET /api/memory/browse`

Lists the most recent facts and procedures (facts newest-first by `updatedAt`), scoped the same inclusive way as search. The handler reads `limit`, `teamId`, and `agentId` from the query string, validates them, then fetches facts and procedures in parallel.

* **Query params**

| Param     | Type    | Required | Notes                                                               |
| --------- | ------- | -------- | ------------------------------------------------------------------- |
| `limit`   | integer | No       | 1–200; default `10` (applied to facts and procedures independently) |
| `teamId`  | string  | No       | Scope filter (inclusive of global rows)                             |
| `agentId` | string  | No       | Scope filter (inclusive of global rows)                             |

* **Request body**: none.

### Responses

**`200 OK`**: facts and procedures side by side:

```ts theme={"theme":{"light":"github-dark","dark":"github-dark"}}
{
  ok: true
  facts: Array<{
    id: string
    title: string
    content: string
    tags: string[]
    scopeAgentId: string | null
    scopeTeamId: string | null
    tenantId: string | null
    createdAt: number
    updatedAt: number
  }>
  procedures: Array<{
    id: string
    name: string
    version: number
    content: string
    scopeAgentId: string | null
    scopeTeamId: string | null
    tenantId: string | null
    createdAt: number
  }>
}
```

**`400 Bad Request`**: `limit` out of the `1..200` range:

```json theme={"theme":{"light":"github-dark","dark":"github-dark"}}
{ "error": "invalid query", "details": { "...": "zod flatten output" } }
```

**`500 Internal Server Error`**: any failure constructing the store or reading:

```json theme={"theme":{"light":"github-dark","dark":"github-dark"}}
{ "error": "<message>" }
```

### Example

```bash theme={"theme":{"light":"github-dark","dark":"github-dark"}}
curl 'http://localhost:18790/api/memory/browse?teamId=<team-uuid>&limit=50'
```

***

## `GET /api/memory/provider`

Reports the embedding provider that backs vector/hybrid search, resolved once at boot (a one-time network probe) and reused. The resolution order is: a reachable Ollama instance (the offline-first default, probed at `http://localhost:11434`), then an OpenAI key (`OPENAI_API_KEY`), then `null`. A `null` provider means the store is FTS-only; vector and hybrid search silently fall back to FTS. The response shape is provider-independent (`{ id, dimensions }`) so the UI can warn when vector search is degraded.

* **Path/query params**: none.
* **Request body**: none.

### Responses

**`200 OK`**: a provider resolved:

```ts theme={"theme":{"light":"github-dark","dark":"github-dark"}}
{
  provider: {
    id: string // e.g. 'ollama:nomic-embed-text', 'openai:text-embedding-3-small', 'deterministic'
    dimensions: number // declared embedding dimensionality
  }
}
```

**`200 OK`**: no provider reachable (FTS-only):

```json theme={"theme":{"light":"github-dark","dark":"github-dark"}}
{ "provider": null }
```

**`500 Internal Server Error`**: an unexpected failure resolving the provider:

```json theme={"theme":{"light":"github-dark","dark":"github-dark"}}
{ "error": "<message>" }
```

### Example

```bash theme={"theme":{"light":"github-dark","dark":"github-dark"}}
curl http://localhost:18790/api/memory/provider
```

***

## Error envelope

Errors on these routes use the standard envelope `{ error: string }`. The two validating GET routes (`/api/memory`, `/api/memory/browse`) and the save route add a `details` field carrying the zod `flatten()` output on a 400, e.g. `{ "error": "invalid query", "details": { … } }`.

## See also

* [Memory (concept)](/concepts/memory), the shared tier, FTS5 + vector, scope inclusivity, scrub-on-write
* [Memory browser (UI)](/using/memory-browser), search/save/browse from the dashboard
* [Tools & MCP API](/reference/rest-api/tools-and-mcp), the Memory MCP server (the model-facing half), attach config, transports
* [@clawboo/db](/reference/packages/db), `SqliteMemoryStore`, the `MemoryStore`/`EmbeddingProvider` seams, the memory schemas
* [REST API overview](/reference/rest-api/index)
