Data Export

Download request logs, traces, anomaly results, and security flags as CSV, JSONL, or JSON. The request export streams CSV and JSONL out of a database cursor and reads that cursor only as fast as your client downloads, so the server holds a small, fixed buffer no matter how many rows you ask for. Load the files into Pandas, BigQuery, Redash, Metabase, or your own pipeline.

Endpoints

EndpointData
GET /api/v1/exports/requestsRequest logs: provider, model, tokens, cost, latency, status, and the user, session, and prompt version each request was tagged with.
GET /api/v1/exports/tracesTraces with span count, total cost, total tokens, and duration.
GET /api/v1/exports/anomaliesThe current anomaly check: one row for each provider, model, and metric (latency, cost, or error rate) whose last hour sits more than 3σ away from the previous 7 days.
GET /api/v1/exports/securityRequests flagged for PII or prompt injection, newest first.

All endpoints require a signed-in dashboard session. Send Authorization: Bearer <supabase_access_token> with each request. Spanlens API keys (sl_live_...) are not accepted on these endpoints.

Parameters by endpoint

EndpointAccepted parameters
/exports/requestsformat, from, to, limit, and every filter in Filters for requests.
/exports/tracesformat (csv or json), status (running, completed, or error), from, to, limit (up to 10,000).
/exports/anomaliesformat (csv or json) and projectId.
/exports/securityformat (csv or json). It returns the latest 10,000 flagged requests inside your retention window.

Request export parameters

ParameterDefaultDescription
formatcsvcsv, jsonl, or json. CSV and JSONL stream; JSON is built in full before it is sent. See Formats below.
fromNoneISO 8601 start time, for example 2026-05-01T00:00:00Z. Without it, the export starts at the oldest row your plan's retention window keeps (Free 14 days, Pro 90 days, Team 365 days).
toNoneISO 8601 end time, inclusive. Without it, the export runs up to the newest row.
limitThe format's capCSV and JSONL: 1 to 1,000,000. JSON: 1 to 10,000. Values outside the range are clamped to it.

Filters for requests

GET /api/v1/exports/requests takes the same filters as GET /api/v1/requests, so you can take a filtered list query, change the path, and export exactly those rows.

ParameterDescription
projectIdOnly requests from one project (UUID).
providerProvider id, for example openai, anthropic, or gemini. Exact match.
modelCase-insensitive substring of the stored model name (e.g. mini). % and _ match themselves, not any character.
providerKeyIdOnly requests that used one provider key (UUID).
promptVersionIdOnly requests linked to one prompt version (UUID).
userIdOnly requests tagged with this end-user id (the x-spanlens-user header, or withUser() in the SDK). Exact match.
sessionIdOnly requests tagged with this session id (x-spanlens-session, or withSession()). Exact match.
statusok or success (below 400), 4xx, 5xx, error (400 and above), or all.
truncatedtrue for streams that were cut off at the stream deadline, false for streams that finished, or all.

A malformed UUID or date, or a status or truncated value not listed above, returns 400 with a VALIDATION_FAILED error before any data is sent.

Formats, when to pick each

FormatStreamed?Row capBest for
csvYes1,000,000BI tools, spreadsheets, ad-hoc analysis. Default.
jsonlYes1,000,000Pipelines that preserve typing (jq, pandas.read_json(lines=True), BigQuery, ClickHouse). One JSON object per line, newline-delimited.
jsonNo, buffered10,000Wrapper object { exported_at, count, data: [...] } for code that wants a single parseable response. Use jsonl for anything larger.

How streamed exports behave

  • They go at your pace. The server fetches rows from the database only as fast as your client reads the response. If your client pauses, the server pauses with it and holds at most about 1 MiB of encoded rows plus one database batch, so its memory use does not grow with limit.
  • Early failures are ordinary errors. The server waits for the first row before it answers, so a query that cannot start returns a JSON error with a 5xx status instead of an empty or broken file.
  • Late failures abort the download. Once rows are flowing, the status line has already said 200. If the export fails after that, the server closes the connection without ending the file properly. curl exits with a non-zero status, and fetch, Pandas, and browsers report the download as failed instead of keeping a truncated file that looks complete.
  • No caching. Streamed responses are sent with Cache-Control: no-store.

