Semant / Ingest
Send runs to Semant
Semant keeps what your agents did. There are two ways in. An agent that emits OpenTelemetry sends spans to Semant's OTLP endpoint. Anything else sends one HTTP request per run. Both authenticate with an ingest key,sk_semant_..., which you create in Semant. A key writes for its organization and reads nothing.
OTLP
Point an OpenTelemetry exporter at Semant. It takes traces over OTLP/HTTP, protobuf or JSON, gzip allowed. Semant reads the gen_ai.* attributes of the OpenTelemetry GenAI conventions, which are still in development: names change between versions, and spans in an older shape are stored but only partly understood.
OTEL_EXPORTER_OTLP_ENDPOINT=https://semant.trysenda.xyz
OTEL_EXPORTER_OTLP_PROTOCOL=http/protobuf
OTEL_EXPORTER_OTLP_HEADERS=Authorization=Bearer%20sk_semant_...Run report
When an agent cannot emit spans, send one request at the end of a run: an HTTP step at the end of a flow. Semant turns the report into spans, an invoke_agent span for the run and one span for each step, and stores them like any others.
curl -X POST https://semant.trysenda.xyz/v1/runs \
-H "Authorization: Bearer sk_semant_..." \
-H "Content-Type: application/json" \
-d '{
"agent": { "name": "support-bot" },
"run_id": "order-4821-reply",
"started_at": "2026-10-01T12:00:00Z",
"ended_at": "2026-10-01T12:00:06Z",
"status": "ok",
"model": "some-model",
"input_tokens": 1200,
"output_tokens": 310,
"steps": [
{ "name": "look up the order", "started_at": "2026-10-01T12:00:01Z", "ended_at": "2026-10-01T12:00:02Z" },
{ "name": "write the reply", "started_at": "2026-10-01T12:00:02Z", "ended_at": "2026-10-01T12:00:06Z" }
],
"metadata": { "channel": "email" }
}'{ "trace_id": "5b8efff798038103d269b633813fc60c", "spans": 3 }Fields
| field | ||
|---|---|---|
agent.name | required | The agent's name. Runs of one name are one agent. |
agent.id | optional | Your own identifier for the agent. |
run_id | optional | Makes the request idempotent: the same id from the same organization stores the run once, so a retry never duplicates it. |
started_at, ended_at | required | ISO 8601 with a time zone. The end is not before the start. |
status | required | ok or error. |
error_message | optional | Shown with an error run. |
model | optional | The model that answered. |
input_tokens, output_tokens | optional | Non-negative integers. |
conversation_id | optional | Groups the runs of one conversation. |
steps | optional | Up to 2047 items of name, started_at, ended_at, and optional kind and status (ok or error). One span each, under the run. |
metadata | optional | A flat map of up to 64 string, number or boolean values. |
Answers
The status codes are the same as OTLP's. An error body names the field when one is at fault.
{ "error": { "code": "invalid_request", "field": "steps[0].ended_at", "message": "steps[0].ended_at must not be before steps[0].started_at" } }| 200 | Stored. The body carries the trace id and the span count. |
| 400 | The body is not valid JSON or a field is wrong. The error names the field. |
| 401 | No key, an unknown key or a revoked key. |
| 403 | The organization of the key is not active. |
| 413 | The body is over 4 MB as sent (16 MB decompressed). |
| 415 | Content-Type is not application/json, or Content-Encoding is not gzip. |
| 429 | Over 300 requests a minute for the key. Retry after the Retry-After seconds. |
| 503 | The store is unavailable. Retry after the Retry-After seconds. |
Limits
A run report carries a run, not every step, unless you send a step for each one. An agent hosted by a third party that you cannot edit reports nothing.