---
title: OpenTelemetry ingestion reference
description: "Reference for Amplitude's OTLP endpoints: which attributes set the session, user, and agent, how GenAI spans and log records map to [Agent] events, request limits, and response codes."
product: general
lang: en
last_updated: 2026-11-09
token_estimate: 3503
---
# OpenTelemetry ingestion reference

This page is the reference for how Amplitude's OTLP endpoints translate OpenTelemetry GenAI spans and log records into Agent Analytics `[Agent]` events. For setup steps, refer to [Send OpenTelemetry traces to Agent Analytics](https://amplitude.com/docs/amplitude-ai/agent-analytics/instrument-opentelemetry).

> For AI agents: a documentation index is available at [/docs/llms.txt](/docs/llms.txt). Append `.md` to any page URL for markdown, or send `Accept: text/markdown`.

## Endpoints and protocol

| Region | Traces | Logs |
| --- | --- | --- |
| US | `https://api.amplitude.com/otlp/v1/traces` | `https://api.amplitude.com/otlp/v1/logs` |
| EU | `https://api.eu.amplitude.com/otlp/v1/traces` | `https://api.eu.amplitude.com/otlp/v1/logs` |

- **Protocol**: OTLP over HTTP only, as `http/protobuf` or `http/json`, with optional gzip. Amplitude doesn't support OTLP/gRPC. Both endpoints use the same authentication, limits, and response codes.
- **Authentication**: The project API key as a `Bearer` token or as an `api_key` query parameter. Amplitude doesn't require a per-project enablement request.
- **Supported instrumentation**: OpenTelemetry GenAI semantic conventions, OpenInference (`llm.*`), OpenLLMetry and Traceloop (`traceloop.*`), OpenLIT, and LiteLLM. Amplitude ignores non-GenAI spans.

## Attribute resolution order

Amplitude reads the first attribute present, in this order:

| Signal | Attributes Amplitude reads, in order |
| --- | --- |
| Session | `gen_ai.conversation.id`, `session.id`, `traceloop.association.properties.session` (or `chat`, `thread`, `conversation`), `gen_ai.session.id`, then resource `session.id` and `gen_ai.conversation.id`. With none of these, Amplitude falls back to the trace ID and marks the session degraded. Claude-native spans are the exception: `anthropic.session.id` is the session, and a different `gen_ai.conversation.id` is kept as the thread. |
| User | `enduser.id`, `gen_ai.request.user`, `gen_ai.user`, `user.id`, `traceloop.association.properties.user`, resource `enduser.id` and `user.id`, then LiteLLM's `metadata.user_api_key_end_user_id`. With none of these, the user resolves to `unknown`. |
| Agent | `gen_ai.agent.id`, else resource `service.name`. Claude-native spans don't fall back to `service.name`, so `claude-platform` isn't stored as the agent. |
| Parent agent | Span attribute `amplitude.parent.agent.id` only. A parent span ID and resource attributes aren't a parent agent. |
| Environment | `deployment.environment`, on the span or the resource. |

## What maps to what

