Appearance
Memory API
Base path: /api (Sanctum bearer + X-Organization-Id or token ability organization:{uuid}). Unknown and unauthorized organization selections both return the same safe 403 response with code: "organization_forbidden".
Boolean parameters (includeDeleted, metadataReplace, continueOnError) accept JSON booleans as well as the strings true / false and 1 / 0. Any other value returns 422.
Policy responses
| Status | code | When |
|---|---|---|
| 403 | organization_forbidden / ability_denied | Organization not resolvable for this credential, or the token lacks the required ability |
| 402 | entitlement_required | Write on an unpaid organization while billing enforcement is on |
| 429 | quota_exceeded | Monthly ingest or search cap reached |
| 429 | rate_limited | Per-minute rate limit reached (Retry-After header) |
| 503 | service_unavailable | Entitlement or quota cannot be resolved safely: Stripe is not configured while enforcement is on (writes), or the subscription price is not recognized (metered operations) |
Reads (search, get, recent, export, stats, changes) never return 402 or 503 for entitlement reasons. See Billing and usage.
Record shape
Successful create/read responses include:
| Field | Description |
|---|---|
id | Thought UUID |
content | Memory body |
metadata | JSON metadata (response may also mirror externalKey / subjectKey here for older clients) |
externalKey | Present when set — column-backed upsert identity (string) |
subjectKey | Present when set — org-typed subject for indexed filtering (integer or string) |
createdAt | ISO-8601 |
deletedAt | Soft-delete timestamp when applicable |
outcome / dedupe | Write-path result fields when applicable |
Ingest
POST /api/memories
Body: content, optional metadata, optional subjectKey. content is limited to OPENBRAIN_MAX_CONTENT_LENGTH UTF-8 characters (default 100000). Oversized bodies return 422 on ingest, upsert, batch items, and update. The same cap is enforced again in the memory writer.
| Field | Required | Description |
|---|---|---|
content | yes | Memory body (NFC-normalized server-side) |
metadata | no | JSON metadata (do not put externalKey / subjectKey here — use top-level fields) |
subjectKey | no | Org subject id (integer or string per org setting). Many memories may share one subject. |
Owners configure subject key type (integer or string) in the dashboard (Organization settings). Until configured, any subjectKey returns 422 with code: subject_key_invalid. Type mismatches also return 422. Changing type is blocked while any memory in the org still has a subject key.
Content safety (OB-045)
When OPENBRAIN_CONTENT_SCAN_ENABLED=true (default in production), ingest, upsert, update, and batch items are scanned for high-risk patterns (API keys, payment card-like sequences, email addresses). Rejected content returns 422:
json
{ "error": "...", "code": "content_rejected" }Disable in local/test with OPENBRAIN_CONTENT_SCAN_ENABLED=false.
outcome | HTTP | Meaning |
|---|---|---|
created | 201 | New row stored |
deduplicated | 200 | Exact content_hash replay; metadata unchanged after shallow merge |
merged_metadata | 200 | Exact or semantic near-duplicate; incoming metadata keys merged (dedupe: exact or semantic) |
409 when externalKey+scope matches a live row with different content.
Keyed upsert (OB-019 / OBS-123)
PUT /api/memories
Idempotent sync by externalKey (stored on thoughts.external_key, not only in JSON metadata). Uniqueness: one active row per (organization, externalKey, scope.source, scope.project).
| Field | Required | Description |
|---|---|---|
externalKey | yes | Stable id from an external system (string) |
content | yes | Memory body (NFC-normalized server-side) |
metadata | no | JSON metadata merged into the row |
scope | no | source, project — part of uniqueness with externalKey |
subjectKey | no | Optional subject for indexed filtering (see ingest) |
outcome | HTTP | Meaning |
|---|---|---|
created | 201 | New row for this key scope |
deduplicated | 200 | Same content replay; metadata merged |
updated | 200 | Content changed; re-embedded |
Errors: 404 if the key matches a soft-deleted row; 409 if content_hash belongs to another row; 422 for invalid subjectKey.
Semantic near-duplicate merge is not applied on this path.
externalKey and subjectKey are different axes: use externalKey for idempotent sync of one fact; use subjectKey when many memories belong to the same end-user (or other subject) and you need fast filters.
Batch ingest (OB-020)
POST /api/memories/batch
| Field | Required | Description |
|---|---|---|
items | yes | Array of { content, metadata?, externalKey?, scope?, subjectKey? } |
continueOnError | no | Default true; stop after first failure when false |
Response 200: results (per-row success fields + index, or { index, error: { code, message } }) and summary: { ok, failed }.
413 when items.length exceeds OPENBRAIN_BATCH_INGEST_MAX (default 50).
Items with externalKey use keyed upsert; others use standard ingest (including dedupe). Only accepted items increment the organization’s ingest_count; failed items do not.
Get
GET /api/memories/{id}
Requires memory:search. Unknown, other-tenant, and malformed (non-UUID) ids return the same 404 { "error": "Not found" }.
Update
PATCH /api/memories/{id} ( PUT alias for backward compatibility)
Requires memory:admin.
| Field | Required | Description |
|---|---|---|
content | no* | Replaces body; re-embeds when changed |
metadata | no* | Merged into existing metadata by default |
metadataReplace | no | When true, replaces metadata instead of merging |
subjectKey | no* | Sets/replaces the subject key columns |
* At least one of content, metadata, or subjectKey is required.
Response 200 with outcome: updated. 409 when new content hashes to another row’s content_hash. 422 for invalid subjectKey.
Delete
DELETE /api/memories/{id}
Requires memory:admin. Soft-deletes the row. Response 204 with an empty body.
Filters
Search, recent, export, stats, and changes accept an optional filter object:
| Key | Description |
|---|---|
subjectKey | Indexed equality on the org-typed subject column |
agentId, source, surface, project, kind, documentId | First-class metadata equality |
match | Map of arbitrary metadata key → value equality; match.externalKey maps to the external_key column |
Example:
json
{ "filter": { "subjectKey": 42, "project": "ops" } }Export (OB-035)
GET /api/memories/export
Requires memory:search. Returns NDJSON (application/x-ndjson) with one memory per line: id, content, metadata, optional externalKey / subjectKey, createdAt, updatedAt, optional deletedAt (no embedding vectors).
Query: limit (default 100, max 500), cursor (from X-Next-Cursor on the previous page), optional filter, includeDeleted.
Rows are ordered by created_at ascending for stable pagination. Set includeDeleted=true to include soft-deleted rows, which contain deletedAt.
Stats (OB-034)
GET /api/memories/stats
Requires memory:search. Returns facet counts for metadata keys agentId, source, surface, project, kind, plus meta.total and meta.filtered. Optional filter (including subjectKey). Set includeDeleted=true for counts and facets that include soft-deleted memories.
Changes / delta (OB-064)
GET /api/memories/changes
Requires memory:search. Returns memories created, updated, or (with includeDeleted) soft-deleted since an ISO-8601 since timestamp.
Query: since (required), limit (default 100, max 500), cursor (from nextCursor in the prior response), optional filter, includeDeleted.
Response 200:
json
{
"changes": [
{ "id": "...", "content": "...", "metadata": {}, "change": "created", "createdAt": "...", "updatedAt": "..." },
{ "id": "...", "deletedAt": "...", "change": "deleted" }
],
"meta": { "count": 2 },
"nextCursor": "2026-05-22T12:00:00+00:00|uuid"
}Active rows use the same fields as single-record reads (no embeddings). Deleted rows omit content.
Large documents (OB-044)
Chunk large inputs across multiple ingest or batch rows using metadata:
| Key | Type | Description |
|---|---|---|
documentId | string | Stable id for the document |
chunkIndex | int | Zero-based index |
chunkTotal | int | Total chunks |
If any chunk field is set, all three are required and 0 <= chunkIndex < chunkTotal. Invalid chunk metadata on POST /api/memories returns 422.
Read concatenated chunks via MCP resource brain://{organizationId}/document/{documentId}.
Recent list (OB-032)
GET /api/memories/recent
Requires memory:search. Query: limit (default 20, max 50), optional filter (including subjectKey), includeDeleted.
Response 200: { "results": [...], "meta": { "count": N } } — newest created_at first. Set includeDeleted=true to include soft-deleted memories.
Search embeddings
Search queries are embedded once with the configured OPENBRAIN_EMBEDDING_PROVIDER, OPENBRAIN_EMBEDDING_MODEL, and OPENBRAIN_EMBEDDING_DIMENSIONS values before vector matching. The query embedding configuration must match the vectors stored for the organization.
Search
POST /api/memories/search
Requires memory:search. Body: query, optional limit, filter (including subjectKey), includeDeleted, minScore, recencyWeight, recencyHalflifeDays.
MCP
| Tool | REST equivalent |
|---|---|
memory_ingest | POST /api/memories |
memory_upsert | PUT /api/memories |
memory_ingest_batch | POST /api/memories/batch |
memory_search | POST /api/memories/search |
memory_stats | GET /api/memories/stats |
memory_changes | GET /api/memories/changes |
memory_recent | GET /api/memories/recent |
memory_get | GET /api/memories/{id} |
memory_update | PATCH /api/memories/{id} |
memory_delete | DELETE /api/memories/{id} |
openbrain_health | readiness checks (MCP) — see health.md |