File names

The response includes a Content-Disposition header with a date-stamped filename.

EndpointExample filename
/exports/requestsspanlens-requests-2026-05-15.csv
/exports/tracesspanlens-traces-2026-05-15.csv
/exports/anomaliesspanlens-anomalies-2026-05-15.csv
/exports/securityspanlens-security-2026-05-15.csv

CSV columns, requests

Columns always come in this order. New columns are only ever added at the end, so scripts that read columns by position keep working.

ColumnDescription
idUnique request ID
project_idProject this request belongs to
providerProvider id, for example openai or anthropic
modelDated variant returned by the provider (e.g. gpt-4o-mini-2024-07-18)
prompt_tokensInput token count (gross, including cached portion)
completion_tokensOutput token count
total_tokensprompt + completion
cost_usdCalculated cost in USD. Empty if the model is not in the price table.
latency_msProvider time (ms): from sending the request to the provider until its response headers arrive (time to first byte for a stream)
status_codeHTTP status code returned by the provider
error_messageError string. Empty for successful requests.
trace_idLinked trace ID. Empty if the call was not made inside an SDK observe().
created_atWhen the request arrived at the proxy (ISO 8601 UTC)
user_idEnd-user id sent with the request. Empty when none was sent, or when the request used x-spanlens-log-body: none.
session_idSession id sent with the request. Empty in the same cases as user_id.
prompt_version_idPrompt version the request was linked to. Empty when it was not linked to one.

CSV columns, traces

ColumnDescription
idUnique trace ID
project_idProject this trace belongs to
nameTrace name (specified in the SDK)
statusrunning, completed, or error
error_messageError string. Empty for successful traces.
duration_msFirst span start to last span end (ms)
total_cost_usdSum of costs across all requests in the trace (USD)
total_tokensSum of tokens across all requests in the trace
span_countNumber of spans in the trace
started_atTrace start time (ISO 8601 UTC)
ended_atTrace end time (ISO 8601 UTC)
created_atWhen the row was saved to the database (ISO 8601 UTC)

curl examples

CSV download

bash
# Request logs, specific date range, GPT-4o only, CSV
curl --fail "https://api.spanlens.io/api/v1/exports/requests?from=2026-05-01T00:00:00Z&to=2026-05-15T23:59:59Z&provider=openai&model=gpt-4o&format=csv" \
  -H "Authorization: Bearer $SUPABASE_ACCESS_TOKEN" \
  -o spanlens-requests.csv

# One customer's requests, errors only
curl --fail "https://api.spanlens.io/api/v1/exports/requests?userId=customer-a&status=error&format=csv" \
  -H "Authorization: Bearer $SUPABASE_ACCESS_TOKEN" \
  -o customer-a-errors.csv

# Traces, last 7 days, JSON
curl --fail "https://api.spanlens.io/api/v1/exports/traces?from=2026-05-08T00:00:00Z&format=json" \
  -H "Authorization: Bearer $SUPABASE_ACCESS_TOKEN" \
  -o spanlens-traces.json

# Current anomaly check, CSV
curl --fail "https://api.spanlens.io/api/v1/exports/anomalies" \
  -H "Authorization: Bearer $SUPABASE_ACCESS_TOKEN" \
  -o spanlens-anomalies.csv

# Flagged requests, CSV
curl --fail "https://api.spanlens.io/api/v1/exports/security" \
  -H "Authorization: Bearer $SUPABASE_ACCESS_TOKEN" \
  -o spanlens-security.csv

--fail makes curl exit non-zero on a 4xx or 5xx status. A download that breaks off partway through already exits non-zero without it.

JSONL download (large exports)

bash
# One million rows, streamed. Pipe straight into jq for filtering.
curl --fail "https://api.spanlens.io/api/v1/exports/requests?format=jsonl&from=2026-01-01T00:00:00Z&limit=1000000" \
  -H "Authorization: Bearer $SUPABASE_ACCESS_TOKEN" \
  | jq -c 'select(.cost_usd != null and .cost_usd > 0.01)' \
  > expensive-requests.jsonl

# Each line is a self-contained JSON object:
# {"id":"req_xxx","provider":"openai","model":"gpt-4o-mini-2024-07-18",...}
# {"id":"req_yyy","provider":"anthropic","model":"claude-sonnet-4-5",...}

