Guardian Logs Ingest API (v1)

The definitive reference for sending application and system logs to Guardian Logs over HTTPS. You push events to us — Guardian Logs never needs inbound access to your servers.

> This is the expanded, canonical API reference. See also: > the API docs index and authentication > for the read API and the two credential types, > ../api.md (the concise version), the > file forwarder guide if you have log files rather than > app-code integration, and the customer onboarding walkthrough > for the full new-customer path. Related topics: > idempotency, rate limits, > errors, security.

Throughout, $BASE is your Guardian Logs base URL and $GUARDIAN_LOGS_TOKEN is your ingest credential. Never paste a real token into a shell history, source file, or ticket — export it from your environment instead.

export BASE="https://logs.example.com"          # your Guardian Logs host
export GUARDIAN_LOGS_TOKEN="gl_live_<publicId>_<secret>"   # from Admin → Ingest credentials

1. Endpoint

Base URLhttps://<your-guardian-logs-host> (local dev: http://localhost:4300)
EndpointPOST /api/v1/ingest
Content-Typeapplication/json
AuthAuthorization: Bearer gl_live_<publicId>_<secret>

Production uses HTTPS with a valid TLS certificate. Never disable certificate verification (no curl -k, no NODE_TLS_REJECT_UNAUTHORIZED=0).


2. Authentication

Every request carries a bearer credential:

Authorization: Bearer gl_live_<publicId>_<secret>

How to get a credential

An organization admin mints one in Admin → Ingest credentials: choose the application + source type, optionally add a label, and copy the token.

SHA-256 hash of the secret is stored, so Guardian Logs can never show it to you again. If you lose it, rotate (see §9).

organization + application + source. Guardian Logs derives the destination entirely from the credential — any org/app/source fields you put in a payload are ignored. A token for one app or org can never write to another.

disrupting everything.


3. Event schema

An event is a JSON object. message is the only required field; everything else is optional structured context that improves grouping, correlation, and search.

FieldTypeRequiredLimit / defaultNotes
messagestringyes≤ 8000 charsThe log line / error message. Longer values should be truncated by the sender.
timestampstringnoISO-8601; defaults to receipt timee.g. 2026-09-13T16:22:11Z. Invalid timestamps are rejected (the event, not the batch).
levelstringnodefault errorerror / warn / info. Aliases such as fatal, debug map sensibly.
errorClassstringnoe.g. TypeError. Improves grouping.
environmentstringnoe.g. production, staging. Structured context.
servicestringnoe.g. web, worker. Structured context.
hoststringnoOriginating host. Structured context.
deploymentstringnoBuild/deploy id (e.g. a git SHA). Aids deploy correlation.
routestringnoPromoted for grouping. Also read from metadata.route.
metadataobjectno≤ 8 KB serializedFree-form structured context (requestId, stack, etc.).
eventId / idempotencyKeystringnoStable dedupe key. See §6.

A request body is either a single event object or a batch envelope { "events": [ … ] } (see §5).


4. Single-event example

curl -X POST "$BASE/api/v1/ingest" \
  -H "Authorization: Bearer $GUARDIAN_LOGS_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{
    "timestamp": "2026-09-13T16:22:11Z",
    "level": "error",
    "message": "Failed to process document",
    "errorClass": "TypeError",
    "environment": "production",
    "service": "web",
    "host": "app01",
    "deployment": "c8381ab",
    "route": "/api/process",
    "metadata": { "requestId": "abc123" }
  }'

Response — 202 Accepted:

{
  "ok": true,
  "batchId": "…",
  "received": 1,
  "accepted": 1,
  "duplicates": 0,
  "rejected": 0,
  "results": [ { "index": 0, "status": "accepted" } ]
}

5. Batch ingestion

Send up to 500 events per request in an events array:

curl -X POST "$BASE/api/v1/ingest" \
  -H "Authorization: Bearer $GUARDIAN_LOGS_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{ "events": [
    { "timestamp": "2026-09-13T16:22:11Z", "level": "error", "message": "boom", "idempotencyKey": "evt-1" },
    { "timestamp": "2026-09-13T16:22:12Z", "level": "warn",  "message": "slow",  "idempotencyKey": "evt-2" }
  ] }'

Batches are partial-success. Each event is validated independently and reported in results[] by index. A malformed *individual* event is rejected without failing the batch; only a body that isn't valid JSON — or isn't an object or array — fails the whole request with 400.

{
  "ok": true,
  "batchId": "…",
  "received": 2,
  "accepted": 1,
  "duplicates": 0,
  "rejected": 1,
  "results": [
    { "index": 0, "status": "accepted" },
    { "index": 1, "status": "rejected", "error": "…" }
  ]
}

results[].status is one of accepted, duplicate, or rejected. Always inspect it — a 202 means the batch was *received*, not that every event was accepted.


6. Idempotency & safe retries

Attach a stable idempotencyKey (or eventId) to each event. Guardian Logs deduplicates on (source, idempotencyKey): re-sending the same key — within a batch or across retries — is a no-op counted under duplicates, never a new event or issue. Events without a key are stored as-is (at-least-once).

Retry policy for senders — on network error, timeout, 429, or 5xx, retry with exponential backoff and jitter (e.g. 1s, 2s, 4s, capped), reusing the same idempotency keys. Do not retry 400, 401, 403, or 413 — fix the request or credential instead.

