/docs /reference

Ingest reference.

The full contract for sending traces to TwoTail. For a guided setup, start with the quickstart.

last updated 2026-07-10 view as markdown

Endpoint

POST https://www.twotail.ai/api/v1/traces
X-API-Key: <TWOTAIL_API_KEY>
Content-Type: application/json

The body is standard OTLP/JSON (the OpenTelemetry OTLP/HTTP JSON encoding). OTLP protobuf, OTLP/gRPC, and gzip-compressed bodies are not accepted. API keys are created in the app under API Keys and sent in the X-API-Key header.

Response

{
  "success": true,
  "message": "Successfully ingested 12 spans from OTel format",
  "spans_inserted": 12
}

Spans that are valid OTLP but fail TwoTail's mapping are skipped, not rejected: the request still succeeds and the skip count usually shows in message as "(N malformed spans skipped)". spans_inserted is the authoritative count. A span missing required fields (traceId, spanId, name, timestamps) fails request validation instead, and the whole request is rejected with 422.

Errors

StatusWhen
401The X-API-Key header is missing, or the key is malformed, revoked, or expired.
400More than 1,000 spans in one request. The whole batch is rejected; split it and resend.
422The body isn't valid OTLP/JSON, e.g. a span missing required fields. The whole request is rejected.
429The account hit a cap: for example, a batch that would push an account pending approval past its 10,000 stored-span limit. The whole batch is rejected; the response body says which limit applies. Contact support@twotail.ai to lift the pending-approval limit.

Retries

Retrying a batch is safe. A span resent with the same spanId refreshes that span's outputs, end time, tokens, cost, and error status instead of duplicating it; its name, trace, parent, start time, inputs, and metadata keep their first-write values. Span ids must therefore be unique across all your traces, not just within one trace: reusing an id for a different span corrupts the stored record.

Payload shape

{
  "resourceSpans": [{
    "resource": {
      "attributes": [
        {"key": "service.name", "value": {"stringValue": "my-agent"}}
      ]
    },
    "scopeSpans": [{
      "spans": [{
        "traceId": "4bf92f3577b34da6a3ce929d0e0e4736",
        "spanId": "00f067aa0ba902b7",
        "parentSpanId": null,
        "name": "chat_completion",
        "startTimeUnixNano": 1783977600000000000,
        "endTimeUnixNano": 1783977600100000000,
        "attributes": [
          {"key": "gen_ai.system", "value": {"stringValue": "openai"}}
        ],
        "status": {"code": 1}
      }]
    }]
  }]
}
FieldNotes
traceId32 lowercase hex chars. One trace id per agent run; every span in the run shares it.
spanId16 lowercase hex chars, unique per span.
parentSpanIdLinks a child span to its parent. Null or omitted for root spans.
startTimeUnixNano / endTimeUnixNanoInteger Unix time in nanoseconds. Millisecond or second values are accepted without error but land decades in the past.
attributesOTLP key-value pairs. Values use typed wrappers: stringValue, intValue, doubleValue, boolValue, arrayValue, kvlistValue.
status.code0 unset, 1 ok, 2 error. Only 2 marks the span as an error.
resource attributesPreserved on each span of the batch, prefixed resource., e.g. resource.service.name.

Span typing

TwoTail assigns each span a type and, where it can, a purpose. Both drive filtering, cost attribution, and analysis.

TypeAssigned when
llmgen_ai.system or gen_ai.request.model is present, or the span name contains "llm" or "chat".
tooltool.name is present, or the span name contains "tool".
runEverything else (agent steps, orchestration, custom spans).

Rules are checked in order: gen_ai attributes, then tool.name, then name keywords.

PurposeAssigned when the span name
evaluationstarts with eval. or eval_, or contains "evaluat".
planningcontains "plan".
generationcontains "generat" or "complet".
retrievalcontains "search" or "retriev".

Attributes TwoTail reads

TwoTail understands several instrumentation conventions side by side. Send whichever your stack produces; you don't need to translate between them.

