API reference
OpenTelemetry
POST /v1/logs — token and cost usage from Claude Code and Codex telemetry (OTLP/HTTP with JSON).
Agentower accepts OpenTelemetry logs over OTLP/HTTP with JSON encoding. Telemetry adds token counts (and, for Claude Code, a cost estimate) to agents; it never changes an agent's state — hooks do that. Scope: telemetry or ingest.
Set your exporter's endpoint to Agentower's address (the exporter adds /v1/logs itself when you use OTEL_EXPORTER_OTLP_ENDPOINT), the protocol to http/json, and the key as a header:
OTEL_EXPORTER_OTLP_PROTOCOL=http/json
OTEL_EXPORTER_OTLP_ENDPOINT=https://ingest.agentower.com
OTEL_EXPORTER_OTLP_HEADERS=authorization=Bearer at_live_…Protobuf isn't supported: a protobuf request gets 415 with a hint to switch to http/json.
#Which key
These three routes — POST /v1/logs, /v1/metrics and /v1/traces — accept a telemetry key or an ingest key. A telemetry key works only here — every other route refuses it — so it's the safer one to put in a tool's settings file, which the agent's own commands can read. If a telemetry key leaks, it can add usage numbers to your board and nothing else. See Keys and scopes.
Create a telemetry key on Keys & connections (step 4, Create a telemetry key) — the settings block there then carries it, along with OTEL_LOGS_EXPORT_INTERVAL and OTEL_METRIC_EXPORT_INTERVAL set to 60000 (send once a minute), and the ~/.agentower/claude-code.env snippet gains an AGENTOWER_TELEMETRY_TOKEN line. The Claude Code setup command (node scripts/claude-code.mjs connect) puts AGENTOWER_TELEMETRY_TOKEN from that file into Claude Code's settings, with the same two intervals; without that line it uses your ingest key and warns you. The Agentower plugin doesn't set up telemetry. See Connect Claude Code.
#POST /v1/logs
Agentower reads these log records and ignores all others:
| Tool | service.name | Log record | Attributes read |
|---|---|---|---|
| Claude Code | claude-code | claude_code.api_request | session.id, model, input_tokens, output_tokens, cache_read_tokens, cache_creation_tokens, cost_usd, vcs.repository.name |
| Codex | starts with codex | codex.sse_event with token counts | conversation.id, input_token_count, output_token_count, reasoning_token_count |
For Claude Code, tokens in = input + cache read + cache creation tokens. model is the model that served the request (e.g. claude-haiku-4-5-20251001 for a subagent), so tokens are counted per model — see Usage. Without it, the tokens count under the session's model. Everything else in a record — including any prompt text — is dropped, and never stored.
curl -X POST "$AGENTOWER_URL/v1/logs" \
-H "authorization: Bearer $AGENTOWER_KEY" \
-H "content-type: application/json" \
-d '{"resourceLogs":[{"resource":{"attributes":[{"key":"service.name","value":{"stringValue":"claude-code"}}]},
"scopeLogs":[{"logRecords":[{"timeUnixNano":"1790000000000000000","attributes":[
{"key":"event.name","value":{"stringValue":"api_request"}},
{"key":"session.id","value":{"stringValue":"3f1c9a52-docs-example"}},
{"key":"input_tokens","value":{"intValue":"1200"}},
{"key":"output_tokens","value":{"intValue":"350"}},
{"key":"cost_usd","value":{"doubleValue":0.021}}]}]}]}]}'{}An empty object is OTLP's "everything accepted". Usage is added to the session with the same session.id as its hook events.
#Metrics and traces
POST /v1/metrics and POST /v1/traces are accepted (so exporters don't keep retrying) and ignored. They answer {}.