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."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 throughparent_span_id:
- The root span has no
parent_span_id. A trace normally has exactly one. - A child’s
parent_span_idmust equal its parent’sspan_idwithin 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_atorder 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_idon every request. - Each span only needs to be sent once, in any batch. Resending a
span_idreplaces that span. - Resend
root_name,environment,sourceandtagson every batch. Each request overwrites them on the trace. - Use a different
Idempotency-Keyper 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 returns400 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.