Build on Memoception.
A REST API over the same engine as the MCP connector — every memory tool as an endpoint, plus format-aware custom pipelines. One key, one memory: what your code stores, your Claude and your dashboard see too. Try every call right here, in the browser.
Quickstart
Create an API key in the app → settings → Developers → create API key (shown once). Send it as Authorization: Bearer or x-api-key. Base URL https://www.engli.org/api/v1.
# store a memory
curl -s https://www.engli.org/api/v1/memory/store \
-H "Authorization: Bearer $MEMOCEPTION_KEY" -H "content-type: application/json" \
-d '{"project":"my-app","content":"We chose Postgres for the event store.","kind":"decision"}'
# recall it — structured, with scores
curl -s https://www.engli.org/api/v1/memory/recall \
-H "x-api-key: $MEMOCEPTION_KEY" -H "content-type: application/json" \
-d '{"project":"my-app","query":"which database did we pick for events?"}'
# → { "ok": true, "request_id": 184467440737, "memories": [ { "id": 12, "score": 0.93, … } ] }The envelope & request ids
Every response is { ok, request_id, …data } or { ok: false, request_id, error }. Your request_id sequence starts at a random large number unique to your account and increments by exactly 1 per request — unguessable across accounts, strictly ordered within yours. Use it to correlate logs, deduplicate retries, and match callback deliveries (each carries the request_id that caused it).
Limits
429 with a retry-after header (a throttled call never consumes a request_id).Boilerplates
Ready-to-run examples — CRM/catalog/incident stores, a research agent, multi-project isolation, Unreal/Godot optimization, precise architecture plans, and the full API test suites. Explore the code, download any file, and copy a share link for a specific one.
Endpoint reference & playground
Every endpoint below has its parameters, an example response, and a live tester — set your key once at the top and send real requests without leaving the page. Requests go straight from your browser to https://www.engli.org/api/v1 (CORS is enabled); your key is stored in this browser only.
Discovery
No auth — describe the API to your own tooling.
GET/v1/formats· List supported formatsno authThe 17 custom-store formats and the standardized rules vocabulary, machine-readable. No auth required — call it to drive a format picker or validate rules client-side.
No parameters.
example response
{
"ok": true,
"formats": [
{
"id": "csv",
"aliases": [],
"splits": [
"row"
],
"default_split": "row",
"summary": "Header + quoted rows — one memory per row."
},
{
"id": "neo4j",
"aliases": [
"cypher"
],
"splits": [
"graph",
"statement"
],
"default_split": "graph",
"summary": "Cypher nodes & relations → node memories + edge tags."
},
"… 15 more"
],
"rules_standard": {
"split": "…",
"keep": "…",
"weights": "…",
"…": "…"
},
"docs": "https://www.memoception.com/developers"
}Memory
The ten core tools: store, recall, ingest, and manage.
GET/v1/projects· List your projectsEvery project in your namespace with its memory count, byte size, and last-activity date. Byte size is what the storage caps are measured against.
No parameters.
example response
{
"ok": true,
"request_id": 184467440737,
"projects": [
{
"project": "memory-science",
"count": 412,
"bytes": 8843120,
"latest": "2026-07-23"
}
]
}POST/v1/memory/store· Store a memorySave one memory — a fact, decision, preference, note, or todo — into a project. Memories persist across sessions and are recalled semantically. Or pass a `file` instead of `content`: it is chunked into recallable memories AND the original is archived (when your account has an archive bucket configured).
content *stringthe memory text (required unless file is given)projectstringproject partition (default "default")titlestringshort label (defaults to the first line of content)kind"note"|"fact"|"decision"|"preference"|"todo"memory type (default "note")tagsstring[]up to 8 free-form tagsfile{ filename, data, encoding? }ingest a file: data is utf8 text or base64 (encoding:"base64"). Chunked + original archived.example response
{
"ok": true,
"request_id": 184467440737,
"result": "Stored memory #128 in project \"memory-science\" (fact). It will be recalled semantically.",
"memory_ids": [
128
]
}POST/v1/memory/recall· Recall memoriesSemantic recall — paraphrases match via concept expansion, learned embeddings rank when the deployment provides them. Returns a structured, scored list. Omit project to search your whole namespace.
query *stringwhat to recallprojectstringlimit to one project (omit = all projects)limitnumbermax results, 1–50 (default 6)example response
{
"ok": true,
"request_id": 184467440737,
"memories": [
{
"id": 128,
"project": "memory-science",
"kind": "fact",
"title": "Endel Tulving distinguished episodic…",
"content": "Endel Tulving distinguished episodic from semantic memory in 1972.",
"tags": [
"episodic-memory",
"history"
],
"weight": 1,
"score": 0.94,
"created_at": "2026-07-23"
}
]
}POST/v1/memory/ingest· Ingest a fileChunk and store a whole file along the format's natural seams — markdown by headings, code by definitions, CSV by rows, JSON/JSONL by entries, prose by paragraphs. Every chunk becomes independently recallable.
filename *stringname with extension — drives format detectioncontent *stringthe file's text contentprojectstringproject partition (default "default")example response
{
"ok": true,
"request_id": 184467440737,
"result": "Ingested tulving-1972.md into project \"memory-science\" as 2 markdown chunk(s) (ids 129–130). Every chunk is now recallable; forget them anytime with memory_forget {source: \"tulving-1972.md\"}."
}POST/v1/memory/history· Index a whole historySave a whole conversation or project history in one call. Pass distilled `entries` (preferred — one per decision/fact/milestone) and/or a raw `transcript` (chunked automatically). Incremental: near-duplicates of existing memories are skipped, so re-runs only add what's new.
project *stringproject to index intoentries{title?,content,kind?,tags?}[]distilled history items (preferred)transcriptstringalternative: raw history text, chunked automaticallysourcestringprovenance label (default "conversation")example response
{
"ok": true,
"request_id": 184467440737,
"result": "Indexed 2 item(s) from reading-notes into \"memory-science\" + 3 distilled fact(s). Re-run any time; only new history lands."
}POST/v1/memory/link· Add a web linkFetch and scrape a web page server-side into recallable memory, and keep the URL itself as a link memory. Your code never parses HTML — pass a URL, get memories. Ideal for building a research corpus from a list of sources.
url *stringthe http(s) linkprojectstringproject partition (default "default")example response
{
"ok": true,
"request_id": 184467440737,
"result": "Added https://plato.stanford.edu/entries/memory/ to “memory-science” — 34 html chunk(s) scraped and the link kept for reference. Ask about it any time; memory_forget {source: \"https://plato.stanford.edu/entries/memory/\"} removes the link note."
}POST/v1/memory/forget· Forget memoriesDelete memories by ids, by whole project, or by source filename/URL. Provide at least one filter. Frees storage against your plan caps.
idsnumber[]specific memory idsprojectstringdelete an entire project partitionsourcestringdelete everything ingested from this filename/URLexample response
{
"ok": true,
"request_id": 184467440737,
"result": "Forgot 2 memories."
}POST/v1/memory/consolidate· Consolidate a projectMerge near-duplicate memories into one stronger memory — its weight accumulates and boosts future recall. Run periodically so long-lived projects compress instead of silting up.
project *stringproject to consolidatethresholdnumbersimilarity to merge at, 0.5–0.99 (default 0.9)example response
{
"ok": true,
"request_id": 184467440737,
"result": "Consolidated project \"memory-science\": 3 cluster(s) merged, 7 duplicate(s) folded in. Survivors carry the group’s weight and rank higher at recall."
}POST/v1/memory/export· Export training dataTurn a project's memories into fine-tuning JSONL — prompt/completion pairs or ChatML messages. The `result` is the JSONL text, one row per memory.
project *stringproject to exportformat"prompt-completion"|"chatml"default "prompt-completion"limitnumbermax rows (default 500, cap 2000)example response
{
"ok": true,
"request_id": 184467440737,
"result": "{\"messages\":[{\"role\":\"user\",\"content\":\"Recall from reading-notes: Fact: Suddendorf & Corballis mental time travel\"},{\"role\":\"assistant\",\"content\":\"Suddendorf & Corballis (2007) framed episodic memory as mental time travel.\"}]}\n…"
}POST/v1/grove/search· Search the GroveSearch the shared Engli Grove knowledge graph — global, verified, read-only nodes (studies, tools, translations, protocols). Separate from your private memory.
query *stringwhat to look forkindstringoptional node-kind filter (study, tool, …)limitnumbermax results (default 5)example response
{
"ok": true,
"request_id": 184467440737,
"result": "1. [study] Miller's law — the magical number seven, plus or minus two\n2. …"
}GET/v1/memory/1· Get one memoryFetch a single memory by id, with its full content. (Path form: GET /v1/memory/:id — the 1 here is just an example id.)
projectstringoptional project scopeexample response
{
"ok": true,
"request_id": 184467440737,
"memory": {
"id": 128,
"project": "memory-science",
"kind": "fact",
"title": "Tulving 1972",
"content": "Endel Tulving distinguished episodic from semantic memory.",
"source": "",
"tags": [
"episodic-memory"
],
"weight": 1,
"created_at": "2026-07-23"
}
}PATCH/v1/memory/1· Update one memoryUpdate a memory in place: change its weight (recall strength), tags, title, kind, or content. The hash signature is recomputed when text changes so recall stays correct. (Path form: PATCH /v1/memory/:id.)
weightnumberrecall strength 1–10tagsstring[]replace the tag settitlestringnew titlekindstringnew kindcontentstringnew content (re-signed)example response
{
"ok": true,
"request_id": 184467440737,
"memory": {
"id": 128,
"project": "memory-science",
"kind": "fact",
"title": "Tulving 1972",
"content": "…",
"tags": [
"episodic-memory",
"landmark"
],
"weight": 8,
"created_at": "2026-07-23"
}
}POST/v1/summarize· Summarize / optimize a projectDistill a whole project into a leaner, stronger version using the inference layer — the same optimize that powers the app. Defaults to Mistral when configured (cheap/free), else the account's model; pass an explicit model to override, deep:true for the multi-step (plan → distill → critic) pass, and an optional instruction to steer it. Requires the deployment's inference to be configured.
project *stringproject to distillinstructionstringsteer the summary (e.g. "focus on decisions")deepbooleanmulti-step deep optimize (default false)modelstringoverride the modelexample response
{
"ok": true,
"request_id": 184467440737,
"project": "memory-science",
"optimized_project": "memory-science · optimized",
"entries": 24,
"source_entries": 210,
"cost_usd": 0
}POST/v1/research· Research & storeResearch a subject online and store the findings in one call. Discovers sources (web search when a subject is given), reaches each page through a clean-extraction reader with automatic fallback routing, chunks it into recallable memory, keeps each URL as a link memory, and writes a research-summary note. Pass explicit sources, a subject to search for, or both — best-effort per source, so one dead link never fails the call.
project *stringproject to store intosubjectstringsubject/query to research (drives discovery + tagging)sourcesstring[]explicit URLs to reach and storemaxSourcesnumbercap on total sources, 1–20 (default 6)discoverbooleanrun web-search discovery when a subject is set (default true)tagsstring[]extra tags on every stored memoryexample response
{
"ok": true,
"request_id": 184467440737,
"subject": "evolution of episodic memory",
"project": "memory-science",
"reached": [
{
"url": "https://plato.stanford.edu/entries/memory/",
"via": "direct",
"chunks": 34
},
{
"url": "https://en.wikipedia.org/wiki/Episodic_memory",
"via": "reader",
"chunks": 21
}
],
"failed": [],
"memoriesCreated": 58,
"summaryId": 191
}GET/v1/original· Get an original backRetrieve a stored original. By content_id it returns the original record's text (or, for a file ingest, its chunks folded back together); by source or memory_id it returns that file/memory's content. When the original was archived to your bucket, it also returns a presigned download_url (15 min) for the raw bytes — pass filename + project to target the archived object directly. Alias: /file.
content_idstringreturn the original behind a content_idsourcestringfold a file's chunks back into its full textmemory_idnumberreturn one memory's full contentfilenamestringthe archived object's filename (for the presigned download_url)projectstringproject partitionexample response
{
"ok": true,
"request_id": 184467440737,
"filename": "tulving-1972.md",
"kind": "original",
"content": "# Episodic and Semantic Memory\n\nTulving proposed…",
"bytes": 4211,
"archived_key": "user_12/memory-science/tulving-1972.md",
"download_url": "https://s3.us-east-005.backblazeb2.com/…?X-Amz-Signature=…",
"parts": null
}Custom pipelines
Format-aware store & shaped recall.
GET/v1/content/cnt_9f3ac1b2e4d5f6a7· Get a content bundleEverything behind one content_id in a single call: the original, the existing-methodology chunks, every format representation grouped by format, and any organized (SQL/graph) views. (Path form: GET /v1/content/:content_id.)
projectstringoptional project scopeexample response
{
"ok": true,
"request_id": 184467440737,
"content_id": "cnt_9f3ac1b2e4d5f6a7",
"original": {
"id": 130,
"title": "landmark-studies",
"content": "…",
"bytes": 412
},
"chunks": [
{
"id": 131,
"kind": "file-chunk",
"title": "…",
"chars": 210
}
],
"formats": {
"csv": [
{
"id": 133,
"kind": "record",
"title": "…",
"chars": 64
}
]
},
"organized": [
{
"id": 140,
"kind": "organized",
"title": "sql · schema",
"chars": 300
}
],
"total_memories": 6
}POST/v1/query· Query records / traverse graphQuery your structured memories two ways. mode:"sql" filters/projects/sorts the records (a practical SQL subset — pass structured where/select/order_by/limit, or a small `sql` string). mode:"graph" does a breadth-first traversal over the relationships (rel: edges) from a start node. Scope with content_id / format / name / memory_ids / tags.
mode"sql" | "graph"default sqlprojectstringproject scopecontent_idstringscope to one bundleformatstringscope to one formatsqlstringsql-mode: a small SELECT … WHERE … ORDER BY … LIMIT stringwhere / select / order_by / limitstructuredsql-mode: structured query instead of a stringfromstringgraph-mode: start noderelstringgraph-mode: only follow this relationshipdepthnumbergraph-mode: hops (1–6, default 2)direction"out"|"in"|"both"graph-mode: edge direction (default out)example response
{
"ok": true,
"request_id": 184467440737,
"mode": "sql",
"columns": [
"theorem",
"year"
],
"rows": [
[
"Godel First Incompleteness",
1931
]
],
"count": 1,
"records_scanned": 6
}POST/v1/batch· Batch many operationsRun 1–100 operations in ONE request (one request_id) — the way to do bulk work under the 20 req/s cap. Each operation is { op, …args } where op is a write/read endpoint path (memory/store, memory/recall, memory/ingest, memory/history, memory/forget, memory/link, memory/consolidate, custom/store, custom/recall, organize). Results come back in order; a failed op is reported, the batch still completes.
operations *{ op, …args }[]1–100 operationsexample response
{
"ok": true,
"request_id": 184467440737,
"count": 3,
"ok_count": 3,
"failed": 0,
"results": [
{
"op": "memory/store",
"ok": true,
"result": "Stored memory #201 …",
"memory_ids": [
201
]
},
{
"op": "memory/store",
"ok": true,
"memory_ids": [
202
]
},
{
"op": "memory/recall",
"ok": true,
"memories": [
{
"id": 201,
"score": 0.9,
"title": "…"
}
]
}
]
}POST/v1/custom/store· Custom store (format-aware)Store the ORIGINAL content and, alongside it, a rule-transformed representation in each chosen `format` (one id or an ARRAY), plus our existing methodology — so everything is custom- and plain-recallable and every artifact carries an id. Returns: content_id (the original), memory_ids (existing-methodology chunks), and per-format custom_memory_ids (the new format representations). The original is always kept: the chunker runs (set "traditional": false to skip) and, with a `file`, the original is archived. Pass rules inline or reference a saved set with "rulesName".
format *string | string[]the format(s) to store the data as — one, or an array of the 17 (GET /formats)content *stringthe raw data (required unless file is given)file{ filename, data, encoding? }supply the original as a file: chunked, archived, and used as the pipeline inputprojectstringproject partition (default "default")namestringsource label — becomes custom:<format>:<name>rulesobjectinline rules document (the rules standard)rulesNamestringreference a saved rule set instead of inline rulestraditionalbooleanalso run the chunk pipeline on the original (default true)example response
{
"ok": true,
"request_id": 184467440737,
"project": "memory-science",
"content_id": "cnt_9f3ac1b2e4d5f6a7",
"original": {
"memory_id": 130,
"archived": null,
"filename": null
},
"memory_ids": [
131,
132
],
"formats": [
{
"format": "csv",
"ok": true,
"custom_memory_ids": [
133,
134
],
"stored": 2,
"source": "custom:csv:landmark-studies",
"dropped": 0
},
{
"format": "neo4j",
"ok": false,
"custom_memory_ids": [],
"stored": 0,
"error": "no graph units found in the content — is it really neo4j?"
}
],
"stored": 2
}POST/v1/custom/recall· Custom recall (shaped)Filter custom-stored data (by format, source name, tags, kind, time range), optionally rank it semantically with a query, then return it in one of five shapes: memories, records, table, series, or graph.
shape"memories"|"records"|"table"|"series"|"graph"output shape (default "memories")projectstringlimit to one projectquerystringsemantic ranking (omit = newest first)formatstringfilter to one formatnamestringfilter to one source namecontent_idstringreturn everything descended from one original (its content_id)tagsstring[]require all of these tagskindstringfilter by memory kindsincestringISO date lower bound (created_at)untilstringISO date upper boundlimitnumbermax results, 1–200 (default 20)example response
{
"ok": true,
"request_id": 184467440737,
"shape": "table",
"count": 2,
"data": {
"columns": [
"_id",
"year",
"author",
"finding"
],
"rows": [
[
131,
1885,
"Ebbinghaus",
"forgetting curve"
],
[
132,
1953,
"Milner",
"patient H.M. and the hippocampus"
]
]
}
}POST/v1/organize· Organize into SQL / graphTake EXISTING memories (by content_id, memory_ids/ids, or a format/name/tags filter) and materialize their structured records as ORGANIZED data — a relational SQL schema (CREATE TABLE + INSERTs) or a Neo4j graph (nodes + relationships). The generated artifact is stored and parsed back through the pipeline, so it's queryable via custom/recall (shape:"table" for sql, shape:"graph" for neo4j), everything linked under a new content_id. `rules` control the shape: table/label, key, columns, and link {from,to,label} for relationships/foreign keys.
into *"sql" | "neo4j"relational (sql) or graph (neo4j) organizationprojectstringproject partitioncontent_idstringorganize everything descended from one originalmemory_idsnumber[]explicit memory/custom-memory ids to organizeformatstringfilter the records to organize by formatsource_namestringfilter by source nametagsstring[]filter by tagsnamestringlabel for the organized artifactrules{ table?, label?, key?, columns?, link? }how to organize — table/node name, primary key/node id, columns, and link {from,to,label}example response
{
"ok": true,
"request_id": 184467440737,
"into": "neo4j",
"content_id": "cnt_1a2b3c4d5e6f7a8b",
"organized_memory_id": 210,
"custom_memory_ids": [
211,
212,
213
],
"stored": 3,
"records_used": 2,
"source": "custom:neo4j:influence-graph",
"generated": "CREATE (n0:Thinker {name: 'Locke', influenced: 'Hume'})\nCREATE (n1:Thinker {name: 'Hume'})\nCREATE (n0)-[:INFLUENCED]->(n1)"
}POST/v1/store-organize· Store & organizeOne call: store the ORIGINAL content (or a `file`) as records in a parse `format` (existing methodology + custom memories, with a content_id), THEN organize those records into a relational SQL schema or Neo4j graph. Returns the full store ID model plus the organized artifact. `format` is how to PARSE the content; `into` is the organized target; `organizeRules` shape the SQL/graph.
format *stringthe format to PARSE the content into records (e.g. "csv", "json")into *"sql" | "neo4j"organize the records into relational or graph formcontentstringthe raw data (required unless file is given)file{ filename, data, encoding? }supply the original as a file (chunked + archived)projectstringproject partitionnamestringsource labelrulesobjectparse rules (the rules standard) for the record extractionorganizeRules{ table?, label?, key?, columns?, link? }how to organize the records into sql/graphexample response
{
"ok": true,
"request_id": 184467440737,
"project": "math-graph",
"content_id": "cnt_aa11bb22cc33dd44",
"original": {
"memory_id": 300,
"archived": null,
"filename": null
},
"memory_ids": [
301,
302
],
"formats": [
{
"format": "csv",
"ok": true,
"custom_memory_ids": [
303,
304
],
"stored": 2
}
],
"organized": {
"into": "neo4j",
"content_id": "cnt_ee55ff66aa77bb88",
"organized_memory_id": 305,
"custom_memory_ids": [
306,
307,
308
],
"records_used": 2,
"source": "custom:neo4j:theorem-deps-neo4j",
"generated": "CREATE (n0:Theorem {theorem: 'FTA', depends_on: 'euclid_lemma'})\n…"
}
}Rule sets
Save and reuse rules documents.
POST/v1/rules· Save a rule setStore a reusable rules document keyed by (project, format, name). Reference it later from /custom/store with "rulesName". Rules are validated on save — every problem is named in the error.
project *stringproject the rules belong toformat *stringformat the rules targetname *stringreference namerules *objectthe rules documentexample response
{
"ok": true,
"request_id": 184467440737,
"saved": {
"project": "memory-science",
"format": "csv",
"name": "studies"
}
}GET/v1/rules· List / fetch rule setsWith no query, lists your saved rule sets (optionally filtered by project). With project + format + name, returns that one rule set's document.
No parameters.
example response
{
"ok": true,
"request_id": 184467440737,
"rules": [
{
"project": "memory-science",
"format": "csv",
"name": "studies",
"updated_at": "2026-07-23T10:12:00Z"
}
]
}Callbacks
Signed, event-driven deliveries.
POST/v1/callback· Register a callbackRegister one HTTPS endpoint that Memoception POSTs signed events to. Returns your signing secret ONCE and immediately sends a signed callback.test delivery. Subscribe to ["*"] (default) to receive event types added later.
url *stringhttps:// endpoint that receives eventseventsstring[]event filter (default ["*"] = all)example response
{
"ok": true,
"request_id": 184467440737,
"callback": {
"url": "https://your-app.com/webhooks/memoception",
"events": [
"memory.stored",
"custom_store.completed"
]
},
"secret": "whsec_… (shown once)",
"note": "the signing secret is shown ONCE — verify deliveries via x-memoception-signature (HMAC-SHA256 of the raw body)"
}GET/v1/callback· Inspect the callbackSee your currently registered callback URL and event filter. The secret is returned truncated — the full value is only shown once at registration.
No parameters.
example response
{
"ok": true,
"request_id": 184467440737,
"callback": {
"url": "https://your-app.com/webhooks/memoception",
"events": [
"memory.stored",
"custom_store.completed"
],
"secret": "whsec_9f3ac1…"
}
}DELETE/v1/callback· Remove the callbackStop all event deliveries and forget the registration.
No parameters.
example response
{
"ok": true,
"request_id": 184467440737,
"callback": null
}History & usage
Every call's request_id, output, and metrics.
GET/v1/history· Call history & usageYour API call history — each request_id with its method, path, status, timestamp, and stored output. Default returns up to 100 recent records WITH outputs; full=1 returns up to 1000 ids + metadata only (no outputs). Page backward with before=<request_id> (use the nextBefore cursor). Add summary=1 for usage metrics (total, ok, errors, last 24h, busiest paths). Powers the in-app API Console.
limitnumberrecords to return (default 50, max 100; max 1000 with full=1)beforenumberreturn records with request_id < this (paging cursor)fullbooleanids + metadata only, no outputs (up to 1000)pathstringfilter to paths containing this substringstatusnumberfilter to an exact HTTP statussummarybooleaninclude the usage-metrics summary blockexample response
{
"ok": true,
"request_id": 184467440737,
"history": [
{
"request_id": 184467440736,
"method": "POST",
"path": "memory/store",
"status": 200,
"output": "{\"ok\":true,\"request_id\":…,\"result\":\"Stored memory #128 …\"}",
"created_at": "2026-07-23T10:20:31Z"
},
{
"request_id": 184467440735,
"method": "POST",
"path": "memory/recall",
"status": 200,
"output": "{\"ok\":true,…,\"memories\":[…]}",
"created_at": "2026-07-23T10:20:12Z"
}
],
"count": 2,
"nextBefore": 184467440735,
"summary": {
"total": 1284,
"ok": 1270,
"errors": 14,
"last24h": 96,
"firstRequestId": 184467440737,
"lastRequestId": 184467440737,
"byPath": [
{
"path": "memory/recall",
"count": 812
},
{
"path": "memory/store",
"count": 402
}
]
}
}The rules standard
One standardized rules document works across every format — each format declares which split units it supports, and the rest of the standard behaves identically everywhere. Invalid rules are rejected with every problem named.
{
"split": "row", // unit of memory — one of the format's splits
"keep": ["id", "user.name", "status"], // whitelist of dot-paths (omit = keep everything)
"drop": ["internal_notes"], // paths removed before storing
"redact": ["user.email"], // paths masked to ••• (kept, but unreadable)
"map": { // correspondences: record path → memory field
"title": "user.name",
"kind": "record_type",
"time": "created_at",
"tags": ["status", "region"]
},
"weights": { // importance (memory strength 1–10)
"status=critical": 5, // exact-match form: path=value
"assignee": 1 // presence form: path is set & truthy
},
"tagRules": [ // conditional tagging
{ "path": "status", "equals": "critical", "tag": "urgent" },
{ "path": "title", "contains": "auth", "tag": "security" }
],
"link": { "from": "src", "to": "dst", "label": "flows" }, // emit graph edges
"time": { "path": "ts", "bucket": "day" }, // temporal bucketing (series formats)
"titleTemplate": "{user.name} — {status}", // used when map.title is absent
"limit": 500 // max units stored (cap 2000)
}Supported formats
POST /v1/custom/store runs your data through two pipelines at once — the traditional chunker (so plain recall finds it) and a format-specific pipeline that understands the structure. GET /v1/formatsreturns this list machine-readably.
json · geojsonrecord · key · documentdocuments & arrays — one memory per element, per top-level key, or per document; GeoJSON splits on featuresjsonl · ndjsonrecordone JSON object per line; non-JSON lines are skippedtoonrecord · documentToken-Oriented Object Notation — tabular arrays become one memory per rowyaml · ymlkey · record · documentnested maps & lists via indentation (documented subset: no anchors/multiline strings)tomlsection · documentone memory per [table] / [[array-of-tables]] sectioncsv / tsvrowheader + quoted rows; cells are typed (numbers, booleans)xmlelement · documentthe most-repeated element becomes the record shape — attributes & simple children become fieldsmarkdown · mdblock · documentone memory per heading-delimited sectionsql · postgres · mysql · sqliterow · statementquote-aware statement split; INSERT … VALUES expands into one memory per rowmongodb · mongodocumentextended JSON ($oid/$date/$number* unwrapped), JSON/JSONL, or db.coll.insertOne/Many(...) shell dumpstimescaledb · timescalerow · buckettime-series rows (JSON/JSONL/CSV, time column auto-detected); bucket aggregates numeric fields per hour/day/monthneo4j · cyphergraph · statement(n:Label {props}) nodes and -[:REL]-> relations become node memories + edge tags the graph shape reads backleandeclarationone memory per theorem / lemma / def / structure / … declarationtslnblocktyped line notation — blocks of `key: value` / `name :: Type = value` linesgraphql · gqldefinitiontype / input / enum / query / mutation / fragment definitions with their field listslogfmtlinekey=value log linesParquet, Avro, Protobuf, EDN, and RDF/Turtle are on the roadmap — request a format.
Recall shapes
POST /v1/custom/recall filters (format / name / tags / kind / time range), optionally ranks semantically with query, then returns one of five shapes:
memoriesthe default — full memories with tags, weight, and scorerecordseach memory's structured payload parsed back into an objecttable{ columns, rows } — the union of record keys as a spreadsheetseries{ points } — time-ordered, numeric fields only; built from time bucketsgraph{ nodes, edges } — edges recovered from rel:… tags (Neo4j pipelines, link rules)Callbacks
Register one HTTPS endpoint per account and Memoception POSTs it signed events — the backbone for event-driven flows: sync pipelines, cache invalidation, downstream tool calls, audit trails.
callback.testsent immediately on registration so you can verify your handlermemory.storedafter /memory/store, /memory/ingest, or /memory/history writesmemory.forgottenafter /memory/forget deletesmemory.updatedafter PATCH /memory/:id changes a memorycustom_store.completedafter a custom pipeline finishes — includes content_id, format ids, stored countsummarize.completedafter /summarize distills a projectorganize.completedafter /organize or /store-organize builds a relational/graph viewresearch.completedafter /research finishes — includes subject, reached/failed counts, memories createdEvery delivery carries x-memoception-event and x-memoception-signature — the HMAC-SHA256 of the raw body with your secret. Verify before trusting; deliveries are at-least-once with a 3-second timeout, so make handlers idempotent (dedupe on request_id + event). Subscribe to ["*"] to receive event types added later.
// node — express receiver
import { createHmac, timingSafeEqual } from "crypto";
app.post("/webhooks/memoception", express.raw({ type: "*/*" }), (req, res) => {
const sig = createHmac("sha256", process.env.MEMO_WHSEC).update(req.body).digest("hex");
const given = req.headers["x-memoception-signature"] ?? "";
if (sig.length !== given.length || !timingSafeEqual(Buffer.from(sig), Buffer.from(given))) return res.sendStatus(401);
const { event, data, request_id } = JSON.parse(req.body); // route on event, correlate on request_id
res.sendStatus(200);
});Errors
400 { ok: false, request_id, error } // named, actionable — e.g. every invalid rule listed
401 missing / unknown key 429 rate limit exceeded (retry-after header)
404 unknown endpoint or resource 5xx transient — retry with backoffPython
A tiny wrapper over requests is all you need — every call is a JSON POST (or GET) to the base URL.
import os, requests
class Memoception:
def __init__(self, key=None, base="https://www.engli.org/api/v1"):
self.base, self.h = base, {"x-api-key": key or os.environ["MEMOCEPTION_KEY"]}
def _post(self, path, **body):
r = requests.post(self.base + path, headers=self.h, json=body, timeout=60).json()
if not r.get("ok"):
raise RuntimeError(f"{path}: {r.get('error')} (request_id={r.get('request_id')})")
return r
def _get(self, path, **params):
return requests.get(self.base + path, headers=self.h, params=params, timeout=60).json()
# --- core memory ---
def store(self, content, project="default", **kw): return self._post("/memory/store", content=content, project=project, **kw)
def recall(self, query, project=None, limit=6): return self._post("/memory/recall", query=query, project=project, limit=limit)["memories"]
def ingest(self, filename, content, project="default"): return self._post("/memory/ingest", filename=filename, content=content, project=project)
def add_link(self, url, project="default"): return self._post("/memory/link", url=url, project=project)
def index_history(self, project, entries): return self._post("/memory/history", project=project, entries=entries)
def forget(self, **filt): return self._post("/memory/forget", **filt)
def consolidate(self, project, threshold=0.9): return self._post("/memory/consolidate", project=project, threshold=threshold)
def projects(self): return self._get("/projects")["projects"]
# --- custom pipelines ---
def custom_store(self, format, content, project="default", rules=None, name=None, traditional=True):
return self._post("/custom/store", format=format, content=content, project=project, rules=rules, name=name, traditional=traditional)
def custom_recall(self, project=None, shape="memories", **filt):
return self._post("/custom/recall", project=project, shape=shape, **filt)["data"]
def save_rules(self, project, format, name, rules):
return self._post("/rules", project=project, format=format, name=name, rules=rules)
# --- callbacks ---
def set_callback(self, url, events=("*",)): return self._post("/callback", url=url, events=list(events))
m = Memoception()
# 1) store a decision and recall it
m.store("We chose Postgres for the event store.", project="my-app", kind="decision")
for hit in m.recall("which database for events?", project="my-app"):
print(round(hit["score"], 2), hit["title"])
# 2) build a research corpus from a list of URLs (scraped server-side)
for url in open("sources.txt"):
url = url.strip()
if url and not url.startswith("#"):
m.add_link(url, project="memory-science")
# 3) index distilled reading notes in one call (incremental — re-runs add only what's new)
m.index_history("memory-science", entries=[
{"title": "Fact: Tulving 1972", "content": "Endel Tulving distinguished episodic from semantic memory.", "kind": "fact"},
{"title": "Fact: H.M. and the hippocampus", "content": "Patient H.M. (Milner, 1953) localized episodic encoding to the hippocampus.", "kind": "fact"},
])
# 4) structured data → the custom pipeline, then read it back as a table
m.custom_store("csv", "year,author,finding\n1885,Ebbinghaus,forgetting curve",
project="memory-science", name="studies",
rules={"map": {"title": "finding"}, "weights": {"year": 1}})
table = m.custom_recall(project="memory-science", format="csv", name="studies", shape="table")
print(table["columns"], table["rows"])
# 5) a knowledge graph via Cypher, read back as nodes + edges
m.custom_store("neo4j", "CREATE (a:Concept {name:'episodic'})-[:PART_OF]->(b:Concept {name:'declarative'})",
project="memory-science", name="ontology")
graph = m.custom_recall(project="memory-science", format="neo4j", name="ontology", shape="graph")
print(graph["nodes"], graph["edges"])
# 6) export the project as fine-tuning JSONL
jsonl = m._post("/memory/export", project="memory-science", format="chatml")["result"]
open("memory-science.jsonl", "w").write(jsonl)Handle rate limits by retrying on 429 after the retry-after header; the API allows 20 requests/second per key, so a small time.sleep(0.06) between tight-loop writes keeps you well under it.
JavaScript / TypeScript
Works in Node ≥ 18 and the browser (CORS is open) with the built-in fetch — no dependencies.
const BASE = "https://www.engli.org/api/v1";
class Memoception {
constructor(key = process.env.MEMOCEPTION_KEY, base = BASE) { this.key = key; this.base = base; }
async #call(method, path, body) {
const res = await fetch(this.base + path, {
method,
headers: { "x-api-key": this.key, ...(body ? { "content-type": "application/json" } : {}) },
body: body ? JSON.stringify(body) : undefined,
});
if (res.status === 429) { // respect the 20 req/s limiter
await new Promise((r) => setTimeout(r, (Number(res.headers.get("retry-after")) || 1) * 1000));
return this.#call(method, path, body);
}
const data = await res.json();
if (!data.ok) throw new Error(`${path}: ${data.error} (request_id=${data.request_id})`);
return data;
}
// core memory
store(content, project = "default", extra = {}) { return this.#call("POST", "/memory/store", { content, project, ...extra }); }
recall(query, project, limit = 6) { return this.#call("POST", "/memory/recall", { query, project, limit }).then((d) => d.memories); }
ingest(filename, content, project = "default") { return this.#call("POST", "/memory/ingest", { filename, content, project }); }
addLink(url, project = "default") { return this.#call("POST", "/memory/link", { url, project }); }
indexHistory(project, entries) { return this.#call("POST", "/memory/history", { project, entries }); }
forget(filter) { return this.#call("POST", "/memory/forget", filter); }
consolidate(project, threshold = 0.9) { return this.#call("POST", "/memory/consolidate", { project, threshold }); }
projects() { return this.#call("GET", "/projects").then((d) => d.projects); }
// custom pipelines
customStore(format, content, opts = {}) { return this.#call("POST", "/custom/store", { format, content, ...opts }); }
customRecall(opts) { return this.#call("POST", "/custom/recall", opts).then((d) => d.data); }
saveRules(project, format, name, rules) { return this.#call("POST", "/rules", { project, format, name, rules }); }
// callbacks
setCallback(url, events = ["*"]) { return this.#call("POST", "/callback", { url, events }); }
}
const m = new Memoception();
// 1) store + recall
await m.store("User prefers dark mode.", "app", { kind: "preference" });
const hits = await m.recall("ui preferences", "app");
console.log(hits.map((h) => [h.score, h.title]));
// 2) research corpus from URLs (scraped server-side)
const urls = (await (await fetch("https://example.com/sources.txt")).text()).split("\n").filter(Boolean);
for (const url of urls) await m.addLink(url, "memory-science");
// 3) index distilled notes in one call
await m.indexHistory("memory-science", [
{ title: "Fact: Tulving 1972", content: "Endel Tulving distinguished episodic from semantic memory.", kind: "fact" },
{ title: "Fact: mental time travel", content: "Suddendorf & Corballis (2007) framed episodic memory as mental time travel.", kind: "fact", tags: ["evolution"] },
]);
// 4) custom pipeline: save reusable rules, then store CSV against them
await m.saveRules("memory-science", "csv", "studies", { map: { title: "finding" }, tagRules: [{ path: "author", contains: "Milner", tag: "hippocampus" }] });
await m.customStore("csv", "year,author,finding\n1953,Milner,patient H.M.", { project: "memory-science", name: "landmark", rulesName: "studies" });
// 5) read it back as a spreadsheet, and as a time series
const table = await m.customRecall({ project: "memory-science", format: "csv", shape: "table" });
const series = await m.customRecall({ project: "metrics", format: "timescaledb", shape: "series", since: "2026-07-01" });
console.log(table.columns, series.points);
// 6) event-driven: register a signed callback
const { secret } = await m.setCallback("https://your-app.com/webhooks/memoception", ["memory.stored", "custom_store.completed"]);
console.log("store this signing secret:", secret);Need a format, a higher rate limit, or a custom integration? Book a call — Enterprise plans remove the ceilings.