HTTP ingest
Push error events over HTTP instead of writing a log file — for apps, background workers, or anything else that would rather report an error the instant it happens than wait for a log file to be parsed. Off by default; enable per application in Settings → Plugins → HTTP ingest.
This is the transport underneath the capture SDKs (Laravel, generic PHP, browser) — those are convenience wrappers; this page covers what actually receives and processes the event.
It's the same pipeline as file-based logs, just a different entry point
A pushed event runs through the exact same normalization, grouping, occurrence-recording, timeline, and alerting pipeline as a parsed log line. It arrives tagged log_type=http, origin=ingested — from the issue list's point of view it's indistinguishable from something Log Lens found in a file, and enabled alert rules fire on it identically.
Authentication is independent of the dashboard
?api=ingest doesn't use the dashboard's API token — it has its own per-application ingest key, a write-only push token (get/rotate it from Settings → Plugins → HTTP ingest). Present it as:
- header
X-Log-Lens-Ingest-Key: <key>, or Authorization: Bearer <key>, or&key=<key>in the query string.
Because this key authenticates itself, ?api=ingest is one of only two routes in the whole API exempt from the dashboard's token/same-origin guard (the other is the Linear webhook) — a browser SDK or a headless worker with no dashboard credentials can still push events. A missing or wrong key returns 401. Rotating the key immediately invalidates the old one — any client still using it starts getting 401s.
What one request looks like
curl -X POST "$LOG_LENS_URL/?api=ingest&app=my-app" \
-H "X-Log-Lens-Ingest-Key: llk_…" -H "Content-Type: application/json" \
-d '{"events":[{"message":"Payment capture failed","severity":"ERROR","exception_class":"RuntimeException","module":"billing","context":{"order":123}}]}'
Send one event object, an array of events, or {"events":[…]} — up to 500 per request. Only message is required; everything else (severity, exception_class, stack, context, environment, channel, module, occurred_at, release, fingerprint, tags, request, user, breadcrumbs, server_name, runtime) enriches the resulting issue. Identical events group into one issue with multiple occurrences, exactly like repeated log lines — a custom fingerprint overrides that if you want to control grouping explicitly. The whole batch runs in one write transaction, so occurrence byte-offsets never race under concurrent requests.
Response is 202 with {accepted, received, dropped}.
Rate limiting
Each application accepts a capped number of events per rolling window — default 1000 events / 60 seconds (ingest.rate_max_events / ingest.rate_window_seconds). This is a sliding-window counter (the previous window's count is weighted by how much of it still overlaps "now"), not a naive fixed window, so you don't get a ~2× burst right at a window boundary.
Two distinct outcomes when you're over budget:
- Partial: if a batch exceeds the remaining budget for this window, the excess events are silently dropped — you still get
202, withdroppedtelling you how many didn't make it. This protects the database from one runaway client without hard-failing a batch that's mostly fine. - Fully exhausted: if the window has no budget left at all, the whole request is rejected with
429and aretry_after(seconds until the window rolls).
There's an optional secondary per-IP limit (off by default, ingest.rate_max_events_per_ip) for the case where you're exposing the browser SDK's key publicly and want to bound any single visitor's impact independent of the application-wide budget.
The current effective limit is always shown in Settings → Plugins → HTTP ingest, since it depends on both settings together.
Related
- Capture SDKs — the Laravel reporter, generic PHP client, and browser SDK all send through this endpoint.
- Release tracking — the
releasefield on a pushed event is howoccurrences.releaseever gets populated. - Alerting — runs over pushed events exactly as it does over parsed ones.
Inside a Laravel app
The Laravel adapter mounts the receiver on its own route, POST {prefix}/ingest (e.g. https://app.test/log-lens/ingest?app=my-app), outside the host's access gate and outside the web middleware group. Both would reject every legitimate caller: an SDK in a browser page or a headless worker has no session and no CSRF token. The ingest key is still required, so the route is not open — a missing or wrong key is a 401.
Settings → Plugins → HTTP ingest shows this URL automatically; use whatever it displays rather than assembling one by hand. To put your own throttling or IP allowlist in front of it, set log-lens.receiver-middleware.