--- title: Custom runtime ingest API description: Push runs and events into ClawMetry over HTTP from any agent runtime — the endpoint reference, event payload schema, auth modes, and idempotency rules. keywords: agent ingest API, custom agent telemetry, push agent events HTTP, agent run tracking API eyebrow: Runtimes --- # Custom runtime ingest API Push events from any runtime into ClawMetry without writing to a supported runtime's filesystem layout. Designed for in-house agents, eval harnesses and web agents that already produce structured run and step records. **Tier:** Pro (entitlement key `custom_runtime_ingest`). ## Endpoints | Method | Path | Description | |---|---|---| | `GET` | `/api/v1/runtimes` | List runtimes ClawMetry knows about. Free. | | `POST` | `/api/v1/runs` | Open a run; returns a `run_id`. | | `POST` | `/api/v1/runs//events` | Append one event or many. | | `POST` | `/api/v1/runs//end` | Mark the run ended. Optional. | | `GET` | `/api/v1/runs/` | Read back: was the run persisted? | ## Authentication Two modes, selected by whether a token is configured: 1. **Localhost only (default).** With `CLAWMETRY_INGEST_TOKEN` unset, only loopback requests are accepted. Zero configuration, local only. 2. **Token header.** Set `CLAWMETRY_INGEST_TOKEN=` on the dashboard; clients then send `X-ClawMetry-Token: `. The comparison is constant-time. A non-loopback request without a matching token gets `401 unauthorized`. ## Quickstart ```bash # 1. Open a run curl -s http://localhost:8900/api/v1/runs \ -H 'content-type: application/json' \ -d '{"runtime": "my_engine", "metadata": {"build": "abc123"}}' # -> {"ok": true, "run_id": "run_a1b2c3d4…", "runtime": "my_engine"} # 2. Push an event curl -s http://localhost:8900/api/v1/runs/run_a1b2c3d4/events \ -H 'content-type: application/json' \ -d '{"event": { "id": "evt_1", "ts": '"$(date +%s)"', "event_type": "model.completed", "model": "claude-sonnet-5", "data": {"input_tokens": 1240, "output_tokens": 312} }}' # -> {"ok": true, "accepted": 1, "ids": ["evt_1"]} # 3. Close it curl -s http://localhost:8900/api/v1/runs/run_a1b2c3d4/end \ -H 'content-type: application/json' -d '{}' ``` ## Event payload | Field | Required | Notes | |---|---|---| | `id` | no | Dedupe key. The server fills `evt_` if absent. Re-ingesting the same id is a no-op. | | `ts` | no | Epoch seconds (float). Server uses now if absent. | | `event_type` | no | Free-form. Conventional: `session.started`, `prompt.submitted`, `model.completed`, `tool.invoked`, `tool.result`, `error`. | | `session_id` | no | Defaults to the `run_id`. | | `tool_name` | no | When the event represents a tool call. | | `model` | no | LLM model id — this is what makes cost derivable. | | `role` | no | `user`, `assistant`, `system`, `tool`. | | `data` | no | Opaque dict. The daemon adds `data.extra.runtime`. | Token counts belong in `data` as `input_tokens`, `output_tokens` and, where you have them, `cache_read_tokens` / `cache_write_tokens`. Supplying `model` alongside them is what lets ClawMetry derive cost — without a model id you get tokens and an honest unknown for dollars. ### Batching ```json {"events": [ {"id": "evt_1", "event_type": "prompt.submitted", "role": "user"}, {"id": "evt_2", "event_type": "tool.invoked", "tool_name": "search"}, {"id": "evt_3", "event_type": "model.completed", "model": "gpt-5.4", "data": {"input_tokens": 900, "output_tokens": 120}} ]} ``` ## Idempotency Event ids are the dedupe key, and re-posting an id already stored is a no-op rather than an error. This is deliberate: a harness that retries a failed push, or replays a run from a log, must not double-count. Generate stable ids from something in your own domain — a step index, a request id — rather than a random value per attempt. ## A worked example ```python import os, time, uuid, requests BASE = os.environ.get("CLAWMETRY_URL", "http://localhost:8900") HDRS = {"content-type": "application/json"} if os.environ.get("CLAWMETRY_INGEST_TOKEN"): HDRS["X-ClawMetry-Token"] = os.environ["CLAWMETRY_INGEST_TOKEN"] run = requests.post(f"{BASE}/api/v1/runs", json={ "runtime": "research_agent", "metadata": {"repo": "acme/api", "trigger": "nightly"}, }, headers=HDRS).json() rid = run["run_id"] def emit(step, **event): event.setdefault("id", f"{rid}-{step}") event.setdefault("ts", time.time()) requests.post(f"{BASE}/api/v1/runs/{rid}/events", json={"event": event}, headers=HDRS) emit(0, event_type="prompt.submitted", role="user", data={"text": "summarise last night's failures"}) emit(1, event_type="tool.invoked", tool_name="query_ci", data={"arguments": {"since": "24h"}}) emit(2, event_type="model.completed", model="claude-sonnet-5", role="assistant", data={"input_tokens": 4210, "output_tokens": 380}) requests.post(f"{BASE}/api/v1/runs/{rid}/end", json={}, headers=HDRS) ``` Those events land in the same store as every other runtime, appear in Conversations and Cost, and are visible to [the MCP server](/docs/mcp/overview/) — so your coding agent can ask about your production agent's night. ## Reading it back ```bash curl -s http://localhost:8900/api/v1/runs/run_a1b2c3d4 | jq curl -s 'http://localhost:8900/api/local/events?session_id=run_a1b2c3d4' | jq '.rows' ``` If a run reads back but nothing appears in the dashboard, check that the daemon is running — the ingest endpoint accepts the write, but the rollups the UI reads are built by the daemon.