Skip to content

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 ​

StatuscodeWhen
403organization_forbidden / ability_deniedOrganization not resolvable for this credential, or the token lacks the required ability
402entitlement_requiredWrite on an unpaid organization while billing enforcement is on
429quota_exceededMonthly ingest or search cap reached
429rate_limitedPer-minute rate limit reached (Retry-After header)
503service_unavailableEntitlement 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:

FieldDescription
idThought UUID
contentMemory body
metadataJSON metadata (response may also mirror externalKey / subjectKey here for older clients)
externalKeyPresent when set — column-backed upsert identity (string)
subjectKeyPresent when set — org-typed subject for indexed filtering (integer or string)
createdAtISO-8601
deletedAtSoft-delete timestamp when applicable
outcome / dedupeWrite-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.

FieldRequiredDescription
contentyesMemory body (NFC-normalized server-side)
metadatanoJSON metadata (do not put externalKey / subjectKey here — use top-level fields)
subjectKeynoOrg 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.

outcomeHTTPMeaning
created201New row stored
deduplicated200Exact content_hash replay; metadata unchanged after shallow merge
merged_metadata200Exact 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).

FieldRequiredDescription
externalKeyyesStable id from an external system (string)
contentyesMemory body (NFC-normalized server-side)
metadatanoJSON metadata merged into the row
scopenosource, project — part of uniqueness with externalKey
subjectKeynoOptional subject for indexed filtering (see ingest)
outcomeHTTPMeaning
created201New row for this key scope
deduplicated200Same content replay; metadata merged
updated200Content 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

FieldRequiredDescription
itemsyesArray of { content, metadata?, externalKey?, scope?, subjectKey? }
continueOnErrornoDefault 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.

FieldRequiredDescription
contentno*Replaces body; re-embeds when changed
metadatano*Merged into existing metadata by default
metadataReplacenoWhen true, replaces metadata instead of merging
subjectKeyno*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:

KeyDescription
subjectKeyIndexed equality on the org-typed subject column
agentId, source, surface, project, kind, documentIdFirst-class metadata equality
matchMap 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:

KeyTypeDescription
documentIdstringStable id for the document
chunkIndexintZero-based index
chunkTotalintTotal 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.

POST /api/memories/search

Requires memory:search. Body: query, optional limit, filter (including subjectKey), includeDeleted, minScore, recencyWeight, recencyHalflifeDays.

MCP ​

ToolREST equivalent
memory_ingestPOST /api/memories
memory_upsertPUT /api/memories
memory_ingest_batchPOST /api/memories/batch
memory_searchPOST /api/memories/search
memory_statsGET /api/memories/stats
memory_changesGET /api/memories/changes
memory_recentGET /api/memories/recent
memory_getGET /api/memories/{id}
memory_updatePATCH /api/memories/{id}
memory_deleteDELETE /api/memories/{id}
openbrain_healthreadiness checks (MCP) — see health.md