JSON download (small, wrapped)

bash
curl --fail "https://api.spanlens.io/api/v1/exports/requests?format=json&limit=1000" \
  -H "Authorization: Bearer $SUPABASE_ACCESS_TOKEN"

# Response shape (buffered, capped at 10,000 rows):
# {
#   "exported_at": "2026-05-19T08:30:00.000Z",
#   "count": 1000,
#   "data": [
#     {
#       "id": "req_xxx",
#       "project_id": "proj_xxx",
#       "provider": "openai",
#       "model": "gpt-4o-mini-2024-07-18",
#       "prompt_tokens": 512,
#       "completion_tokens": 128,
#       "total_tokens": 640,
#       "cost_usd": 0.000096,
#       "latency_ms": 843,
#       "status_code": 200,
#       "error_message": null,
#       "trace_id": null,
#       "created_at": "2026-05-15T09:00:00.000Z",
#       "user_id": "customer-a",
#       "session_id": null,
#       "prompt_version_id": null
#     },
#     ...
#   ]
# }

BI tool tips

Pandas (Python)

python
import pandas as pd

token = "YOUR_SUPABASE_ACCESS_TOKEN"

# Small / medium, CSV, single response.
url = "https://api.spanlens.io/api/v1/exports/requests?from=2026-05-01T00:00:00Z&format=csv"
df = pd.read_csv(url, storage_options={"Authorization": f"Bearer {token}"})

# Million-row pipeline, JSONL, streamed line-by-line. Pandas reads it in
# chunks so peak memory stays bounded.
url = "https://api.spanlens.io/api/v1/exports/requests?format=jsonl&limit=1000000"
chunks = pd.read_json(url, lines=True, chunksize=50_000,
                      storage_options={"Authorization": f"Bearer {token}"})
totals = pd.concat(chunk.groupby("model")["cost_usd"].sum() for chunk in chunks).groupby(level=0).sum()
print(totals)

Excel

Download the .csv file with curl, then import it into Excel via Data → From Text/CSV. The created_at column is an ISO 8601 string. Convert it with DATEVALUE + TIMEVALUE or Power Query's date/time type conversion before using it in pivot tables.

Exporting from the dashboard

The Export button on the Requests, Traces, Anomalies, and Security pages calls these endpoints and saves the result as CSV or JSON. The browser puts the whole file together in memory before it saves it, so for very large exports use curl or a script instead. The button shows how much has arrived while a large file downloads. If a download fails partway through, the dashboard shows the error and saves nothing, and Retry runs the same export again.

Limitations

  • Row caps. /exports/requests goes up to 1,000,000 rows on the streamed formats (csv, jsonl) and 10,000 on json. The other endpoints (/traces, /security, /anomalies) stay at 10,000. An export that reaches its cap simply ends there, so if the row count equals the cap, split the time range with from and to and export each part. Multi-GB exports with completion emails or S3 pre-signed URLs are on the roadmap; contact support if you need one sooner.
  • Five minutes per export. On the hosted service each export runs inside a single request, and the whole download has to finish within that time: the database query behind a streamed export is stopped after 290 seconds, counting the time spent waiting for your client to read, and the request itself ends at 300. Over a slow connection a million-row CSV may not finish in time, so export smaller date ranges instead.
  • One streamed export at a time per server. A streamed export keeps a database connection open for as long as it downloads. If another one is already running on the server that picks up your request, you get 429 with a Retry-After header and an error whose details.source is export_concurrency. Wait a few seconds and send it again. In a script, run exports one after another rather than in parallel.
  • request_body / response_body are not included. Body content is excluded for security and size reasons. View individual request bodies in the /requests detail view or via GET /api/v1/requests/:id.
  • Not real-time. Exports are a point-in-time snapshot. In-flight streaming requests or async logging delays may mean the most recent rows are not yet present.
  • Rate limit. Export calls count toward the dashboard API limit of 120 requests per minute per access token. Space out calls in bulk batch pipelines.
  • Plan retention applies. The window of accessible rows is bounded by your plan's log retention (Free 14d / Pro 90d / Team 365d). Older rows are unavailable even via from.

Related: Requests, Traces, Anomalies, Security.