HTTP API reference (advanced)
The engine's raw HTTP surface — 11 tool endpoints over a bounded concurrent HTTP server, plus a separate /admin/* control-plane surface.
Most agents should use Quickstart or MCP tools instead — this page is the raw HTTP surface underneath, for scripting, debugging, or a self-hosted deployment.
The engine exposes a small HTTP API (stdlib ThreadingHTTPServer, no web framework)
wrapping the claim store, bounded by a threading.BoundedSemaphore(ERGO_HTTP_MAX_WORKERS)
(default 8) so one slow request can't head-of-line-block the rest.
The examples below are self-hosted, using the legacy single-tenant token
(ERGO_API_TOKEN) on http://127.0.0.1:8788 with the token in $TOK — that mode takes
org_id/who from the request body, so every example includes them. Against the
hosted endpoint (https://api.ergomem.com), or any self-hosted deployment using a
tenant token (see Multi-tenancy), the bearer resolves org_id
and who server-side instead — drop both fields from every request body below. Sending
them in that mode has no effect; they're never trusted from the body.
Auth
If ERGO_API_TOKEN is set, every route except GET /health requires:
Authorization: Bearer <token>If the var is unset, the server logs a warning at boot and runs open (dev/test only).
In a multi-tenant deployment, tokens are minted per-principal through the admin API
(see Multi-tenancy) rather than a single shared secret. The bearer
token resolves server-side to an org_id and who — never self-asserted from the request
body — so a caller can't spoof which tenant it's writing to.
Quotas
Rate limit and active-claim cap apply only to a real tenant token — the legacy
single-tenant token, the superadmin bearer, and GET /health are all exempt.
-
Rate limit — 120 requests/min per tenant by default, enforced as a fixed 60-second window counter (in-memory only, so a process restart resets every tenant's window). Over the limit:
429with aRetry-Afterheader naming the seconds until the window rolls over.429 → {"error":"tenant rate limit exceeded","limit_per_min":120,"retry_after_seconds":37} -
Active-claim cap — 50,000 active claims per tenant by default. Only the routes that add an active claim count against it:
POST /remember,POST /learn,POST /ingest(ingest is per-chunk aware and atomic — a batch that would cross the cap is refused entirely, up front, before any chunk is stored). Over the cap:403naming the cap and the current count.403 → {"error":"tenant active-claim cap of 50000 reached (current: 50000)","max_active_claims":50000,"active_claims":50000} -
POST /supersedeis never subject to the claim cap — it's net-zero (one active claim replaces another), so it can't push a tenant over the ceiling.POST /retract(removal only) and every read endpoint (/recall,/why,/active,/history,/diagnose) are never capped either. All of these still count against the rate limit like any other route.
Write endpoints (contradiction-guarded)
These run the contradiction guard. A hard conflict returns 409 with the prior claim.
POST /remember
Guarded write for decisions / constraints / rejections / conventions.
| field | required | description |
|---|---|---|
org_id | yes | tenant id |
project | yes | project namespace within the tenant |
who | yes | actor (audit row) |
statement | yes | the claim text |
reason | no | why (free text; surfaced via /why) |
force_exception | no | if set, stores even on conflict, with this string as the exception reason |
curl -s -H "$TOK" -X POST http://127.0.0.1:8788/remember -d '{
"org_id":"acme","project":"platform","who":"alice",
"statement":"Deploy on Tuesdays","reason":"team is on-call Mon/Wed"
}'
# 200 → {"result":"stored","claim_id":"...","warnings":[...]}
# 409 → {"result":"conflict","conflicts":[{"claim":{...},"tier":"block","score":0.97,...}]}| code | meaning |
|---|---|
200 | stored (MED-tier warnings included if any) |
409 | blocked — conflict set returned for the client to resolve |
400 | missing required field |
500 | normalizer / judge / store failure |
POST /learn
The bridge between ingest (raw chunks, no gate) and remember (decisions, full gate).
Use it when an agent read something and formed a belief. Same pipeline as remember;
folds source into the stored reason for provenance. The contradiction check runs on the
statement only — the source never pollutes the judge.
| field | required | description |
|---|---|---|
org_id, project, who | yes | as /remember |
statement | yes | the belief to check + store |
source | yes | provenance (file:line, URL, doc id). Blank source is a 400 |
reason | no | extra rationale |
curl -s -H "$TOK" -X POST http://127.0.0.1:8788/learn -d '{
"org_id":"acme","project":"platform","who":"agent",
"statement":"Sessions are never reused across restarts",
"source":"sessions.py:847","reason":"observed in the restart path"
}'
# 200 → {"result":"learned","claim_id":"...","source":"sessions.py:847"}POST /supersede
Change of mind. Replaces an active claim with a new one, and runs the guard against all other active claims first — if the new claim would itself contradict another, the supersede is refused.
curl -s -H "$TOK" -X POST http://127.0.0.1:8788/supersede -d '{
"org_id":"acme","project":"platform","who":"alice","existing_id":"<id>",
"statement":"Deploy on Wednesdays","reason":"Tuesdays now collide with standup"
}'
# 200 → {"result":"superseded","claim_id":"<new id>"}Side effects: old row → status='superseded'; new row inserted with supersedes pointing
at the old id (this is what /why walks).
POST /retract
Wrong-at-birth removal (typo, test junk, mis-scoped write). Flips an active claim to
retracted. Retract ≠ supersede — if a decision genuinely evolved, use supersede to
keep the why-chain. No gate runs (removal can't create a contradiction). The row is not
deleted — it stays in /history with retract_reason; /active, /recall, /why, and
the judge all exclude it.
curl -s -H "$TOK" -X POST http://127.0.0.1:8788/retract -d '{
"org_id":"acme","project":"platform","who":"alice","claim_id":"<id>",
"reason":"test junk written against the prod scope"
}'
# 200 → {"result":"retracted","claim_id":"<id>","retracted":true}All five fields (org_id, project, who, claim_id, reason) are required; a blank
reason is a 400.
Fast document path
POST /ingest
Bulk reference content. Skips the normalizer + judge (fact = upsert freely). Embeds each chunk and stores it. Requires the embedder to be enabled.
| field | required | description |
|---|---|---|
org_id, project, who | yes | as /remember |
text | yes | the document body (any size) |
source | no | provenance string, appended to each chunk for citations |
curl -s -H "$TOK" -X POST http://127.0.0.1:8788/ingest -d '{
"org_id":"acme","project":"kb","who":"alice","text":"...markdown...","source":"NOTES.md"
}'
# 200 → {"result":"ingested","chunks":49,"stored":49,"source":"NOTES.md"}Chunking is paragraph-aware and word-safe (≈1200 chars, ≈150 overlap). Ingested facts are also skipped on the candidate side — a bulk chunk can never 409 a decision; only prior decisions can.
Read endpoints
GET /recall
Hybrid semantic + keyword search over active claims.
| param | required | default | description |
|---|---|---|---|
org_id, project | yes | — | scope |
q (alias query) | yes | — | search text |
limit | no | 10 | clamped to [1, 50] |
curl -s -H "$TOK" "http://127.0.0.1:8788/recall?org_id=acme&project=kb&q=when%20to%20deploy&limit=3"Score = 0.7 × vec_sim + 0.3 × keyword_overlap. Superseded / retracted claims are excluded.
GET /why
Like /recall but reasoned-only (a reason-less fact can never win a "why" query),
returns the top hit, and walks the supersede chain.
curl -s -H "$TOK" "http://127.0.0.1:8788/why?org_id=acme&project=kb&q=when%20to%20deploy"
# → {"why":{"claim":"...","reason":"...","history":[ {newest}, {superseded} ]}}
# no reasoned match → {"why": null}GET /active & GET /history
/active lists active claims with a typed state (project_missing / empty /
has_active_claims). /history returns everything — active + superseded + retracted —
ordered by sequence.
GET /diagnose
The ops view — the running binary answers for itself. Live counts, conflict telemetry,
version-drift detection, and a warnings[] array flagging things the operator must act on
(no backup configured, version drift, etc.). Bearer-authed.
GET /health
Liveness + an allowlisted config snapshot. No auth — so health checks don't need the
secret. Ops metadata (owner, backup path) is deliberately kept off /health and lives on
the authed /diagnose.
Admin / control-plane
Tenant provisioning and token minting live under /admin/*, gated by a separate
ERGO_SUPERADMIN_TOKEN. See Multi-tenancy for the full model —
silo isolation, roles, and request/response shapes for POST /admin/tenants,
POST /admin/tokens, GET /admin/control-health, and POST /admin/tenants/delete.
MCP endpoint
https://api.ergomem.com/mcp serves the same 11 tools as this API (one tool per route
above) over the MCP streamable-HTTP transport — a second transport on the same engine,
not a separate service. The same auth applies: your own Authorization: Bearer <token>
header, forwarded straight through to the underlying route. See
Quickstart for Claude Code, Codex CLI, and Cursor config, or
MCP tools for the tool-level reference. A self-hosted engine can serve
the same transport:
ERGO_API_TOKEN="your-secret-token" \
python -m ergo.mcp.server --transport streamable-http --host 127.0.0.1 --port 8790Don't bind
--host 0.0.0.0without a token. OmittingERGO_API_TOKENleaves the server open (see Auth); combined with0.0.0.0that exposes everyergo_*tool, unauthenticated, to anyone who can reach the port. Bind0.0.0.0only behind a reverse proxy/tunnel that terminates auth, and always set a token first.
End-to-end
TOK="Authorization: Bearer $ERGO_API_TOKEN"
curl -s -H "$TOK" -X POST .../remember -d '{"org_id":"acme","project":"platform","who":"alice","statement":"Deploy on Tuesdays","reason":"on-call Mon/Wed"}'
curl -s -H "$TOK" -X POST .../remember -d '{"org_id":"acme","project":"platform","who":"bob","statement":"Never deploy on Tuesdays","reason":"incident risk"}'
# → 409 conflict against alice's claim
curl -s -H "$TOK" -X POST .../supersede -d '{"org_id":"acme","project":"platform","who":"alice","existing_id":"<id>","statement":"Never deploy on Tuesdays","reason":"incident risk outweighs on-call"}'
curl -s -H "$TOK" ".../why?org_id=acme&project=platform&q=when%20to%20deploy"
# → history with both entries