Base URL
The writer is not on a public hostname. The Cloudflare Tunnel routes onlyapp., 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 theAuthorization header:
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.
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.
/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.One of
1h, 24h, 7d or 30d. The window ends at to. An unrecognized value is treated as 24h. Ignored when from is set.Start of the window, inclusive, as an ISO 8601 timestamp such as
2026-11-06T00:00:00Z. Defaults to to minus range.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
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.
Response
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.
errorRatecounts 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 /healthzreturns{"ok":true}after a database round trip. It needs no key.GET /dashboardserves 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.