gen_ai.* attributes), alongside Writer-specific attributes under writer.*. Set format=native to receive a Writer-native JSON shape instead. The agent-feedback endpoint only emits native JSON.
Prerequisites
Before you export logs, make sure you have:- A Writer API key. See instructions in the API keys guide.
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.organizationId returns 403 Forbidden. The gateway or API-key verifier can also reject access; body export still requires an authorized organization-scoped API key.
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 withorder=tail and neither cursor nor start_date. The stream starts at the current time, with no backfill.
Backfill from a date
Passstart_date with order=tail to request retained records from that ingestion time, then continue polling 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. Available history depends on source telemetry and completed backfills; this does not guarantee full historical coverage.
Stay inside the retention window
Astart_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 storex-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. Call filters qualify agent runs through matching LLM calls.
For
order=browse, provide both start_date and end_date, or omit both. Providing only one date returns 400 Validation Error.
For order=browse, continue with both cursor_started_at_ms and cursor_log_id. Read them from pagination.next_cursor with format=native, or from x-next-cursor-started-at-ms and x-next-cursor-log-id with format=otlp. The opaque cursor is tail-only, and the two-part browse parameters are rejected in tail mode.
Read the response
Tail responses put the resume token and the caught-up signal in headers. The body shape depends onformat.
Read response headers
Use these headers to resume and decide when the settled backlog is drained. Nativepagination.has_more matches x-has-more. In tail mode, native pagination.next_cursor is null; read the resume token from x-next-cursor.
Tail responses omit total_count, total_count_is_capped, and total-count headers. Browse-only bounded counts appear in those native fields, or in x-total-count and x-total-count-is-capped for OTLP. These counts are not tail catch-up signals.
Read a native body
Withformat=native, the body is a JSON object containing heterogeneous rows. result[].inserted_at_ms and result[].log_id are the two components of the tail ordering axis. Upsert by log_id, retaining the revision with the newest inserted_at_ms. This example shows selected fields from an LLM call and an agent run:
Distinguish calls and runs
Userecord_type to distinguish llm_call from agent_run. The fields describe different units of work:
Run rows describe terminal execution legs with a qualifying LLM call. Still-running and tool-only legs are excluded. Runs have no inference model, token, or body values.
Interpret run duration
A run’sduration_ms is lifecycle timing for that execution leg. It is not:
- A sum of call latency
- A session total
- Exact user-visible latency
null; do not infer a duration or replace it with zero. Parent, child, and resumed legs remain separate records. There is no session-total record.
Correlate records
Correlate records usingagent_run_id, root_agent_run_id, request_id, and session_id where present. A root ID does not guarantee a user turn or identify a subagent role. Uncorrelated calls remain exportable.
Late lifecycle events or call correlation can revise the same run identity. Accept later revisions rather than permanently discarding known IDs. See Apply delivery guarantees.
Handle unavailable bodies
When you requestinclude_bodies=true, an unavailable call body is null with a body_unavailable_reason. The reason codes distinguish:
- Capture:
not_captured - Processing or retrieval:
pending,missing,failed - Authorization:
ekm_denied,unauthorized - Retention or deletion:
expired,deleted - Other unavailability:
unavailable
body: null without hydration or an unavailable-body reason.
Interpret watermark request observations
Where available, LLM-call records include nullable observations of the actual outbound watermark request:
Missing historical fields remain unknown, not disabled. An omitted parameter
object does not identify why it was omitted. Agent-run rows have no corresponding
provider attempt and carry null facts. These six request observations do not
establish that the output was watermarked; no application-status field is
reported. OTLP omits unknown
writer.watermark_* attributes. Hash keys, wire
messages/codes, labels and the original watermark object are excluded, including
from protected bodies. Existing access and masking rules remain unchanged.
Existing model-visibility rules can also withhold watermark metadata. In that
case all six facts are null, including the support and parameter-presence
booleans. A null value is not evidence that watermarking was unsupported,
disabled or absent. These metadata rules do not expand access to protected
request or response content.
Read an OTLP body
Withformat=otlp (the default), the body is an OTLP/JSON payload. Choose the body shape with signal:
Pagination stays in the response headers. In tail mode, use
x-next-cursor, x-has-more, and x-tail-watermark.
Agent runs become writer.agent.run spans with kind INTERNAL, or log records with event name writer.agent.run.terminal.
Spans end at the terminal event time and start at that time minus the run duration. Unknown duration produces a point span without writer.agent.run.duration_ms; that does not mean the run took zero time.
Run spans and logs expose stable writer.record.id and correlation attributes. They do not include inference model, token, or body attributes.
Follow the OpenTelemetry semantic conventions for generative AI
Attributes on LLM-call spans follow the OpenTelemetry semantic conventions for generative AI where one exists, withgen_ai.* names such as gen_ai.request.model, gen_ai.response.model, gen_ai.usage.input_tokens, and gen_ai.usage.output_tokens. Prompt and response bodies, when included, follow the OpenTelemetry generative AI message schemas on gen_ai.input.messages and gen_ai.output.messages. These conventions are still in development; expect additive changes over time.
Writer-specific attributes keep the writer. prefix. Agent-run spans use the span name writer.agent.run, log records use the event name writer.agent.run.terminal, and both carry the stable writer.record.id and, when known, writer.agent.run.duration_ms.
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 thecursor parameter.
Apply delivery guarantees
Tail mode is an ordered revision feed. The settlement delay does not make a record immutable, and a cursor identifies a position rather than a fixed snapshot.- 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_atis at or beforex-tail-watermark. The watermark is now minus a 5-minute settle lag. A row with an older event time and a newerinserted_atarrives when the watermark passes thatinserted_at. Settlement is not record finality. - Revision-aware upserts. Retries, overlapping workers, and later revisions can deliver the same
log_idagain. Upsert onlog_id, retaining the newestinserted_at_ms, so an older replay cannot overwrite a newer revision. Never permanently discard an ID because you have already seen it. - Replay. Replaying a cursor can observe newer rows at or after that position. After the stream is caught up, polling again can return newly settled rows. Missing source telemetry is not reconstructed, and the feed does not promise lossless real-time delivery.
Stay within limits
Each organization has a fixed window of 120 requests per 60 seconds by default. A request over that limit returns429 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
Service error bodies use the Problem+JSON shape, withcode and detail. Gateway authentication errors can use a different envelope: an unauthenticated public request can return 401 with generic JSON rather than the service Problem+JSON shape. Handle the HTTP status even when the body does not contain a service error code.
Poll for new logs
Poll with the returnedx-next-cursor to pick up newly settled rows and revisions:
- Drain and pause: Drain settled pages until
x-has-moreisfalse, then pause before polling again. - Upsert revisions: Upsert by
log_id, retaining the newestinserted_at_ms. A primary key alone does not prevent older retries from overwriting newer revisions. - Persist cursor: Save the cursor only after the complete page is successfully processed. Restart from that saved cursor. These examples keep the cursor in memory only.
- Handle rate limits: On
429, wait forretry-afterbefore retrying. The 30-second pause in these examples applies after the stream is caught up.
Export agent feedback
Stream thumbs up and thumbs down ratings, including optional comments, with the same authentication, cursor, rate limit, and error contract as the logs export. The feedback endpoint returns native JSON only. URL:GET https://api.writer.com/api/observability/v1/organization/{organizationId}/agent-feedback/export
Omit cursor and start_date to start at the current time. Only newly settled rows stream forward.
Set feedback query parameters
The feedback endpoint accepts only the shared paging parameters. It has no filters, noformat option, and no total counts.
Both orders page with the opaque
cursor. A browse continuation takes its window from the cursor and does not require dates.
Cursors are not interchangeable between endpoints. A cursor issued by the
logs export returns
400 Validation Error on the feedback endpoint, and a
browse cursor presented with order=tail (or the reverse) also returns
400.Read a feedback response
Every response setsx-next-cursor and x-has-more. Tail responses also set x-tail-watermark. The body is always native JSON.
Handle re-ratings
A user can change their rating or edit the comment. Each change emits a new revision of the samefeedback_id with a newer emitted_at_ms. The two orders surface revisions differently:
- Browse collapses to one row per
feedback_id—the revision with the latestemitted_at_ms—newest-first by(emitted_at, feedback_id)within[start_date, end_date]. Feedback first given in one window and re-rated in a later window appears only in the later window. - Tail returns every revision as a row, ordered
(inserted_at ASC, feedback_id ASC). A re-rated message is redelivered as a second row with the samefeedback_idand a newerinserted_at_ms. Upsert byfeedback_idand retain the newestinserted_at_msto match the latest-rating view browse provides.
x-tail-watermark) is the only completeness boundary.
Connect Airbyte, Fivetran, or Singer
The cursor and caught-up signal are header strings, so an incremental connector can store them as a bookmark. Useformat=native and read x-next-cursor from the header. Emit every revision and configure the destination to upsert on log_id, retaining the newest inserted_at_ms. Save the bookmark only after the destination processes the page.
Configure Airbyte
Airbyte’s cursor pagination can read a response-header token and inject it into the next request. This handles pages within a sync, not the persisted bookmark needed to resume this feed across scheduled syncs. Use these mappings for pagination within a sync:
For cross-sync resumption, implement a custom incremental source with the Airbyte Python CDK. The pagination mappings alone are not a complete connector. The source must:
- Restore the saved opaque
x-next-cursorfrom stream state and send it ascursoron the first request of each sync. Omitstart_datewhen resuming. Without saved state, choose a start point as described in Choose a start point. - Emit every row in
result, including revisions of knownlog_idvalues, before emitting a state message for that page’sx-next-cursor. Do not advance state after a partially processed page. - Continue while
x-has-moreistrue. When it isfalse, retain the returnedx-next-cursorfor the next scheduled sync, even if the final page is empty. - Use Airbyte’s state protocol: a checkpoint becomes safe to resume from only after the destination acknowledges that all preceding records are committed. Emitting records or state from the source alone is not an acknowledgment.
inserted_at_ms for it. Airbyte’s incremental sync configuration is not a replacement for restoring this bookmark. Resumable full refresh is not cross-sync incremental state either: its state is deleted at the start of a new sync job.
Set the record primary key to log_id and separately configure revision-aware destination updates that retain the newest inserted_at_ms. The primary key alone does not enforce revision ordering. On 429, honor retry-after before retrying.
Configure Fivetran or Singer
Singer taps and Fivetran custom connectors use the same header bookmark.- Singer. Persist
x-next-cursoras the stream replication bookmark inSTATEafter the target processes the page. On the next run, send that value ascursor. Emit each revision withlog_idas the record key and retain the newestinserted_at_msin the target. - Fivetran. Loop while
x-has-moreistrue, emit every revision withlog_idas the primary key, and retain the newestinserted_at_msat the destination. Storex-next-cursorin connector state only after processing the page.
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 Control Center, or track organization-wide spend.- Event logs: View, filter, and search request logs in Control Center
- Usage and spend: Track organization-wide costs and token consumption