Traces
Ingest distributed traces (native or OTLP), and list/inspect sampled traces and their spans.
Traces are ingested as spans grouped by trace_id. Only a subset of ingested traces are kept for querying — see the sampling note below. There is no separate /traces/service-map endpoint.
POST /api/traces
Ingest a batch of spans in ScryWatch’s native format (up to 50 per request).
Auth: API key (Bearer) · Content-Type: application/json
Request body
| field | type | required | description |
|---|---|---|---|
| spans | array | yes | 1–50 span objects |
| spans[].trace_id | string | yes | Groups spans into one trace |
| spans[].span_id | string | yes | Unique span ID |
| spans[].name | string | yes | Span name |
| spans[].service | string | yes | Service name |
| spans[].start_time | number | yes | Epoch ms |
| spans[].end_time | number | yes | Epoch ms |
| spans[].parent_span_id | string | no | Parent span ID; omit/null for a root span |
| spans[].status | string | no | ok | error. Defaults to ok |
| spans[].attributes | object | no | Arbitrary JSON. http.method, http.url, http.status_code on the root span populate the trace’s HTTP fields |
| spans[].events | array | no | Arbitrary JSON, stored as-is |
| spans[].user_id | string | no | User identifier (read from the root span) |
| spans[].session_id | string | no | Session identifier (read from the root span) |
| spans[].environment | string | no | e.g. production (read from the root span) |
The root span is the one with no parent_span_id (falls back to the first span in the batch if every span has a parent). Its fields populate the parent trace row; duration_ms on both the trace and each span is computed server-side as end_time - start_time. A trace’s status is error if any of its spans has status: "error".
Response 202
{ "status": 202, "inserted": 4 }
Errors: 400 validation errors, shape { "status": 400, "error": "<message>" }:
"spans array is required and must not be empty""batch size must not exceed 50 spans""each span requires trace_id, span_id, name, service, start_time, end_time"
Common auth responses (401) apply — see Authentication. Unlike /api/ingest, this endpoint does not enforce usage limits or rate limiting, so 402 and 429 don’t apply despite being API-key authenticated.
POST /api/traces/otlp
Ingest spans in OpenTelemetry OTLP/JSON format (resourceSpans → scopeSpans → spans). The payload is normalized into the same span shape as POST /api/traces and passed through the same ingest path — same response and error shapes as above, same 401-only auth behavior.
Auth: API key (Bearer) · Content-Type: application/json
Request body: a standard OTLP JSON traces payload. service.name is read from each resourceSpans[].resource.attributes; span status.code === 2 maps to status: "error", anything else to "ok". startTimeUnixNano/endTimeUnixNano are converted to epoch ms.
Response 202 — same shape as POST /api/traces.
GET /api/traces
List sampled traces. Also available at /api/orgs/:orgSlug/projects/:projectSlug/traces.
Auth: API key (Bearer) or session cookie
Query params
| param | type | description |
|---|---|---|
| service | string | Exact match on service |
| status | string | Exact match on status (ok | error) |
| environment | string | Exact match on environment |
| from | number | Epoch ms, matched against trace start_time |
| to | number | Epoch ms, matched against trace start_time |
| min_duration | number | Minimum duration_ms |
| max_duration | number | Maximum duration_ms |
| limit | number | Max results. Default 50 |
| offset | number | Pagination offset. Default 0 |
Response 200
{
"traces": [
{
"id": "trace_01hxyz",
"project_id": "proj_01abc",
"name": "POST /checkout",
"service": "checkout-api",
"start_time": 1720392000000,
"end_time": 1720392000240,
"duration_ms": 240,
"status": "ok",
"http_method": "POST",
"http_url": "/checkout",
"http_status_code": 200,
"user_id": "user_42",
"session_id": "sess_abc123",
"environment": "production",
"sampled": 1,
"created_at": 1720392000300
}
],
"total": 87
}
Sampling note: ingested traces are written unsampled (sampled: 0) and are not returned by this endpoint until a background job evaluates them (roughly 60 seconds after ingest, to allow all of a trace’s spans to arrive). A trace is kept (sampled: 1) if any of its spans is status: "error", or if its duration is at least 2 seconds; otherwise it’s purged. limit/offset are not capped server-side.
GET /api/traces/:traceId
Fetch one trace and all of its spans. Also available at /api/orgs/:orgSlug/projects/:projectSlug/traces/:traceId.
Auth: API key (Bearer) or session cookie
Response 200
{
"trace": { "id": "trace_01hxyz", "...": "same shape as one item in GET /api/traces" },
"spans": [
{
"id": "span_01",
"trace_id": "trace_01hxyz",
"project_id": "proj_01abc",
"parent_span_id": null,
"name": "POST /checkout",
"service": "checkout-api",
"start_time": 1720392000000,
"end_time": 1720392000240,
"duration_ms": 240,
"status": "ok",
"attributes": "{\"http.method\":\"POST\"}",
"events": null,
"has_error": 0
}
]
}
Spans are returned ordered by start_time ascending. attributes/events are stored and returned as JSON-encoded strings (not parsed objects). This lookup is not restricted to sampled traces — an unsampled or not-yet-evaluated trace can still be fetched directly by ID.
Errors: 404 — { "error": "Trace not found" }
Common responses (401; 402 and 429 don’t apply) — see Authentication.