Live request-log tail
GET /api/v1/logs/tail opens a Server-Sent Events (SSE) connection that streams every API request handled for your tenant, sub-second after each response completes. It is the data source behind the Live tail toggle on the developer Request Logs page, and you can consume it directly from your own tooling.
The stream excludes health, readiness, and metrics probes, provider webhook ingress (those receiver 401s are provider-side noise, not your API traffic), and the tail endpoint itself.
Base path: /api/v1/logs/tail
Authentication: API key (X-API-Key) or session JWT. The caller must hold the owner, admin, or developer role.
Using the SDKs
This endpoint has no typed SDK helper, and the genericrequest() escape hatch does not fit SSE: it buffers the whole body, waits for the response to complete, then returns it as one raw non-JSON string. A live tail never completes — the sample below honors the request timeout, returns the beginning of the stream as a chunk of raw text/event-stream, and disconnects. Anything you persisted would be only the backfill:
event: log frames as they arrive. Only browser customers use EventSource; every server-side consumer (Node fetch, Python requests, Go net/http, Ruby Net::HTTP, PHP cURL) is a plain GET plus an incremental frame parser. Full SDK index at SDK quickstart.
Connect
Browsers useEventSource, which cannot set request headers, so pass your credential as the token query parameter instead. A dv_-prefixed value is promoted to X-API-Key; any other value is treated as a bearer JWT. When you call from a server you can send the X-API-Key or Authorization header directly and omit token.
Worked example: read status and latency off the stream
The frame format to parse is exactly the payload above — a namedevent: log line, an id: cursor line, and one data: line holding a single JSON envelope per frame (multi-line data: is legal per the SSE spec but this endpoint always sends a single line). This Python loop opens the stream, splits frames on a blank line, and extracts status_code + duration_ms off each log row — the same fields you would chart in an error-rate or latency dashboard:
Frames
After connecting, the server sends, in order:- An
event: connectedhandshake frame withdata: {"type":"connected", ...}. - A backfill of up to the most recent ~100 request rows.
- New rows live, as each request completes.
: heartbeat) are sent every 30 seconds to keep the connection open. Each request row arrives as a named log frame — listen with addEventListener("log", …), not onmessage, because EventSource only routes named frames to a matching listener.
Each log frame’s data: line is one JSON object:
Adding fields is backwards-compatible — ignore keys you don’t recognize, and drop any row missing a required field.
Resume after a disconnect
Every frame carries anid (a stream cursor). To resume without losing rows, reconnect with the Last-Event-ID request header set to the last id you received — browser EventSource does this automatically. From a client that can’t set the header, pass the same value as the lastEventId query parameter:
Consume the SSE stream from code
Server-side consumers — log forwarders, ETL jobs, on-call bots — authenticate with theX-API-Key header (or an Authorization bearer header). Never send token as a query parameter from server code: a URL ends up in access logs and proxy history, and a credential in it is exposed. If your server library parses named SSE events, use the named event (addEventListener("log", …)), not a generic message handler — the rows arrive as event: log frames, so an onmessage-style handler never sees them. The curl sample above is a one-off tail; the sections below are the full client loops you keep in your own tooling.
The sections below give a full client loop per language — header auth, frame parsing, cursor persistence, and reconnect — for Node.js, Python, Go, Ruby, and PHP.
Node.js
- eventsource
- Manual parse (fetch)
Python
sseclient dispatches each frame’s event: field to callbacks you register per name — register log, not message. With requests you iterate lines and track the fields yourself:
- sseclient
- requests
Go
go-sse subscribes per event name and re-sends the last id itself while reconnecting — subscribe to log, not the unnamed default. With the standard library you scan lines with bufio and track the fields yourself:
- go-sse
- Manual parse (net/http)
Ruby
em-eventsource dispatches named frames to per-name callbacks and re-sends the last id itself while reconnecting — register log, not the generic message handler. With Net::HTTP you buffer chunks, split lines, and track the fields yourself:
- em-eventsource
- Manual parse (Net::HTTP)
PHP
Guzzle returns the body as a stream when you passstream => true, so read it incrementally and track the fields yourself. The cURL extension takes a write callback — the manual-parse form without Guzzle:
- Guzzle (stream)
- Manual parse (cURL extension)
Last-Event-ID on reconnect — the server replays the rows you missed.
Limits and errors
On
429 or 503 from a server-side consumer, back off before retrying instead of reconnecting in a tight loop:
- Node.js
- Python