Ingestion
How a batch of spans reaches a project over the HTTP API, what is checked before anything is stored, and what the receipt says.
EvalBook stores traces: trees of spans that share a trace_id. One schema serves every producer — SDKs, direct HTTP, the OpenTelemetry adapter and CodeBook's run export — and every producer gets the same durable receipt.
The tenant is never taken from the body. A batch lands in the organization and project of the API key that sent it, and in the environment that key is bound to.
Status (25 October 2026). The traces component is built in the application (ingestion, explorer, settings, exports and deletions pages are live in the shell); its requirements EV-002 to EV-009 are not yet marked implemented in the requirements ledger and are being verified now. Nothing on this page is marked verified.
The endpoint and the credential
The path is /api/v1/traces on the app's origin:
curl -X POST "$EVALBOOK_APP/api/v1/traces" \
-H "Authorization: Bearer evb_…" \
-H "Content-Type: application/json" \
--data @batch.json
The key needs the ingest scope, which maps to the trace.ingest capability (see API keys). The body must be application/json; any other media type is 415. A batch naming another organization or project is 403 TENANT_MISMATCH.
The environment comes from the key. A key bound to an environment sends only there; a batch naming a different one is 403 ENVIRONMENT_MISMATCH and nothing is stored. An unbound key may name an environment in the batch (a lower-case key: letters, digits, ., _, -, at most 64 characters); with none, the batch lands in default. Environments and targets are described under Environments and targets.
The batch
{ "batch_id": "b-2026-10-15-0001",
"environment": "production",
"sdk": { "name": "evalbook-ts", "version": "1.0.0" },
"spans": [ … ] }
batch_id(1–200 characters) makes a resend idempotent. The same id with the same content answers the original receipt withduplicate: true; the same id with different content is409 BATCH_CONFLICT.- At most 1,000 spans and 5 MB (decompressed) per batch. Over that is
413before anything is stored.
A span
| Field | Rules |
|---|---|
trace_id |
16–64 characters [A-Za-z0-9_-]; OpenTelemetry's 32-hex ids are accepted |
span_id, parent_span_id |
8–64 characters; the parent must be in the same trace and may arrive in a later batch |
name |
1–200 characters |
kind |
run, step, llm, tool, retrieval, embedding, evaluator, human, http, function, other |
purpose |
application (default), evaluation, investigation, simulation |
start_time, end_time |
milliseconds or ISO-8601; end_time is null while the span runs |
status, error |
ok, error, cancelled, unset; error is { type, message (≤2,000), retryable? } |
input, output |
any JSON; subject to the capture policy; at most 256 KB each inline |
attributes |
flat string → string, number or boolean; at most 128 keys of at most 128 characters |
model, usage |
{ provider, requested, resolved, temperature?, max_tokens? }; { input_tokens, output_tokens, cached_tokens, ledger_ref?, cost? } |
tool, retrieval |
{ name, call_id, arguments?, result? }; { query?, documents: [{ id, score?, source? }] } |
links, events |
links to artifacts, commits, pull requests, dataset examples, evaluations, CodeBook runs and cards; at most 128 events |
Two attributes mean something to EvalBook: evalbook.target names a target by its public id, and evalbook.judge = forbidden marks a span that no external judge may see (see Redaction and capture). Costs in usage are allocated here, never billed.
What happens before storage
In this order, on every batch:
- Shape and limits —
413before anything is stored. - Tenant — the key's, never the body's.
- Idempotency by
batch_id. - Quota and backpressure —
429withRetry-After; the refusal is counted (see Sampling and backpressure). - Every span through the schema. A refused row is refused whole and listed in the receipt; nothing of it is stored.
- Deleted traces refused (
TRACE_DELETED); sampling decided per trace; dropped spans counted. - The capture policy and redaction, before storage;
metadata_onlystores no body. - Bodies over the inline cap go to Bookbag SSO's file store, outside the transaction.
- One transaction, committed before the producer is answered.
The receipt
{ "data": true, "receipt": { "id": "rcpt_…", "batch_id": "…", "accepted": 998,
"rejected": [ { "index": 4, "code": "INVALID_SPAN", "field": "end_time", "error": "end_time before start_time", "retryable": false } ],
"dropped_by_sampling": 0, "redacted_fields": 12, "duplicate": false } }
The answer is 202, and the receipt is written durably before it is sent. Row codes are INVALID_SPAN, TENANT_MISMATCH, TRACE_DELETED and SPAN_ALREADY_ENDED — a span with no end_time may be completed by a later batch, but an ended span is never rewritten.
Batch refusals: 403 TENANT_MISMATCH / ENVIRONMENT_MISMATCH, 409 BATCH_CONFLICT, 413 BATCH_TOO_LARGE, 415 UNSUPPORTED_MEDIA_TYPE, 422 INVALID_BATCH, 429 QUOTA_EXCEEDED / INGEST_RATE_LIMITED. A refused batch is counted and never half-stored, so the producer can resend it as it is.
Receipts in the app
Traces → Ingestion (capability trace.read) shows the allowance and limits, spans per hour (received, stored, rejected, sampled out, quota), and the receipts newest first. Opening a receipt shows the batch, producer, environment, spans, traces, bodies, producer lag, size and every rejected row with its reason.
Other producers
- OpenTelemetry exporters post to a separate endpoint; see OpenTelemetry.
- A linked CodeBook workspace sends its runs as spans over the signed peer route, into the linked project only; see CodeBook.
- EvalBook's own evaluation and judge spans are recorded with
purpose: evaluationin a reserved environment and are never sampled.