Skip to main content
The writer serves a read-only JSON API over the API metrics it stores: totals per project and a breakdown per route. The dashboard reads your API numbers through it. The writer has no public hostname: on the hosted service you read these numbers in the dashboard. The API itself is for self-hosters and for operators with access to the stack.

Base URL

The writer is not on a public hostname. The Cloudflare Tunnel routes only app., t. and otlp., and otlp. only to the collector. The dashboard calls the writer over the stack’s private network with an operator key and filters the results to your project on its server before anything reaches your browser. The examples use $WRITER_URL for the base URL. The writer listens on the port in its PORT variable, 8080 by default.

Authentication

Every request needs a read key in the Authorization header:
string
required
Bearer followed by a space and the read key, for example Bearer scf_read_.... The scheme is case-sensitive: write Bearer with a capital B.
A read key has one of two scopes: Read keys start with scf_read_, are shown once and are stored only as SHA-256 hashes. Each use updates the key’s last_used_at. Issue and revoke them with the writer’s CLI, from apps/writer:
key:create prints one JSON line with apiKeyId, scope, tenantId, expiresAt and key. --days sets an expiry. Without it the key does not expire. SCF_BOOTSTRAP_OPERATOR_KEY is an operator key held in the writer’s environment instead of the database. It is used only when it is at least 16 characters long, and it is compared with timingSafeEqual.
Issuing and revoking read keys from the dashboard’s Keys and health page is planned for Milestone 2. See the roadmap.
The writer also has an operator-only control API under /v1/admin/, which the dashboard uses for tenants, ingest keys and enrollment. It is not covered here.

Time range

Both endpoints take the same query parameters.
string
default:"24h"
One of 1h, 24h, 7d or 30d. The window ends at to. An unrecognized value is treated as 24h. Ignored when from is set.
string
Start of the window, inclusive, as an ISO 8601 timestamp such as 2026-11-06T00:00:00Z. Defaults to to minus range.
string
End of the window, exclusive, as an ISO 8601 timestamp. Defaults to now.
from must be earlier than to, and the window can be at most 800 days. Rows are per UTC minute, and a row is included when its minute starts inside the window.

List tenants

GET /v1/api-metrics/tenants Totals per tenant for the window. An operator key gets every tenant, including tenants with no data in the window, sorted by requests, most first, then by name. A tenant key gets a list with its own tenant only.
Response
string
operator or tenant: the scope of the key that made the request.
string
Start of the window that was used, ISO 8601.
string
End of the window that was used, ISO 8601.
object[]
One entry per tenant.

List routes

GET /v1/api-metrics/tenants/:id/routes Requests, status codes and latency per service, method and route for one tenant. Status codes are summed into classes. At most 500 rows are returned, sorted by requests, most first. Routes with no requests in the window are not listed.
string
required
Tenant UUID in lower case. Any other form returns 404 not found.
Response
object
The tenant the routes belong to.
string
Start of the window that was used, ISO 8601.
string
End of the window that was used, ISO 8601.
object[]
One entry per service, method and route.

How the numbers are computed

  • One source per service. For each service, if any rows in the window came from SDK metrics, only those rows are used. Otherwise the rows built from spans are used. A service that sends both is never counted twice.
  • Percentiles. Bucket counts are summed over the window and across instances, then p50, p95 and p99 are interpolated linearly inside the bucket that holds the rank. A percentile in the overflow bucket, above 10 seconds, reads 10000. See Histogram buckets.
  • Error rate. errorRate counts status codes of 500 and above. 4xx responses are not errors here.

Errors

Errors are JSON with one field:
Authentication runs before the time range is checked, so a request with a bad key and a bad range returns 401. Every response carries cache-control: no-store.

Other writer endpoints

  • GET /healthz returns {"ok":true} after a database round trip. It needs no key.
  • GET /dashboard serves a single HTML page that calls the two endpoints above with a read key you paste in. The page keeps the key in the browser’s session storage. It is meant for self-hosters and local development.
Last modified on November 14, 2026