KeyWhat it carries
OpenTelemetry GenAI conventions
gen_ai.systemProvider, e.g. "openai", "anthropic". Marks the span as an LLM call.
gen_ai.request.modelModel name. Also drives cost when no explicit cost is sent.
gen_ai.prompt / gen_ai.input.messagesThe input prompt or structured message list.
gen_ai.completion / gen_ai.output.messagesThe output completion or structured messages.
gen_ai.request.temperature, gen_ai.request.max_tokensRequest parameters.
gen_ai.response.finish_reasons, gen_ai.response.idResponse metadata.
gen_ai.usage.input_tokens, gen_ai.usage.output_tokensToken counts.
gen_ai.usage.cached_input_tokensCached input tokens, counted as a subset of input tokens.
gen_ai.usage.costExplicit cost in USD. A nonzero value overrides computed cost.
Tool calls
tool.nameTool being called. Marks the span as a tool call.
tool.parameters / tool.resultTool input and output.
Vercel AI SDK telemetry
ai.model.idModel name.
ai.prompt, ai.prompt.messagesPrompt and message list.
ai.response.text, ai.response.toolCalls, ai.response.finishReasonResponse content.
ai.toolCall.args / ai.toolCall.resultTool call input and output.
ai.usage.cachedInputTokensCached input tokens.
OpenInference (e.g. LlamaIndex instrumentation)
input.value / output.valueSpan input and output.
llm.input_messages / llm.output_messagesStructured messages.
llm.invocation_parametersRequest parameters.
llm.token_count.prompt / llm.token_count.completionToken count fallbacks.
OpenLLMetry
traceloop.entity.input / traceloop.entity.outputEntity input and output.

Values for these keys that arrive as stringified JSON are parsed into structured objects, so message lists and tool arguments render properly in the trace view. The exception is gen_ai.prompt, which is stored verbatim: prefer gen_ai.input.messages for structured message lists.

Everything else

Attributes TwoTail doesn't specifically read are preserved as span metadata and stay queryable in analysis: business ids like user.id or document.id, feature flags, whatever you send. Values keep the type you sent them with. One consequence worth knowing: a flag sent as the string "1" is compared as a string, not the number 1, when you later ask questions about it.

Sessions

For multi-turn agents, TwoTail groups traces into sessions by conversation id: one trace per turn, the same conversation id on every turn. Set gen_ai.conversation.id on the spans of each turn.

Aliases accepted, first match wins: gen_ai.conversation.id, session.id, session_id, sessionId, conversation.id, conversation_id, thread.id, thread_id.

Evals

Send evaluations as child spans of the span they evaluate: set the eval span's parentSpanId to the evaluated span's id.

  • Naming. Start the span name with eval. or eval_ (e.g. eval.relevance), or use a name containing "evaluation" or "evaluate", so the span gets purpose evaluation.
  • Scores. Use the OTel GenAI eval conventions: gen_ai.evaluation.name, gen_ai.evaluation.score.value (numeric), gen_ai.evaluation.score.label (e.g. "pass"), gen_ai.evaluation.explanation. Legacy eval.name / eval.score / eval.passed / eval.reason are still accepted.
  • LLM judges. If an LLM produces the evaluation, include gen_ai.* attributes on the eval span so the judge call itself is captured with model, tokens, and cost.

Cost

Cost is attributed to the exact LLM span that spent it; it is not rolled up onto parents, so grouping and slicing spend stays accurate at any level.

  • Explicit wins. If you send a nonzero gen_ai.usage.cost, that value is used as-is; a cost of 0 falls back to the computed estimate.
  • Otherwise computed. Cost is derived from token counts when gen_ai.request.model matches a model in TwoTail's pricing table of known models. The match is exact: a provider-specific alias or dated snapshot name that isn't recognized yields no cost rather than a wrong guess.
  • Cached tokens priced. Cached input tokens (see attributes) are priced at the provider's cached rate where TwoTail has one for the model, otherwise at a default of 10% of the input rate.
  • Recommendation. Send canonical model names, or send gen_ai.usage.cost explicitly if you use gateways or exotic model ids.

Gotchas

  • Errors are status-code only. A span is an error if and only if status.code is 2. Exception text in the name or attributes does not mark it.
  • Nanoseconds. Timestamps in milliseconds or seconds are accepted silently and produce 1970-era spans.
  • Token values must be numeric. Malformed token counts coerce to 0 rather than erroring.
  • Strings compare as strings. Custom flags sent as "0"/"1" strings are compared as text in analysis, not as numbers.
  • Span ids must be unique across all your traces. Resending a span id refreshes the original span; reusing one for a different span corrupts it.
  • Watch spans_inserted. It is the authoritative count of what landed; don't rely on the message text alone.

Spot a problem on this page? Email us.