Authentication
Guardian Logs uses bearer credentials on the Authorization header. There are two credential systems and they are strictly separate — see README.
Authorization: Bearer <credential>
Credential types
| Ingest token | General API key | |
|---|---|---|
| Format | gl_live_<publicId>_<secret> | gl_api_<publicId>_<secret> |
| Direction | Write-only | Read-only |
| Endpoints | POST /api/v1/ingest only | every GET /api/v1/* resource |
| Bound to | one org + app + source | one organization |
| Access control | implicit (source binding) | explicit read scopes |
The publicId (16 hex chars) is a non-secret handle used for O(1) lookup and rotation. Only a SHA-256 of the secret (64 hex chars) is stored; the plaintext is shown once at creation and never persisted.
The two systems never overlap. An ingest token presented to a read endpoint fails the API-key parser (wrong prefix) → 401. An API key presented to /api/v1/ingest fails the ingest parser → 401.
How authentication is evaluated
For every authenticated request the server:
1. Reads Authorization: Bearer …. Missing/empty → 401. 2. Parses the token for the correct prefix and shape. Malformed → 401. 3. Looks up the row by publicId and verifies the secret in constant time. Unknown / wrong secret → 401. 4. Rejects revoked (revokedAt) or expired (expiresAt) credentials → 401. 5. (API keys) checks the endpoint's required scope. Missing → 403. 6. Applies rate limits. Over budget → 429.
Uniform 401
Steps 1–4 all return the same 401 body regardless of the underlying reason (missing, malformed, unknown, wrong secret, revoked, or expired). This is deliberate: a caller can never probe for the existence of a credential, org, or app.
{ "error": { "code": "unauthorized", "message": "Invalid or expired API key." } }
The organization is always taken from the credential row, never from the request — you cannot access another tenant's data by any request parameter.
Examples
Read (API key):
curl "https://guardianlogs.com/api/v1/apps" \
-H "Authorization: Bearer $GUARDIAN_API_KEY"
Ingest (ingest token):
curl -X POST "https://guardianlogs.com/api/v1/ingest" \
-H "Authorization: Bearer $GUARDIAN_INGEST_TOKEN" \
-H "Content-Type: application/json" \
-d '{ "message": "hello" }'
Lifecycle
- Mint — created by an org admin in-app; plaintext shown once.
- Rotate — mint a replacement and retire the old credential; deploy the new
one first. For ingest tokens, rotation is per-source and stored data is unaffected (events belong to the app/source, not the token).
- Revoke — takes effect immediately; the next request gets
401.
See also: scopes.md · rate-limits.md · errors.md · security.md.