RememberStackremember.dev/docs

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:

GroupPageRoutes
Ingest, readiness, documentsIngestPOST /ingest, POST /readiness, GET /documents, DELETE /documents/{doc_id}
Assured operationsOperationsGET /operations, POST /operations/{name}
Entities and factsEntities and factsGET /resolve, GET /lookup/relations, GET /lookup/observations, GET /hydrate/relation/{relation_id}, GET /transcript/relation/{relation_id}
SearchSearchGET and POST /search/claims, GET and POST /search/chunks, GET /chunks/{chunk_id}/adjacent, POST /chunks/adjacent
GraphGraphPOST /graph/neighborhood, POST /graph/path, POST /graph/citation-path
SQL queriesSQL queriesPOST /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
DeploymentDeploymentGET /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:8000

The 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

CredentialWhere it comes fromScope 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: keyA long-lived key a person created at a key issuer. It can cover several projects.From its permissions
Signed key, kind: sessionA short-lived key an issuer derives for this one deployment, for example for a browser.From its permissions
Signed key, kind: serviceA 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:

ScopeMay call
readEvery route marked read below. Nothing that changes memory.
ingestPOST /ingest only. It cannot read.
writeEverything.

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.

MethodPathScope
GET/healthznone (no credential needed)
GET/resolveread
GET/lookup/relationsread
GET/lookup/observationsread
GET/transcript/relation/{relation_id}read
GET/hydrate/relation/{relation_id}read
GET/search/claimsread
POST/search/claimsread
GET/search/chunksread
POST/search/chunksread
GET/chunks/{chunk_id}/adjacentread
POST/chunks/adjacentread
POST/graph/neighborhoodread
POST/graph/pathread
POST/graph/citation-pathread
POST/query/sqlread
POST/query/sql/explainread
GET/query/spaceread
GET/query/space/searchread
GET/query/savedread
GET/query/saved/{namespace}/{name}read
POST/query/saved/{namespace}/{name}/runread
POST/readinessread
GET/documentsread
DELETE/documents/{doc_id}write
GET/operationsread
POST/operations/{name}read for the four shipped operations (see below)
POST/ingestingest (or write)
GET/deploymentread
any otherany otherwrite

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:

FormExampleWhere
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

StatusBodyWhen
401a perimeter credential is required / perimeter authentication failedMissing or unaccepted credential (authentication on).
403credential is for another deployment / credential may not perform this operationWrong deployment, or scope too narrow.
404Not FoundUnknown path. The credential is checked first, so an unauthenticated request gets 401.
405Method Not AllowedKnown path, wrong method.
422validation listA 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.
500Internal Server ErrorAn 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.

LimitSetting
Requests per minute, per credentialREMEMBERSTACK_SELFHOST_API_ADMISSION_KEY_PER_MINUTE
Requests running at once, per credentialREMEMBERSTACK_SELFHOST_API_ADMISSION_KEY_IN_FLIGHT
Requests per minute, per deploymentREMEMBERSTACK_SELFHOST_API_ADMISSION_DEPLOYMENT_PER_MINUTE
Requests running at once, per deploymentREMEMBERSTACK_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:

codeMeaningRetry-After
rate_limitedNo request token is left for this credential or the deployment.Seconds until the next token.
concurrency_limitedToo 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 /documents pages with an opaque cursor. Pass the cursor from one page to get the next; null means there is no next page.
  • Envelope results carry a truncation block when a cap applied: truncated, returned, estimated_total, total_is_exact, an optional continuation and an optional reason. Only POST /graph/neighborhood accepts a continuation back today.
  • SQL query results (QueryResult/v1) carry truncated, truncation_reason (row_cap, byte_cap, or a graph budget) and the limits the 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, POST and DELETE (DELETE still needs a write credential),
  • request headers Authorization and Content-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:

RouteStatus
GET /healthzServed 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}/pauseDefined 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/v1Served 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.