HTTP API conventions
Every way into RememberStack — the remember Python package, the remember
CLI, the MCP server — ends in the same HTTP API. This page covers the rules
that hold for all of its routes: where to send requests, how to authenticate,
which credential may call what, how errors look, and which limits apply. The
group pages then describe each route.
The API has 28 documented operations:
| Group | Page | Routes |
|---|---|---|
| Ingest, readiness, documents | Ingest | POST /ingest, POST /readiness, GET /documents, DELETE /documents/{doc_id} |
| Assured operations | Operations | GET /operations, POST /operations/{name} |
| Entities and facts | Entities and facts | GET /resolve, GET /lookup/relations, GET /lookup/observations, GET /hydrate/relation/{relation_id}, GET /transcript/relation/{relation_id} |
| Search | Search | GET and POST /search/claims, GET and POST /search/chunks, GET /chunks/{chunk_id}/adjacent, POST /chunks/adjacent |
| Graph | Graph | POST /graph/neighborhood, POST /graph/path, POST /graph/citation-path |
| SQL queries | SQL queries | POST /query/sql, POST /query/sql/explain, GET /query/space, GET /query/space/search, GET /query/saved, GET /query/saved/{namespace}/{name}, POST /query/saved/{namespace}/{name}/run |
| Deployment | Deployment | GET /deployment |
Every deployment serves all of them, including the SQL query routes (/query/*). SQL queries run over the query
space, memory_v1: a fixed set of prepared, read-only views and functions.
Every statement is parsed and validated against it before it runs, and
anything outside it is rejected.
The machine-readable schema is attached to every GitHub release: openapi.json.
Base URL
Each deployment answers on its own address. There is no shared gateway in front of the memory routes: requests go straight to the deployment.
export REMEMBER_API_URL=http://localhost:8000The engine listens on port 8000 inside its container
(REMEMBERSTACK_SELFHOST_API_PORT, default 8000). The Compose file
publishes it on the host's loopback address, 127.0.0.1.
Paths in this reference are relative to that base URL. The remember client
reads the same variable (REMEMBER_API_URL) and falls back to
http://127.0.0.1:8000.
Authentication
Send the credential in the Authorization header with the Bearer scheme:
curl -s "$REMEMBER_API_URL/operations" \
-H "Authorization: Bearer $REMEMBER_API_KEY"The remember client adds the Bearer prefix for you when you pass a bare
secret (Client(api_key=...) or REMEMBER_API_KEY).
A self-hosted deployment requires it only when you configure a credential
(REMEMBERSTACK_SELFHOST_API_BEARER_BIND,
REMEMBERSTACK_SELFHOST_API_BEARER_TOKEN or
REMEMBERSTACK_SELFHOST_API_KEY_ISSUER). With none of them set, the API is
open and ignores the header; protecting it is then the job of your network.
See Authentication and scopes.
When authentication is on, a request without the header is refused with
401 and {"detail": "a perimeter credential is required"}. A credential the
deployment does not accept is 401 with {"detail": "perimeter authentication failed"}.
A valid credential issued for a different deployment is 403 with
{"detail": "credential is for another deployment"}.
GET /healthz is the only route that never asks for a credential.
Credential kinds
| Credential | Where it comes from | Scope it carries |
|---|---|---|
| Shared secret (self-hosted) | You choose it; the engine keeps only its SHA-256 digest (REMEMBERSTACK_SELFHOST_API_BEARER_BIND is {deployment-uuid}:{sha256-hex}). | write (unrestricted) |
Signed key, kind: key | A long-lived key a person created at a key issuer. It can cover several projects. | From its permissions |
Signed key, kind: session | A short-lived key an issuer derives for this one deployment, for example for a browser. | From its permissions |
Signed key, kind: service | A short-lived machine credential for this one deployment. | From its permissions |
A deployment verifies signed keys when it is given a key issuer
(REMEMBERSTACK_SELFHOST_API_KEY_ISSUER and the settings beside it). It
fetches the issuer's public keys and a signed revocation document, and
refuses every signed key while it has no fresh revocation document. A
signed key's memory:read, memory:write or memory:ingest permission
becomes the read, write or ingest scope. The claims each kind must
carry are in Signed keys.
Scopes
A credential carries exactly one of three scopes:
| Scope | May call |
|---|---|
read | Every route marked read below. Nothing that changes memory. |
ingest | POST /ingest only. It cannot read. |
write | Everything. |
A signed key with no memory: permission carries none of them and gets 403
on every route.
read and ingest do not overlap. A credential with too narrow a scope gets
403 with {"detail": "credential may not perform this operation"}. Scopes
apply only when authentication is on; an open self-hosted deployment serves
every route to every caller.
The HTTP method does not tell you the scope. Several reads use POST because
their arguments do not fit in a query string.
| Method | Path | Scope |
|---|---|---|
GET | /healthz | none (no credential needed) |
GET | /resolve | read |
GET | /lookup/relations | read |
GET | /lookup/observations | read |
GET | /transcript/relation/{relation_id} | read |
GET | /hydrate/relation/{relation_id} | read |
GET | /search/claims | read |
POST | /search/claims | read |
GET | /search/chunks | read |
POST | /search/chunks | read |
GET | /chunks/{chunk_id}/adjacent | read |
POST | /chunks/adjacent | read |
POST | /graph/neighborhood | read |
POST | /graph/path | read |
POST | /graph/citation-path | read |
POST | /query/sql | read |
POST | /query/sql/explain | read |
GET | /query/space | read |
GET | /query/space/search | read |
GET | /query/saved | read |
GET | /query/saved/{namespace}/{name} | read |
POST | /query/saved/{namespace}/{name}/run | read |
POST | /readiness | read |
GET | /documents | read |
DELETE | /documents/{doc_id} | write |
GET | /operations | read |
POST | /operations/{name} | read for the four shipped operations (see below) |
POST | /ingest | ingest (or write) |
GET | /deployment | read |
| any other | any other | write |
One entry needs a note: POST /operations/{name} decides per
operation. The route asks the operation's descriptor whether it changes
memory (mutates). All four shipped operations only read and declare
mutates: false, so a read credential can run them. An operation that
did not declare itself read-only would need write.
Any route added later without a classification also requires write.
Content types
Requests with a body send JSON (Content-Type: application/json), except
POST /ingest, which sends the raw file bytes
(Content-Type: application/octet-stream). Every response body is JSON.
Timestamps are ISO 8601 strings. Wherever a timestamp is part of a result, it is UTC. Identifiers are UUID strings.
Errors
A failed request returns a non-2xx status and a JSON body with a single
detail key. detail takes one of four forms:
| Form | Example | Where |
|---|---|---|
| Short string | {"detail": "body_too_large"} | Most refusals: authentication, scope, limits, ingest checks, graph. |
Object with code | {"detail": {"code": "forget_in_progress"}} | 503 while a deletion (hard forget) runs. |
Object with code and message | {"detail": {"code": "saved_query_not_found", "message": "no saved query named examples.nope"}} | SQL query routes (/query/*), argument errors on POST /operations/{name}, and 429 admission refusals. |
| List of validation errors | {"detail": [{"type": "missing", "loc": ["query", "name"], "msg": "Field required", "input": null}]} | 422 when a parameter or body fails its declared type or bounds. |
Branch on the status and on code (or the short string), never on message.
Messages are written for people and can change.
The remember client raises remember.MemoryApiError for every failure, with
status_code, detail and, on the /query/* routes, code. A 429 raises
its subclass remember.RateLimited, with code and retry_after. A network
failure has status_code 0.
Errors and status codes lists every status, code and what to do about it.
Status codes that can come from any route
| Status | Body | When |
|---|---|---|
401 | a perimeter credential is required / perimeter authentication failed | Missing or unaccepted credential (authentication on). |
403 | credential is for another deployment / credential may not perform this operation | Wrong deployment, or scope too narrow. |
404 | Not Found | Unknown path. The credential is checked first, so an unauthenticated request gets 401. |
405 | Method Not Allowed | Known path, wrong method. |
422 | validation list | A parameter or body is missing, has the wrong type, or is out of bounds. |
429 | {"code": "rate_limited", …} / {"code": "concurrency_limited", …} | An admission limit was reached. Wait the Retry-After seconds, then retry. |
500 | Internal Server Error | An unhandled failure: a defect. Report it. |
503 | {"code": "forget_in_progress"} | A hard forget is running. Every route except the credential check is closed until it finishes. Retry later. |
Admission limits
Admission limits are optional and off by default: a self-hosted
deployment refuses no request for its rate or concurrency until you set one of
the settings below. Each limit
applies only when its setting is a positive number; unset or 0 means no
limit.
When a limit is set, every request except GET /healthz is counted against it
after the credential check and before routing,
for its credential or for the whole deployment. A request to an unknown path
counts too.
| Limit | Setting |
|---|---|
| Requests per minute, per credential | REMEMBERSTACK_SELFHOST_API_ADMISSION_KEY_PER_MINUTE |
| Requests running at once, per credential | REMEMBERSTACK_SELFHOST_API_ADMISSION_KEY_IN_FLIGHT |
| Requests per minute, per deployment | REMEMBERSTACK_SELFHOST_API_ADMISSION_DEPLOYMENT_PER_MINUTE |
| Requests running at once, per deployment | REMEMBERSTACK_SELFHOST_API_ADMISSION_DEPLOYMENT_IN_FLIGHT |
The request rate works as a bucket of tokens that refills continuously at the
per-minute rate, up to a burst of a quarter of it (30 at 120 per minute); each
request takes one. A
signed credential is counted by its id (jti). The shared secret, and every
caller when authentication is off, is counted against the deployment limits
only. A refused request counts against nothing.
Over a limit the deployment answers 429 with a Retry-After header in whole
seconds:
code | Meaning | Retry-After |
|---|---|---|
rate_limited | No request token is left for this credential or the deployment. | Seconds until the next token. |
concurrency_limited | Too many requests of this credential, or of the deployment, are still running. | 1 |
{"detail": {"code": "rate_limited", "message": "request rate limit reached; retry after Retry-After seconds"}}The counters live in the memory of each API process: they start empty when the process starts, and with several API processes each has its own, so the effective limits are that many times larger. A request whose client disconnects stays counted as running until its handler has actually stopped.
Idempotency
There is no Idempotency-Key header. Ingest is idempotent by content: the
engine hashes the bytes (SHA-256). Sending the same bytes again returns the
existing version with "created": false and starts no new work. For a
document with a stable source identity (source_kind and source_ref), bytes
identical to its latest version are the same no-op; changed bytes become a new
version of the same document, and so do bytes that match only an older
version (a revert). See Ingest.
Every other route only reads, so repeating it is safe.
Pagination and truncation
Results are bounded everywhere, and a bound is never silent.
GET /documentspages with an opaquecursor. Pass thecursorfrom one page to get the next;nullmeans there is no next page.- Envelope results carry a
truncationblock when a cap applied:truncated,returned,estimated_total,total_is_exact, an optionalcontinuationand an optionalreason. OnlyPOST /graph/neighborhoodaccepts acontinuationback today. - SQL query results (
QueryResult/v1) carrytruncated,truncation_reason(row_cap,byte_cap, or a graph budget) and thelimitsthe statement ran under.
See Result types.
Size limits
POST /ingest bodies can be capped per deployment. A self-hosted deployment
has no cap by default; set REMEMBERSTACK_SELFHOST_INGEST_BODY_MAX_BYTES to
add one. With a cap set, a body over it gets 413 body_too_large and a
request without Content-Length gets 411 length_required.
Other bounds are per route: search and lookup k up to 400, search query up to 4,096
characters in the POST form, operation queries up to 8,192 characters, SQL
text up to 65,536 bytes, and so on. Each group page lists them.
CORS
A deployment sends no CORS headers unless it is told which browser origins may
call it (REMEMBERSTACK_SELFHOST_BROWSER_ORIGINS, a comma-separated list of
exact https origins, or http origins on localhost, 127.0.0.1 or
[::1]; empty by default). For those origins it allows:
- methods
GET,POSTandDELETE(DELETEstill needs awritecredential), - request headers
AuthorizationandContent-Type, - no credentialed (cookie) mode,
- preflight caching for 600 seconds.
Wildcards are refused at startup, and so is an origin with a path, a user,
the default port (443, or 80 for http) written out, or upper-case
letters.
Routes outside the documented API
The published OpenAPI document (openapi.json in the repository) describes
the 27 operations above. A few routes exist in the code but are not part of
it:
| Route | Status |
|---|---|
GET /healthz | Served by the self-hosted profile as the container's liveness probe. Returns {"status": "ok"} after a SELECT 1 against PostgreSQL. No credential needed. Not in the OpenAPI document. See Deployment. |
GET /connectors, POST /connectors, GET /connectors/{connector_id}, POST /connectors/{connector_id}/pause | Defined in the engine, but the shipped profile does not mount them, so they answer 404. The remember client's connectors(), add_connector(), pause_connector() and connector_status() methods therefore fail against a stock deployment. |
GET /ops/cost-export/v1 | Served on a separate listener, only when REMEMBERSTACK_COST_EXPORT_BIND is set. Self-hosted operators only. See Deployment. |
The running server does not serve its own schema: /openapi.json, /docs
and /redoc answer 404. Those pages would answer without a credential, and
the API is for programs, not browsers, so they are switched off. Use the
openapi.json file checked into the repository for the release you run.