Skip to main content
This guide shows you how to stream unified LLM logs with the Logs Export API. After you connect a poller or an incremental connector, you can land those logs in a data warehouse, SIEM, or analytics platform.

Prerequisites

Before you export logs, make sure you have:

Call the export endpoint

Request logs for one organization. URL: GET https://api.writer.com/api/observability/v1/organization/{organizationId}/logs/export Omit cursor and start_date to start at the current time. Only newly settled rows stream forward.

Authenticate requests

Send the Writer API key on every request.
Each key is bound to a single organization and can tail logs only for that organization. A request that uses a key against a different organizationId returns 403 Forbidden.

Choose a start point

You choose where the stream begins. After the first response, page with the cursor alone. The cursor already encodes your position.

Start from the current time

Call the endpoint with order=tail and neither cursor nor start_date. The stream starts at the current time, with no backfill.

Backfill from a date

Pass start_date with order=tail to replay from that time, then continue live after the backlog drains. start_date accepts an ISO-8601 timestamp, epoch seconds, or epoch milliseconds. For example, start_date=2026-07-01T00:00:00Z.

Stay inside the retention window

A start_date older than the retention window (90 days by default) returns 400 Validation Error. The detail field includes the window length and the earliest allowed ISO date. The API returns that error instead of truncating the backfill.

Page with the cursor alone

After you store x-next-cursor, send it as the cursor query parameter and omit start_date. Sending start_date together with cursor returns 400 because the cursor already encodes the position.

Set query parameters

These parameters control paging, the response shape, and filters. Filters apply in tail mode and keep (inserted_at, log_id) order across pages.
include_bodies=true requires the Content Export entitlement. Without that entitlement, the request returns 403 Forbidden.

Read the response

Tail responses put the resume token and the caught-up signal in headers. The body shape depends on format.

Read response headers

Use these headers to resume and to decide when the settled backlog is drained.

Read a native body

With format=native, the body is a JSON object. result[].inserted_at_ms and result[].log_id are the two components of the tail ordering axis. Use log_id as the key for an idempotent upsert. Other log fields, such as model, appear on each row.
pagination.has_more matches the x-has-more header. In tail mode, pagination.next_cursor is null. Read the resume token from the x-next-cursor header.

Read an OTLP body

With format=otlp (the default), the body is an OTLP/JSON payload. signal=traces returns { "resourceSpans": [...] }. signal=logs returns { "resourceLogs": [...] }. Pagination stays in the response headers: x-next-cursor, x-has-more, and x-tail-watermark.

Store and resend the cursor

The tail cursor is a versioned, HMAC-signed token. Treat it as an opaque string. Store it verbatim and send it back unchanged on the cursor parameter.
Do not parse, truncate, or reconstruct the cursor. A tampered or version-incompatible token returns 400 Validation Error with detail Invalid tail cursor.

Apply delivery guarantees

Tail mode returns rows in a fixed order and withholds unsettled rows so a delivered page stays final. Dedupe on log_id because a retry can deliver the same row again.
  • Ordering. Rows return in (inserted_at ASC, log_id ASC) order. Replaying a cursor returns no row that ordered before that cursor.
  • Settled window. A page contains only rows whose inserted_at is at or before x-tail-watermark. The watermark is now minus a 5-minute settle lag. A row with an older event time and a newer inserted_at arrives when the watermark passes that inserted_at.
  • Dedupe. The store resolves each log_id to one row. Delivery is at-least-once across retries and overlapping workers. Upsert on log_id.
  • Idempotency. Replaying a cursor before the watermark moves returns the same rows. Two workers that send that cursor at the same time observe the same rows. After the stream is caught up, polling that cursor again can return newly settled rows once the watermark advances.

Stay within limits

Each organization has a fixed window of 120 requests per 60 seconds by default. A request over that limit returns 429 Rate Limited with a retry-after header. The value is the whole seconds until the window resets, with a minimum of one second. The limit is checked before authentication. Wait for retry-after, then retry. limit is clamped to 1000. A larger value is reduced to 1000. When include_bodies=true, set limit to 50 or less. The default of 100 is above that maximum and returns 400 Validation Error.

Handle errors

Error bodies use the Problem+JSON shape, with code and detail.

Poll for new logs

Drain settled pages, store x-next-cursor from every response, and pause when x-has-more is false. Then poll again with the cursor you stored to pick up newly settled rows. Upsert each row on log_id. The in-memory set in the Python and JavaScript samples covers one process only. On 429, sleep for the retry-after header before the next request. The 30 second pause below is an example interval after the stream is caught up.

Connect Airbyte, Fivetran, or Singer

The cursor and caught-up signal are header strings, so an incremental connector can store them as a bookmark. Use format=native and read x-next-cursor from the header.

Configure Airbyte

Airbyte low-code CursorPagination reads the next token from a response header and sends it back as a request parameter. Map the export contract as follows, and set the stream primary key to log_id.
Configure WaitTimeFromHeader backoff on the Retry-After header for 429 responses.

Configure Fivetran or Singer

Singer taps and Fivetran custom connectors use the same header bookmark.
  • Singer. Persist x-next-cursor as the stream replication bookmark in STATE. On the next run, send that value as cursor. Use log_id as the record key so the target upserts idempotently.
  • Fivetran. Store x-next-cursor in connector state. Loop while x-has-more is true, and emit log_id as the primary key.

Review default limits

These operational defaults apply to the export stream. To request different values, contact your Customer Success team.

Next steps

View logs in AI Studio, or inspect a single agent.
  • Event logs: View, filter, and download request logs in AI Studio
  • Agent observability: Track session logs and performance metrics for individual agents