# OTLP endpoint

> Reference for the OTLP/HTTP endpoint that receives traces, its responses, limits and the attributes it reads.

## Endpoint

```text
POST https://api.divergentlabs.xyz/api/otlp/v1/traces
```

The endpoint receives OTLP/HTTP trace exports. It does not receive gRPC, metrics or logs. Set the base URL, and your exporter adds `/v1/traces`:

```bash
export OTEL_EXPORTER_OTLP_ENDPOINT=https://api.divergentlabs.xyz/api/otlp
export OTEL_EXPORTER_OTLP_HEADERS="Authorization=Bearer%20$DIVERGENT_API_KEY"
```

If you also set `OTEL_EXPORTER_OTLP_TRACES_ENDPOINT`, it must be the full URL, ending in `/v1/traces`.

## Request

| Header | Values |
| --- | --- |
| `Authorization` | `Bearer <key>`. The key needs the `traces:write` scope. |
| `Content-Type` | `application/x-protobuf` or `application/json` (OTLP/JSON, with hex trace and span ids). |
| `Content-Encoding` | `gzip`, or none. |

The body is an `ExportTraceServiceRequest`, at most 4 MiB as sent and 16 MiB uncompressed. A request with no spans succeeds.

## Responses

Every response is encoded like the request: protobuf for a protobuf request, JSON for a JSON request. Errors are a `google.rpc.Status` whose `message` is the error code, a colon and what to do.

