POST/1.0/caliper/traces
Send a batch of traces. A trace is one agent run, the reply to one turn: what came in, what went out, and the steps in between. Each trace names its source, your name for the app sending it, and a new name starts a new source.
Call it after your agent replies, or batch several replies into one request.
Required scope: caliper:traces:write (scopes reference). Workspace-kind keys only. The scope can send and nothing else.
Returns 202 Accepted.
Request body
tracesarrayRequired1 to 500 traces.
Each trace
Only source is required. Anything you don't send isn't stored, so leave out whatever you'd rather not keep.
sourcestringRequiredYour name for the app sending it, such as "support-bot". Names starting with workbench/ belong to Workbench flows and are refused.
idstringYour id for this trace. Sending a trace again with the same id replaces it, so a retry never counts twice. Leave it out and Caliper assigns one.
messagesarrayThe conversation as OpenAI-style chat messages (role, content, tool_calls, tool_call_id). Each assistant message becomes a model call and each tool call a tool step, with its result taken from the matching tool message. Send this or steps.
stepsarrayWhat the agent did, in order. See Steps below.
inputanyWhat came in. Text, a message list, or any JSON. With messages, defaults to the conversation up to the last user message.
outputanyWhat your agent answered. With messages, defaults to the last assistant reply.
conversationIdstringGroups turns of one conversation.
userIdstringYour id for the person on the other end.
startedAtstringWhen it started, ISO-8601 with a timezone. Defaults to when it arrives. With endedAt or durationMs, any two of the three are enough.
endedAtstringWhen it ended.
durationMsnumberHow long it took, in milliseconds.
status"ok" | "error"Defaults to "error" when any step failed, otherwise "ok".
errorstringWhat went wrong, for a failed trace.
metadataobjectAnything else worth keeping: plan, region, prompt version. Other top-level fields you send are kept here too.
tagsstring[]Up to 50 labels.
Steps
Every field but type is optional, and any extra field you include is kept.
type"model" | "tool" | "lookup" | "other"RequiredWhat kind of step it is.
namestringThe tool's name for a tool call; a label for anything else.
idstringYour id for the step.
startedAtstringWhen the step started. Steps with start times are drawn on the trace's timeline.
durationMsnumberHow long the step took.
status"ok" | "error"Whether the step succeeded.
inputanyA tool's arguments, or what a model was sent.
outputanyA tool's result, or what a model returned.
modelstringFor a model call: which model.
inputTokensnumberTokens in. Also outputTokens, reasoningTokens, and cachedTokens.
costUsdnumberWhat the step cost, in US dollars.
reasoningstringThe model's reasoning, when you have it.
querystringFor a lookup: what was searched for.
documentsarrayFor a lookup: what came back, as { name, id?, score? }. These are the documents a source's Lookups chart counts.
Example
await fetch("https://api.zerowidth.ai/1.0/caliper/traces", {
method: "POST",
headers: {
Authorization: `Bearer ${process.env.ZEROWIDTH_API_KEY}`,
"Content-Type": "application/json",
},
body: JSON.stringify({
traces: [
{
source: "support-bot",
id: runId,
conversationId,
durationMs: Date.now() - startedAt,
messages, // the conversation, tool calls included
},
],
}),
})
With explicit steps:
curl -X POST https://api.zerowidth.ai/1.0/caliper/traces \
-H "Authorization: Bearer $ZEROWIDTH_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"traces": [{
"source": "support-bot",
"id": "run_8123",
"input": "Do you ship to Canada?",
"output": "Yes, in 5 to 7 business days.",
"steps": [
{ "type": "lookup", "query": "shipping canada",
"documents": [{ "name": "shipping-faq.md", "score": 0.83 }] },
{ "type": "model", "model": "gpt-5-mini",
"inputTokens": 1180, "outputTokens": 42, "costUsd": 0.0011 }
]
}]
}'
Response (202)
creatednumberTraces stored for the first time.
updatednumberTraces that replaced one sent before with the same id.
idsstring[]Each trace's id, in the order sent: yours, or the one Caliper assigned.
Limits
Your plan sets how many traces a workspace can send each month, how many requests a key can make to this endpoint each minute, and how long conversations are kept (see pricing). A batch that would go past the month's limit is refused whole with 402 and code plan_limit, so nothing is half-stored. Going past the per-minute limit returns 429 with a Retry-After header; send larger batches less often.