Skip to main content
This page is the field-by-field reference for the body of POST /api/v1/traces/ingest. One request carries one trace and between 1 and 500 spans of it. If a trace has more than 500 spans, send it in several requests with the same trace_id (see Sending a trace in several batches). Unknown fields are ignored and dropped, so you can’t add your own top-level fields. Put custom data in a span’s attributes object, or in tags.

Trace object

string
required
Your identifier for this run. 1 to 128 characters. Unique per project: sending the same trace_id again adds spans to (or updates spans in) the existing trace rather than creating a second one. Any string works, such as a UUID, your request id, or an OpenTelemetry-style hex id. If you use characters like / or #, URL-encode the id when you put it in an API path.
string
default:"\"\""
The label shown for the trace in the Traces list, such as agent_run or support_bot.reply. Up to 512 characters.
Every request for a trace overwrites its stored root_name. If you send a trace in several batches and leave root_name out of a later batch, the name is reset to an empty string. Send the same root_name on every batch.
"OK" | "ERROR"
Optional. It only affects the status echoed in this request’s response. The stored trace status is always derived from the spans: the trace is ERROR if any span ever stored for it has status: "ERROR", otherwise OK. To mark a run as failed, set status: "ERROR" on the span that failed.
string
default:"\"production\""
Free text, up to 64 characters, such as production, staging or development. Overwritten by each batch for the same trace.
"live" | "simulation"
default:"\"live\""
Where the trace came from. Use live for real traffic. simulation is what AgentHub’s own Cost Calculator and simulation runs write; you’d normally leave this at the default. Overwritten by each batch.
string (ISO 8601, UTC)
Optional. When omitted, the earliest span started_at is used. Either way, once the spans are stored the trace start is restated as the earliest started_at of all its stored spans, so in practice the spans decide it.
string[]
default:"[]"
Labels for filtering and grouping, each up to 128 characters. Overwritten by each batch for the same trace, so resend the full list every time.
Span[]
required
The spans in this batch. At least 1, at most 500.

Span object

string
required
Unique within the trace. 1 to 128 characters. Sending a span with a span_id that already exists in the trace replaces that span completely (every field below is overwritten), which is what makes retries safe.
string | null
The span_id of the span this one ran inside. Up to 128 characters. Leave it out or set it to null for the root span. The parent doesn’t have to be in the same request, or to have arrived yet. See Parent and child spans.
string
required
What the step was, such as chat.completions.create, search_docs or GET /v1/orders. 1 to 512 characters.
string
default:"\"function\""
One of the span kinds below. The kind decides which trace totals the span counts towards.
string | null
The model identifier for llm spans, such as gpt-4o-mini or claude-sonnet-4-5. Up to 256 characters. Stored as you send it.
"OK" | "ERROR"
default:"\"OK\""
Whether this step succeeded. Any ERROR span makes the whole trace ERROR, and that sticks: later batches of OK spans don’t clear it.
integer
default:"0"
Input (prompt) tokens. Whole number, 0 or more.
integer
default:"0"
Output (completion) tokens. Whole number, 0 or more.
number
default:"0"
What this step cost, in US dollars. 0 or more. Stored with 6 decimal places, so anything below a millionth of a dollar is rounded away. AgentHub does not price JSON spans for you: if you leave this at 0, the span and the trace show zero cost. Compute it from your provider’s pricing and send it.
integer
How long the step took, in milliseconds. Whole number, 0 or more. When omitted or 0, it’s computed as ended_at minus started_at. When you do send it, it wins over the timestamps.
object
default:"{}"
What went into the step. Must be a JSON object (not a string, array or number). See Input and output conventions.
object
default:"{}"
What came out of the step. Must be a JSON object.
object
default:"{}"
Any extra key/value data you want to keep with the span, such as temperature, tool arguments, user id or retry count. Must be a JSON object.
string (ISO 8601, UTC)
required
When the step started.
string (ISO 8601, UTC)
required
When the step finished.

Enums

Span kinds

Any other value is rejected with 400 VALIDATION_ERROR.

Status

OK or ERROR, uppercase, for both traces and spans.

Source

live (default) or simulation.

Environment

Not an enum. Any string up to 64 characters; production when omitted.

Timestamps

started_at and ended_at (and the optional trace started_at) must be ISO 8601 date-times in UTC with a trailing Z. Any number of fractional-second digits is accepted. JavaScript’s Date.prototype.toISOString() produces the right format. Python’s datetime.isoformat() does not (it writes +00:00); format with strftime("%Y-%m-%dT%H:%M:%S.%fZ") on a UTC datetime instead.

Parent and child spans

Spans form a tree through parent_span_id:
  • The root span has no parent_span_id. A trace normally has exactly one.
  • A child’s parent_span_id must equal its parent’s span_id within the same trace.
  • AgentHub doesn’t check that the parent exists when you send a child. Children can arrive before their parent, in a different request, and they’re linked when the trace is displayed. This is what lets you send spans as they finish rather than all at the end.
  • Spans are listed in started_at order inside a trace.

Input and output conventions

input and output accept any JSON object, but the trace view and the evaluators look for a few keys first. Using them makes traces easier to read and evaluate: These are the same keys AgentHub writes when it converts OpenTelemetry spans, so JSON and OTLP traces look the same in the UI.

How trace totals are computed

You don’t send trace totals. After every request, AgentHub recomputes them from all spans stored for the trace, not just the ones in the request. Totals are therefore correct whether you send a trace in one request or many, in order or out of order, and when you resend spans. Token and cost sums include every span kind. If you record a model call’s tokens on both an llm span and its parent function span, they’re counted twice; put them only on the span that made the call.

Sending a trace in several batches

You can split one trace across requests, for example to stay under the 500-span limit or to stream spans as steps finish:
  • Use the same trace_id on every request.
  • Each span only needs to be sent once, in any batch. Resending a span_id replaces that span.
  • Resend root_name, environment, source and tags on every batch. Each request overwrites them on the trace.
  • Use a different Idempotency-Key per batch (or none). Reusing one key for two different batches returns the first batch’s cached response and silently drops the second. See Idempotency and retries.

Validation errors

A payload that doesn’t match the schema returns 400 VALIDATION_ERROR with the problems in error.details.issues:
fieldErrors is keyed by the top-level field name. A problem inside any span is reported under spans without the span’s index, so check every span when you see it. Nothing from a rejected request is stored.

Full annotated example

A support agent that looks up an order, calls a model twice, and has one failed tool call. Comments are for explanation only; strip them before sending, because JSON doesn’t allow comments.
After this is ingested, the trace shows: