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 tokenGeneral API key
Formatgl_live_<publicId>_<secret>gl_api_<publicId>_<secret>
DirectionWrite-onlyRead-only
EndpointsPOST /api/v1/ingest onlyevery GET /api/v1/* resource
Bound toone org + app + sourceone organization
Access controlimplicit (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

one first. For ingest tokens, rotation is per-source and stored data is unaffected (events belong to the app/source, not the token).

See also: scopes.md · rate-limits.md · errors.md · security.md.