| OpenTelemetry source | `[Agent]` result |
| --- | --- |
| `gen_ai.operation.name` of `chat`, `text_completion`, or `generate_content` | `[Agent] AI Response`, plus an `[Agent] User Message` synthesized from the new input messages |
| `gen_ai.operation.name` of `execute_tool` | `[Agent] Tool Call`, with `gen_ai.tool.name`, `gen_ai.tool.call.id`, arguments, and result. A `commit_sha` event property is added from the `[branch sha]` line of `git commit` output, or from the `sha` in a JSON result when the tool name or arguments name a commit, pull request, or file push as a whole word. `git log`, `git show`, and other hex strings are never read. |
| Any other operation, such as `invoke_agent`, `retrieval`, `plan`, or the memory operations | `[Agent] Span` |
| `gen_ai.request.model`, `gen_ai.response.model` | `[Agent] Model Name` |
| `gen_ai.provider.name`, or the deprecated `gen_ai.system` | `[Agent] Provider` |
| `gen_ai.usage.input_tokens`, `gen_ai.usage.output_tokens`, and the reasoning and cache token attributes | Token properties |
| `gen_ai.usage.cost` | `[Agent] Cost USD`. This is an Amplitude extension: OpenTelemetry doesn't standardize cost, so most instrumentation omits it. |
| `gen_ai.response.finish_reasons` | `[Agent] Finish Reason` |
| `gen_ai.input.messages`, `gen_ai.output.messages` | `$llm_message` |
| `gen_ai.system_instructions`, or a `system`-role message | `[Agent] System Prompt` on the AI Response |
| Span status of `ERROR` | `[Agent] Is Error`, `[Agent] Error Message` |
| `trace_id`, `span_id`, `parent_span_id`, start time, duration | `[Agent] Trace ID`, `[Agent] Span ID`, `[Agent] Parent Span ID`, event timestamp, `[Agent] Latency Ms` |
| `amplitude.parent.agent.id` | `[Agent] Parent Agent ID` |
| The seven agent context attributes, such as `amplitude.peer.session.id` | The matching agent context properties. Refer to [Agent context attributes](#agent-context-attributes). |
| Derived on the synthesized user message | `[Agent] Message Source`: explicit `amplitude.message.source` wins; otherwise `agent` when a parent agent is present, else `user` |

Amplitude doesn't calculate a cost when `gen_ai.usage.cost` is absent. Refer to [How cost is calculated](https://amplitude.com/docs/amplitude-ai/agent-analytics/taxonomy#how-cost-is-calculated).

A chat call re-sends the whole prompt history, so only the user messages after the last assistant message become a new `[Agent] User Message`. Amplitude doesn't re-emit replayed history, and a tool-loop continuation whose new input is a tool result doesn't produce a user turn.

The AI SDK's in-process OpenTelemetry exporter uses its own mapping. Refer to [OpenTelemetry attribute mapping](https://amplitude.com/docs/sdks/agent-analytics/sdk#opentelemetry-attribute-mapping) in the SDK reference.

## Agent context attributes

These span attributes record how your agent handled a call from another agent. Amplitude maps each one to its `[Agent]` property. For what the values mean, refer to [Agent context properties](https://amplitude.com/docs/amplitude-ai/agent-analytics/taxonomy#agent-context-properties).

| Span attribute | `[Agent]` property |
| --- | --- |
| `amplitude.peer.agent.id` | `[Agent] Peer Agent ID` |
| `amplitude.peer.session.id` | `[Agent] Peer Session ID` |
| `amplitude.peer.trace.id` | `[Agent] Peer Trace ID` |
| `amplitude.context.trust` | `[Agent] Context Trust` |
| `amplitude.context.verify.result` | `[Agent] Context Verify Result` |
| `amplitude.actor.type` | `[Agent] Actor Type` |
| `amplitude.identity.type` | `[Agent] Identity Type` |

Amplitude trusts span attributes from any exporter that holds your project API key, the same as events. It doesn't check agent context signatures on spans. Set these attributes from your own verification logic, such as the AI SDK's, and never copy them from an inbound request.

`[Agent] Context Trust` follows `amplitude.context.verify.result`: `ok` records `trusted`, and any other result records `untrusted`, whatever `amplitude.context.trust` says. Without a verify result, Amplitude keeps `amplitude.context.trust` only when it's `none`.

> **Warning:** Don't copy inbound baggage into identity attributes
>
> OpenTelemetry baggage span processors copy inbound W3C `baggage` entries into span attributes. A caller from outside your trust boundary can then set your session, user, or agent context. Don't copy inbound baggage keys for `session.id`, `user.id`, `enduser.id`, `gen_ai.conversation.id`, or any `amplitude.*` attribute from untrusted callers. Allowlist only the baggage keys you set yourself.

## Feature flag attributes

Amplitude reads OpenTelemetry feature flag evaluations from GenAI spans and records each one as an experiment. It reads every `feature_flag.evaluation` span event first, then the span's own `feature_flag.*` attributes. Resource attributes don't count.

| OpenTelemetry source | Experiment field |
| --- | --- |
| `feature_flag.key` | Key |
| `feature_flag.result.variant`, or the older `feature_flag.variant` | Variant |
| `feature_flag.provider.name`, or the older `feature_flag.provider_name` | Provider |
| `amplitude.experiment.unit` of `agent_session` or `turn` | Unit. Without it, or with any other value, the unit is `span`. |

Every event the span produces carries `[Agent] Experiments`, plus `[Agent] Experiment Key`, `Variant`, `Unit`, and `Provider` when exactly one evaluation remains. An evaluation without a variant, such as one that returned an error, adds nothing. When two evaluations share a key, the later one wins, so a span attribute overrides a span event with the same key. The validation rules and the 10-experiment limit match the AI SDK. Refer to [Record experiment variants in Agent Analytics](https://amplitude.com/docs/amplitude-ai/agent-analytics/track-experiments).

## Claude Platform spans

Amplitude applies these rules only when the span name starts with `anthropic.` and the span also carries `gen_ai.provider.name=anthropic`, a `com.anthropic.claude-platform` or `com.anthropic.sdk` instrumentation scope, or resource `service.name=claude-platform`. A generic `invoke_agent` span, and an Anthropic chat span with any other name, stay on the mapping above.

| Claude span | `[Agent]` result |
| --- | --- |
| `anthropic.messages.create` | User message and AI response. This span keeps the token total. |
| `anthropic.model_request` under Messages API | `[Agent] Span` with usage removed, so the parent total isn't counted again. |
| `anthropic.session.turn` with message text | User message and AI response, with no usage copied onto those events. |
| `anthropic.session.turn` with no message text | `[Agent] Span`, with usage removed. |
| `anthropic.model_request` with `gen_ai.operation.name=chat` | `[Agent] Span` that keeps model, tokens, cache, latency, and error. Several model requests each keep their own usage. |
| Span name starting with `anthropic.tool_use`, `anthropic.mcp_tool_use`, or `anthropic.custom_tool_use` | `[Agent] Tool Call`. `anthropic.tool_result.content` is the tool output. `anthropic.mcp.server.name` is kept as an extra property. |
| `anthropic.test` | Accepted and dropped. It doesn't appear in Live Events. |

`anthropic.message.stop_reason`, then `anthropic.session.turn.stop_reason`, becomes `[Agent] Finish Reason` on the AI Response when `gen_ai.response.finish_reasons` is absent. Amplitude doesn't invent `enduser.id` or `gen_ai.usage.cost`. Parent span IDs stay hierarchy. They aren't `[Agent] Parent Agent ID`.

To add the destination, refer to [Send Claude Platform traces](https://amplitude.com/docs/amplitude-ai/agent-analytics/instrument-opentelemetry#send-claude-platform-traces).

## Log records

The logs endpoint turns an allowlist of agent log events into `[Agent]` events. Amplitude accepts every other log record and drops it, so you can point a mixed logs pipeline at Amplitude.

| Log event | `[Agent]` result |
| --- | --- |
| `claude_code.user_prompt` | `[Agent] User Message` |
| `claude_code.api_request` | `[Agent] AI Response` with model, tokens, cost, and latency |
| `claude_code.assistant_response` | Reply text on the `[Agent] AI Response` with the same `request_id` |
| `claude_code.tool_result` | `[Agent] Tool Call`. A record that carries `vcs.ref.head.revision` adds a `commit_sha` event property. Claude Code v2.1.269 or later sends it only with `OTEL_LOG_TOOL_DETAILS=1`, for a successful `git commit` run by the Bash or PowerShell tool. |
| `claude_code.subagent_completed` | `[Agent] Span` |
| `gen_ai.user.message` | User message text for the span with the same trace and span ID |
| `gen_ai.assistant.message`, `gen_ai.choice` | Reply text for the span with the same trace and span ID |
| `gen_ai.tool.message` | Tool content for the span with the same trace and span ID |

Some sources split one model call across two records: one with tokens and cost, and one with the text. Amplitude merges them into one turn in either arrival order. Tokens, cost, and turn counts are counted once, and a resent record changes nothing.

### Log identity

Amplitude reads the session, user, and agent from the log record and its resource. A record without a session ID, or without a user ID that can join your data, is skipped rather than stored under a trace ID or as `unknown`.

| Signal | Claude Code (`service.name=claude-code`) | Other sources |
| --- | --- | --- |
| Session | `session.id` | The [attribute resolution order](#attribute-resolution-order) |
| Trace | `prompt.id` | The record's trace ID |
| User | `enduser.id`, then `user.email`, then `user.account_uuid`. `user.id` only when `identity.source=gateway-oidc`. | Resource `enduser.id`, then the attribute resolution order |
| Agent | `agent_id`, else `claude-code` | `gen_ai.agent.id`, then `gen_ai.agent.name`, then resource `service.name` |
| Parent agent | `parent_agent_id` | `amplitude.parent.agent.id` |

Set `OTEL_RESOURCE_ATTRIBUTES=enduser.id=<user ID>` to choose the user explicitly. It overrides every other rule.

### One canonical signal per source

Each source has one signal that carries its turns, so nothing counts twice when you export both traces and logs.

- **Claude Code**: Logs carry the turn. Exported `claude_code.*` spans become `[Agent] Span` events without tokens or cost.
- **Every other source**: Spans carry the turn. GenAI log events only add text to the turn the matching span produced.

## Delivery, limits, and responses

Delivery is at least once. Run a Collector with a persistent sending queue and keep retries enabled. An in-memory queue loses buffered spans and log records on restart.

| Limit | Behavior |
| --- | --- |
| 4 MB decompressed request body | `413` |
| 2,000 translated events per export | `413`. Lower your Collector batch size. |

| Response | Meaning |
| --- | --- |
| `200` | Accepted. A `partial_success` body reports any events that ingestion rejected. Watch for it, because a `200` tells your Collector to delete the batch. |
| `400` | Malformed request body or OTLP payload. Not retryable. |
| `401` | Missing or invalid API key. |
| `415` | A content type other than `application/x-protobuf` or `application/json`. |
| `503` | Temporary, including downstream ingestion outages. Your Collector should retry from its queue. |

The OTLP receiver never returns `403`. Treat a `403` from another hop, such as a CDN or WAF, as a configuration error, not a missing Amplitude enablement.

