> ## Documentation Index
> Fetch the complete documentation index at: https://to11.ai/docs/llms.txt
> Use this file to discover all available pages before exploring further.

# Direct Ingestion

> How to send custom OpenTelemetry spans directly to the to11 collector from any OTel SDK.

The gateway instruments LLM calls automatically. For custom spans — non-LLM operations, your own pipeline steps, or enriching traces from your own services — you can send OpenTelemetry data directly to to11. Anything the gateway never sees, you can still put on the record.

## When to use direct ingestion

* **Custom spans for non-LLM operations** — data preprocessing, post-processing, or any application logic you want visible alongside LLM calls.
* **Enriching gateway traces with application-side timing** — capture your full RAG pipeline end to end, including database queries and retrieval steps.
* **Agent lifecycle spans** — operations like `create_agent` or session management that are better emitted from your client than from the gateway.

## Prerequisites

* A to11 API key with `otel:write` scope.
* An OpenTelemetry SDK in your language of choice.

## Exchange an API key for a collector token

Direct ingestion authenticates with a short-lived token. Exchange your API key for one before configuring your SDK:

```bash theme={null}
curl -X POST https://api.to11.ai/v1/ingest-token/exchange \
  -H "Content-Type: application/json" \
  -d '{
    "credential": "your-api-key"
  }'
```

Response:

```json theme={null}
{
  "token": "eyJ...",
  "projectId": "proj_abc123",
  "scopeClass": "project",
  "scopes": ["otel:write"],
  "expiresAt": "2026-03-25T17:30:00Z"
}
```

The returned `projectId` is the project the token is scoped to. to11 binds every ingested span to this project, so you do not pass it explicitly when sending spans.

If your API key is org-scoped rather than project-scoped, include `requestedProjectId` in the request body to select the target project. The token is rejected if the key is not authorized for that project.

<Note>
  Tokens expire after 15 minutes. Refresh by calling the exchange endpoint again before expiry.
</Note>

## Configure your OTel SDK

to11 accepts OTLP on two public endpoints:

| Protocol | Endpoint                              |
| -------- | ------------------------------------- |
| gRPC     | `collector.to11.ai:443`               |
| HTTP     | `https://collector.to11.ai/v1/traces` |

<CodeGroup>
  ```python Python theme={null}
  from opentelemetry import trace
  from opentelemetry.sdk.trace import TracerProvider
  from opentelemetry.sdk.trace.export import BatchSpanProcessor
  from opentelemetry.exporter.otlp.proto.grpc.trace_exporter import OTLPSpanExporter
  from opentelemetry.sdk.resources import Resource

  resource = Resource.create({
      "service.name": "my-rag-pipeline",
  })

  exporter = OTLPSpanExporter(
      endpoint="collector.to11.ai:443",
      headers={"authorization": f"Bearer {token}"},
      # TLS is on by default
  )

  provider = TracerProvider(resource=resource)
  provider.add_span_processor(BatchSpanProcessor(exporter))
  trace.set_tracer_provider(provider)
  ```

  ```typescript Node.js theme={null}
  import { OTLPTraceExporter } from "@opentelemetry/exporter-trace-otlp-grpc";
  import { BasicTracerProvider, BatchSpanProcessor } from "@opentelemetry/sdk-trace-base";
  import { resourceFromAttributes } from "@opentelemetry/resources";

  const resource = resourceFromAttributes({
    "service.name": "my-rag-pipeline",
  });

  const exporter = new OTLPTraceExporter({
    url: "https://collector.to11.ai",
    headers: { "authorization": `Bearer ${token}` },
  });

  const provider = new BasicTracerProvider({
    resource,
    spanProcessors: [new BatchSpanProcessor(exporter)],
  });

  provider.register();
  ```
</CodeGroup>

<Note>
  You do not set `project.id` yourself. to11 binds the spans to your project automatically — the project the token was issued for — and overwrites any value you send.
</Note>

## Send spans

<CodeGroup>
  ```python Python theme={null}
  tracer = trace.get_tracer("my-rag-pipeline")

  with tracer.start_as_current_span("document-preprocessing") as span:
      span.set_attribute("pipeline.stage", "preprocessing")
      span.set_attribute("document.count", 42)
      # ... your processing code ...
  ```

  ```typescript Node.js theme={null}
  import { trace } from "@opentelemetry/api";

  const tracer = trace.getTracer("my-rag-pipeline");

  const span = tracer.startSpan("document-preprocessing");
  span.setAttribute("pipeline.stage", "preprocessing");
  span.setAttribute("document.count", 42);
  // ... your processing code ...
  span.end();
  ```
</CodeGroup>

## Correlate with gateway traces

To place your custom spans in the same trace as gateway LLM calls, propagate the W3C `traceparent` header. See [Distributed Tracing](/docs/instrument/distributed-tracing) for details.

```python theme={null}
from opentelemetry.propagate import inject

headers = {}
inject(headers)  # Adds traceparent to headers dict

# Pass these headers to the gateway
response = requests.post(
    "https://gw.to11.ai/v1/chat/completions",
    headers={
        **headers,
        "x-to11-authorization": f"Bearer {to11_api_key}",
        "Authorization": f"Bearer {provider_api_key}",
        "Content-Type": "application/json",
    },
    json={"model": "gpt-4o", "messages": [...]},
)
```