| Status | Code | Meaning | Exporter retries |
| --- | --- | --- | --- |
| `200` | | Every span was stored, or had been stored before. | No |
| `200` | | Some spans were refused. `partial_success` has their count and the reason. See [Refused spans](#refused-spans). | No |
| `400` | `invalid_otlp` | The body is not an `ExportTraceServiceRequest` in its content type and encoding. | No |
| `401` | `unauthorized` | The key is missing or invalid. | No |
| `403` | `tracing_not_enabled` | The org is not in the tracing beta. Request access on the **Tracing** page in the dashboard. | No |
| `403` | `insufficient_scope` | The key lacks `traces:write`. Run `div login`, or add the scope to the key under **Settings**. | No |
| `413` | `payload_too_large` | Over 4 MiB as sent or 16 MiB uncompressed. Set `OTEL_BSP_MAX_EXPORT_BATCH_SIZE` lower and `OTEL_EXPORTER_OTLP_COMPRESSION=gzip`. | No |
| `415` | `unsupported_media_type` | A content type other than protobuf or JSON, or an encoding other than gzip. | No |
| `503` | `tracing_unavailable` | Nothing from the request was stored. Sent with `Retry-After`. | Yes |

Sending a span again is safe. A span with a trace id and span id that tracing has already stored succeeds and is not stored or counted twice.

## Refused spans

A refused span is counted in `partial_success.rejected_spans`, and `partial_success.error_message` says why, for example:

```text
Rejected 312 spans: this org has kept 1,000,000 spans in October 2026, the most tracing keeps during the beta. Spans are accepted again on 2026-11-01.
```

| Reason | Message |
| --- | --- |
| Bad id | `<n> had a trace or span id that is missing, all zeros or the wrong length.` |
| Bad time | `<n> had a start or end time past 2262.` |
| Trace full | `<n> would have taken a trace past 10,000 spans.` |
| Deleted trace | `<n> belong to traces deleted in the last 24 hours.` |
| Monthly spans | `this org has kept 1,000,000 spans in <month>, the most tracing keeps during the beta. Spans are accepted again on <date>.` |
| Monthly data | `this org has kept 5 GiB of spans in <month>, the most tracing keeps during the beta. Spans are accepted again on <date>.` |

Within one request, spans are accepted in the order they were sent until a limit is reached.

## Limits

| Limit | Value |
| --- | --- |
| Request body | 4 MiB as sent, 16 MiB uncompressed |
| Spans per org per UTC month | 1,000,000 |
| Stored span data per org per UTC month | 5 GiB |
| Spans per trace | 10,000 |
| Span name | 256 bytes |
| Input, output | 128 KiB each, encoded as JSON. The longest strings are cut first, and `input_truncated` or `output_truncated` is set. |
| Other string values | 8 KiB |
| Attributes per span or event | 128 |
| Events per span | 128 |
| Links per span | 32 |
| Resource attributes | 4 KiB |
| Span, in all | 384 KiB |
| Retention | 30 days from when tracing first received a span of the trace. Spans that arrive later are deleted with the trace. |

A longer string is cut at a character boundary and ends `…[truncated <n> bytes]`.

## Attributes tracing reads

Tracing reads these attributes into a span's kind, model, tokens, input, output, session and user. The first attribute in each list that a span has is used. Attributes that become input or output are removed from the span's attributes, so they are stored once. Other attributes are stored as they were sent.

### Kind

| Kind | Read from |
| --- | --- |
| `generation` | `openinference.span.kind` `LLM` or `EMBEDDING`; `gen_ai.operation.name` `chat`, `text_completion`, `generate_content` or `embeddings`; AI SDK `ai.operationId` ending `.doGenerate`, `.doStream` or `.doEmbed`; `llm.request.type` `chat`, `completion` or `embedding` |
| `tool` | `openinference.span.kind` `TOOL`; `gen_ai.operation.name` `execute_tool`; `ai.operationId` `ai.toolCall`; `traceloop.span.kind` `tool` |
| `agent` | `openinference.span.kind` `AGENT` or `CHAIN`; `gen_ai.operation.name` `invoke_agent` or `create_agent`; any other `ai.operationId`; `traceloop.span.kind` `agent` or `workflow` |
| `retrieval` | `openinference.span.kind` `RETRIEVER` or `RERANKER` |
| `span` | anything else |

A trace's tokens are the sum over its `generation` spans. Agent spans repeat the usage of the calls inside them and are not added again.

### Model and tokens

| Field | Read from |
| --- | --- |
| Model | `gen_ai.response.model`, `gen_ai.request.model`, `llm.model_name`, `ai.response.model`, `ai.model.id` |
| Input tokens | `gen_ai.usage.input_tokens`, `gen_ai.usage.prompt_tokens`, `llm.token_count.prompt`, `ai.usage.inputTokens`, `ai.usage.promptTokens` |
| Output tokens | `gen_ai.usage.output_tokens`, `gen_ai.usage.completion_tokens`, `llm.token_count.completion`, `ai.usage.outputTokens`, `ai.usage.completionTokens` |

### Input and output

| Convention | Attributes |
| --- | --- |
| GenAI | `gen_ai.input.messages`, `gen_ai.output.messages`, `gen_ai.system_instructions`, or the same on a `gen_ai.client.inference.operation.details` event |
| GenAI, indexed | `gen_ai.prompt.<n>.role`, `gen_ai.prompt.<n>.content`, `gen_ai.completion.<n>.*`, including `tool_calls` |
| GenAI tool calls | `gen_ai.tool.call.arguments`, `gen_ai.tool.call.result` |
| AI SDK | `ai.prompt.messages`, `ai.prompt`, `ai.response.text`, `ai.response.toolCalls`, `ai.toolCall.args`, `ai.toolCall.result` |
| OpenInference | `llm.input_messages.<n>.message.*`, `llm.output_messages.<n>.message.*`, `input.value`, `output.value` |
| Traceloop | `traceloop.entity.input`, `traceloop.entity.output` |

Messages are stored as `{"messages": [{"role": ..., "parts": [...]}]}`, with `text`, `tool_call` and `tool_call_response` parts. Other input and output is stored as `{"value": ...}`, parsed as JSON when it is JSON.

### Session, user, service and environment

| Field | Read from |
| --- | --- |
| Session | `session.id`, `gen_ai.conversation.id`, `langfuse.session.id`, `ai.telemetry.metadata.sessionId`, `ai.settings.context.sessionId`, `traceloop.association.properties.session_id` |
| User | `user.id`, `enduser.id`, `langfuse.user.id`, `ai.telemetry.metadata.userId`, `ai.settings.context.userId`, `traceloop.association.properties.user_id` |
| Service | resource `service.name` |
| Environment | resource `deployment.environment.name`, `deployment.environment` |

A trace takes each of these from its root span, or from its earliest span that has one.

## Removed before storing

Tracing removes credentials from every span before storing it. See [What tracing stores](https://docs.divergentlabs.xyz/tracing.md#what-tracing-stores). A span's `redacted_count` is the number of values removed.
