API Reference

OpenTelemetry Ingestion

Receive Claude Code's native OTLP log export directly, alongside the plugin's own events.

POST /api/v1/otel/v1/logs

An OTLP/HTTP receiver for Claude Code's built-in telemetry export. This is separate from, and complementary to, the plugin's POST /api/v1/events: the plugin gives you hook-level detail, while Claude Code's own export gives authoritative session cost and, if your key permits, prompt and response content.

/trenchcoat:connect configures this for you. You should rarely need to call it by hand — but the exact configuration matters, and three details fail silently when wrong.

Required scope: read:sessions for ingestion. Content scopes (read:prompts, read:events, read:full) are additive: attributes above the key's level are stripped before the row is written, rather than rejected.

Availability: gated by the OTEL_INGEST_ENABLED server setting, which defaults to off. When disabled the endpoint returns 404, indistinguishable from an unrecognized route. It is enabled on app.trenchcoat.io; on a self-hosted instance you must turn it on.


Configuration

Claude Code reads these from the env block of ~/.claude/settings.json:

{
  "env": {
    "CLAUDE_CODE_ENABLE_TELEMETRY": "1",
    "OTEL_LOGS_EXPORTER": "otlp",
    "OTEL_EXPORTER_OTLP_PROTOCOL": "http/json",
    "OTEL_EXPORTER_OTLP_ENDPOINT": "https://app.trenchcoat.io/api/v1/otel",
    "OTEL_EXPORTER_OTLP_HEADERS": "X-API-Key=ct_live_..."
  }
}

Three of these are total failures if wrong, and none of them report an error:

The protocol must be exactly http/json. http/protobuf — a common default — fails every request against this endpoint's JSON schema. There is no partial-failure mode: either all exports succeed or all of them silently fail.

The endpoint omits /v1/logs. Claude Code appends that itself, landing on .../api/v1/otel/v1/logs. Writing the full path yourself produces .../api/v1/otel/v1/logs/v1/logs and a 404.

Authentication is X-API-Key, not a bearer token. A bearer token returns 401.

Claude Code does not surface OTLP export failures, which is why /trenchcoat:doctor checks all three explicitly.


Request

A standard OTLP/HTTP logs payload in JSON encoding:

curl -X POST https://app.trenchcoat.io/api/v1/otel/v1/logs \
  -H "Content-Type: application/json" \
  -H "X-API-Key: ct_live_a1b2c3d4e5f6g7h8i9j0k1l2m3n4o5p6" \
  -d '{
    "resourceLogs": [
      {
        "resource": {
          "attributes": [
            { "key": "service.name", "value": { "stringValue": "claude-code" } }
          ]
        },
        "scopeLogs": [
          {
            "logRecords": [
              {
                "timeUnixNano": "1767182400000000000",
                "body": { "stringValue": "user_prompt" },
                "attributes": [
                  { "key": "event.name", "value": { "stringValue": "claude_code.user_prompt" } }
                ]
              }
            ]
          }
        ]
      }
    ]
  }'

resourceLogs must contain at least one entry. The envelope's structure is validated; individual attribute values are handled defensively at runtime rather than strictly typed, so an unexpected value shape is ignored rather than rejecting the batch.


Response — 201

{
  "data": {
    "inserted": 12,
    "skipped": 1,
    "rejected": [],
    "identityCaptured": 3,
    "identityMismatchCount": 0
  },
  "meta": {
    "requestId": "req_msdf3l4f_lmjnos",
    "timestamp": "2026-01-15T10:30:00.000Z"
  }
}
FieldDescription
insertedLog records written.
skippedRecords recognized but not stored — typically an event type this endpoint does not persist.
rejectedRecords that could not be decoded, with reasons.
identityCapturedRecords that contributed session identity information.
identityMismatchCountRecords whose identity conflicted with an existing session. Persistently non-zero suggests two machines sharing one key.

A 201 with inserted: 0 and skipped: N is a successful request that stored nothing — worth checking if you expected data.


Content is stripped, not rejected

Attributes above the key's capture level are removed before the row is built. Sending prompt text with a read:sessions key succeeds; the text is dropped.

This is why local OTEL_LOG_* variables cannot widen capture. They control what Claude Code sends; the key's scopes control what is stored. Setting OTEL_LOG_USER_PROMPTS=1 on a key without read:prompts just spends bandwidth on content the server discards.

/trenchcoat:connect reads the key's scopes and sets these flags to match, which is why hand-editing them tends to produce a configuration that looks right and captures nothing.

See Authentication for what each level records.


Errors

404 — Endpoint disabled

OTEL_INGEST_ENABLED is off on this instance. Deliberately indistinguishable from an unrecognized route.

400 — Malformed envelope

resourceLogs missing, empty, or structurally invalid.

401 — Invalid key

Missing or wrong X-API-Key. Also returned for a bearer token, which is the most common cause.

On this page