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
| Endpoint | Data |
|---|---|
GET /api/v1/exports/requests | Request logs: provider, model, tokens, cost, latency, status, and the user, session, and prompt version each request was tagged with. |
GET /api/v1/exports/traces | Traces with span count, total cost, total tokens, and duration. |
GET /api/v1/exports/anomalies | The 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/security | Requests 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
| Endpoint | Accepted parameters |
|---|---|
/exports/requests | format, from, to, limit, and every filter in Filters for requests. |
/exports/traces | format (csv or json), status (running, completed, or error), from, to, limit (up to 10,000). |
/exports/anomalies | format (csv or json) and projectId. |
/exports/security | format (csv or json). It returns the latest 10,000 flagged requests inside your retention window. |
Request export parameters
| Parameter | Default | Description |
|---|---|---|
format | csv | csv, jsonl, or json. CSV and JSONL stream; JSON is built in full before it is sent. See Formats below. |
from | None | ISO 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). |
to | None | ISO 8601 end time, inclusive. Without it, the export runs up to the newest row. |
limit | The format's cap | CSV 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.
| Parameter | Description |
|---|---|
projectId | Only requests from one project (UUID). |
provider | Provider id, for example openai, anthropic, or gemini. Exact match. |
model | Case-insensitive substring of the stored model name (e.g. mini). % and _ match themselves, not any character. |
providerKeyId | Only requests that used one provider key (UUID). |
promptVersionId | Only requests linked to one prompt version (UUID). |
userId | Only requests tagged with this end-user id (the x-spanlens-user header, or withUser() in the SDK). Exact match. |
sessionId | Only requests tagged with this session id (x-spanlens-session, or withSession()). Exact match. |
status | ok or success (below 400), 4xx, 5xx, error (400 and above), or all. |
truncated | true 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
| Format | Streamed? | Row cap | Best for |
|---|---|---|---|
csv | Yes | 1,000,000 | BI tools, spreadsheets, ad-hoc analysis. Default. |
jsonl | Yes | 1,000,000 | Pipelines that preserve typing (jq, pandas.read_json(lines=True), BigQuery, ClickHouse). One JSON object per line, newline-delimited. |
json | No, buffered | 10,000 | Wrapper 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.
| Endpoint | Example filename |
|---|---|
/exports/requests | spanlens-requests-2026-05-15.csv |
/exports/traces | spanlens-traces-2026-05-15.csv |
/exports/anomalies | spanlens-anomalies-2026-05-15.csv |
/exports/security | spanlens-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.
| Column | Description |
|---|---|
id | Unique request ID |
project_id | Project this request belongs to |
provider | Provider id, for example openai or anthropic |
model | Dated variant returned by the provider (e.g. gpt-4o-mini-2024-07-18) |
prompt_tokens | Input token count (gross, including cached portion) |
completion_tokens | Output token count |
total_tokens | prompt + completion |
cost_usd | Calculated cost in USD. Empty if the model is not in the price table. |
latency_ms | Provider time (ms): from sending the request to the provider until its response headers arrive (time to first byte for a stream) |
status_code | HTTP status code returned by the provider |
error_message | Error string. Empty for successful requests. |
trace_id | Linked trace ID. Empty if the call was not made inside an SDK observe(). |
created_at | When the request arrived at the proxy (ISO 8601 UTC) |
user_id | End-user id sent with the request. Empty when none was sent, or when the request used x-spanlens-log-body: none. |
session_id | Session id sent with the request. Empty in the same cases as user_id. |
prompt_version_id | Prompt version the request was linked to. Empty when it was not linked to one. |
CSV columns, traces
| Column | Description |
|---|---|
id | Unique trace ID |
project_id | Project this trace belongs to |
name | Trace name (specified in the SDK) |
status | running, completed, or error |
error_message | Error string. Empty for successful traces. |
duration_ms | First span start to last span end (ms) |
total_cost_usd | Sum of costs across all requests in the trace (USD) |
total_tokens | Sum of tokens across all requests in the trace |
span_count | Number of spans in the trace |
started_at | Trace start time (ISO 8601 UTC) |
ended_at | Trace end time (ISO 8601 UTC) |
created_at | When the row was saved to the database (ISO 8601 UTC) |
curl examples
CSV download
# 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)
# 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)
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)
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/requestsgoes up to 1,000,000 rows on the streamed formats (csv,jsonl) and 10,000 onjson. 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 withfromandtoand 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
429with aRetry-Afterheader and an error whosedetails.sourceisexport_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.