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.namerequiredThe agent's name. Runs of one name are one agent.
agent.idoptionalYour own identifier for the agent.
run_idoptionalMakes the request idempotent: the same id from the same organization stores the run once, so a retry never duplicates it.
started_at, ended_atrequiredISO 8601 with a time zone. The end is not before the start.
statusrequiredok or error.
error_messageoptionalShown with an error run.
modeloptionalThe model that answered.
input_tokens, output_tokensoptionalNon-negative integers.
conversation_idoptionalGroups the runs of one conversation.
stepsoptionalUp to 2047 items of name, started_at, ended_at, and optional kind and status (ok or error). One span each, under the run.
metadataoptionalA 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" } }
200Stored. The body carries the trace id and the span count.
400The body is not valid JSON or a field is wrong. The error names the field.
401No key, an unknown key or a revoked key.
403The organization of the key is not active.
413The body is over 4 MB as sent (16 MB decompressed).
415Content-Type is not application/json, or Content-Encoding is not gzip.
429Over 300 requests a minute for the key. Retry after the Retry-After seconds.
503The 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.