DEVELOPER DOCS

REST API

Ingest API (OTLP)

On this page

Base URL: https://ingest.opexia.dev — a separate service from everything above.

Most integrators never call these directly: the SDK or a standard OpenTelemetry collector does it. Call them directly only when writing a custom exporter.


Ingest spans

HTTP
POST /v1/otlp/traces

The main ingestion endpoint. Also aliased at POST /v1/traces.

Auth: API key only (x-opexia-api-key or dr_* bearer). The key determines the destination workspace — there is no org/workspace in the path.

Returns 202 Accepted. Ingestion is asynchronous: a 202 means the batch was accepted for processing, not that every span passed validation. Check ingestion health to confirm nothing dead-lettered.

Content type

JSON only. Sending protobuf returns 415.

If you use an OpenTelemetry collector, its otlphttp exporter must set encoding: json. The protobuf default is the single most common cause of a silent "no spans arriving".

YAML
exporters:
  otlphttp:
    endpoint: https://ingest.opexia.dev
    encoding: json                       # REQUIRED — protobuf returns 415
    headers:
      x-opexia-api-key: opx_live_xxxxxxxxxxxxxxxxxxxx

Request body

Two shapes are accepted:

  1. Standard OTLP/JSON — a resourceSpans envelope. Stock OpenTelemetry exporters produce this, and it works with zero opexia.* attributes: the API key backfills org, workspace, and project.
  2. Flat span array — a bare JSON list of span objects, for custom exporters.

OTLP/JSON envelope:

JSON
{
  "resourceSpans": [{
    "resource": { "attributes": [
      { "key": "service.name", "value": { "stringValue": "checkout-agent" } }
    ]},
    "scopeSpans": [{
      "spans": [{
        "traceId": "4bf92f3577b34da6a3ce929d0e0e4736",
        "spanId": "00f067aa0ba902b7",
        "name": "llm.call",
        "kind": 3,
        "startTimeUnixNano": "1756736585000000000",
        "endTimeUnixNano":   "1756736593420000000",
        "status": { "code": 1 },
        "attributes": [
          { "key": "opexia.project_id", "value": { "stringValue": "checkout-agent" } },
          { "key": "gen_ai.request.model", "value": { "stringValue": "claude-opus-4-6" } }
        ]
      }]
    }]
  }]
}

Flat array:

JSON
[{
  "trace_id": "4bf92f3577b34da6a3ce929d0e0e4736",
  "span_id": "00f067aa0ba902b7",
  "name": "llm.call",
  "start_time_unix_ns": 1756736585000000000,
  "end_time_unix_ns":   1756736593420000000,
  "status_code": "OK",
  "attributes": { "opexia.project_id": "checkout-agent" }
}]

Examples

import httpx, os, time

now = time.time_ns()
batch = [{
    "trace_id": "4bf92f3577b34da6a3ce929d0e0e4736",
    "span_id":  "00f067aa0ba902b7",
    "name": "llm.call",
    "start_time_unix_ns": now,
    "end_time_unix_ns":   now + 1_000_000_000,
    "status_code": "OK",
    "attributes": {
        "opexia.project_id": "checkout-agent",
        "gen_ai.request.model": "claude-opus-4-6",
    },
}]

r = httpx.post(
    "https://ingest.opexia.dev/v1/otlp/traces",
    headers={
        "x-opexia-api-key": os.environ["OPEXIA_API_KEY"],
        "content-type": "application/json",     # JSON only — protobuf returns 415
    },
    json=batch,
    timeout=30,
)
print(r.status_code)          # 202 = accepted for async processing

Errors

StatusCause
400Body is not valid JSON, or a span is malformed (resourceSpans not a list, attributes not an object)
401Missing or invalid API key
415Protobuf content type — send JSON

Claude Code ingestion

HTTP
POST /v1/otlp/claude-code/v1/traces
POST /v1/otlp/claude-code/v1/metrics
POST /v1/otlp/claude-code/v1/logs
POST /v1/otlp/claude-code/v1/activity

Dedicated endpoints for Claude Code telemetry, feeding the Claude Code Analytics endpoints. Same auth and JSON-only rules as /v1/otlp/traces.

Point Claude Code's OTLP configuration at these endpoints — see claude-code-enterprise-setup.md.


SDK config

HTTP
GET /v1/workspaces/{ws}/sdk-config

Returns the workspace's SDK-relevant configuration. Called automatically by opexia.trace.init() — you rarely call it yourself.

Auth: API key

Response 200

JSON
{ "capture_text": false }

Examples

import httpx, os

cfg = httpx.get(
    f"https://ingest.opexia.dev/v1/workspaces/{ws_id}/sdk-config",
    headers={"x-opexia-api-key": os.environ["OPEXIA_API_KEY"]},
    timeout=5,
).json()
print("capture_text:", cfg["capture_text"])

Fail-closed. If this lookup fails, the SDK sets capture_text = false and sends no text bodies. A network problem can never cause text to be captured against policy.


Whoami

HTTP
GET /v1/auth/whoami

Resolves a credential to its org and workspace. The fastest way to verify a key works and is pointed at the workspace you expect.

Auth: API key

Examples

import httpx, os

who = httpx.get(
    "https://ingest.opexia.dev/v1/auth/whoami",
    headers={"x-opexia-api-key": os.environ["OPEXIA_API_KEY"]},
    timeout=10,
).json()
print(who)