REST API
Ingest API (OTLP)
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
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
otlphttpexporter must setencoding: json. The protobuf default is the single most common cause of a silent "no spans arriving".YAMLexporters: 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:
- Standard OTLP/JSON — a
resourceSpansenvelope. Stock OpenTelemetry exporters produce this, and it works with zeroopexia.*attributes: the API key backfills org, workspace, and project. - Flat span array — a bare JSON list of span objects, for custom exporters.
OTLP/JSON envelope:
{
"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:
[{
"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
const now = BigInt(Date.now()) * 1_000_000n;
const batch = [{
trace_id: "4bf92f3577b34da6a3ce929d0e0e4736",
span_id: "00f067aa0ba902b7",
name: "llm.call",
start_time_unix_ns: Number(now),
end_time_unix_ns: Number(now + 1_000_000_000n),
status_code: "OK",
attributes: {
"opexia.project_id": "checkout-agent",
"gen_ai.request.model": "claude-opus-4-6",
},
}];
const res = await fetch("https://ingest.opexia.dev/v1/otlp/traces", {
method: "POST",
headers: {
"x-opexia-api-key": process.env.OPEXIA_API_KEY!,
"content-type": "application/json", // JSON only — protobuf returns 415
},
body: JSON.stringify(batch),
});
console.log(res.status); // 202 = accepted for async processing
Errors
| Status | Cause |
|---|---|
400 | Body is not valid JSON, or a span is malformed (resourceSpans not a list, attributes not an object) |
401 | Missing or invalid API key |
415 | Protobuf content type — send JSON |
Claude Code ingestion
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
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
{ "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"])
const cfg = await (await fetch(
`https://ingest.opexia.dev/v1/workspaces/${wsId}/sdk-config`,
{ headers: { "x-opexia-api-key": process.env.OPEXIA_API_KEY! } },
)).json();
console.log("capture_text:", cfg.capture_text);
Fail-closed. If this lookup fails, the SDK sets
capture_text = falseand sends no text bodies. A network problem can never cause text to be captured against policy.
Whoami
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)
const who = await (await fetch("https://ingest.opexia.dev/v1/auth/whoami", {
headers: { "x-opexia-api-key": process.env.OPEXIA_API_KEY! },
})).json();
console.log(who);