Security
API keys and scopes
An API key is how your own code, your CI and your agents talk to Engram. Keys belong to the workspace rather than to a person, so they keep working after whoever made them leaves, and scopes let you hand an agent exactly the authority it needs and nothing else.
What a key is
- It starts with
ek_, so anything reading a credential can tell a key from a session token by looking at the first three characters. - The secret is shown once, at creation. We store only a SHA-256 of it plus a short display prefix, so there is no endpoint that can ever return it again, and a database read does not hand anybody a live credential.
- It belongs to the workspace, not to the person who minted it, and it carries no platform authority: no key can reach the cross-workspace console, whatever scopes it has.
- It records when it was last used, so you can find the ones nobody needs.
Use it as a bearer token on any REST or MCP call:
curl -s https://api.engramdynamics.org/corpora \
-H "Authorization: Bearer <your key>"
Scopes
Scopes limit what a key can change or spend, which is what you actually care about when you hand a key to an agent. Reads are deliberately not scoped: any valid key may read its own workspace's state.
| Scope | What it allows |
|---|---|
ingest | Create a document base, upload documents, import from a connected source, start the guided onboard, and retrain. |
query | Ask questions, streamed or not. This is the scope that spends the query allowance. |
admin | The workspace administration surface: members, invites, usage, billing, and the API keys themselves. |
A key with admin acts as a workspace admin; a key without it acts as a member. That is also what stops an escalation: an ingest-only key leaked into a CI log cannot mint itself a full-access one, because minting a key needs admin.
A call missing a scope is a 403 that names what is missing, because the caller owns the key and telling them saves a support round trip:
{ "detail": "API key missing required scope: query" }
Create a key
In the app: Settings, API keys. Over the API, with an admin credential:
curl -s -X POST https://api.engramdynamics.org/api-keys \
-H "Authorization: Bearer <your key>" \
-H "Content-Type: application/json" \
-d '{
"name": "nightly sync",
"scopes": ["ingest"],
"expires_at": "2027-01-01T00:00:00Z"
}'
{
"id": "b1c2d3e4...",
"name": "nightly sync",
"prefix": "ek_7Fq2pXk9",
"scopes": ["ingest"],
"expires_at": "2027-01-01T00:00:00Z",
"last_used_at": null,
"revoked_at": null,
"created_at": "2026-09-13T09:00:00Z",
"key": "ek_7Fq2pXk9..."
}
key is the secret and this is the only response that will ever carry it. Put it straight into your secret store.
Two things the API refuses rather than letting you find out later: an unknown scope is a 422 naming the typo and the allowed values, so a misspelled "quary" fails at the call that made it instead of surfacing as a mystery 403 a week later; and an expiry must be in the future, because a key minted already expired is a credential that can never work, and the secret is shown exactly once.
List and revoke
curl -s https://api.engramdynamics.org/api-keys \
-H "Authorization: Bearer <your key>"
Newest first, secrets structurally absent, and revoked keys included with their revoked_at set. The tombstone is the record that a credential was turned off, which is exactly what an auditor wants to see.
curl -s -X DELETE https://api.engramdynamics.org/api-keys/<key id> \
-H "Authorization: Bearer <your key>"
Revocation takes effect on the very next request: every call re-reads the key's state, so there is no cached-credential window. Re-revoking an already-revoked key returns it unchanged rather than erroring, so a retrying client is never told a shutdown failed that actually succeeded. A key id from another workspace returns 404, never 403, so the route cannot be used to probe for ids.
Rotating
There is no rotate call, and that is deliberate: mint the new key, deploy it, confirm the old key's last_used_at has stopped moving, then revoke it. Two live keys for the length of a deploy is the safe way round, and the audit log records both halves.
Every mint and every revoke writes an apikey.create or apikey.revoke row to the audit log, carrying the name, the display prefix and the scopes, never the secret. Key lifecycle is exactly the kind of thing a reviewer asks about after the fact.
What a rejected key looks like
An unknown, revoked or expired key all return the same generic 401. Distinct messages would turn the endpoint into an oracle confirming that a guessed key is a real one. If a key stops working, check the listing for its revoked_at and expires_at rather than reading the error.
Two other refusals are about the workspace rather than the key: a suspended workspace refuses every credential, session and key alike, and an IP allowlist refuses a key used from outside the allowed networks with ip_not_allowed.
Next
IP allowlist narrows where a key can be used from. Audit log is where every key event lands.