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 URL | https://<your-guardian-logs-host> (local dev: http://localhost:4300) |
| Endpoint | POST /api/v1/ingest |
| Content-Type | application/json |
| Auth | Authorization: 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.
- The plaintext
gl_live_…token is shown exactly once at creation. Only a
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).
- A credential is ingest-only and permanently bound to one
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.
- Use one credential per host/source so you can rotate narrowly without
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.
| Field | Type | Required | Limit / default | Notes |
|---|---|---|---|---|
message | string | yes | ≤ 8000 chars | The log line / error message. Longer values should be truncated by the sender. |
timestamp | string | no | ISO-8601; defaults to receipt time | e.g. 2026-09-13T16:22:11Z. Invalid timestamps are rejected (the event, not the batch). |
level | string | no | default error | error / warn / info. Aliases such as fatal, debug map sensibly. |
errorClass | string | no | — | e.g. TypeError. Improves grouping. |
environment | string | no | — | e.g. production, staging. Structured context. |
service | string | no | — | e.g. web, worker. Structured context. |
host | string | no | — | Originating host. Structured context. |
deployment | string | no | — | Build/deploy id (e.g. a git SHA). Aids deploy correlation. |
route | string | no | — | Promoted for grouping. Also read from metadata.route. |
metadata | object | no | ≤ 8 KB serialized | Free-form structured context (requestId, stack, etc.). |
eventId / idempotencyKey | string | no | — | Stable 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.
| Outcome | Retry? |
|---|---|
202 Accepted | — (done; inspect results[]) |
| Network error / DNS / connection reset | Yes |
| Request timeout | Yes |
429 Rate limited | Yes — honor error.retryAfterSec / Retry-After |
5xx Server error | Yes |
400 Malformed | No — fix the request |
401 Auth | No — fix the credential |
403 Source disabled | No — re-enable the source |
413 Too large | No — 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.
| Scope | Default | Env override |
|---|---|---|
| Per token | 600 events/min | GL_RATELIMIT_TOKEN_EVENTS_PER_MIN |
| Per org | 3000 events/min | GL_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
| Limit | Value | Exceeded → |
|---|---|---|
| Request body size | 1 MB | 413 |
| Events per request | 500 | 413 |
message length | 8000 chars | truncate at the sender |
metadata serialized size | 8 KB | keep metadata small |
9. Response codes
| Code | Meaning | Body | |
|---|---|---|---|
202 | Accepted. Inspect results[] for per-event outcome. | ingest summary (see §4/§5) | |
400 | Malformed JSON / empty body / not an object or array. | { "error": { "code": "…", "message": "…" } } | |
401 | Missing, invalid, revoked, or expired credential. Uniform — we never reveal which. | { "error": { "code": "…", "message": "…" } } | |
403 | The credential's log source is disabled. | { "error": { "code": "…", "message": "…" } } | |
413 | Body > 1 MB or > 500 events. | { "error": { "code": "…", "message": "…" } } | |
429 | Rate limit exceeded. | `{ "error": { "code": "rate_limited", "retryAfterSec": N, "scope": "token"\ | "org" } }` |
5xx | Unexpected 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.
- Mint — pick app + source type; the plaintext
gl_live_…is shown once. - Rotate — mints a replacement for the same source and revokes the old
token in one step. Deploy the new token first, *then* the old one stops working. No ingested data is affected.
- Revoke — takes effect immediately; the next request with that token
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:
- Passwords, passphrases, PINs
- Session tokens, cookies,
Set-Cookievalues Authorizationheaders or bearer tokens (including yourgl_live_token —
never echo it back inside message or metadata)
- API secrets / keys (yours or third parties':
sk-…, AWS keys, etc.) - Database connection strings with inline credentials
- Payment data (full card numbers, CVV)
- Unnecessary PII (SSNs, full contact details) not required for debugging
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
| Symptom | Likely cause | Fix |
|---|---|---|
400 on the whole request | Body 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 request | Missing/typo'd/revoked/expired token, or wrong Authorization header format. | Re-check Authorization: Bearer gl_live_…; if unsure, rotate the credential and redeploy. |
403 | The credential's log source is disabled. | Re-enable the source in Admin, or use a credential for an enabled source. |
413 | Body > 1 MB or > 500 events. | Split into smaller batches (≤ 500 events, ≤ 1 MB); truncate oversized message/metadata. |
429 frequently | Exceeding 600/min per token or 3000/min per org. | Honor retryAfterSec; batch events; spread load across sources; request higher limits. |
| Events accepted but not appearing | Sent to the wrong source, or filtered by level. | Confirm the credential maps to the expected app/source; check the level you sent. |
| Duplicate events / issues | Reused or missing idempotencyKey across retries. | Attach a stable idempotencyKey/eventId per logical event; reuse it on retry. |
| TLS/certificate errors | Attempting 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.