Authentication
API keys and dashboard sessions — what each one can do, and the one thing only a session can.
Two credentials reach the same API. An API key is what your server uses. A
dashboard session is a Cognito ID token, held by the browser app at
https://app.gitloom.cloud. Both are presented the same way:
Authorization: Bearer <credential>
Authorization happens at the gateway, in a Lambda authorizer, before any handler runs.
It accepts either credential and resolves both to exactly one account. A missing,
malformed, expired or revoked credential is 401 with no detail about which — telling
you which half of a guess was right is a gift to whoever is guessing.
GET /health is the only unauthenticated route.
Which credential am I holding?
curl -s "$GITLOOM_API/v1/whoami" \
-H "Authorization: Bearer $GITLOOM_KEY"
{"account":"acme","auth":"api_key","env":"live"}
| field | values |
|---|---|
account |
your account id — the owner of every namespace you can reach |
auth |
api_key or jwt |
env |
live or test for a key; empty for a dashboard session |
API keys
A key looks like this:
gl_live_3f9a2b7c1d4e6f80_kQ8vN2xR7mZpLd3Wc0Ty5Ub9Aj1Hs4Ge
└┬─┘└─┬┘ └──────┬───────┘ └──────────────┬──────────────┘
│ │ │ └─ secret: 24 random bytes, base64url
│ │ └─ key id: 16 hex chars, appears in logs, used to revoke
│ └─ environment: live | test
└─ prefix, so secret scanners can match it
Only the SHA-256 of the secret is stored. The full key exists exactly once, in the
201 response that created it. There is no endpoint that returns it again, and no
operator can recover it. Lost key: revoke it, mint another.
Creating a key
Key management requires a dashboard session. An API key calling any /v1/keys
route gets:
{"error":{"code":"dashboard_only","message":"API keys cannot manage API keys; sign in to the dashboard"}}
403. That is the whole point: a leaked key that can mint its own replacements
survives the revocation meant to contain it.
With a dashboard token:
curl -s -X POST "$GITLOOM_API/v1/keys" \
-H "Authorization: Bearer $ID_TOKEN" \
-H "content-type: application/json" \
-d '{"name":"production worker","env":"live"}'
{
"id": "3f9a2b7c1d4e6f80",
"env": "live",
"name": "production worker",
"key": "gl_live_3f9a2b7c1d4e6f80_kQ8vN2xR7mZpLd3Wc0Ty5Ub9Aj1Hs4Ge",
"note": "store this now; it cannot be retrieved again"
}
env defaults to live when omitted. Anything other than live or test is
400 invalid_env.
Listing and revoking
curl -s "$GITLOOM_API/v1/keys" -H "Authorization: Bearer $ID_TOKEN"
{"keys":[{"id":"3f9a2b7c1d4e6f80","env":"live","name":"production worker","revoked":false,"created_at":"2026-07-31T09:14:02Z"}]}
Revoke by key id — the middle segment, never the secret:
curl -s -X DELETE "$GITLOOM_API/v1/keys/3f9a2b7c1d4e6f80" \
-H "Authorization: Bearer $ID_TOKEN"
{"id":"3f9a2b7c1d4e6f80","revoked":true}
An unknown id is 404 not_found.
Everything except key management: create and list namespaces, write memories, retrieve, read usage. There is no scoping — a key is account-wide read and write over every namespace. Two consequences:
- Keys belong on a server. A key in a browser bundle, a mobile app, or a public repo is every one of your users' memories.
- Per-user isolation is a property of which namespace your server asks for, not of the credential. See Namespaces and multi-user patterns.
live and test are not a data boundary
The environment is in the prefix and reported by whoami, and that is all it does
today. A gl_test_ key reads and writes exactly the same namespaces as a gl_live_
key on the same account. It is useful for spotting a production credential pasted into
a test config, and for secret scanners. It is not a sandbox. If you need one, use a
separate namespace — that is a hard boundary — or a separate account.
Dashboard sessions
The dashboard signs in through the Cognito hosted UI and calls the same API with the
resulting ID token (not the access token). The account comes from the
custom:tenant_id claim, which a Pre-Token-Generation trigger injects from DynamoDB
at sign-in — the user cannot set it, and the app client's writable attributes are
restricted so a signup cannot claim someone else's tenant.
Tokens expire. A 401 in the dashboard means refresh the session, not that anything
is wrong with the account.
Browsers and CORS
CORS on the API allows GET and OPTIONS only, from the dashboard's own origins,
with authorization and content-type headers. So from a browser:
GET /v1/retrieve,GET /v1/whoami,GET /v1/usage,GET /v1/namespaceswork.POST /v1/memories,POST /v1/namespacesand every write to/v1/keysdo not — the preflight fails.
This is a real limitation of the current stack rather than a security model. The advice would be identical without it: call GitLoom from your backend and let your backend decide what a browser may ask for.
Handling failures
| status | code | what to do |
|---|---|---|
401 |
unauthorized |
check the key is set, unrevoked, and not a stale token |
403 |
dashboard_only |
you used a key on /v1/keys; sign in instead |
404 |
namespace_not_found |
create the namespace before reading or writing |
429 |
quota_exceeded or rate_limited |
see Plans and pricing and Limits. A monthly limit will not clear in a second — a rate limit will, within a minute |
5xx |
internal |
retry with exponential backoff and jitter |
Every route returns the same envelope:
{"error":{"code":"namespace_not_found","message":"create namespace \"user-8213\" before writing to it"}}
Branch on code. message is prose and may be reworded.