---
title: "Send OTLP directly"
description: "Export OpenTelemetry traces to Judgment over OTLP/HTTP without the judgeval SDK."
sidebar:
  label: "Send OTLP directly"
seo:
  title: "Send OTLP directly | Judgment Docs"
  description: "Export OpenTelemetry traces to Judgment over OTLP/HTTP without the judgeval SDK."
---

This fits a service in Go or Java, an OpenTelemetry Collector you already run, or a tool like OpenClaw that ships its own exporter. Any OTLP/HTTP exporter works.

## Connection details

| Setting | Value |
|---|---|
| Endpoint | `https://api.judgmentlabs.ai/otel/v1/traces` (POST) |
| `Authorization` | `Bearer <JUDGMENT_API_KEY>` |
| `X-Organization-Id` | your organization ID. Needed for a personal API key; ignored for an ingest API key |
| `X-Project-Id` | the project's ID, not its name |
| Body | OTLP protobuf by default. Send `Content-Type: application/json` for OTLP/JSON |
| Compression | `Content-Encoding: gzip` is supported |

Use an ingest API key for `JUDGMENT_API_KEY`: it can only send traces, so it is safer than a personal API key, which also works. An ingest API key already belongs to one organization, so the server ignores `X-Organization-Id` for it. See [Authentication and projects](/documentation/reference/authentication-and-projects#ingest-api-keys).

The main difference from the SDK is the project ID. The SDK turns `project_name` into an ID and creates the project if it's missing. Raw OTLP does neither. Create the project first (running the SDK once with the name works), then copy the ID from the project's URL in the app, or press Ctrl-K and search for the project ID. That ID goes in `X-Project-Id`.

The examples below read the ID from `JUDGMENT_PROJECT_ID`. That's your own variable for your exporter config; the SDK doesn't read it.

**Python**

```bash
pip install opentelemetry-sdk opentelemetry-exporter-otlp-proto-http
```

```python title="otlp.py"
import os

from opentelemetry import trace
from opentelemetry.exporter.otlp.proto.http.trace_exporter import OTLPSpanExporter
from opentelemetry.sdk.resources import Resource
from opentelemetry.sdk.trace import TracerProvider
from opentelemetry.sdk.trace.export import BatchSpanProcessor

resource = Resource.create(
    {"service.name": "lunch-order-agent", "deployment.environment.name": "production"}
)
provider = TracerProvider(resource=resource)
provider.add_span_processor(
    BatchSpanProcessor(
        OTLPSpanExporter(
            endpoint="https://api.judgmentlabs.ai/otel/v1/traces",
            headers={
                "Authorization": f"Bearer {os.environ['JUDGMENT_API_KEY']}",
                "X-Organization-Id": os.environ["JUDGMENT_ORG_ID"],
                "X-Project-Id": os.environ["JUDGMENT_PROJECT_ID"],
            },
        )
    )
)
trace.set_tracer_provider(provider)

tracer = trace.get_tracer("lunch-order-agent")
with tracer.start_as_current_span("handle_request"):
    pass

provider.force_flush()
```

**TypeScript**

```bash
npm install @opentelemetry/api @opentelemetry/exporter-trace-otlp-http @opentelemetry/resources @opentelemetry/sdk-trace-node
```

```typescript title="otlp.ts"
import { trace } from "@opentelemetry/api";
import { OTLPTraceExporter } from "@opentelemetry/exporter-trace-otlp-http";
import { resourceFromAttributes } from "@opentelemetry/resources";
import { BatchSpanProcessor, NodeTracerProvider } from "@opentelemetry/sdk-trace-node";

const exporter = new OTLPTraceExporter({
  url: "https://api.judgmentlabs.ai/otel/v1/traces",
  headers: {
    Authorization: `Bearer ${process.env.JUDGMENT_API_KEY}`,
    "X-Organization-Id": process.env.JUDGMENT_ORG_ID ?? "",
    "X-Project-Id": process.env.JUDGMENT_PROJECT_ID ?? "",
  },
});

const provider = new NodeTracerProvider({
  resource: resourceFromAttributes({
    "service.name": "lunch-order-agent",
    "deployment.environment.name": "production",
  }),
  spanProcessors: [new BatchSpanProcessor(exporter)],
});
provider.register();

const span = trace.getTracer("lunch-order-agent").startSpan("handle_request");
span.end();
await provider.forceFlush();
```

**Go**

```bash
go get go.opentelemetry.io/otel go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp go.opentelemetry.io/otel/sdk
```

```go title="otlp.go"
package main

import (
	"context"
	"os"

	"go.opentelemetry.io/otel"
	"go.opentelemetry.io/otel/attribute"
	"go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp"
	"go.opentelemetry.io/otel/sdk/resource"
	sdktrace "go.opentelemetry.io/otel/sdk/trace"
)

func main() {
	ctx := context.Background()

	exporter, err := otlptracehttp.New(
		ctx,
		otlptracehttp.WithEndpointURL("https://api.judgmentlabs.ai/otel/v1/traces"),
		otlptracehttp.WithHeaders(map[string]string{
			"Authorization":     "Bearer " + os.Getenv("JUDGMENT_API_KEY"),
			"X-Organization-Id": os.Getenv("JUDGMENT_ORG_ID"),
			"X-Project-Id":      os.Getenv("JUDGMENT_PROJECT_ID"),
		}),
	)
	if err != nil {
		panic(err)
	}

	provider := sdktrace.NewTracerProvider(
		sdktrace.WithBatcher(exporter),
		sdktrace.WithResource(resource.NewWithAttributes(
			"",
			attribute.String("service.name", "lunch-order-agent"),
			attribute.String("deployment.environment.name", "production"),
		)),
	)
	defer func() {
		if err := provider.Shutdown(ctx); err != nil {
			panic(err)
		}
	}()

	otel.SetTracerProvider(provider)
	tracer := otel.Tracer("lunch-order-agent")

	_, span := tracer.Start(ctx, "handle_request")
	span.End()
}
```

`deployment.environment.name` on the resource sets the environment the trace lands in. Leave it out and the trace shows as **No environment**.

## From an OpenTelemetry Collector

If your services already send to a Collector, add Judgment as one more exporter instead of changing application code:

```yaml title="otel-collector.yaml"
receivers:
  otlp:
    protocols: { http: {}, grpc: {} }

exporters:
  otlphttp/judgment:
    traces_endpoint: https://api.judgmentlabs.ai/otel/v1/traces
    headers:
      Authorization: "Bearer ${env:JUDGMENT_API_KEY}"
      X-Organization-Id: "${env:JUDGMENT_ORG_ID}"
      X-Project-Id: "${env:JUDGMENT_PROJECT_ID}"

service:
  pipelines:
    traces:
      receivers: [otlp]
      exporters: [otlphttp/judgment]
```

## What Judgment expects in the spans

- A trace is evaluated once its root span (no parent) arrives with a nonzero duration. Send child spans before or with the root.
- The session comes from `judgment.session_id` on the root span only.
- A span whose attributes exceed 8 MiB fails the whole request with 413. Request bodies are capped at 50 MB, and a gzip body over 20 MB after decompression fails with 400. Once your organization is over its usage limit, requests fail with 402.

The full contract, including every status code, is in [Ingest contract](/documentation/reference/ingest-contract). Attribute names Judgment reads are in [Attribute keys and mappers](/documentation/reference/attribute-keys-and-mappers).

## OpenClaw

OpenClaw's `diagnostics-otel` plugin exports OpenClaw Gateway traces over OTLP/HTTP. Install it with `openclaw plugins install clawhub:@openclaw/diagnostics-otel`, then point it at Judgment in your OpenClaw config:

```json5 title="openclaw.config.json5"
{
  plugins: { allow: ["diagnostics-otel"], entries: { "diagnostics-otel": { enabled: true } } },
  diagnostics: {
    enabled: true,
    otel: {
      enabled: true,
      endpoint: "https://api.judgmentlabs.ai/otel",
      protocol: "http/protobuf",
      serviceName: "openclaw-gateway",
      headers: {
        Authorization: "Bearer <JUDGMENT_API_KEY>",
        "X-Organization-Id": "<JUDGMENT_ORG_ID>",
        "X-Project-Id": "<JUDGMENT_PROJECT_ID>",
      },
      traces: true, metrics: false, logs: false,
      captureContent: true,
    },
  },
}
```

- Judgment ingests traces only, so leave `metrics` and `logs` off.
- Without `captureContent`, OpenClaw exports model, token counts, and timing but no prompt or response text, and judges have nothing to read. Turn it on only if your data policy allows sending that text.
- If you configure OpenClaw through `OTEL_EXPORTER_OTLP_ENDPOINT` instead of the config file, also set `OTEL_EXPORTER_OTLP_HEADERS` with the three headers. The endpoint alone gets a 401.