Skip to content

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.

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.

RouteReturns
GET /api/healthLiveness, collector identity, store path, retention, size cap and ingest receipt
GET /api/pingCheap liveness check with start time and version
GET /openapi.jsonOpenAPI 3.0.3 document of every route
GET /docsHTML route index
Terminal window
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.

RouteAcceptsSuccess
POST /v1/tracesOTLP/HTTP JSON or protobuf{ "accepted": SPAN_COUNT }
POST /v1/logsOTLP/HTTP JSON or protobuf{ "accepted": LOG_COUNT }
POST /v1/metricsOTLP/HTTP JSON or protobuf; gauge and sum pointsOTLP response; partialSuccess counts rejected points
Terminal window
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 }
RuleValue
Content typeapplication/json (also when absent), application/x-protobuf or application/protobuf
Content encodingNone, identity or gzip. OTLP/gRPC, Brotli and Deflate are not supported.
Request size16 MiB, checked before and after decompression
Concurrent writes8 pending requests; more return 503 with Retry-After: 1
MetricsGauge and sum points are stored, temporality kept; histograms and summaries are rejected and counted in partialSuccess
StatusMeaning
400Malformed payload, or an invalid hook batch
403Trace owned by an enrolled machine; loopback intake cannot overwrite it
409Hook event hash conflicts with one already stored
410Hook event older than the 720-hour replay horizon
413Payload too large
415Unsupported content type or encoding
503Queue 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.

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:

FieldCap
Application log body4 KiB
Attribute value2 KiB
Attribute key, span or log name256 B
Attribute map16 KiB
GenAI content field, GenAI content map16 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.

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.

RouteMain parameters
GET /api/serviceslookback, summary=1, series=1
GET /api/traces (alias /api/traces/search)service, status, operation, durationMin, lookback, limit, cursor, attr.KEY
GET /api/traces/statsgroupBy (service, operation, status, attr.KEY), agg (count, avg_duration, p95_duration, error_rate), trace filters
GET /api/traces/:traceIdinclude
GET /api/traces/:traceId/spansNone
GET /api/traces/:traceId/logslookback, limit, cursor
GET /api/spans/searchservice, traceId, operation, parentOperation, status, lookback, limit, attr.KEY, attrContains.KEY
GET /api/spans/:spanIdNone
GET /api/spans/:spanId/logslookback, 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/statsgroupBy (service, severity, scope, attr.KEY), agg=count, log filters
GET /api/facetstype=traces with field = service, operation, status, attribute_keys or attribute_values (needs key); type=logs with service, severity or scope
Terminal window
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.

RouteReturns
GET /api/runsRecorded runs; limit, branch, status
GET /api/runs/:idOne run with counts, new issues and the previous run on its branch; timeline=1 adds its spans
GET /api/runs/:id/settledWhether issue detection still waits on this run
GET /api/issuesIssues, filtered by state, branch, time, run, session, trace and service
GET /api/issues/:id, /fix-context, /samplesOne issue; its Markdown fix context; sample traces
GET /api/whyThe blop.why/v1 explanation blop why prints
GET /api/streamServer-sent events for issues, regressions, runs and sessions; activity=1 adds activity
GET /api/usageToken usage from span attributes; since, groupBy=service|agent|model, service, agent, limit. Not billing.
GET /api/metricsStored metric points and per-series totals; session, source, since, name, worktree, limit
GET /api/auditDeterministic audit findings; service, lookbackMinutes, baselineMinutes, durationMin
GET /api/failure-contextblop.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/alertsblop.alerts/v1 rules, state and recent events
GET /api/workersblop.worker/v1 worker lifecycle from enrolled machines only
Terminal window
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 }] }

These serve the Inspector and send no CORS headers.

RouteReturns
GET /api/inspector/home, /runs, /failures, /insights, /sourcesblop.inspector-summary/v1 page summaries
GET /api/inspector/activity, /activity/detailSession activity list and one session or trace
GET /api/inspector/session-tree, /session-structureSession, trace or conversation tree; the structure omits captured text
GET /api/inspector/session-observation-contentExact content for one revision; subject to capture and retention
GET /api/inspector/logs, /step-logsLog search; logs for one recorded step
GET /api/inspector/traces/:traceIdOne trace; optional exact log
GET /api/inspector/agent-graph, /agent-mapAgent graph of one session (≤ 200 nodes); agent map over a window
GET /api/graphs/:sessionIdblop.graph/v1 session graph; limit 1–200
GET /api/inspector/settingsEffective settings and capture policies, without secrets
GET /api/inspector/chat/config, /chat/threads, /chat/threads/:threadIdChat model configuration (never the key) and stored threads
GET /api/test-coverageLatest 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.

Open Sessions → Observations at /inspector?view=sessions&tab=observations. These routes return { "data": RESULT }.

RouteReturns
GET /api/observations/sourcesUp to 200 source keys with service and count; cursor, limit
GET /api/observations/query?request=JSONTyped query page with facets, coverage and nextCursor
GET /api/observations/export?request=JSONThe same page as a JSON attachment
GET /api/observations/users?request=JSONPer-source application end-user summaries
GET /api/observations/graph?request=JSONCalls, grouped steps, recorded parent edges and an accessible list
GET /api/observations/prices?source=KEY, /views?source=KEYLatest 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.

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.

RouteMarker headerEffect
POST /api/runs, POST /api/runs/:id/finishx-blop-runRecord a run’s start and end; never runs a command
DELETE /api/runsx-blop-run-deleteDelete 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/limitsx-blop-settingsSave retentionHours (1 to 8,760) and maxDbSizeMb and apply them to the running collector
POST /api/issues/mark-wrong, POST /api/issues/:id/statex-blop-issueMark a session or trace wrong; change issue state
POST /api/inspector/session-bookmarkx-blop-sessionBookmark a session, trace or conversation
POST /api/inspector/connect/doctorx-blop-doctorRun setup checks with a probe span
POST /api/inspector/capture-policyx-blop-capture-policySave a capture policy
POST /api/inspector/chatx-blop-chatAsk the chat; streams NDJSON and sends context to the configured provider
POST /api/observations/views, /views/delete, /pricesx-blop-observationsSave 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/deliveryx-blop-alertsManage alert rules and delivery
POST /api/workers/ackx-blop-ledger-ackAcknowledge 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.

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.