Collector HTTP API
The collector serves OTLP ingestion and bounded JSON queries on loopback. Coding agents should prefer the blop CLI, which adds blop.cli/v1 envelopes and exit codes; use HTTP for SDK export and direct clients. This is the local API, not a Blop Cloud API.
Discovery routes
Section titled “Discovery routes”The base URL is http://127.0.0.1:27811 unless a project or explicit setting changes it; use the URL that blop status --json prints. Direct clients choose their base URL themselves; BLOP_MONITOR_URL points the CLI at another collector.
| Route | Returns |
|---|---|
GET /api/health | Liveness, collector identity, store path, retention, size cap and ingest receipt |
GET /api/ping | Cheap liveness check with start time and version |
GET /openapi.json | OpenAPI 3.0.3 document of every route |
GET /docs | HTML route index |
curl -s http://127.0.0.1:27811/api/health{ "ok": true, "reachable": true, "sourceLifecycle": "unknown", "ingestion": { "lastSuccessfulIngestAt": "2026-11-03T13:07:25.855Z", "acceptedSpans": 1, "acceptedLogs": 1 }, "retentionHours": 720, "maxDbSizeMb": 1024}lastSuccessfulIngestAt is the collector’s receipt time of its latest non-empty successful batch; it survives restarts, and empty probes and failed batches do not move it. Receipt does not prove the application is running or that a request succeeded. stats.latestTelemetryAt is the event time reported by the source.
OTLP ingestion
Section titled “OTLP ingestion”| Route | Accepts | Success |
|---|---|---|
POST /v1/traces | OTLP/HTTP JSON or protobuf | { "accepted": SPAN_COUNT } |
POST /v1/logs | OTLP/HTTP JSON or protobuf | { "accepted": LOG_COUNT } |
POST /v1/metrics | OTLP/HTTP JSON or protobuf; gauge and sum points | OTLP response; partialSuccess counts rejected points |
curl -s -X POST http://127.0.0.1:27811/v1/logs -H 'Content-Type: application/json' \ -d '{"resourceLogs":[{"resource":{"attributes":[{"key":"service.name","value":{"stringValue":"checkout"}}]},"scopeLogs":[{"logRecords":[{"severityText":"ERROR","body":{"stringValue":"card declined"}}]}]}]}'{ "accepted": 1 }| Rule | Value |
|---|---|
| Content type | application/json (also when absent), application/x-protobuf or application/protobuf |
| Content encoding | None, identity or gzip. OTLP/gRPC, Brotli and Deflate are not supported. |
| Request size | 16 MiB, checked before and after decompression |
| Concurrent writes | 8 pending requests; more return 503 with Retry-After: 1 |
| Metrics | Gauge and sum points are stored, temporality kept; histograms and summaries are rejected and counted in partialSuccess |
| Status | Meaning |
|---|---|
400 | Malformed payload, or an invalid hook batch |
403 | Trace owned by an enrolled machine; loopback intake cannot overwrite it |
409 | Hook event hash conflicts with one already stored |
410 | Hook event older than the 720-hour replay horizon |
413 | Payload too large |
415 | Unsupported content type or encoding |
503 | Queue full or capture process unavailable; retry |
A write completes before the response, so an accepted span is immediately queryable. Replaying a span with the same trace and span ID replaces it; logs have no identity, so retries can duplicate them. Coding agent hook batches use POST /v1/traces too and can return exact receipts. An exact metric export replay is deduplicated.
Application source capture
Section titled “Application source capture”A resource attribute blop.source.kind=application or filelog applies that source’s capture policy, keyed by blop.source.id; a service name never selects it. Without consent, custom attributes and log bodies are dropped. Consented content is redacted and capped before storage:
| Field | Cap |
|---|---|
| Application log body | 4 KiB |
| Attribute value | 2 KiB |
| Attribute key, span or log name | 256 B |
| Attribute map | 16 KiB |
| GenAI content field, GenAI content map | 16 KiB, 64 KiB |
Exact original timestamps are kept as blop.otlp.start_time_unix_nano, blop.otlp.end_time_unix_nano, blop.otlp.time_unix_nano and blop.otlp.observed_time_unix_nano, and numeric severity as blop.otlp.severity_number; receipt uses the collector clock. Without severity text, numeric ranges map to TRACE through FATAL, and zero stays UNSPECIFIED.
GET /api/sources/check?id=SOURCE_ID returns reachability, capture configuration (unconfigured, enabled or disabled) and an ingestion receipt for that source ID. Disabling a source stops new intake and keeps retained history.
Trace, span and log queries
Section titled “Trace, span and log queries”List routes take durations like 15m, 2h or 1d. Without lookback they default to one hour, capped at 24 hours. Traces default to 20 results (at most 100); spans and logs to 100 (at most 500). Copy meta.nextCursor unchanged to get the next page.
| Route | Main parameters |
|---|---|
GET /api/services | lookback, summary=1, series=1 |
GET /api/traces (alias /api/traces/search) | service, status, operation, durationMin, lookback, limit, cursor, attr.KEY |
GET /api/traces/stats | groupBy (service, operation, status, attr.KEY), agg (count, avg_duration, p95_duration, error_rate), trace filters |
GET /api/traces/:traceId | include |
GET /api/traces/:traceId/spans | None |
GET /api/traces/:traceId/logs | lookback, limit, cursor |
GET /api/spans/search | service, traceId, operation, parentOperation, status, lookback, limit, attr.KEY, attrContains.KEY |
GET /api/spans/:spanId | None |
GET /api/spans/:spanId/logs | lookback, limit, cursor |
GET /api/logs (alias /api/logs/search) | service, severity, traceId, spanId, body or contains, lookback, limit, cursor, attr.KEY, attrContains.KEY |
GET /api/logs/stats | groupBy (service, severity, scope, attr.KEY), agg=count, log filters |
GET /api/facets | type=traces with field = service, operation, status, attribute_keys or attribute_values (needs key); type=logs with service, severity or scope |
curl -s 'http://127.0.0.1:27811/api/traces?service=checkout&lookback=15m&limit=1'{ "data": [{ "traceId": "4bf92f3577b34da6a3ce929d0e0e4736", "serviceName": "checkout", "rootOperationName": "POST /pay", "durationMs": 1000, "spanCount": 1, "errorCount": 1 }], "meta": { "limit": 1, "lookback": "15m", "returned": 1, "truncated": false, "nextCursor": "eyJr…" }, "observation": { "collectorInstanceId": "00167e64-…", "evidenceRevision": "4", "consistency": "independent-read" }}observation, and the X-Blop-Collector-Instance and X-Blop-Evidence-Revision headers, identify one independent read, not a snapshot across routes. Errors are usually JSON { "error": "…" }, but Host and Origin refusals are plain text and some guarded routes return no body: check the status before parsing.
Runs, issues and analysis routes
Section titled “Runs, issues and analysis routes”| Route | Returns |
|---|---|
GET /api/runs | Recorded runs; limit, branch, status |
GET /api/runs/:id | One run with counts, new issues and the previous run on its branch; timeline=1 adds its spans |
GET /api/runs/:id/settled | Whether issue detection still waits on this run |
GET /api/issues | Issues, filtered by state, branch, time, run, session, trace and service |
GET /api/issues/:id, /fix-context, /samples | One issue; its Markdown fix context; sample traces |
GET /api/why | The blop.why/v1 explanation blop why prints |
GET /api/stream | Server-sent events for issues, regressions, runs and sessions; activity=1 adds activity |
GET /api/usage | Token usage from span attributes; since, groupBy=service|agent|model, service, agent, limit. Not billing. |
GET /api/metrics | Stored metric points and per-series totals; session, source, since, name, worktree, limit |
GET /api/audit | Deterministic audit findings; service, lookbackMinutes, baselineMinutes, durationMin |
GET /api/failure-context | blop.failure-context/v1 evidence for a source plus a trace, service, session or attempt; since ≤ 1 day, limit ≤ 100, max_bytes 4096–262144, deadline_ms 1–5000; a pinned until and revision return 409 when invalidated |
GET /api/alerts | blop.alerts/v1 rules, state and recent events |
GET /api/workers | blop.worker/v1 worker lifecycle from enrolled machines only |
curl -s 'http://127.0.0.1:27811/api/issues?limit=1'{ "data": [{ "id": "iss_4adb40de97fb", "code": "app_error", "title": "checkout failed on POST /pay", "service": "checkout", "state": "open", "occurrences": 1 }] }Inspector routes
Section titled “Inspector routes”These serve the Inspector and send no CORS headers.
| Route | Returns |
|---|---|
GET /api/inspector/home, /runs, /failures, /insights, /sources | blop.inspector-summary/v1 page summaries |
GET /api/inspector/activity, /activity/detail | Session activity list and one session or trace |
GET /api/inspector/session-tree, /session-structure | Session, trace or conversation tree; the structure omits captured text |
GET /api/inspector/session-observation-content | Exact content for one revision; subject to capture and retention |
GET /api/inspector/logs, /step-logs | Log search; logs for one recorded step |
GET /api/inspector/traces/:traceId | One trace; optional exact log |
GET /api/inspector/agent-graph, /agent-map | Agent graph of one session (≤ 200 nodes); agent map over a window |
GET /api/graphs/:sessionId | blop.graph/v1 session graph; limit 1–200 |
GET /api/inspector/settings | Effective settings and capture policies, without secrets |
GET /api/inspector/chat/config, /chat/threads, /chat/threads/:threadId | Chat model configuration (never the key) and stored threads |
GET /api/test-coverage | Latest browser check coverage report, or { "report": null } |
The Sessions view returns at most 50 observations and 256 KiB per page, with 4,000-byte previews. A full content read is capped at 64 KiB (413 above it), and a changed revision returns 409 stale_revision. Metadata is normalized to 8 KiB, 64 members, 6 levels, 128-byte keys and 1 KiB strings, with metadataTruncated set when cut.
Application observation routes
Section titled “Application observation routes”Open Sessions → Observations at /inspector?view=sessions&tab=observations. These routes return { "data": RESULT }.
| Route | Returns |
|---|---|
GET /api/observations/sources | Up to 200 source keys with service and count; cursor, limit |
GET /api/observations/query?request=JSON | Typed query page with facets, coverage and nextCursor |
GET /api/observations/export?request=JSON | The same page as a JSON attachment |
GET /api/observations/users?request=JSON | Per-source application end-user summaries |
GET /api/observations/graph?request=JSON | Calls, grouped steps, recorded parent edges and an accessible list |
GET /api/observations/prices?source=KEY, /views?source=KEY | Latest 200 local price definitions; saved views |
source is the 64-hex key from discovery, not a service name, and every query needs it. request is JSON of at most 32 KiB with filter, search, facets, limit (1–200, default 50), maxBytes (1 KiB–1 MiB) and cursor. A filter is a condition, an and, a same-field or, or a not:
{ "kind": "and", "filters": [ { "kind": "condition", "field": "type", "type": "string", "op": "eq", "value": "generation" }, { "kind": "condition", "field": "metadata.retries", "type": "number", "op": "gte", "value": 1 } ]}Unknown keys, duplicate parameters and type mismatches return 400; a filter is never dropped. search takes mode (token, phrase or exact), field (input, output or both) and text, and only covers masked, consented content (--capture-gen-ai-content). An empty result with incomplete coverage does not prove content was absent. Recorded costs, including zero, win over computed prices, and totals are exact decimal strings per currency. A price write needs sourceKey, exact model, a three-letter currency, effectiveFromMs and increasing tiers starting at minInputTokens: "0"; it applies only to new observations and never reprices old ones.
Guarded local writes
Section titled “Guarded local writes”Every write needs the collector’s loopback Host and exact Origin, Content-Type: application/json, its marker header set to 1, no Content-Encoding, and no cross-site fetch. Without them it returns 403.
| Route | Marker header | Effect |
|---|---|---|
POST /api/runs, POST /api/runs/:id/finish | x-blop-run | Record a run’s start and end; never runs a command |
DELETE /api/runs | x-blop-run-delete | Delete 1 to 50 finished runs with their spans, logs and issue occurrences; { "ids": [...], "dryRun": true } reports the counts first and a running run answers 409 |
POST /api/inspector/settings/limits | x-blop-settings | Save retentionHours (1 to 8,760) and maxDbSizeMb and apply them to the running collector |
POST /api/issues/mark-wrong, POST /api/issues/:id/state | x-blop-issue | Mark a session or trace wrong; change issue state |
POST /api/inspector/session-bookmark | x-blop-session | Bookmark a session, trace or conversation |
POST /api/inspector/connect/doctor | x-blop-doctor | Run setup checks with a probe span |
POST /api/inspector/capture-policy | x-blop-capture-policy | Save a capture policy |
POST /api/inspector/chat | x-blop-chat | Ask the chat; streams NDJSON and sends context to the configured provider |
POST /api/observations/views, /views/delete, /prices | x-blop-observations | Save or delete a view (expectedRevision: null to create, else the current revision); add a price |
POST /api/alerts, /api/alerts/:id/(enabled|delete|test), /api/alerts/delivery | x-blop-alerts | Manage alert rules and delivery |
POST /api/workers/ack | x-blop-ledger-ack | Acknowledge a worker attempt |
The rebuilt Inspector pages also read these JSON routes: GET /api/inspector/overview (KPI tiles, run days, issue patterns and the agent map for a range or from/to), GET /api/inspector/memory (memory graph nodes filtered by kind, status, authority and q, with per-kind counts), GET /api/inspector/cloud (the local sign-in profile: who and where, never a token) and GET /api/test-coverage/history (the last 20 coverage runs, one summary per run).
The capture-policy body is at most 4,096 bytes and needs exactly adapterId, projectId, the current revision, mode (full, conversation-only or metadata-only), exclude and contentRetentionHours (1–720). It returns { "data": POLICY }, or 400 invalid, 405 wrong method, 409 stale revision or unknown binding, 413 too large. It cannot create or enable a binding, and only affects future events. Observation writes are capped at 32 KiB.
POST /api/test-coverage is not a guarded write. It takes JSON up to 8 MiB from blop test run --coverage --coverage-endpoint <base-url>, validates the report, replaces the latest one and returns { "runId": "…" }; the Inspector shows it at /inspector?view=coverage. The collector never fetches URLs in the report. See Chromium JavaScript coverage.
Security boundary
Section titled “Security boundary”The collector refuses a non-loopback bind. Every route checks the Host header, and every route except OTLP intake refuses a foreign Origin; only OTLP intake sends wildcard CORS. There is no bearer token or TLS on these routes, so never expose the port through a proxy. Remote OTLP intake from other machines uses machine enrollment, which does not expose queries. The collector never executes submitted code.