Traces — User Feedback API
All endpoints verified working via curl. Document updated only after curl confirmation.
User/human feedback attached to a trace, or to one span within it. Feedback is
free-form: a caller supplies at least one of rating, label, comment —
none are individually required. Auth: requireAnyAuth (session cookie or
personal/team API key) for both posting and reading; any team role. Team-scoped
throughout — a trace outside the caller's team behaves as if it doesn't exist
(404), same as GET /api/v1/traces/:id.
spanId in every request/response is the caller-supplied OTel span reference
(e.g. "s1", the same value used in POST /api/v1/traces and returned by
GET /api/v1/traces/:id) — never the internal span UUID. Trace-level
feedback (no span) has spanId: null.
Feedback can be edited after posting (PATCH /api/v1/traces/:id/feedback/:feedbackId),
but only by the row's original author (createdBy) — see that section below.
Deleting feedback is not supported. updatedAt equals createdAt until the row
has been edited.
POST /api/v1/traces/:id/feedback
Attaches feedback to a trace. Body accepts rating (integer -1..5), label
(string, <=200 chars), comment (string, <=5000 chars), spanId (OTel ref,
optional — omit for whole-trace feedback), and source (one of user,
developer, end_user, api; defaults to user). At least one of
rating/label/comment is required. createdBy is the caller's user id;
verified non-null here for a personal API key (per the controller, it is
req.user?.id ?? null — a team-scoped key with no associated user would
yield null, though that path was not separately exercised).
curl -X POST $ACRUXCORE_BASE_URL/traces/fbb5f16e-521b-4839-b9ab-78ace8c1f0a4/feedback \
-H "Authorization: Bearer $ACRUXCORE_API_KEY" \
-H "Content-Type: application/json" \
-d '{"rating": -1, "label": "wrong_answer", "comment": "The tool call missed relevant docs.", "source": "end_user"}'
Response (status 201):
{
"id": "d71ba68f-a7bf-424d-a424-a0ed80ec1592",
"traceId": "fbb5f16e-521b-4839-b9ab-78ace8c1f0a4",
"spanId": null,
"rating": -1,
"label": "wrong_answer",
"comment": "The tool call missed relevant docs.",
"source": "end_user",
"createdBy": "8e16d0ed-e41a-4006-b098-5e4aecd4998d",
"createdAt": "2026-07-05T02:33:11.976Z",
"updatedAt": "2026-07-05T02:33:11.976Z"
}
Span-level feedback (spanId)
Supplying spanId attaches the feedback to one span instead of the whole
trace. The value is the OTel ref from the original POST /api/v1/traces call
(here "s1", the LLM span) — the response echoes that same ref back, not the
span's internal UUID.
curl -X POST $ACRUXCORE_BASE_URL/traces/fbb5f16e-521b-4839-b9ab-78ace8c1f0a4/feedback \
-H "Authorization: Bearer $ACRUXCORE_API_KEY" \
-H "Content-Type: application/json" \
-d '{"rating": 5, "spanId": "s1"}'
Response (status 201):
{
"id": "bd505193-ea39-4e23-a9b5-c356d38b5064",
"traceId": "fbb5f16e-521b-4839-b9ab-78ace8c1f0a4",
"spanId": "s1",
"rating": 5,
"label": null,
"comment": null,
"source": "user",
"createdBy": "8e16d0ed-e41a-4006-b098-5e4aecd4998d",
"createdAt": "2026-07-05T02:33:24.590Z",
"updatedAt": "2026-07-05T02:33:24.590Z"
}
A second span-level example — label only, no rating, source "developer", against span "s2" (a tool span, child of "s1"):
curl -X POST $ACRUXCORE_BASE_URL/traces/fbb5f16e-521b-4839-b9ab-78ace8c1f0a4/feedback \
-H "Authorization: Bearer $ACRUXCORE_API_KEY" \
-H "Content-Type: application/json" \
-d '{"label": "good_tool_use", "spanId": "s2", "source": "developer"}'
Response (status 201):
{
"id": "f848d26d-5f1f-4d18-8d47-2dc76675fb21",
"traceId": "fbb5f16e-521b-4839-b9ab-78ace8c1f0a4",
"spanId": "s2",
"rating": null,
"label": "good_tool_use",
"comment": null,
"source": "developer",
"createdBy": "8e16d0ed-e41a-4006-b098-5e4aecd4998d",
"createdAt": "2026-07-05T02:33:24.608Z",
"updatedAt": "2026-07-05T02:33:24.608Z"
}
PATCH /api/v1/traces/:id/feedback/:feedbackId
Edits an existing feedback row's rating/label/comment in place (author-only —
only the user who originally posted the row, matched via createdBy, may
edit it; rows posted via a team API key have createdBy: null and so cannot
be edited by anyone). Fields omitted from the body keep their existing value;
null explicitly clears a field. Deleting feedback is not supported.
curl -X POST $ACRUXCORE_BASE_URL/traces/a856684b-79cf-4a83-ac8f-f50294ec07dc/feedback \
-H "Authorization: Bearer $ACRUXCORE_API_KEY" -H "Content-Type: application/json" \
-d '{"rating": -1, "label": "wrong_answer", "comment": "The tool call missed relevant docs."}'
Response (status 201):
{
"id": "7133430d-9e2c-4bb3-90b9-f56e95bde619",
"traceId": "a856684b-79cf-4a83-ac8f-f50294ec07dc",
"spanId": null,
"rating": -1,
"label": "wrong_answer",
"comment": "The tool call missed relevant docs.",
"source": "user",
"createdBy": "e0016d77-79da-4094-8bc9-e5ba5ff84527",
"createdAt": "2026-07-05T18:14:13.237Z",
"updatedAt": "2026-07-05T18:14:13.237Z"
}
The author corrects the rating; label/comment are omitted, so they keep their existing values:
curl -X PATCH $ACRUXCORE_BASE_URL/traces/a856684b-79cf-4a83-ac8f-f50294ec07dc/feedback/7133430d-9e2c-4bb3-90b9-f56e95bde619 \
-H "Authorization: Bearer $ACRUXCORE_API_KEY" -H "Content-Type: application/json" \
-d '{"rating": 1}'
Response (status 200) — rating flipped, label/comment unchanged, updatedAt advanced:
{
"id": "7133430d-9e2c-4bb3-90b9-f56e95bde619",
"traceId": "a856684b-79cf-4a83-ac8f-f50294ec07dc",
"spanId": null,
"rating": 1,
"label": "wrong_answer",
"comment": "The tool call missed relevant docs.",
"source": "user",
"createdBy": "e0016d77-79da-4094-8bc9-e5ba5ff84527",
"createdAt": "2026-07-05T18:14:13.237Z",
"updatedAt": "2026-07-05T18:14:22.536Z"
}
Error responses
Clearing rating/label/comment down to nothing (status 400, same message as the create-side validation — the merged result, not just the patch body, is checked):
curl -X PATCH $ACRUXCORE_BASE_URL/traces/a856684b-79cf-4a83-ac8f-f50294ec07dc/feedback/7133430d-9e2c-4bb3-90b9-f56e95bde619 \
-H "Authorization: Bearer $ACRUXCORE_API_KEY" -H "Content-Type: application/json" \
-d '{"rating": null, "label": null, "comment": null}'
{ "error": { "code": "VALIDATION_ERROR", "message": "Provide at least one of rating, label, or comment." } }
A teammate who is not the row's author (status 403) — the row is left unchanged:
curl -X PATCH $ACRUXCORE_BASE_URL/traces/a856684b-79cf-4a83-ac8f-f50294ec07dc/feedback/7133430d-9e2c-4bb3-90b9-f56e95bde619 \
-H "Authorization: Bearer $ACRUXCORE_API_KEY" -H "Content-Type: application/json" \
-d '{"rating": -1}'
{ "error": { "code": "FORBIDDEN", "message": "Only the original author can edit this feedback." } }
Unknown feedback id, or one belonging to another team's trace (status 404 — never distinguishes the two, same as the trace-lookup 404s above):
curl -X PATCH $ACRUXCORE_BASE_URL/traces/a856684b-79cf-4a83-ac8f-f50294ec07dc/feedback/00000000-0000-0000-0000-000000000000 \
-H "Authorization: Bearer $ACRUXCORE_API_KEY" -H "Content-Type: application/json" -d '{"rating": 1}'
{ "error": { "code": "NOT_FOUND", "message": "Feedback not found." } }
No Authorization header (status 401):
curl -X PATCH $ACRUXCORE_BASE_URL/traces/a856684b-79cf-4a83-ac8f-f50294ec07dc/feedback/7133430d-9e2c-4bb3-90b9-f56e95bde619 \
-H "Content-Type: application/json" -d '{"rating": 1}'
{ "error": { "code": "UNAUTHORIZED", "message": "API key required." } }
GET /api/v1/traces/:id/feedback
Lists all feedback on a trace (trace-level and span-level, mixed), newest first. Continuing the example above, after the three POSTs:
curl -H "Authorization: Bearer $ACRUXCORE_API_KEY" \
"$ACRUXCORE_BASE_URL/traces/fbb5f16e-521b-4839-b9ab-78ace8c1f0a4/feedback"
Response (status 200):
{
"data": [
{
"id": "f848d26d-5f1f-4d18-8d47-2dc76675fb21",
"traceId": "fbb5f16e-521b-4839-b9ab-78ace8c1f0a4",
"spanId": "s2",
"rating": null,
"label": "good_tool_use",
"comment": null,
"source": "developer",
"createdBy": "8e16d0ed-e41a-4006-b098-5e4aecd4998d",
"createdAt": "2026-07-05T02:33:24.608Z",
"updatedAt": "2026-07-05T02:33:24.608Z"
},
{
"id": "bd505193-ea39-4e23-a9b5-c356d38b5064",
"traceId": "fbb5f16e-521b-4839-b9ab-78ace8c1f0a4",
"spanId": "s1",
"rating": 5,
"label": null,
"comment": null,
"source": "user",
"createdBy": "8e16d0ed-e41a-4006-b098-5e4aecd4998d",
"createdAt": "2026-07-05T02:33:24.590Z",
"updatedAt": "2026-07-05T02:33:24.590Z"
},
{
"id": "d71ba68f-a7bf-424d-a424-a0ed80ec1592",
"traceId": "fbb5f16e-521b-4839-b9ab-78ace8c1f0a4",
"spanId": null,
"rating": -1,
"label": "wrong_answer",
"comment": "The tool call missed relevant docs.",
"source": "end_user",
"createdBy": "8e16d0ed-e41a-4006-b098-5e4aecd4998d",
"createdAt": "2026-07-05T02:33:11.976Z",
"updatedAt": "2026-07-05T02:33:11.976Z"
}
]
}
The same three feedback rows also appear, unchanged, in the additive
feedback field of GET /api/v1/traces/:id (see docs/api/traces/traces.md) —
verified byte-for-byte identical to the data array above.
GET /api/v1/traces/feedback/summary
Average rating + counts, grouped by model or prompt_version (query param
group_by, default prompt_version). Optional from/to (ISO 8601)
restrict the window; default is the last 30 days. downCount is the number
of rows with rating < 0 (thumbs-down) in that bucket; avgRating is null
for a bucket with no rated feedback.
Attribution is per-TRACE, not per-span: every feedback row on a trace is
counted against every distinct model/prompt_version used by that trace's
llm spans, regardless of which span (or none) the feedback itself is
attached to (see aggregate() in feedback.repository.ts). This trace has one
llm span ("s1", model "gpt-4o-mini") and no other model, so all 3 feedback
rows land in the same bucket — including the trace-level row (spanId null)
and the "s2" row (a tool span with no model of its own).
curl -H "Authorization: Bearer $ACRUXCORE_API_KEY" \
"$ACRUXCORE_BASE_URL/traces/feedback/summary?group_by=model"
Response (status 200) — count 3; ratings -1, 5, null average (non-null only) to 2; downCount 1 (the one row with rating < 0):
{
"groupBy": "model",
"buckets": [
{ "key": "gpt-4o-mini", "count": 3, "avgRating": 2, "downCount": 1 }
]
}
Grouped by prompt_version (default) — empty because none of this trace's spans carry a promptVersionId:
curl -H "Authorization: Bearer $ACRUXCORE_API_KEY" \
"$ACRUXCORE_BASE_URL/traces/feedback/summary"
Response (status 200):
{ "groupBy": "prompt_version", "buckets": [] }
Error responses
Empty body — none of rating/label/comment supplied (status 400):
curl -X POST $ACRUXCORE_BASE_URL/traces/fbb5f16e-521b-4839-b9ab-78ace8c1f0a4/feedback \
-H "Authorization: Bearer $ACRUXCORE_API_KEY" -H "Content-Type: application/json" -d '{}'
{ "error": { "code": "VALIDATION_ERROR", "message": "Provide at least one of rating, label, or comment." } }
rating outside -1..5 (status 400):
curl -X POST $ACRUXCORE_BASE_URL/traces/fbb5f16e-521b-4839-b9ab-78ace8c1f0a4/feedback \
-H "Authorization: Bearer $ACRUXCORE_API_KEY" -H "Content-Type: application/json" -d '{"rating": 9}'
{ "error": { "code": "VALIDATION_ERROR", "message": "rating must be between -1 and 5." } }
spanId does not belong to this trace (status 400, distinct error code):
curl -X POST $ACRUXCORE_BASE_URL/traces/fbb5f16e-521b-4839-b9ab-78ace8c1f0a4/feedback \
-H "Authorization: Bearer $ACRUXCORE_API_KEY" -H "Content-Type: application/json" -d '{"rating": 1, "spanId": "does-not-exist"}'
{ "error": { "code": "INVALID_SPAN", "message": "spanId does not belong to this trace." } }
Unknown traceId (status 404):
curl -X POST $ACRUXCORE_BASE_URL/traces/00000000-0000-0000-0000-000000000000/feedback \
-H "Authorization: Bearer $ACRUXCORE_API_KEY" -H "Content-Type: application/json" -d '{"rating": 1}'
{ "error": { "code": "NOT_FOUND", "message": "Trace not found." } }
Trace exists but belongs to another team (status 404) — same message as unknown, so the endpoint never confirms whether a foreign trace exists:
{ "error": { "code": "NOT_FOUND", "message": "Trace not found." } }
No Authorization header, on either POST or the summary GET (status 401):
{ "error": { "code": "UNAUTHORIZED", "message": "API key required." } }
GET /api/v1/traces/feedback
T10: the team-wide raw feed backing the feedback visualization page —
distinct from GET /:id/feedback (one trace) and /feedback/summary (aggregated).
Newest-first, paginated (page/limit, limit capped at 100 like other list
endpoints). Same row shape as the other feedback endpoints; spanId is the
OTel ref (null for whole-trace feedback).
curl -H "Authorization: Bearer $ACRUXCORE_API_KEY" \
"$ACRUXCORE_BASE_URL/traces/feedback?limit=2"
Response (status 200) — 3 feedback rows exist across the team's traces; this page shows the 2 newest:
{
"data": [
{
"id": "c80dfe36-cf93-4a0a-9a74-2a32ae2e33d2",
"traceId": "b6f37c03-30f6-4b8a-9082-24c9cbc4f8f4",
"spanId": "s2",
"rating": 1,
"label": null,
"comment": null,
"source": "developer",
"createdBy": "c7f4e80d-7873-445a-92a2-eaadfeb88896",
"createdAt": "2026-07-05T17:11:43.283Z",
"updatedAt": "2026-07-05T17:11:43.283Z"
},
{
"id": "ded66aec-e08f-48f9-93da-fcb45bbb6a87",
"traceId": "b6f37c03-30f6-4b8a-9082-24c9cbc4f8f4",
"spanId": "s2",
"rating": -1,
"label": null,
"comment": "Flight summary was thin",
"source": "developer",
"createdBy": "c7f4e80d-7873-445a-92a2-eaadfeb88896",
"createdAt": "2026-07-05T17:08:20.923Z",
"updatedAt": "2026-07-05T17:08:20.923Z"
}
],
"total": 3,
"page": 1,
"limit": 2
}
No Authorization header (status 401):
{ "error": { "code": "UNAUTHORIZED", "message": "API key required." } }