Traces API
All endpoints verified working via curl. Document updated only after curl confirmation.
POST /api/v1/traces
Ingests a batch of OTel-shaped traces. Accepts EITHER a session cookie, a
personal API key, or a virtual key (requireAnyAuthOrVirtualKey — no role gate).
Each trace's spans form a tree via spanId/parentSpanId. Omit traceId to
mint a new trace; supply a UUID to append spans to an existing one.
A supplied traceId that does not exist yet is created under that id rather
than rejected, so a client may mint its own UUID and use it immediately — that
is what lets a non-blocking report (traces.ingest(..., wait=False) in the
SDKs) hand back a usable trace id without waiting for a response. Verified:
posting a freshly generated UUID returns it in traceIds, and
GET /traces/<that id> reads the trace back.
curl -X POST $ACRUXCORE_BASE_URL/traces \
-H "Authorization: Bearer $ACRUXCORE_API_KEY" -H "Content-Type: application/json" \
-d '{"traces":[{"traceId":"db41e83d-c1e9-48c8-9fa4-67907baebef0","name":"client-minted-id","spans":[{"spanId":"s1","name":"vector-search","kind":"retrieval","status":"ok","startTime":"2026-09-06T12:35:00Z","endTime":"2026-09-06T12:35:00.100Z"}]}]}'
{ "accepted": 1, "traceIds": ["db41e83d-c1e9-48c8-9fa4-67907baebef0"] }
A trace id supplied by a caller in another team is a 404, not a cross-tenant append.
- curl
- TypeScript
- Python
curl -X POST $ACRUXCORE_BASE_URL/traces \
-H "Authorization: Bearer $ACRUXCORE_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"traces": [
{
"name": "support-agent-run",
"sessionId": "session-abc-123",
"spans": [
{"spanId": "s1", "name": "gpt-4o-mini", "kind": "llm", "startTime": "2026-07-04T10:00:00Z", "endTime": "2026-07-04T10:00:01.200Z", "model": "gpt-4o-mini", "provider": "openai", "usage": {"promptTokens": 120, "completionTokens": 40, "totalTokens": 160}, "costUsd": 0.0000234},
{"spanId": "s2", "parentSpanId": "s1", "name": "search_docs", "kind": "tool", "startTime": "2026-07-04T10:00:00.100Z", "endTime": "2026-07-04T10:00:00.300Z", "status": "ok"},
{"spanId": "s3", "parentSpanId": "s1", "name": "custom-post-process", "kind": "other", "startTime": "2026-07-04T10:00:01.000Z", "endTime": "2026-07-04T10:00:01.150Z", "status": "ok", "attributes": {"tokensSaved": 12}}
]
}
]
}'
const { traceId } = await hub.traces.ingest({
name: 'support-agent-run',
sessionId: 'session-abc-123',
spans: [
{ spanId: 's1', name: 'gpt-4o-mini', kind: 'llm', startTime: '2026-07-04T10:00:00Z', endTime: '2026-07-04T10:00:01.200Z', model: 'gpt-4o-mini', provider: 'openai', usage: { promptTokens: 120, completionTokens: 40, totalTokens: 160 }, costUsd: 0.0000234 },
{ spanId: 's2', parentSpanId: 's1', name: 'search_docs', kind: 'tool', startTime: '2026-07-04T10:00:00.100Z', endTime: '2026-07-04T10:00:00.300Z', status: 'ok' },
],
});
result = await hub.traces.ingest({
'name': 'support-agent-run',
'sessionId': 'session-abc-123',
'spans': [
{'spanId': 's1', 'name': 'gpt-4o-mini', 'kind': 'llm', 'startTime': '2026-07-04T10:00:00Z', 'endTime': '2026-07-04T10:00:01.200Z', 'model': 'gpt-4o-mini', 'provider': 'openai', 'usage': {'promptTokens': 120, 'completionTokens': 40, 'totalTokens': 160}},
{'spanId': 's2', 'parentSpanId': 's1', 'name': 'search_docs', 'kind': 'tool', 'startTime': '2026-07-04T10:00:00.100Z', 'endTime': '2026-07-04T10:00:00.300Z', 'status': 'ok'},
],
})
Response (status 200):
{
"accepted": 3,
"traceIds": ["a74e5dee-f4a6-482f-88cc-89fa332b7cc5"]
}
accepted is the total span count across the whole batch (not trace count).
traceIds has one entry per input trace, in order, so index i of traceIds
corresponds to index i of the request's traces array.
Appending spans to an existing trace
Supplying traceId appends to a trace already owned by the caller's team
(or creates it with that id if absent for the team). Cross-team traceIds are
rejected — see the 404 case below.
curl -X POST $ACRUXCORE_BASE_URL/traces \
-H "Authorization: Bearer $ACRUXCORE_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"traces": [
{
"traceId": "a74e5dee-f4a6-482f-88cc-89fa332b7cc5",
"spans": [
{"spanId": "s4", "parentSpanId": "s1", "name": "followup-tool", "kind": "tool", "startTime": "2026-07-04T10:00:01.300Z", "endTime": "2026-07-04T10:00:01.400Z", "status": "ok"}
]
}
]
}'
Response (status 200):
{
"accepted": 1,
"traceIds": ["a74e5dee-f4a6-482f-88cc-89fa332b7cc5"]
}
Error responses
No Authorization header (status 401):
{ "error": { "code": "UNAUTHORIZED", "message": "Authentication required." } }
Empty spans array (status 400) — spans must have at least 1 element:
{ "error": { "code": "VALIDATION_ERROR", "message": "Array must contain at least 1 element(s)" } }
Duplicate spanId within one trace (status 400):
{ "error": { "code": "VALIDATION_ERROR", "message": "Duplicate spanId \"dup1\" within a trace." } }
parentSpanId does not match any spanId in this trace's batch or prior stored spans (status 400):
{ "error": { "code": "INVALID_SPAN_PARENT", "message": "parentSpanId \"ghost\" does not reference a known span in this trace." } }
Supplied traceId exists but belongs to another team (status 404) — same message as an unknown id, so the endpoint never confirms whether a foreign trace exists:
{ "error": { "code": "NOT_FOUND", "message": "Trace not found." } }
A span's promptVersionId that is not a version of one of your own prompts (status 404)
— again the same message whether the id belongs to another team or to nobody:
{ "error": { "code": "NOT_FOUND", "message": "Prompt version not found." } }
This one is checked for the whole batch before any trace is written, so a rejected batch
leaves nothing behind (unlike the traceId case — see the transaction note below).
Batch exceeds the 200-span-per-request cap (status 413):
{ "error": { "code": "PAYLOAD_TOO_LARGE", "message": "Batch has 201 spans; the per-request limit is 200." } }
Design note: ingestion transaction scope is per-trace
Each trace in the traces array is resolved and appended inside its own
prisma.$transaction, run sequentially in a loop — there is no single
transaction wrapping the whole batch. In a multi-trace batch, an earlier trace
can commit successfully even if a later trace in the same request fails.
Verified: posting a batch of 2 traces where trace 1 is new/valid and trace 2
references another team's traceId (→ 404) returns the 404 for the whole
HTTP response, but trace 1 was already committed:
curl -X POST $ACRUXCORE_BASE_URL/traces \
-H "Authorization: Bearer $ACRUXCORE_API_KEY" -H "Content-Type: application/json" \
-d '{
"traces": [
{"name": "partial-commit-1", "spans": [{"spanId": "pc1", "name": "a", "startTime": "2026-07-04T10:00:00Z"}]},
{"traceId": "<another-teams-trace-id>", "spans": [{"spanId": "pc2", "name": "b", "startTime": "2026-07-04T10:00:00Z"}]}
]
}'
Response (status 404):
{ "error": { "code": "NOT_FOUND", "message": "Trace not found." } }
select id, name, team_id from traces where name = 'partial-commit-1';
-- id | name | team_id
-- c6350930-9324-4ea9-8cee-15ba2d8dc881 | partial-commit-1 | 76ee7c44-b305-4730-9755-a194d3d420e9
partial-commit-1 is present in the database even though the request that
created it returned an error. Callers that need all-or-nothing semantics
across multiple traces must split them into separate requests and handle
partial failure themselves — this endpoint does not provide batch atomicity
across traces (only within a single trace's spans).
Trace query, detail, and reverse prompt-version lineage
Read-only, team-scoped. All three routes below accept a session cookie or a
personal API key (requireAnyAuth — no role gate, any team member can read).
Everything runs off the single spans table (no UNION): span-level filters on
GET /traces resolve via EXISTS (SELECT 1 FROM spans …) sub-queries, so each
matching trace is returned exactly once even when several of its spans match.
GET /api/v1/traces
Paginated trace list, newest first. Query params (all optional):
from,to— ISO 8601, filtered againsttraces.created_at; default window is the last 30 days.status—ok|error|unset.model,session_id,prompt_version_id— exact match against any span in the trace (prompt_version_idis the UUID fromPOST /prompts/:id/versions).prompt_id— any version of that prompt. Use this when you know the prompt but not which version was live at the time.min_latency_ms,min_cost_usd,min_tokens— inclusive lower bounds.error_type— what kind of failure a span recorded:transport,http_status,tool_declared,schema_mismatch,transformorprovider_error. Any other value is a 400.error_code— the slug a tool's own owner declared, e.g.location_not_found. Free text, matched exactly; blank is a 400.has_warning—trueorfalse. A warned trace is usuallystatus=ok, so no status filter can find it. Omit the param to mean "either".q— free-text match over trace/span name, span attributes, and the captured input and output; blank/whitespace is treated as "no filter".q_in— narrowsqtoinput,output, orname. Defaults toall.page(default 1),limit(default 20, max 100).
Every row carries hasWarning — true when any span on that trace recorded a
warning attribute, using the same predicate as the has_warning filter. The
field is on the row so a reader who was not already looking for warned runs still
sees them; a filter only helps someone who suspects the problem is there.
- curl
- TypeScript
- Python
curl -H "Authorization: Bearer $ACRUXCORE_API_KEY" "$ACRUXCORE_BASE_URL/traces"
const result = await hub.traces.list();
// with filters
const errors = await hub.traces.list({ status: 'error' });
result = await hub.traces.list()
# with filters
errors = await hub.traces.list(status='error')
{
"data": [
{
"id": "6870cb11-b3a6-429a-9d8b-3952a519bf78",
"name": "t6-error-run",
"sessionId": "t6-session-2",
"status": "error",
"startedAt": "2026-07-05T00:05:00.000Z",
"endedAt": "2026-07-05T00:05:00.800Z",
"spanCount": 1,
"totalCostUsd": 0.000015,
"totalTokens": 50,
"durationMs": 800
},
{
"id": "354d5268-197d-4a8d-a664-005e8d20bbbb",
"name": "t6-support-run",
"sessionId": "t6-session-1",
"status": "ok",
"startedAt": "2026-07-05T00:00:00.000Z",
"endedAt": "2026-07-05T00:00:02.500Z",
"spanCount": 3,
"totalCostUsd": 0.0000456,
"totalTokens": 280,
"durationMs": 2500
}
],
"total": 2,
"page": 1,
"limit": 20
}
Filtering by status
curl -H "Authorization: Bearer $ACRUXCORE_API_KEY" "$ACRUXCORE_BASE_URL/traces?status=error"
{
"data": [
{
"id": "6870cb11-b3a6-429a-9d8b-3952a519bf78",
"name": "t6-error-run",
"sessionId": "t6-session-2",
"status": "error",
"startedAt": "2026-07-05T00:05:00.000Z",
"endedAt": "2026-07-05T00:05:00.800Z",
"spanCount": 1,
"totalCostUsd": 0.000015,
"totalTokens": 50,
"durationMs": 800
}
],
"total": 1,
"page": 1,
"limit": 20
}
Filtering by model (span-level EXISTS)
curl -H "Authorization: Bearer $ACRUXCORE_API_KEY" "$ACRUXCORE_BASE_URL/traces?model=gpt-4o-mini"
{
"data": [
{
"id": "354d5268-197d-4a8d-a664-005e8d20bbbb",
"name": "t6-support-run",
"sessionId": "t6-session-1",
"status": "ok",
"startedAt": "2026-07-05T00:00:00.000Z",
"endedAt": "2026-07-05T00:00:02.500Z",
"spanCount": 3,
"totalCostUsd": 0.0000456,
"totalTokens": 280,
"durationMs": 2500
}
],
"total": 1,
"page": 1,
"limit": 20
}
Filtering by prompt_version_id
Only returns traces that carry a span stamped with that exact prompt version
(set via promptVersionId on a span in POST /traces, or by a gateway
completion that resolved a prompt reference):
curl -H "Authorization: Bearer $ACRUXCORE_API_KEY" \
"$ACRUXCORE_BASE_URL/traces?prompt_version_id=4772e4b5-b34f-4cda-bc9e-f3df4de9426f"
{
"data": [
{
"id": "354d5268-197d-4a8d-a664-005e8d20bbbb",
"name": "t6-support-run",
"sessionId": "t6-session-1",
"status": "ok",
"startedAt": "2026-07-05T00:00:00.000Z",
"endedAt": "2026-07-05T00:00:02.500Z",
"spanCount": 3,
"totalCostUsd": 0.0000456,
"totalTokens": 280,
"durationMs": 2500
}
],
"total": 1,
"page": 1,
"limit": 20
}
Filtering by prompt_id
Every version of one prompt, in one filter. prompt_version_id narrows to a
single version; prompt_id is what you want when the question is "how is the
checkout prompt behaving" and several versions have shipped since.
curl -H "Authorization: Bearer $ACRUXCORE_API_KEY" \
"$ACRUXCORE_BASE_URL/traces?prompt_id=a290cf2f-a724-4389-8bea-da8b53229e00&limit=1"
{
"data": [
{
"id": "b89e372a-fe98-4d03-bebf-cfa87613e4a0",
"name": "asked from an MCP client",
"sessionId": null,
"status": "ok",
"startedAt": "2026-09-06T17:40:21.410Z",
"endedAt": "2026-09-06T17:40:22.931Z",
"spanCount": 1,
"totalCostUsd": 0.00004305,
"totalTokens": 143,
"durationMs": 1521,
"tags": []
}
],
"total": 5,
"page": 1,
"limit": 1
}
Filtering by how a run failed (error_type, error_code, has_warning)
status=error says a run broke. It cannot say why: every kind of failure rolls
up to the same red trace. These three read the classification a failing span
carries instead.
error_type is one of six fixed values the platform assigns. error_code is the
slug your tool declared — ToolResult.error("location_not_found", ...) — so
the vocabulary is your team's, not ours. List the codes your tools have actually
declared with GET /traces/facets.
Reach for error_code rather than q when you want a count. q is a substring
match over the whole attributes JSON, so q=location_not_found also matches a
trace that merely mentions the slug somewhere else; error_code matches the
recorded value exactly.
curl -H "Authorization: Bearer $ACRUXCORE_API_KEY" \
"$ACRUXCORE_BASE_URL/traces?error_type=tool_declared&error_code=location_not_found&limit=1"
{
"data": [
{
"id": "9fe7edab-9cdf-4a51-9813-e9d0b03970ad",
"name": "weather-brief-atlantis",
"sessionId": null,
"status": "error",
"startedAt": "2026-09-13T18:11:45.000Z",
"endedAt": null,
"spanCount": 1,
"totalCostUsd": null,
"totalTokens": 0,
"durationMs": null,
"tags": []
}
],
"total": 3,
"page": 1,
"limit": 1
}
has_warning=true finds runs carrying something worth seeing that did not fail —
those traces are usually status=ok, so no status filter reaches them. Both of the
rows below are green, and hasWarning is what says otherwise.
curl -H "Authorization: Bearer $ACRUXCORE_API_KEY" \
"$ACRUXCORE_BASE_URL/traces?has_warning=true&limit=2"
{
"data": [
{
"id": "ec60bcfb-ede3-4160-86bf-dc47f3966025",
"name": "Say hi",
"sessionId": null,
"status": "ok",
"startedAt": "2026-09-13T20:07:55.139Z",
"endedAt": "2026-09-13T20:08:01.318Z",
"spanCount": 1,
"totalCostUsd": 0.000002375,
"totalTokens": 15,
"durationMs": 6179,
"tags": [],
"hasWarning": true
},
{
"id": "9102d6a7-35b9-475e-86d2-669fa5a46300",
"name": "retry-demo",
"sessionId": null,
"status": "ok",
"startedAt": "2026-09-13T20:06:25.898Z",
"endedAt": "2026-09-13T20:06:29.189Z",
"spanCount": 1,
"totalCostUsd": null,
"totalTokens": 40,
"durationMs": 3291,
"tags": [],
"hasWarning": true
}
],
"total": 15,
"page": 1,
"limit": 2
}
What each of those two warnings says is on the span, documented under
GET /api/v1/traces/:id.
Searching what was actually said (q and q_in)
q searches the captured request and response payloads as well as names and
attributes, so a run can be found by a phrase from the conversation — the way
you remember it. q_in narrows that to one side: input for what the user
sent, output for what the model answered, name for the old
names-and-attributes behaviour.
Payload text is only searchable where payload capture was on for the trace. A team with capture off matches on names and attributes only.
curl -H "Authorization: Bearer $ACRUXCORE_API_KEY" \
"$ACRUXCORE_BASE_URL/traces?q=london&q_in=input&limit=1"
{
"data": [
{
"id": "dd71b6cb-8c20-c67b-fdbe-a47acde27b19",
"name": "research-agent",
"sessionId": "research-agent-degraded-202609071819",
"status": "ok",
"startedAt": "2026-09-07T18:21:32.832Z",
"endedAt": "2026-09-07T18:21:38.287Z",
"spanCount": 15,
"totalCostUsd": 0.0020097,
"totalTokens": 12915,
"durationMs": 5455,
"tags": []
}
],
"total": 77,
"page": 1,
"limit": 1
}
Filtering by tags and metadata (T8)
tags is repeatable (?tags=a&tags=b) and matches traces that carry all
supplied tags (AND / has-all, via Postgres array containment @>). metadata
uses bracket-notation, also repeatable (?metadata[k1]=v1&metadata[k2]=v2),
and matches traces whose metadata contains every supplied key/value pair
(AND / jsonb containment @>). Every list row also now includes a tags
array (metadata is only on the detail response, GET /traces/:id). Pass
-g to curl, or URL-encode the brackets (%5B/%5D) — curl treats literal
[/] in a URL as its own globbing syntax.
curl -g -H "Authorization: Bearer $ACRUXCORE_API_KEY" \
"$ACRUXCORE_BASE_URL/traces?tags=prod&tags=nl"
{
"data": [
{
"id": "1b2d4e99-8bfb-49f0-af63-97ae6da301f0",
"name": "trace-both",
"sessionId": null,
"status": "ok",
"startedAt": "2026-07-05T12:25:11.000Z",
"endedAt": null,
"spanCount": 1,
"totalCostUsd": null,
"totalTokens": 0,
"durationMs": null,
"tags": ["prod", "nl"]
}
],
"total": 1,
"page": 1,
"limit": 20
}
curl -g -H "Authorization: Bearer $ACRUXCORE_API_KEY" \
"$ACRUXCORE_BASE_URL/traces?metadata[env]=prod&metadata[lang]=nl"
{
"data": [
{
"id": "1b2d4e99-8bfb-49f0-af63-97ae6da301f0",
"name": "trace-both",
"sessionId": null,
"status": "ok",
"startedAt": "2026-07-05T12:25:11.000Z",
"endedAt": null,
"spanCount": 1,
"totalCostUsd": null,
"totalTokens": 0,
"durationMs": null,
"tags": ["prod", "nl"]
}
],
"total": 1,
"page": 1,
"limit": 20
}
GET /api/v1/traces/:id
Full trace detail: the trace header plus every span assembled into a
parent/child tree by parentSpanId (roots are spans with no parent, or a
dangling/cyclic parentSpanId). Each span carries its inline observability
fields (model, usage, costUsd, …) plus promptVersionId/
gatewayRequestId when the span was stamped with one. payload (input/
output) is present only on spans ingested with capturePayloads: true on
their trace.
- curl
- TypeScript
- Python
curl -H "Authorization: Bearer $ACRUXCORE_API_KEY" \
"$ACRUXCORE_BASE_URL/traces/354d5268-197d-4a8d-a664-005e8d20bbbb"
const trace = await hub.traces.get('354d5268-197d-4a8d-a664-005e8d20bbbb');
trace = await hub.traces.get('354d5268-197d-4a8d-a664-005e8d20bbbb')
{
"trace": {
"id": "354d5268-197d-4a8d-a664-005e8d20bbbb",
"name": "t6-support-run",
"sessionId": "t6-session-1",
"status": "ok",
"startedAt": "2026-07-05T00:00:00.000Z",
"endedAt": "2026-07-05T00:00:02.500Z",
"spanCount": 3,
"totalCostUsd": 0.0000456,
"totalTokens": 280
},
"spans": [
{
"spanId": "root",
"parentSpanId": null,
"kind": "agent",
"name": "support-agent",
"status": "ok",
"startedAt": "2026-07-05T00:00:00.000Z",
"endedAt": "2026-07-05T00:00:02.500Z",
"latencyMs": 2500,
"model": null,
"provider": null,
"promptTokens": null,
"completionTokens": null,
"totalTokens": null,
"costUsd": null,
"promptVersionId": null,
"gatewayRequestId": null,
"errorMessage": null,
"attributes": {},
"tags": [],
"metadata": {},
"children": [
{
"spanId": "llm1",
"parentSpanId": "root",
"kind": "llm",
"name": "gpt-4o-mini",
"status": "ok",
"startedAt": "2026-07-05T00:00:00.100Z",
"endedAt": "2026-07-05T00:00:01.300Z",
"latencyMs": 1200,
"model": "gpt-4o-mini",
"provider": "openai",
"promptTokens": 200,
"completionTokens": 80,
"totalTokens": 280,
"costUsd": 0.0000456,
"promptVersionId": "4772e4b5-b34f-4cda-bc9e-f3df4de9426f",
"gatewayRequestId": null,
"errorMessage": null,
"attributes": {},
"tags": ["flights"],
"metadata": { "segment": "flights" },
"children": [],
"payload": {
"input": { "prompt": "You are a helpful support agent." },
"output": { "text": "Sure, I can help with that." }
}
},
{
"spanId": "tool1",
"parentSpanId": "root",
"kind": "tool",
"name": "lookup_order",
"status": "ok",
"startedAt": "2026-07-05T00:00:01.400Z",
"endedAt": "2026-07-05T00:00:02.000Z",
"latencyMs": 600,
"model": null,
"provider": null,
"promptTokens": null,
"completionTokens": null,
"totalTokens": null,
"costUsd": null,
"promptVersionId": null,
"gatewayRequestId": null,
"errorMessage": null,
"attributes": { "orderId": "ord_123" },
"tags": [],
"metadata": {},
"children": []
}
]
}
]
}
llm1 carries promptVersionId (set on ingest) and a payload (because the
trace was posted with capturePayloads: true); root and tool1 have neither.
tags/metadata (T9/T10) default to []/{} on every span; llm1 shows a
non-empty example, of the kind set via the gateway's x-span-tags/
x-span-metadata headers on a per-call basis.
A call the gateway rescued (trail and warning)
An llm span the gateway wrote carries three extra keys in attributes:
attempts (provider calls across the whole chain), cacheHit, and trail — one
entry per deployment tried, in the order they were tried. Each trail entry holds:
modelId— the registered model's UUID.model— that model's public name, the one you pass asmodel.upstreamModel— the name the provider itself knows the model by.credentialId— the provider connection the attempt used.attempts— how many times this deployment was called before its turn ended. The span-levelattemptsis the chain total, so only this per-entry count separates "retried, then fell back" from "fell back without retrying".error— the status code that ended this deployment's turn, as a string. Absent on the entry that answered.errorMessage— what the provider itself said, truncated at 300 characters with a trailing…. Absent on the entry that answered.retriedAfter—{ "error": "429", "errorMessage": "…" }on a deployment that failed, was retried, and then answered. That entry has noerrorof its own, because its turn ended in success, so this is the only record of what the retries were for.
A call that only succeeded because the gateway retried or fell back also carries a
warning object — type is model_fallback or provider_retry, and message is a
one-line summary. The span stays status: "ok", because the request did succeed; the
warning is what keeps a primary model that fails every time from being invisible while
its fallback keeps answering. It is written only on an ok span — a span that is
already red says the call failed. has_warning=true on
GET /traces
lists these runs.
unstable-4o-mini has a revoked credential and mistral-small behind it, so asking
for it returns 200 from the fallback:
curl -H "Authorization: Bearer $ACRUXCORE_API_KEY" \
"$ACRUXCORE_BASE_URL/traces/ec60bcfb-ede3-4160-86bf-dc47f3966025"
{
"trace": {
"id": "ec60bcfb-ede3-4160-86bf-dc47f3966025",
"name": "Say hi",
"sessionId": null,
"status": "ok",
"startedAt": "2026-09-13T20:07:55.139Z",
"endedAt": "2026-09-13T20:08:01.318Z",
"spanCount": 1,
"totalCostUsd": 0.000002375,
"totalTokens": 15,
"tags": [],
"metadata": {}
},
"spans": [
{
"spanId": "858591a7-24c6-4960-b6ea-fd4de7adf2d0",
"parentSpanId": null,
"kind": "llm",
"name": "2026-09-13T20:07:55.139Z",
"status": "ok",
"startedAt": "2026-09-13T20:07:55.139Z",
"endedAt": "2026-09-13T20:08:01.318Z",
"latencyMs": 6179,
"model": "mistralai/mistral-small-3.2-24b-instruct",
"provider": "openai_compatible",
"promptTokens": 5,
"completionTokens": 10,
"totalTokens": 15,
"costUsd": 0.000002375,
"promptVersionId": null,
"gatewayRequestId": "b1ab662c-93bb-41c5-9bc1-88c5f3003cc0",
"errorMessage": null,
"attributes": {
"trail": [
{
"error": "401",
"model": "unstable-4o-mini",
"modelId": "4eeda09c-0bf6-409d-b19e-9f8b728d4013",
"attempts": 1,
"credentialId": "e9d85f7f-df56-427f-a324-c6dc3501df87",
"errorMessage": "Incorrect API key provided: sk-revok********************demo. You can find your API key at https://platform.openai.com/account/api-keys.",
"upstreamModel": "gpt-4o-mini"
},
{
"model": "mistral-small",
"modelId": "f02ba5ce-8064-40fc-a5d6-840040698ba8",
"attempts": 1,
"credentialId": "ae9d2cc1-cf07-4f78-9605-74596fc79b0e",
"upstreamModel": "mistralai/mistral-small-3.2-24b-instruct"
}
],
"warning": {
"type": "model_fallback",
"message": "unstable-4o-mini failed 401, so mistral-small answered instead."
},
"attempts": 2,
"cacheHit": false
},
"tags": [],
"metadata": {},
"children": [],
"payload": {
"input": [{ "role": "user", "content": "Say hi" }],
"output": {
"id": "gen-1789330077-IA7FaZdBoBo7XZFI5mBa",
"model": "mistralai/mistral-small-3.2-24b-instruct",
"usage": { "total_tokens": 15, "prompt_tokens": 5, "completion_tokens": 10 },
"object": "chat.completion",
"choices": [
{
"index": 0,
"message": { "role": "assistant", "content": "Hi there! 😊 How can I help" },
"finish_reason": "length"
}
],
"created": 1789330077
},
"variables": null
}
}
],
"feedback": [],
"evalScores": []
}
The trail's first entry names the model that failed, why (401), and the provider's
own sentence; the second names the model that answered. The span's model is the
upstream name of whichever deployment served the request.
When a single model answers only after being called again, the trail is one entry
whose attempts is above 1 and the warning is a provider_retry:
curl -H "Authorization: Bearer $ACRUXCORE_API_KEY" \
"$ACRUXCORE_BASE_URL/traces/9102d6a7-35b9-475e-86d2-669fa5a46300"
That trace's single span returned these attributes (the rest of the body has the
same shape as above):
{
"trail": [
{
"model": "free-tier-model",
"modelId": "96e72f1b-9346-4ce2-a38f-c674b949391a",
"attempts": 3,
"credentialId": "ae9d2cc1-cf07-4f78-9605-74596fc79b0e",
"upstreamModel": "liquid/lfm-2.5-2.6b:free"
}
],
"warning": {
"type": "provider_retry",
"message": "free-tier-model answered only on attempt 3."
},
"attempts": 3,
"cacheHit": false
}
A call answered first time by the model that was asked for carries no warning at
all, and its one trail entry has attempts: 1 and no error.
Unknown or foreign trace id (404)
curl -H "Authorization: Bearer $ACRUXCORE_API_KEY" \
"$ACRUXCORE_BASE_URL/traces/00000000-0000-0000-0000-000000000000"
{ "error": { "code": "NOT_FOUND", "message": "Trace not found" } }
Verified the same 404 (same message, no existence leak) when a second team's API key requests a trace id that belongs to the first team.
Trace facets
Discovering the team's distinct tags and metadata keys/values (for populating
a filter bar's pickers) is documented separately in
Trace Facets — see that page for GET /api/v1/traces/facets
and GET /api/v1/traces/facets/values.
GET /api/v1/prompts/:id/versions/:version_number/traces
Reverse lineage — every trace with at least one span stamped with that exact
prompt version. Same paginated envelope as GET /traces (page, limit — up
to 100 — the only query params). The :id/:version_number pair is resolved
team-scoped (via the same version lookup GET /prompts/:id/versions/:n uses),
so an unknown or cross-team prompt/version 404s before any trace query runs.
Windowing note (intentional): this lineage route is all-time — it has no
from/toparams and applies no default date window. That differs fromGET /traces?prompt_version_id=<id>, which is a general list endpoint and so applies the standard last-30-days default window. Asking "which traces used this version?" via the two routes can therefore return different counts: the lineage route counts every matching trace ever; the filtered list counts only those in the (default or supplied) window. Use this route for a complete version history, the?prompt_version_id=filter for a time-bounded slice.
- curl
- TypeScript
- Python
curl -H "Authorization: Bearer $ACRUXCORE_API_KEY" \
"$ACRUXCORE_BASE_URL/prompts/f5250666-5775-4198-b9ef-75068249529e/versions/1/traces"
const traces = await hub.prompts.tracesForVersion('f5250666-5775-4198-b9ef-75068249529e', 1);
traces = await hub.prompts.traces_for_version('f5250666-5775-4198-b9ef-75068249529e', 1)
{
"data": [
{
"id": "354d5268-197d-4a8d-a664-005e8d20bbbb",
"name": "t6-support-run",
"sessionId": "t6-session-1",
"status": "ok",
"startedAt": "2026-07-05T00:00:00.000Z",
"endedAt": "2026-07-05T00:00:02.500Z",
"spanCount": 3,
"totalCostUsd": 0.0000456,
"totalTokens": 280,
"durationMs": 2500
}
],
"total": 1,
"page": 1,
"limit": 20
}
Error responses
Unknown prompt id for the team (status 404) — same message for a cross-team prompt id, verified with a second team's API key:
curl -H "Authorization: Bearer $ACRUXCORE_API_KEY" \
"$ACRUXCORE_BASE_URL/prompts/00000000-0000-0000-0000-000000000000/versions/1/traces"
{ "error": { "code": "NOT_FOUND", "message": "Prompt not found" } }
Version number that doesn't exist on that prompt (status 404):
curl -H "Authorization: Bearer $ACRUXCORE_API_KEY" \
"$ACRUXCORE_BASE_URL/prompts/f5250666-5775-4198-b9ef-75068249529e/versions/99/traces"
{ "error": { "code": "NOT_FOUND", "message": "Version 99 not found for this prompt" } }