OutcomeRetry?
202 Accepted— (done; inspect results[])
Network error / DNS / connection resetYes
Request timeoutYes
429 Rate limitedYes — honor error.retryAfterSec / Retry-After
5xx Server errorYes
400 MalformedNo — fix the request
401 AuthNo — fix the credential
403 Source disabledNo — re-enable the source
413 Too largeNo — send fewer/smaller events

The @guardian-logs/client and the file forwarder implement this policy for you.


7. Rate limits

Budgets are counted in events and enforced per-credential and per-organization, so one noisy token or tenant can't starve the platform.

ScopeDefaultEnv override
Per token600 events/minGL_RATELIMIT_TOKEN_EVENTS_PER_MIN
Per org3000 events/minGL_RATELIMIT_ORG_EVENTS_PER_MIN

Over budget → 429 with:

{ "error": { "code": "rate_limited", "retryAfterSec": 12, "scope": "token" } }

scope is "token" or "org". Wait retryAfterSec (or the Retry-After header) before retrying. Batching reduces request overhead but does not raise the per-event budget — a 500-event batch counts as 500 events.


8. Payload & size limits

LimitValueExceeded →
Request body size1 MB413
Events per request500413
message length8000 charstruncate at the sender
metadata serialized size8 KBkeep metadata small

9. Response codes

CodeMeaningBody
202Accepted. Inspect results[] for per-event outcome.ingest summary (see §4/§5)
400Malformed JSON / empty body / not an object or array.{ "error": { "code": "…", "message": "…" } }
401Missing, invalid, revoked, or expired credential. Uniform — we never reveal which.{ "error": { "code": "…", "message": "…" } }
403The credential's log source is disabled.{ "error": { "code": "…", "message": "…" } }
413Body > 1 MB or > 500 events.{ "error": { "code": "…", "message": "…" } }
429Rate limit exceeded.`{ "error": { "code": "rate_limited", "retryAfterSec": N, "scope": "token"\"org" } }`
5xxUnexpected server error — safe to retry.{ "error": { "code": "…", "message": "…" } }

Errors have the shape { "error": { "code": "…", "message": "…" } }. Guardian Logs never reveals whether another org/app/token exists.

Example error bodies

400 Bad Request:

{ "error": { "code": "bad_request", "message": "Request body is not valid JSON." } }

401 Unauthorized (uniform — same body regardless of the underlying reason):

{ "error": { "code": "unauthorized", "message": "Invalid credentials." } }

403 Forbidden:

{ "error": { "code": "source_disabled", "message": "This log source is disabled." } }

413 Payload Too Large:

{ "error": { "code": "payload_too_large", "message": "Body exceeds 1 MB or 500 events." } }

429 Too Many Requests:

{ "error": { "code": "rate_limited", "retryAfterSec": 12, "scope": "org" } }

10. Token rotation & revocation

Managed in Admin → Ingest credentials. Ingested data belongs to the app/source, not the token, so credential changes never affect stored events.

token in one step. Deploy the new token first, *then* the old one stops working. No ingested data is affected.

gets 401.

Only a SHA-256 of the secret is stored; the non-secret publicId gives O(1) lookup. Plaintext is never persisted or written to logs/audit.


11. What NOT to send (security)

Guardian Logs stores log content. Redact secrets and PII at the source before sending. Never transmit:

never echo it back inside message or metadata)

Defense in depth: Guardian Logs also applies best-effort server-side redaction at ingestion — it scrubs obvious secret shapes (bearer tokens, API keys, JWTs, PEM private-key blocks, DB connection strings, and sensitively-named keys like password/secret/token) from the raw record, message, and context before persistence. This is pattern matching, not a guarantee: novel formats can slip through. You remain responsible for what you transmit.

Operational tips: keep the token in an environment variable or a protected config file (never in source control), and never put it inside the metadata you send.


12. Troubleshooting

SymptomLikely causeFix
400 on the whole requestBody isn't valid JSON, is empty, or isn't an object/array.Validate JSON; send a single event object or { "events": [ … ] }.
Individual event rejected in results[]Missing/empty message, message > 8000 chars, invalid timestamp, or metadata > 8 KB.Read results[i].error; fix that event. Other events still succeeded.
401 on every requestMissing/typo'd/revoked/expired token, or wrong Authorization header format.Re-check Authorization: Bearer gl_live_…; if unsure, rotate the credential and redeploy.
403The credential's log source is disabled.Re-enable the source in Admin, or use a credential for an enabled source.
413Body > 1 MB or > 500 events.Split into smaller batches (≤ 500 events, ≤ 1 MB); truncate oversized message/metadata.
429 frequentlyExceeding 600/min per token or 3000/min per org.Honor retryAfterSec; batch events; spread load across sources; request higher limits.
Events accepted but not appearingSent to the wrong source, or filtered by level.Confirm the credential maps to the expected app/source; check the level you sent.
Duplicate events / issuesReused or missing idempotencyKey across retries.Attach a stable idempotencyKey/eventId per logical event; reuse it on retry.
TLS/certificate errorsAttempting to bypass verification, or a proxy MITM.Use HTTPS with a valid cert; never use -k / NODE_TLS_REJECT_UNAUTHORIZED=0.

13. Legacy endpoint

POST /api/ingest (header x-ingest-token: opsc_…) is deprecated but still works for existing exporters. Its responses carry Deprecation: true and a Link to the successor. Migrate to POST /api/v1/ingest with a gl_live_ credential; existing ingested data is unaffected by switching.