Skip to content

Latest commit

 

History

History
1077 lines (963 loc) · 95 KB

api-reference.md

File metadata and controls

1077 lines (963 loc) · 95 KB

API Reference

The source of truth is the explicit route table in dashboard/server/index.js, with SQL and result shaping in queries.js. Paths below are relative to the dashboard origin; the local Compose origin is http://localhost:8080.

Authentication and request parameters

AUTH_MODE=basic (default) applies Basic Auth globally using BASIC_AUTH_USER and BASIC_AUTH_PASSWORD. Without both, startup fails unless AUTH_ALLOW_INSECURE=1 explicitly enables local unauthenticated use. Only probes bypass this gate. Explicit AUTH_MODE=oidc uses organization login/session cookies; login/callback and origin-checked local logout are protocol exceptions. Telemetry/config/static routes require a session and never fall back to Basic. Chat requires analyst/admin permission in OIDC plus existing readonly/activation gates. The opt-in device-ingestion namespace uses separate machine credentials as described below; browser cookies never authorize that namespace. See SSO operations.

GET /api/session returns mode, role, permissions and, for OIDC, current-user name and expiresAt, without raw subject/IdP tokens. Optional refreshExpiresAt is the absolute login deadline, present only while renewal is enabled and a valid refresh cookie matches the current principal; expiresAt remains the short authorization deadline. /api/session renews credentials within 60 seconds of short-session expiry when a valid refresh cookie is present and renewal can extend that deadline; other protected requests renew expired principals. /api/config includes this session. Unauthenticated OIDC APIs return JSON 401, error=authentication_required, loginUrl=/auth/login. Unsafe session requests without the configured Origin or without required permission return 403. Transient refresh failures return 503, error=authentication_temporarily_unavailable, with Retry-After: 5. A rotated refresh response requires a fresh login because this mode does not retain rotating credentials.

GET /auth/login starts OIDC; GET /auth/callback completes it. POST /auth/logout requires the configured Origin, clears local cookies, revokes any retained refresh credential and redirects to /. Concurrent or replayed revocations share bounded work; see SSO operational limits. Failed revocation or admission exhaustion returns a visible 503 after clearing cookies. Logout does not end IdP SSO or immediately revoke copied short principal cookies. Protocol responses are no-store.

GET /api/installation requires installation:read in OIDC mode; Basic/development retain their existing full-installation access. It uses the shared response/error wrapper, but is a current snapshot without usage-range/filter semantics. A single per-process 30-second memo deduplicates DB/receipt probes. Unauthorized roles are rejected before probing; disabled clients are not queried or shown.

The response has checkedAt, state=operational|needs_attention, release, authentication, database, schema, clients, staleAfterMinutes and explicit limits. Each enabled client's seven-day receipt is receiving, stale, empty or unavailable. Empty is not a token/cost zero; unavailable is not empty. Readonly/schema fields include their cached probe timestamps. Ledger records do not prove actual DDL/rebuild completion; receipt does not prove complete telemetry, OIDC configuration does not prove MFA, and no backup or full commercial-release claim is made. release.revision is null unless the runtime supplies a valid Git SHA through APP_REVISION.

Device collection

/api/collector management routes require OIDC collector:manage (analyst/admin). Unsafe requests retain the existing exact-Origin gate. Responses and downloads are no-store; viewer/demo requests are denied before store or file access.

Method and path Contract
GET /api/collector Feature state, public ingestion endpoint, installScriptUrl, browserEnrollment (helper and package manifest available), Collector version, supported platforms and 30-day token lifetime.
GET /api/collector/enrollments/:handle Read-only view of a terminal's request: {name, platform, code, status, createdAt, expiresAt, userId}. status is pending, approved (by this account), denied, expired or unavailable (decided by another account). userId is the caller's verified email or null; never the polling credential. 404 for unknown handles.
POST /api/collector/enrollments/:handle/approve Same-origin, empty body. Creates the one device under the caller and returns {status, device, userId}; repeating returns the same device. 409 enrollment_denied/enrollment_unavailable, 410 enrollment_expired. 30 decisions per account per hour per replica.
POST /api/collector/enrollments/:handle/deny Same-origin, empty body. Terminal; {status: "denied"}, idempotent. 409 when already approved, 410 when expired.
GET /api/collector/devices Owner-scoped devices and nextCursor; up to 50 records per page. Supply the returned cursor unchanged.
POST /api/collector/devices {name, platform} where name is 1–64 characters without controls and platform is linux or darwin; 201 returns device and the token once.
DELETE /api/collector/devices/:id Revoke an owned device; 204 for existing/already revoked, 404 for absent.
GET /api/collector/package/:platform Authenticated installer archive for linux or darwin; no credentials embedded.
GET /api/ingest/install.sh Public, no-store POSIX sh bootstrap for curl -fsSL URL | sh; embeds only the ingestion endpoint and the enroll.py SHA-256. GET/HEAD only, no query. By default it runs browser enrollment; sh -s -- --token prompts for the attribution ID and device token instead, which is also the only mode without a helper. --user-id ID sets attribution in the token flow; browser enrollment uses the approving login's verified email and applies --user-id only when it has none.
GET /api/ingest/enroll.py Public enrollment helper pinned by install.sh; 404 when browser enrollment is unavailable.
POST /api/ingest/enroll/start Public. JSON {secretHash, name, platform} (SHA-256 hex of the device's own 43-character base64url secret; at most 1 KiB). 201 {verificationUrl, code, pollToken, interval: 5, expiresAt} with a ten-minute deadline. Per replica: 200 per client address and 1,200 in total per ten minutes (429 enroll_rate_limited).
POST /api/ingest/enroll/poll Authorization: Bearer <pollToken>, empty body; 30 per request per minute (429 poll_rate_limited). 200 {status: "pending", interval} or {status: "approved", owner, id, userId, expiresAt, packageSha256}; the device builds ccd1.<owner>.<id>.<secret> locally. 403 enrollment_denied, 410 enrollment_expired/device_revoked, 401 invalid_enrollment. Approved results repeat until the deadline.
GET /api/ingest/package/:platform Installer archive for linux or darwin with exactly one device bearer token; ten per device per hour (429 package_rate_limited, Retry-After: 3600), charged before the file is read, so a missing archive (503) or HEAD also counts. Token checks share the gateway's eight in-flight slots (429 ingest_busy). Does not update lastSeenAt.
POST /api/ingest/v1/{logs,metrics,traces} Device bearer token only; OTLP JSON/protobuf, at most 2 MiB, no content compression.
POST /api/ingest/renew Device bearer token; JSON content type, empty body; six per device per hour. 200 {renewed: false, expiresAt} while more than seven days remain or for an unused replacement, else {renewed: true, token, expiresAt} with a new 30-day token. Device views add renewedAt and reuseDetectedAt (first use of a superseded token).

Device fields are id, name, platform, createdAt, expiresAt, revokedAt, lastSeenAt and renewedAt (last completed renewal); dates are ISO strings or null. Tokens are ingestion-only, expire 30 days after issuance and may remain cached on another replica for up to ten seconds after revocation. A replacement token becomes current on its first use (delivery or renewal request) and the previous token is then rejected, subject to the same cache window. Renewing again before that replaces the unused replacement. Last receipt does not prove database persistence. These APIs do not use usage range/filter semantics. See device operations for installation, identity limitations, resource bounds and offboarding.

Wrapped GET data routes validate these parameters before querying or caching:

Parameter Contract
from, to Date strings; use ISO 8601 with timezone. Defaults: to=now, from=to-DEFAULT_RANGE_DAYS. Require from < to; default range is two days and default cap is 90 days.
intervalHours Default 24. Must be finite and in (0,744], even on routes that ignore buckets. Values below one are clamped to one beyond four hours, except the bounded shared-overview fine resolution below. Only rows marked B below and shared overview honor this parameter.
group Exact inferred channel match, normally bedrock, enterprise or unknown; it is not a coding-client selector.
user Case-insensitive substring of the query's identity expression, usually UserEmail.
model Case-insensitive substring after the query's model normalization; scope varies by endpoint.
project Exact project tag, only for the four Usage routes listed below and only while schema.projectColumns === true. (untagged) maps to the stored empty string.
email Supply an exact identity for the two user drilldown routes (/api/users/daily, /api/users/decisions-by-tool). Omission is coerced to an empty string, not rejected as a missing required parameter; callers must not rely on uniform behavior without it.
includeUnknown Only /api/cost/by-user-model recognizes the literal 1, including unknown-channel rows in that result.

Minute/day widths are rounded by bucket() where needed. Dates are bound to ClickHouse at second precision. Historical rollup ends, latest-hour data and existence queries have specific approximations; see time boundaries. Config, health and chat routes do not use this range wrapper.

Wrapped data responses negotiate gzip for JSON bodies of at least 4 KiB. They retain Cache-Control: no-store and add Vary: Accept-Encoding; compression does not change authentication, range/filter cache keys, null values, or JSON escaping. Health responses and streaming chat keep their separate response handling.

Coding-client views

Activation controls /api/config's enabledClients/codexEndpoint; disabled Claude routes return 404, stop warming/chat.

GET /api/clients/overview returns clients, totals, by_client, by_model, by_user, by_project, timeseries, tools and metadata below. GET /api/clients/activity returns daily timeseries rows containing client, t and sessions, plus clients, bucket_hours=24, effective_range and applied_filters. It requires userExact; intervalHours must be omitted or 24. It uses the same authentication, range limits and client/backend/user/model/team filters as overview. The query aggregates in ClickHouse before returning rows, so a 90-day user heatmap does not consume overview's 50,000-row detail budget. Claude counts sessions with token/cost counter increments or supported operational logs, deduplicating the session across both sources with the existing baselines and historical end alignment: the counter and log reads run as separate identity-narrowed queries returning distinct day/session pairs, and the server unions them per day. Codex counts sessions with supported operational or completion events; setup metadata and stream deltas only supply filter evidence. Counts are distinct within each client and UTC day, not across clients or the entire period. client=all|claude|codex selects enabled sources; backend=all|bedrock-mantle|bedrock-runtime|anthropic|unknown. Invalid/disabled clients, arrays and nonempty group/project filters fail before caching. An exact team filter is accepted with the session semantics in Filter scope, applied to Claude counters, Claude and Codex logs and the Codex cost facts; /api/codex/insights rejects it. Backend is resolved from the model id prefix (region prefix → runtime, bare vendor namespace → mantle), falling back to the resource tag (Codex); otherwise unknown. Claude rows are anthropic first for an enterprise session or a bare claude-* record that carried organization.id; see Claude channels and ADR-017. Task traces from device collectors lack the span attribute, so their login calls stay unknown.

Claude model terms normalize. Modeled rows match directly. Model-less rows use same-client model evidence (logs; Claude also uses token/cost counters) within the range and identical session (nonempty), user, backend and project. Coarse session attribution can match several models; rows retain model="".

observed_tokens sums known input/output pairs; tokens_partial discloses incomplete usage/validation or unsafe sums. Cache and reasoning subsets are not added again. Unpriced models can still have complete observed tokens. All-unknown is null and known zero is retained. Primary token displays/CSV use observed counts with coverage status; canonical tokens and components retain strict nulls for ratios and existing consumers. These fields also appear in Codex detail summary/Effort rows. See ADR-014.

by_client rows also carry observed_input: { input_tokens, cache_read_tokens, cache_write_tokens, partial }, the known non-cached input, cache read and cache write sums from usage rows whose three parts are all known. partial is true when a usage row lacked a part or a Codex request had no usage-bearing completion (for example an interrupted stream); parts are null when none is known or a sum is unsafe. The input/cache composition card shows this labelled subtotal when the strict components are null.

cost_usd uses cost_basis=client_reported (Claude) or aws_list_estimate (Codex), or (Claude, this endpoint only) computed_estimate/mixed for a token-priced fallback when no report is usable (ADR-017); cost_estimated counts those rows. Claude rows for non-Anthropic models carry aws_list_estimate: their counter cost is rescaled to the AWS list price (ADR-017 amendment), on this endpoint and in the legacy cost views' reported_cost. It sums usable reports/estimates even when other records are unpriced. cost_partial discloses exclusions or an unsafe aggregate; unpriced counts remain. Groups with only unpriced evidence remain null; explicit zero stays zero. Positive Claude reports remain usable with missing token fields; zero reports still require known zero usage. Codex estimates still require valid usage and a known rate (its own table, or the Claude table for an Anthropic model; price_source: "claude_table"). Table rates come from the merged price catalog: operator overrides, then the AWS Price List layer, then code tables (ADR-017 amendment). Cost unit values use the known subtotal and observed denominators, with partial labels in the UI/CSV. See ADR-013. User counts union IDs; sessions include client; model identity counts overlap. by_project uses Codex project.name, not billing projects. Request/TTFT durations are observed means or null. requests includes api_error attempts.

observed_records: deduplicated logs plus active Claude usage rows. Zero can coexist with idle counter observations in timeseries; check both for an empty response. Idle timeline rows carry timeline_observed=true, independent token/cost availability and partial flags without adding active users/sessions or changing other aggregates. See ADR-015 for chart zero semantics.

rejected_requests counts explicit HTTP rejections. Identified Codex scopes containing only rejections have zero recorded completion usage and retain request/error counts. request_rejections_only labels those shared groups; chart tooltips disclose the rejected count. Accepted/uncertain requests without usage and unpriced completions remain unavailable. See ADR-016. quality.missing_usage counts Codex session/user/model/backend/project scopes without usage across the range, making affected canonical token folds null and adding to unpriced. Known cost and observed-token subtotals remain visible with coverage status. Unpriced counts can combine usage-record counts and unmatched usage-scope counts; they are not a request-coverage percentage. This cannot detect every dropped response.

Folds share rows; full-usage validity and input/output-pair validity are separate SQL grouping dimensions, preserving usable costs and observed token pairs. Breakdowns sort by observed tokens. Active rows and compact idle observations each have a 50,000-row guard (overflow returns 400); idle rows do not consume active-row capacity. Claude counter/baseline rules remain. effective_range={from,to,requested_to} applies Claude's resolved end to both clients when selected; the UI discloses trimming. Native buckets are one minute through four hours, otherwise one hour. On this endpoint only, an explicit intervalHours between ten minutes and one hour (exclusive) enables ten-minute native raw counter buckets through one day. Finer requests over four hours and subhour requests over one day fall back to hourly rollups; the three-day counter baseline, row limits and query budgets remain. Fine historical queries keep their requested end instead of flooring it to the hour. A coarser intervalHours is honored, rounded up to a whole multiple of the native bucket, and reported as bucket_hours; finer values fall back to native. Codex buckets in SQL at the native bucket; both clients' rows are then regrouped, so distinct sessions stay exact, coverage identity stays at the native bucket (scope_t), and bucket-only evidence merges as within one bucket. Sizes use Claude's resolved end; eligibility for one-day raw resolution uses the original requested span. Cache keys include display/native bucket sizes and resolved end. Optional userExact and modelExact select exact row identities in addition to existing partial user/model filters. Claude model IDs are normalized; Codex routing prefixes remain. These validated, bound parameters have separate cache keys. /api/codex/insights rejects modelExact and rejects userExact unless profile=tools, which then compares the identity by equality (UserEmail, or user.email falling back to enduser.id on the raw-log path) beside any substring user filter. Other profiles, including the default, return 400 unsupported filter. Exact responses echo applied_filters={userExact,modelExact,backend}; the UI verifies this acknowledgment before presenting identity detail, including during a rolling upgrade. Raw Claude counter inputs also narrow user/model partitions throughout the baseline. Session channel/backend inference and model-less coverage evidence retain their existing scope. Bulk delta scope evidence keeps extracted scope dimensions and a fixed delta marker, without retaining or sorting arbitrary attribute maps; completion deduplication keeps the original full identity. Token-like attributes on a delta never become completion usage.

Optional view=cost selects the bounded cost profile on this endpoint. Any other supplied value, including an empty value or array, returns 400 before caching. Its cache identity is separate from the default view. Cost, token coverage, populations, rejected-request evidence, effective bounds and exact-filter acknowledgments retain the same semantics. The response echoes view="cost". It omits by_project, tools, and row fields api_errors, tool_calls, tool_errors, request_duration_ms, ttft_ms; omitted diagnostics are not measured zeros. It adds by_user_model, grouped by client, raw user ("" when absent), model, backend and, for Claude, the inferred session channel (bedrock/enterprise/unknown), using the same bounded records and cost accumulator as by_user. These range-level rows retain cost basis, known subtotals, token coverage, and per-group sessions. Model-level sessions must not be summed into user sessions; use by_user for user totals. Like by_model_time, it keeps a group only when usage or a Codex missing-usage scope backs it, so operational-only groups never appear as a known $0 and a by_user user may have no by_user_model rows. This breakdown is absent from the default profile. SQL still retains operational records needed for scope coverage, but skips duration/error calculations and tool-name grouping. Observed requests remains available, including in the Trends period table and CSV; unsupported counts retain null. Shared cost/executive/trend views, trend baselines and cost drilldowns use this profile. The default profile retains diagnostic fields. See cost query performance.

Codex detail log summaries use one query for full-identity deduplication, per-response pricing and scope evidence. The 50,000-row transfer budget applies to aggregate dimensions, not raw events. Families are counted before transfer; if their combined size exceeds the budget, oversized families return only a limit marker under an equal allowance. Whole-window usage totals are independent of Effort rows. coverage.logs.limited_sections names capped families (event, usage, effort, scope, operations, latency, tool, approval, runtime) and sets partial=true. Only affected results are withheld: limited scope evidence keeps known subtotals partial while withholding strict ratios and session units. See Codex bounds.

Codex insights additionally returns two log-derived objects in both profiles:

  • completion_usage, with basis="recorded_completions" and status="observed"|"empty"|"limited"|"unavailable", describes only deduplicated usage-bearing completion logs. Fields include completions, canonical tokens, tokens_partial, cache_hit_rate, cache_write_share, reasoning_share, tokens_per_completion, cost_usd, unpriced, cost_partial, and cost_per_completion. Costs retain cost_basis="aws_list_estimate". identified_sessions, identified_completions, unidentified_completions, identified_cost_usd, identified_unpriced, and identified_cost_partial describe the subset with conversation IDs. cost_per_identified_session divides that subset's known cost by its distinct sessions. Anonymous costs never enter this session unit. Unknown components/prices, zero denominators and limited usage remain unavailable or partial; this cohort is not a replacement for whole-window completeness.
  • completeness contains boolean missing_usage, missing_session, usage_limited, scope_limited, operations_limited, and incomplete_tokens reasons. request_count and session_count are observed whole-window denominators, null when their aggregate family is limited.

Existing summary and Effort fields are unchanged. Both objects use the same log query and filters; they add no queries or identifiers to the response.

profile=effort on /api/codex/insights selects one log query instead of the default log, four metric and trace queries. It preserves Effort/usage totals, denominators, full event identity and missing-usage scope evidence. It skips latency/tool/approval/runtime breakdowns and quantile work, omits those arrays and summary.tool_success_rate, and reports coverage.logs.omitted_sections. Unqueried metric/trace coverage has status="not_requested", records=null, last_seen=null; empty signal arrays do not assert absent collection. The response echoes profile="effort". Invalid profile values fail before caching; the profile has a distinct cache identity. The default profile remains full. See Effort profile.

GET /api/clients/claude-effort supplies the common cost page's independent Claude effort card. It accepts the same range, user, model and backend filters; only client=claude|all is valid. Group/project and exact drilldown filters are unsupported. Authentication, disabled-client checks, validation and cache isolation use the shared route wrapper. The response includes range, cost_scope="reported_or_rescaled_counters" and effort rows with effort, backend, nullable cost_usd, cost_partial, cost_basis and unpriced. Empty effort becomes unknown; backend identities remain separate. unpriced counts aggregated user/session/model scopes, not requests. cost_basis describes the known subtotal's contributing scopes; when all costs are unknown, it describes their source bases. More than 50,000 scopes returns 400 and asks for a narrower range, without returning a truncated subtotal. Known counter reports and ADR-017 rescaled estimates retain cumulative/delta boundaries. Token-derived overview fallback estimates are outside this card's scope, so its total need not equal the overview total. All-unknown cost is null; known zero requires observed zero-cost evidence. Filtered responses are cached independently; this endpoint is not warmed.

Cost pages keep model, effort and input-cache headings visible during loading, empty results and errors. Model sectors retain version/backend identities and family colors. Input-cache composition requires complete canonical components; an observed token subtotal cannot replace a missing cache denominator. The Codex effort card and detail table share one profile=effort response using the overview's effective bounds.

GET /api/clients/productivity returns shared Productivity rows for the selected enabled clients, in one shape: {clients, by_client: [row], effective_range: {from, to, requested_to}}. It accepts the overview's client, user, userExact, model, modelExact, backend and team parameters with the same validation; group/project, invalid backends and disabled clients return 400 before caching or database work. The effective end is the overview's default (Claude historical ends beyond four hours floor to the hour); the SPA passes the overview's effective bounds, which resolve unchanged. It is cached per filter set and not warmed. Users, sessions, tokens, cost and cost basis are not repeated: the page joins each row with the overview's by_client row for the same client.

Each row has client, prompts, tool_calls, tool_successes, tool_failures, tool_unknown, tool_success_rate, decisions_total, decisions_allowed, decisions_denied, decisions_unknown, permission_allow_rate, tool_calls_per_prompt, ttft_samples, ttft_p50_ms, ttft_p90_ms, ttft_source, permission_wait_samples, permission_wait_p50_ms, by_tool_decisions, by_tool_results, top_users, activity_index_users, activity_index_scored, active_user_seconds, active_cli_seconds, commits, prs, loc_added, loc_removed and unavailable.

Field Claude Code Codex
prompts user_prompt log events codex.user_prompt log events (one per user turn)
Tool outcomes tool_result: success is success=true without error; failure is success=false or an error codex.tool_result success=true/false
Decisions tool_decision accept/reject; source from source codex.tool_decision approved/denied families and source, as in /api/codex/insights
ttft_p50_ms, ttft_p90_ms Beta llm_request span ttft_ms (per model request) codex.turn_ttft duration_ms (per turn)
permission_wait_p50_ms Beta tool.blocked_on_user span duration_ms (the permissionWaitOverhead source, v2.1.214+) null, not_aggregated
top_users Activity index over userLeaderboard inputs null, not_derived
Active time, commits, PRs, LOC Counter increases (incFlat), null without in-window counter evidence null; see unavailable

Totals and per-tool rows come from one log scan per client: each total is the sum of its kind's rows. by_tool_results and by_tool_decisions list, per client and kind, the ten tools with the most records (ties by name), then at most one remainder row with other: true, tool: null and tools (the number of folded tools); its values are computed over the folded records, so its p50 is exact. A missing tool name is "". Result rows have tool, other, tools, calls, successes, failures, unknown, success_rate, duration_samples and duration_p50_ms (valid duration_ms only). Decision rows have tool, other, tools, decisions, allowed, denied, unknown, allow_rate, source (the most frequent source, ties by name; "" when missing) and source_decisions. An empty list is a measured absence; a missing response is null. Permission decisions include automatic approvals and are not code acceptance.

top_users lists at most ten Claude users (UserEmail, unmasked like /api/users/leaderboard; the SPA masks them) with user, productivity_score, accept_rate, loc_added, commits, sessions, active_days and decisions. Inputs mirror /api/users/leaderboard (session count, added lines, commits, code-edit decisions from incFlat; distinct hourly session-count days) and withProductivityScore over the effective range, but per user across channels and under the shared filters below rather than the Claude detail grouping. Users rank by score; a user without code-edit decisions or with a malformed input has a null score and ranks last. activity_index_users counts the observed users and activity_index_scored those with a score.

Log events are deduplicated by timestamp and both attribute maps; spans by trace and span id. Rates use known outcomes only (tool_successes / (tool_successes + tool_failures), decisions_allowed / (decisions_allowed + decisions_denied)); the unknown remainder is reported separately, and an empty denominator yields null. P50/P90 are exact nearest-rank lower quantiles of valid nonnegative samples; no samples yields null. A measured zero count stays 0. unavailable maps a null field to a reason: not_collected (default OTel has no source; Codex prs, commits, loc_added, loc_removed need Git/CI evidence), not_aggregated (no confirmed source metric) or not_derived (a dashboard-derived value whose inputs are missing; Codex active time, because codex.turn.e2e_duration_ms is turn elapsed time, not human active time, and Codex top_users, because the activity index needs lines and commits Codex does not emit). Codex permission_wait_p50_ms is not_aggregated. Claude rows have an empty map.

Filters: Codex follows the overview's event-level rules (user identity, model with same-session evidence for model-less events, backend from model/resource tag, session team). Claude events, spans and counters carry no usable model, so model, modelExact and backend select sessions whose token/cost counters match in the range plus the three-day lookback (the overview's counter backend rule); user/userExact match the counter UserEmail or the log/span identity; team is the session team. The same rules apply to the per-tool rows, latency and top_users (whose active days use the same session scope on the hourly rollup). All Claude channels, including unknown, are included. These are activity observations, not productivity, code quality or saved time.

modelTime=1 is accepted only by /api/clients/overview; any other value returns 400 before caching. It is part of the cache key; the shared default cost profile warms view=cost&modelTime=1&timeFormat=compact. Custom windows, filters, resolutions and legacy time-row responses may remain cold; /api/codex/insights ignores it. It adds by_model_time: one row per client, t, model, backend from the same fold and buckets as timeseries, with Codex raw model IDs (routing prefixes kept; Claude models are normalized as in by_model) and the usual cost/token/coverage fields. A row exists only where Claude counter usage, a Codex usage-bearing completion or a Codex missing-usage scope backs it; operational-only records (requests, tools, TTFT, stream errors, rejected-only requests) attach to such a row but never create one, so their buckets appear only in timeseries. Each row's unpriced_reasons counts Codex unknown_backend, scope, unknown_model, invalid_usage, or Claude report_missing, report_zero_with_tokens, plus missing_usage (usage scopes without usage; model-less scopes stay under model=""); the counts sum to unpriced. Claude report_missing also covers an invalid (negative or non-numeric) report and a zero report on a counter row whose cost_observed is 0; report_zero_with_tokens also covers a zero report beside unknown tokens. Per client, t, known cost_usd sums to the timeseries row; idle Claude observations stay in timeseries only. Codex estimates are never recomputed from aggregated tokens. Each unpriced Codex response has one unpriced_reason, checked in order: backend, scope (us-gov., a commercial routing prefix on mantle, or no rate for the scope/tier), model rate, then usage validity; a configured rate with a non-finite estimate reports unknown_model. It also adds by_user_time: the same backed usage regrouped per client, t, user (raw identity, "" when absent) with the same fields except model (backend is the rows' single backend or mixed), plus unpriced_reasons; per client, t its known cost_usd and unpriced sum to the by_model_time rows. Each client keeps its top 20 users with backed usage (known range cost_usd, then observed_tokens, then identity) and ""; the rest fold into one user: null row per client, t whose rest_users is the client's folded user count (null on named rows), sorted last.

Optional timeFormat=compact requires view=cost&modelTime=1; other supplied values or combinations return 400 before cache lookup or database work. It has a separate cache identity and echoes time_format="compact-v1". Only by_model_time and by_user_time change representation to {columns, dictionaries, rows}. Each row is an array in columns order. Columns present in dictionaries use zero-based indices into their dictionary; null remains null. Other cells retain their original values. No fields, identities, buckets or coverage information are removed. The format changes only the two time tables; totals, rankings and timeseries retain their representation. Filters and cost calculation are unchanged. User rankings sort descending by observed tokens, then client and raw user identity for ties, so identical usage does not reorder users during refresh. The SPA requests this format and reads rows lazily; older array responses remain supported. Malformed or unknown compact formats are unavailable, never zero. This reduces response and cache size; it does not reduce the underlying SQL scan or prove a fleet capacity limit.

GET /api/clients/skills

Skill invocations per selected enabled client (client=all|claude|codex) with the shared filters (user/userExact, model, backend, team; Codex model filtering uses the overview's session model evidence). Claude reads skill_activated logs, Codex codex.skill_invocation logs; exact duplicate deliveries (same Timestamp and attribute maps) count once. The window ends at the overview's effective end, so the page joins it to the overview's bounds.

Response {clients, skill_limit, by_client, effective_range}. Each by_client row is {client, skills, skills_limited, totals}: skills are {skill, calls, explicit, automatic, nested, users, sessions}, most called first, at most skill_limit (200; skills_limited past that). explicit is Claude user-slash or Codex explicit, automatic Claude claude-proactive or Codex implicit, nested Claude nested-skill (null for Codex, which has no such kind); other triggers count only in calls. Events without a label-shaped name stay in one unnamed row (skill null; its users/sessions null when several raw names fold into it). totals sums the returned rows ({skills, calls, explicit, automatic, nested}; skills counts named rows; a malformed count makes its total null). skills is null for a malformed read, shown as not collected. The cache key carries the client, backend, exact filters and effective end. An empty list means no skill event was recorded, not that no skill was used.

GET /api/clients/token-economics

Returns the token economics cost chain for the selected enabled clients. It accepts the overview's client, user, userExact, model, modelExact, backend, team and intervalHours parameters with the same validation; group/project, invalid backends and disabled clients return 400 before caching or database work. Only enabled clients are queried. It is cached per filter set, window and bucket, and not warmed.

{ effective_range: {from, to}, previous_range: {from, to}, bucket_hours,
  by_client: [{ client, cost_basis, current, previous, decomposition,
    unavailable: {chain, decomposition, outcome},
    coverage, previous_coverage, trend, models, outcome, previous_outcome, exemplars }],
  combined: { cost, previous_cost, decomposition, unavailable_clients } }

effective_range.from floors from to the hour; to floors the overview's effective end to the hour. previous_range is the window of the same length ending at effective_range.from. bucket_hours is the trend bucket: the overview's bucket for the range and intervalHours, rounded up to whole hours and at least one hour.

Field Content
current, previous {users, sessions, turns, requests, tokens, cost, factors, cost_per_million_tokens, unavailable: null}; factors has users, sessions_per_user, turns_per_session, requests_per_turn, tokens_per_request, cost_per_token; a factor whose denominator is zero is null (undefined), not 0, while the totals stay. Null when that window's chain is unavailable or the window is gated.
decomposition {contributions, bundles, total_change, unavailable}; contributions per factor, bundles growth/waste/price. total_change is current.cost - previous.cost, null when either chain is missing. unavailable is the current chain's reason when it is missing, else the previous window's gate, no_previous or zero_factor.
unavailable Reason or null for chain (current window), decomposition and outcome.
coverage, previous_coverage {sessions, matched_sessions, excluded: {no_turns, no_requests, cost_incomplete, tokens_invalid}, cost_known, cost_matched, cost_unattributed}. tokens_invalid covers sessions with an unusable token subtotal or with requests but zero canonical tokens. cost_known includes cost_unattributed, the known cost of usage rows without a session.
trend Current-window buckets {t, cost_per_user, cost_per_session, cost_per_1k_requests, tokens_per_request, cache_hit_rate}; null values where the bucket's chain is unavailable (no matched session or no identified user).
models {model, cost, cost_partial, requests, request_share, tokens, cost_per_million_tokens, small} over matched sessions, by cost. A model seen only in request rows, with no counter usage row in any matched session, has cost, tokens and cost_per_million_tokens null (unavailable, not zero); cost_partial marks a known subtotal when some of its sessions lack usage rows.
outcome, previous_outcome Claude: {commits, prs, loc_added, cost_per_commit, cost_per_pr, cost_per_kloc} over matched sessions; a count and its ratio are null without in-window counter evidence (ADR-018). Codex: null (unavailable.outcome = "not_collected").
exemplars {eligible_users, levers: [{lever, direction, median, top: [{user, value, sessions, turns, commits}]}]}; top holds up to 10 eligible users in lever order and median covers every eligible user; commits is null for Codex and for users without commit evidence. Identities are unmasked like other per-user responses; the SPA masks them.
combined cost/previous_cost sum the per-client matched costs, null if any client's chain is missing; decomposition sums per-factor contributions and is unavailable (first blocking reason) if any client's decomposition is. unavailable_clients lists each blocking {client, reason} in by_client order, empty when the decomposition is available.

Reasons: window_too_short (effective window under one hour), not_backfilled (SESSION_TURNS_READ_FROM unset or after the window's start), retention (the window's start older than 90-day log retention), no_matched_sessions, no_users, no_previous (previous chain missing), zero_factor (a cost or factor is zero in either window) and not_collected (Codex outcomes). Each window is gated before any query. window_too_short, or not_backfilled/retention for effective_range.from, makes every section null and runs no query. not_backfilled/retention for previous_range.from skips only the previous-window queries: previous, previous_coverage and previous_outcome are null, decomposition and unavailable.decomposition carry the reason, unavailable.chain stays null, and the combined previous_cost is null.

Filters: team passes to the usage sources and user/userExact to the turns source (log identity). Codex usage also applies user/userExact (the same log identity); Claude usage is read without them and kept for sessions whose turns matched, so counters without UserEmail (Bedrock) stay counted. model, modelExact and backend select whole sessions with at least one matching usage row (Claude normalized model, Codex raw model; backend equality). With a team, model or backend filter, sessions absent from the filtered usage rows are dropped from the turns as well, so they never enter coverage. Outcome counters are read per session and kept only for matched sessions.

Filter scope

Forwarded parameters are not universally implemented. filterCond applies a filter only when the query supplies its column expression.

Queries Actual scope
Most A/B queries Exclude unknown by default; an explicit group=unknown bypasses that default exclusion.
kpiSummary, costSummary, activeUsers, adoptionLevels, adoptionTimeseries Include unknown-channel rows when no channel is selected.
activeUsers, adoption routes Honor group/user; ignore model and project. activeUsers honors team; adoption routes reject it. Adoption snapshots ignore from and use windows ending at to.
Integrity version cohorts Honor group and team; ignore user/model/project.
Plugin inventory Range and team only; no group/user/model/project filtering.
Subagent fanout, skill activations, compaction, refusals, exhausted retries, permission wait, interaction breakdown Honor group/user; ignore model/project. TTFT separately supports row-level model filtering.
Activity density, tool outcomes, permission occupancy Honor group/user and session-level model filtering; ignore project. Unknown channels are excluded by default.
Session turns Honor group/user. A model filter selects sessions that used a matching model (rollup lookback semi-join) and keeps each selected session's whole cost and turn count. Ignore project. Unknown channels are excluded by default.
Mixed metric queries Model-bearing rows match directly; model-less session/commit/PR/LOC/decision/activity rows use a matching-session semi-join when modelMixed is configured.
Many log queries modelViaSession accepts every matching session's events if that session has a matching model in the rollup lookback. This is not event-level model attribution.
Reported-vs-computed and entrypoints Match the model on each api_request log row.
User daily/decisions/heatmap Exact email and optional group; ignore global user/model/project. With no group, include all channels. Heatmap uses 91 days ending at to, ignoring from. Reject team.
Projects, permission modes, decision sources, entrypoints The only queries applying project filters. Projects filter identity using the email/EndUserId fallback; the other three filter UserEmail.
Team (all applying routes) Session semi-join (teamViaSession, else the declared modelViaSession/modelMixed session), not event-level attribution. Each session's team is its most frequent otel_logs value of resource, else log, attribute team (ties lexically first) over the range plus the three-day counter lookback. Rows without a session, and sessions without team evidence, never match.

GROUP_MODE=single affects frontend presentation only. The project filter is silently dropped on both server and frontend unless the project probe is true. /api/usage/projects also returns [] while gated off; the other three routes can run without the new columns when no project predicate is applied. The probe checks logs only, not every table or migration.

team is a client-provided OTel resource attribute, not verified identity (like user.email). The parameter is an exact match after trimming; empty means no filter. A non-string or a value longer than 256 characters returns 400 invalid filter before caching. It is part of the result cache keys, and team-filtered requests never mark a legacy route for warming. The adoption routes, the two email drilldowns, the task routes, /api/codex/insights and the two team lookups return 400 unsupported filter with detail the team filter does not apply to this endpoint. The SPA does not forward team to them; the filter bar discloses the views it does not cover.

Response conventions

All endpoint tables below describe GET JSON responses. Unless explicitly marked object, a response is an array. Numeric ClickHouse fields may arrive as strings; derived JavaScript fields are numbers or null. B means a configurable time bucket, not necessarily a day.

Token fields means input_tokens, output_tokens, cache_read_tokens, cache_write_tokens. Priced row means those fields plus reported_cost, computed cost, and unpriced. An unknown server rate yields cost: null, unpriced: true; it does not invalidate a valid client-reported spend value. Summary/effort/agent aggregates retain unpriced_tokens.

Trace result means {unsupported:false, rows:[...]} when matching spans exist, otherwise {unsupported:true, minVersion, rows:[]}. minVersion is present on the unsupported result, not guaranteed on success. An empty trace result is not measured zero; missing tables or query failures still use the normal error path.

Overview

Path Fields and grain
/api/overview/kpi Per group: sessions, users, commits, prs, total_tokens, input_tokens, output_tokens, lines_of_code; observation flags commits_observed, prs_observed, lines_of_code_observed. Its row-existence user count is not a substitute for active-users.
/api/overview/active-users Object: users, bedrock_users, enterprise_users; distinct nonempty emails with session-counter rows in range.
/api/overview/tokens-timeseries B, per t, group: tokens, input_tokens, output_tokens.
/api/overview/cache-efficiency Per group: cache_read, input_side, cache_read_ratio, uncached_input, cache_write, output_tokens. Input side includes uncached input, cache read and cache creation.
/api/overview/model-distribution Per group, model: tokens, input_tokens, output_tokens.
/api/overview/session-turns Object {summary, users, sessions}, Claude Code only. summary: sessions, sessions_with_turns, turns, median_turns_per_session, p90_turns_per_session, known_cost, cost_partial, cost_per_turn, model_calls_per_turn, sessions_without_turns, sessions_cost_unavailable. users (top 20 by known cost): user, sessions, turns, known_cost, cost_partial, cost_per_turn, cost_per_session. sessions (top 20 by known cost, unavailable last): session, user, group, started (first in-window prompt/model-call event, ISO UTC, or null), turns, model_calls, known_cost, cost_partial, cost_per_turn. Unavailable costs and ratios are null, never 0. Definitions: session turns.

Productivity

Path Fields and grain
/api/productivity/normalized Per group: loc, tokens, loc_per_million_tokens, commits, commits_per_million_tokens.
/api/productivity/decisions Per group, decision: n, from the code-edit permission-decision counter.
/api/productivity/decisions-by-tool Per group, tool, decision: n; excludes empty tool names.
/api/productivity/active-time B, per t, group: active_seconds, summing active-time types.
/api/productivity/active-time-summary Per group: user_seconds, cli_seconds, user_seconds_observed, cli_seconds_observed, and distinct sessions in the active-time aggregation.
/api/productivity/activity-density Per group: observed_scopes, matched_scopes, nullable matched_commits, user_hours, commits_per_user_hour, and fractional coverage. Scopes are user/session pairs with either relevant counter observed; ratios use only matched scopes.
/api/productivity/tool-outcomes Per group: total, succeeded, failed, unknown, nullable success_rate, unknown_rate. Fractions, no top-tool cap; both unprefixed and claude_code.tool_result events.
/api/productivity/permission-occupancy {unsupported, limited, rows}. Per group: interactions, matched_interactions, ambiguous_interactions, nullable interaction_ms, blocked_ms, blocked_share, and fractional coverage.
/api/productivity/agenticness B, per t, group: prompts, tool_calls, tool_calls_per_prompt, from log events.
/api/productivity/engagement B, per t after filters, with no group column: users, sessions, prs, prs_per_user.
/api/productivity/loc-timeseries B, per t, group: loc_added, loc_removed.
/api/productivity/languages Per group, language: edits, accepted; empty language becomes unknown. These are decision counts, not quality measurements.
/api/productivity/permission-wait Trace result, minVersion: "2.1.214"; per group, app_version: p50_wait_ms, p95_wait_ms, n, from tool.blocked_on_user.
/api/productivity/ttft Trace result, minVersion: null; per group, model: p50_ttft_ms, p95_ttft_ms, n, from llm_request.
/api/productivity/interaction-breakdown Trace result, minVersion: "2.1.214"; per group: interactions, p50_interaction_ms, p95_interaction_ms, llm_share, tool_exec_share, blocked_share.

Trace shares join child spans to interactions by TraceId. Overlap means shares can sum above one; they are not a partition of elapsed time. These three trace routes are excluded from cache warming. The beta setting and matching span/version coverage must be checked before interpreting absence.

The three activity-density/tool-outcomes/permission-occupancy routes also skip cache warming and use the authenticated Claude route wrapper. Occupancy's blocked_share unions overlapping wait intervals within each matched interaction; it is distinct from the additive share in interaction breakdown. No matched wait produces null, not zero. unsupported=true means no valid eligible interactions; limited=true with empty rows means the 20,000-span bound or a conflicting span identity prevented calculation. Neither state swallows database errors. See measurement and coverage rules.

Usage

Path Fields and grain
/api/usage/tool-mcp Per group, tool, mcp_server: ok, fail, total from tool_result; ordered by group then volume, 50 returned rows total. Only explicit success/false flags enter ok/fail.
/api/usage/tool-decisions Per group, tool, source: accepts, rejects, n, accept_rate, from tool_decision. Selects the top 20 tool names by unfiltered fleet volume in range, then applies outer filters. Rate is accepts/(accepts+rejects), not accepts/n.
/api/usage/skills Per group, skill: invocations, est_cost_usd. Counts incFlat cost-series rows and sums reported cost; not a direct activation count.
/api/usage/connectors Per group, connector: users, calls, ok, from tool_result with a nonempty MCP server.
/api/usage/subagent-fanout Per group: subagent_completions, interactions, avg_subagents_per_interaction; denominator is distinct nonempty PromptId among completion events.
/api/usage/skill-activations Per group, skill, trigger: invocations, counting skill_activated events.
/api/usage/compaction Per group, trigger: compactions, sessions, compactions_per_session, avg_compression_ratio; counts only compactions with positive pre-token count.
/api/usage/plugins Fleet plugin, marketplace: session_loads, sessions, from nonempty plugin_loaded names.
/api/usage/tool-latency Per group, tool: uses, errors, p50_ms, p95_ms; errors include explicit false success or a nonempty error attribute. Ordered by group then uses, 50 returned rows total.
/api/usage/commands Object {commands, prompts}. Commands: group, command, uses, users, excluding empty command names. Prompts: group, prompts, p50_len, p95_len, over all user-prompt events.
/api/usage/hook-overhead Per group: executions, total_seconds, p95_ms, blocked; blocked counts executions with num_blocking > 0, not the sum of that attribute.
/api/usage/mcp-health Per group, server: attempts, connected, failed, p95_ms from mcp_server_connection. Other statuses also enter attempts.
/api/usage/projects Per group, project: reported cost_usd, tokens, sessions, users; empty project becomes (untagged). Counts sessions/users only with cost/token rows in range. Returns [] unless the project gate is true.
/api/usage/permission-modes Per group, from_mode, to_mode: changes, sessions from permission_mode_changed; mode values pass through.
/api/usage/decision-sources Per group, decision_source, decision_type: tool_results, share from tool_result, excluding empty decision sources. Share is within channel, rounded to three decimals.
/api/usage/entrypoints Per group, entrypoint: requests, sessions, reported cost_usd, users from api_request; empty entrypoint becomes terminal. Reads the entrypoint map directly.

tool_decision.source and tool_result.decision_source describe different event populations. MCP health reads server_name, while connector/tool-result queries use promoted fields parsed from tool_parameters. Counts, quantiles and rates therefore need their own denominators.

Reliability and integrity

Path Fields and grain
/api/reliability/refusals Per group: user_visible_refusals, server_hidden_refusals; the latter have server_fallback_hop='true'. Keep them separate when describing visible failures.
/api/reliability/retries-exhausted Per group: exhausted_retries, avg_total_attempts, avg_retry_duration_ms. It does not isolate throttling as the cause.
/api/reliability/api-errors Object {byModel, byStatus}. By model: group, model, requests, errors, total, error_rate. By status: group, status_code, errors; missing status becomes no-http-status.
/api/reliability/api-latency Object {byModel, byEffort}; each row has group, model/effort, requests, p50_ms, p95_ms. Empty effort becomes unknown; duration comes from api_request.duration_ms.
/api/reliability/reported-vs-computed Bare array per group, app_version, model: requests, priced-row fields, ratio = reported_cost / cost when computed cost is positive (otherwise null), plus cost_5m/cost_1h (computed cost at each cache-write TTL, independent of the server's PRICING_CACHE_WRITE_TTL default) and ttl_share = (reported_cost - cost_5m) / (cost_1h - cost_5m) — near 0 means the reported cost prices cache writes at the 5-minute tier, near 1 means the 1-hour tier; null when the denominator is 0 (no cache-write tokens). Claude Code 2.1.300+ also logs cache_creation_5m_tokens/cache_creation_1h_tokens; the rows add cache_write_5m_tokens/cache_write_1h_tokens sums, cache_split_coverage (split share capped at 1 for positive total writes, otherwise null), cost_split (split tokens at their TTL list price, a nonnegative remainder at the server's PRICING_CACHE_WRITE_TTL assumption) and ratio_split = reported_cost / cost_split. Coverage 1 means a zero assumed remainder, not agreement between the total and split fields; 0 means cost_split equals cost. Over-total splits remain priced with a zero remainder, including when total writes are zero and coverage is null. Source is api_request logs.
/api/integrity/version-cohort-sessions Per group, app_version: distinct sessions with nonempty version and session-counter rows in range.
/api/integrity/version-cohort-cost Per group, version_cohort: reported cost_usd, tokens, usd_per_million_tokens. Cohorts are pre-2.1.214 and >=2.1.214; raw series are differenced locally.

API error rate is api_error / (api_request + api_error) event counts. The code does not establish whether failed requests also emit api_request, so this is not a deduplicated request failure probability. Reported/computed ratios diagnose differences in pricing, TTL assumptions or collection; they do not prove a specific bug or invoice accuracy.

ttl_share isolates the cache-write TTL axis specifically, since the four token types stored by OTel (input, output, cacheRead, cacheCreation) carry no TTL dimension. It does not attribute any remaining reported/computed gap to collection completeness or a client pricing bug — see cost accuracy review.

Users

Path Fields and grain
/api/users/leaderboard Per user, group: sessions, tokens, input_tokens, output_tokens, loc, commits, prs, accepted, decisions, active_days, user_active_days, accept_rate, productivity_score (both null without a positive decision denominator).
/api/users/tools Per user, group, tool: uses from tool-result logs.
/api/users/skills Per user, group, skill: invocations, counting raw cost datapoints rather than activation events.
/api/users/cost-efficiency Per user, group: computed cost, unpriced, reported_cost, reported_unpriced, additive known_reported_cost and reported_partial, loc, commits, cost_per_loc, cost_per_commit. Ratios require usable reports from every included model; partial costs do not establish valid ratios.
/api/users/daily Exact-email daily t, sessions, loc, tokens, commits; fixed daily buckets.
/api/users/decisions-by-tool Exact-email group, tool, decision, n.

user_active_days is the distinct-day union within the query's filtered population, shared across a user's channel rows; summing per-channel active days can double-count dates. Both email drilldowns are excluded from cache warming.

/api/users/tasks (not warmed; team rejected) lists at most 20 tasks for one exact user: required userExact (email, else enduser.id, equality match), optional client, backend, group (Claude tasks only; 400 unless Claude is selected), workflowRunId, workflowName and sort=tokens|cost|subagents|duration (default tokens): most observed tokens, highest known cost, most subagents, slowest. Unknown values (null cost, Codex subagents) sort after every known value; ties fall back to observed tokens, then the task id. Response {tasks, unattached_codex, coverage, partial: true}; coverage.<client> is observed, empty or not_requested (Codex also limited when a row guard is hit). Only trace-enabled users emit spans, so partial is always true. Claude and Codex tasks are merged, sorted and capped together. Claude cost is priced after the query (reported cost joined from logs, else pricing), so sort=cost folds the top 100 Claude interactions by observed tokens and orders that pool by cost; the response then adds sort_candidates: {claude, claude_limit: 100, limited} (claude: rows considered, limited: the pool was full, so a costlier task outside the token top 100 may be missing). It is present only for sort=cost with Claude selected. workflowRunId (wf_ plus 1-128 identifier characters, else 400) keeps Claude tasks with any span whose workflow.run_id equals it; a workflow's agents nest under the interaction that started it, so the run usually lists as one task. workflowName (a label: letter or digit, then up to 127 of letters, digits, _.:-; else 400) keeps tasks with a span whose workflow.name equals it and whose workflow.run_id is non-empty (a name without a run id is not a workflow run). Both require client=claude alone (400 otherwise, including the default when Codex is enabled) and match only trace-enabled clients' spans. Codex turns are all costed before sorting. A Claude task is one interaction span starting in [from, to); its llm_request spans are read through to + 24h. Each task has id (claude:<TraceId>), started_at, duration_ms (duration_basis: "interaction_span"), normalized models, llm_calls, subagents (distinct non-empty agent_id), tokens (input, cache_read, cache_write, output from span attributes, observed_tokens, tokens_partial; unknown is null) and cost_usd/cost_partial/cost_basis. Claude tasks add workflows: one {run_id, name} per distinct valid run id on any of the trace's spans, sorted by run id ([] when none); name is null when missing or not label-shaped, and the smallest valid label wins if spans of one run disagree. Only wf_-shaped run ids count, for the filters too. Device collectors drop names that are not label-shaped before ingest. Per call: Anthropic models use the api_request cost_usd joined by request id (client_reported), else computeCost() (computed); non-Anthropic models use the ADR-017 per-request AWS list estimate (aws_list_estimate, null when unpriced). Calls with different bases make mixed; cost_basis is null for a task without calls.

A Codex task is one turn (id codex:<conversation.id>:<turn id>) of client=codex spans whose first span starts in [from, to) and whose turn id is UUID-shaped; spans group by turn id alone, so a synthetic id that repeats across conversations (auto-compact-N) cannot identify one turn and is never listed; spans are read in [from - 24h, to + 24h) so a turn crossing a bucket edge is listed once, by its starting bucket. Turns starting in the neighbouring 24h are attachment context only: completions they win are neither listed nor unattached here. duration_ms is the observed span window (min start .. max end, duration_basis: "observed_span_window"), llm_calls counts the turn's model-call spans (codex.sampling; a turn without one, from older Codex such as 0.159.3, counts run_sampling_request instead, never both), and subagents is null (no attribute links a subagent conversation; the detail's agents follows the trace). Spans carry no usage: tokens and cost come only from unique usage-bearing response.completed completions of the same conversation whose event time falls in [start, end + 5s] (latest-starting turn when several; each completion once), priced per completion with the Codex AWS list estimate (cost_basis: "linked_estimate"). tokens.input is the uncached remainder beside cache_read/ cache_write, so observed_tokens is input + output without re-adding cache or reasoning (ADR-014). Unpriced or token-less completions leave the cost known-partial (cost_partial) or null. Codex tasks are never group-filtered; with backend, a turn needs a linked completion on that backend (a turn without one is unknown). When Codex is selected, unattached_codex is {completions, cost_usd, cost_partial, turns_without_conversation, turns_without_unique_id}: the user's completions in [from, to) linked to no traced turn and their known cost (0 when none, null when all unpriced), plus the bucket's turns skipped for lacking a conversation id and, separately, for a non-UUID turn id (distinct ids: one synthetic id shared by several conversations counts once). Completions inside a skipped turn's window are unattached unless another turn wins them; otherwise it is null.

/api/users/tasks/detail (not warmed; team rejected) returns one task of the list: required id (exactly as listed: claude:<32 lowercase hex> or codex:<conversation id>:<turn id> with identifier characters and a UUID-shaped turn id; else 400), optional userExact, from and to (the list's bucket). A disabled client's id is 400. A task that does not belong to userExact in that bucket, or does not exist, returns 404 {"error": "not found"} either way. Without userExact (a shared task link) the owner is resolved: the Claude interaction's identity, or the single identity on the Codex turn's spans of that conversation (none or two: 404). Any reader can already pass any exact user, so this adds no access. Response {task, lanes, spans, truncated, span_total, why, context, environment, owner} (Codex adds agents and activity), owner being the exact identity the task belongs to:

  • task: the list row (same fold, same totals) for that task; Codex adds completions_without_span (linked completions matching no model-call span).
  • lanes: {id, label, kind, spawned_by, llm_calls, cost_usd, cost_partial}. Claude has main plus one lane per agent_id; kind is general-purpose, Explore, fork or custom, label is the spawning span's subagent_type (null when not found) and spawned_by is the drawn tool span that spawned the lane's first llm_request (the tool span when the tool.execution parent is folded into it). Codex has only main.
  • spans: {id, parent, lane, kind: llm|tool|wait, name, start_ms, duration_ms, attrs}; start_ms is the offset from the task start and parent is null when the parent is not in the payload. Claude emits one tool span per tool call: a tool.execution span whose parent tool span is present folds into that span (its allowlisted attrs fill keys the tool span lacks; execution_ms is its duration, while duration_ms stays the whole call including permission wait), and children of the execution span point at the tool span. An execution span without its tool span is returned as the tool span. Codex spans are the turn's spans that carry the task's conversation id or none. LLM spans add tokens, cost_usd, cost_basis; Codex model-call spans (codex.sampling, or run_sampling_request for legacy turns) carry the linked completion's values and completion: {model, effort, ttft_ms, duration_ms, count}. attrs holds only allowlisted keys: LLM model, token counts, ttft_ms, attempt, error_class, status_code, stop_reason, query_source_safe, effort, speed; tool tool_name, subagent_type, skill_name, anthropic.mcp.server.name, bash_command_class, success, error_class; wait decision, source; Codex model, tool_name, aborted. Current Codex puts tool_name on every tool-call span (collaboration tools included), so a Codex tool span without it is treated as one of Codex's own operations (turn scaffolding, mcp.runtime.*, codex.tool_blocking) and carries internal: true. This is a heuristic tied to that emission; the UI hides those spans by default and says how many.
  • truncated/span_total: at most the newest 5000 spans. Truncated Claude details null whole-task and lane totals and task.models (the newest spans' models are not the task's) and return no why; Codex totals do not depend on spans.
  • why: {rule, value} for cache_read_share (share of complete canonical tokens), context_growth ({first, last, calls} main-lane input context), top_lane_share ({lane, share}, two or more lanes, all costs known), most_expensive_call ({span, cost_usd}, all costs known), retries (calls with attempt > 1; omitted unless every call carries attempt) and permission_wait (Claude wait-span milliseconds). A rule whose inputs are unavailable is omitted, never zero-filled. Data rules: data reference.
  • environment: what the Claude task's session carried, from its mcp_server_connection and plugin_loaded events between 14 days before the task start and its end (lookback_days): mcp {connected, failed, other, servers: [{name, status}]} by each server's latest status (name/status null unless label-shaped) and plugins {count, skills, agents, commands, with_mcp, counts_partial} (path counts summed where reported). Either is null when its events are absent; environment is null when both are, and always null for Codex, which emits neither. OTel carries no tool-definition count or schema size, so neither is reported. The session id that keys the lookup is never returned.
  • context: {points, missing, partial, compactions, limits}. points are {span, lane, start_ms, tokens} per LLM call in time order: Claude input + cache read + cache write, Codex the completion's input including cache (span null when no sampling span matched). tokens is null when any part is unknown and counted in missing; partial marks a truncated Claude detail or a limited Codex turn set. compactions (Claude) are {start_ms, pre_tokens, post_tokens, trigger, success} of compaction events whose prompt.id is a prompt of the task's calls, at most 100; null when no call has a prompt id, and always null for Codex. limits (Codex) is the latest codex.conversation_starts of the conversation stored up to the turn's end (a start can be stored with the collector's later observed time), looking back 7 days: {context_window, auto_compact_token_limit}, null when none is reported; always null for Claude, which reports no context window.
  • agents (Codex only): subagent turns linked through the trace's parent-span chain (a subagent turn's session_task.turn ← op.dispatch.inter_agent_communication ← dispatch_tool_call_with_state ← the parent turn's collaboration<tool> span), read from the traces of the task's spans between 10 minutes before the turn and 24h after it ends. parent is {id, tool, started_at, link} of the turn that started this one, else null (started_at/link null when the parent's turn row is absent). children are up to 50 subagent turns, three levels deep, in tree order: {id, parent_id, depth, tool, started_at, duration_ms, models, llm_calls, cost_usd, cost_partial, link}: llm_calls is the subagent turn's model-call span count and cost_usd its completion-log estimate, folded like a list row (null when its turn row is absent). Each child's link {from, to} is the UTC hour containing that turn's start (from its turn row, else its lineage span), for opening its own detail; the parent's link is the same for the parent turn. Subagent turns and completions are read in the detail's window (from - 24h .. to + 24h): a linked subagent outside it is listed with unknown calls and cost and a partial total. subagents counts their distinct conversations; task.subagents is that count when positive, else null (a turn without links may lack traces or run a Codex version without collaboration spans). spawn_calls/unlinked_calls count the turn's spawn_agent/followup_task spans and those with no linked turn (null when spans are truncated). total is {llm_calls, llm_calls_partial, cost_usd, cost_partial} over the turn and every child; limited marks a depth, count, trace or row bound. Traces only establish lineage: calls remain model-call span counts and costs completion-log estimates.
  • activity (Codex only): {skills, skills_limited, tools, tools_partial}. skills are the turn's codex.skill_invocation logs (exact conversation.id and turn.id) grouped as {name, invocation: explicit|implicit|null, scope, calls}, at most 50 (skills_limited past that); name/scope are null unless label-shaped, and plugin ids are not returned. tools are the returned spans carrying tool_name, grouped as {name, mcp, calls, duration_ms, duration_partial, aborted} by call count, at most 50; mcp marks mcp__<server><tool> names (Codex joins server and tool, so they are not split) and duration_ms sums known span durations, so concurrent calls add up beyond wall time (null when none is known). Skill events are deduplicated on timestamp and attribute maps first. tools_partial marks a truncated detail or more than 50 tools. Empty lists mean nothing was recorded, not that nothing was used.

Teams

Both lookups are range-only over enabled clients: they reject team and ignore the other filters.

Path Fields and grain
/api/teams Object {teams, sources, labels, historyStart, directory}: sorted distinct resolved teams of sessions with team evidence inside the range. sources maps each team to "directory", "device" or "both" (where its sessions' team came from). labels maps raw team values to display names (filters, grouping and URLs use raw values; display and CSV use labels), {} when no directory is configured. historyStart is the epoch-ms time history began (set once the selected attribute has stored history), or null. directory is true while directory teams are applied (an attribute is selected and its history is within the bound); the UI marks device-provided teams only then.
/api/users/teams Object {users}: per raw user identity (email, else enduser.id), {user, team, teams, sources} with every resolved team of the user's sessions with in-range team evidence, most recent first; team is the first; sources[i] ("directory", "device" or "both") is where teams[i] came from. Users without team evidence are absent. IDs are raw like other user APIs; UI masking is not access control.

Logs are retained 90 days (data retention), so older counter-only sessions have no team.

Team settings

Admin-only (settings:manage); responses are Cache-Control: no-store. In Basic mode the routes are reachable but directory teams are unavailable, so reads report why and PUT never saves.

Path Behavior
GET /api/settings/teams {available, reason, poolId, status, attributes, current, labels, options, directoryError, version}. available:false carries reason basic_auth, not_cognito or ingest_disabled (directory teams need AUTH_MODE=oidc, a Cognito OIDC_ISSUER and INGEST_TOKEN_TABLE). status is the sync state (lastSync, lastError, stale, users, historyStart, reason; reason directory_too_large means more than 4,000 history intervals, so the directory is not applied). attributes is the timeline of selected attributes with their from times; current is the latest. options lists selectable Cognito attributes (string custom:*, name, derived email_domain). During a Cognito outage options is null with directoryError: "directory_unavailable" while stored settings stay readable.
GET /api/settings/teams/preview?attribute= {attribute, values:[{value, users}], missing, excluded}: user counts per value for attributable users. missing has no value; excluded has no usable (ASCII) email or shares an email with another user; such users get no new history, so sessions not covered by history stored earlier use device teams. Errors: invalid_attribute (400), directory_unavailable (503; 409 when unavailable).
GET /api/settings/display settings:manage. {available, reason, teamMask:{deployment, override, effective}, version}. override is true, false or null (follow the deployment). available:false (same reason values as team settings) means no settings store: only the deployment default applies. Reads the store directly; store_unavailable (503) when it fails.
PUT /api/settings/display Body {teamMask, version} with teamMask true, false or null and version the last read value (null before the first save). Returns the GET view. Errors: invalid_team_mask, invalid_version, invalid_json (400), version_conflict or settings_unavailable (409), store_unavailable (503), body_too_large (413, body over 4 KiB).
PUT /api/settings/teams Body {attribute, labels, version} (at most 16 KiB; version is the last read value, null for the first save). Saves the attribute and labels and triggers a sync; returns the same view as GET. Errors: invalid_attribute, invalid_labels, duplicate_label, invalid_version (400), version_conflict (409), directory_unavailable (409 when unavailable, 503 on a Cognito failure), store_unavailable, body_too_large (413), invalid_json (400).

KPI rows also expose commits_observed, prs_observed, and lines_of_code_observed. Active-time rows expose separate user/CLI observation flags. These require in-window samples with matching metric/type identities; lookback-only counters do not establish zero. Above four hours, the first hour uses raw evidence and later hours use rollup presence. Live ends retain the trailing partial hour, whose samples can extend beyond the requested end; historical ends use the usual alignment. These flags inherit that approximation. The flags establish observation, not complete collection. Detail Productivity and Executive withhold aggregate outcome/time values if any returned channel lacks the relevant evidence. See activity availability.

Cost

Primary spend consumers select reported costs through frontend spend.js. Original cost or summary computed_cost remains a token-price diagnostic. The API does not add display-status fields globally. Missing reports and zero reports with positive tokens are unpriced at spend consumers; positive aggregates are not proof of complete capture. See metrics.

Path Fields and grain
/api/cost/summary Per group: computed_cost, strict reported_cost, additive known_reported_cost and reported_partial, token fields, unpriced_tokens, sessions; includes unknown-channel rows by default.
/api/cost/by-model Per group, model: priced row plus tokens, known_reported_cost, reported_partial, cost_counter_rows, and observed_usage_rows; empty model stays model="".
/api/cost/by-user-model Per user, group, model: priced row plus tokens; nonempty email required; empty model stays model="". includeUnknown=1 broadens channel coverage.
/api/cost/by-model-daily B, per day, group, model: priced row plus the report coverage fields below. The field is named day even for sub-day buckets.
/api/cost/by-team Per session team (the team semi-join resolution): team (null = no team evidence, last), known_reported_cost, reported_partial, reported_cost, computed computed_cost, token subtotals and tokens, unpriced_tokens, users, sessions, requests. A total metric: the unknown channel is included, so Σ rows matches the /api/cost/summary known total for the same filters when every amount is known. A non-Anthropic model without a usable factor (ADR-017) makes only the affected team's slice unknown, while the summary loses the whole channel and model amount, so the team known sum can then exceed the summary's. Rows need in-window samples: a team seen only in the counter lookback has no row. sessions counts distinct sessions with in-window usage (not claude_code.session.count); requests counts api_request events in [from, to). A team filter returns that team only.
/api/cost/by-user-daily B, per day, group, user: the same per-(model, session) report scopes, each attributed to one user (max(UserEmail), so a non-empty address wins; "" means no identity), regrouped by user. The top 20 users by known report over the whole requested range are separate, and "" always is; the other users fold into one user: null row per day, group, whose rest_users is the range-wide count of folded users (constant across rows). Report coverage fields and tokens, no computed cost/unpriced (a user row mixes models).
/api/cost/by-team-daily B, per day, group, team: the same per-(model, session) report scopes attributed to each session's team, regrouped by team. The top 8 teams by known report over the whole range are separate; "" (no team evidence) always is and is never folded; the rest fold into one team: null row per day, group with the range-wide rest_teams count. Includes the unknown channel (a total metric), so buckets are not comparable one-to-one with /api/cost/by-model-daily. Report coverage fields and tokens, no computed cost/unpriced.
/api/cost/by-model-compare Per model across selected channels: current priced-row fields, prev_reported_cost, previous token fields prefixed prev_, and computed prev_cost; previous period derived server-side.
/api/cost/tiers Object {bedrock, enterprise}, each with computed uncachedInput, cacheRead, cacheWrite, output; unknown rates are skipped.
/api/cost/effort-mix Per group, effort: computed cost, strict reported_cost, additive known_reported_cost and reported_partial, tokens, unpriced_tokens; empty effort becomes unknown.
/api/cost/by-agent Per group, agent: the same cost/coverage fields as Effort; empty agent becomes main. Returns the top 30 by computed cost after aggregation.

Summary, model, Effort, agent, and efficiency known_reported_cost retain usable model totals inside each channel and endpoint dimension, with reported_partial when other model totals are unavailable. All-unpriced evidence remains null. Summary/model/Effort/agent zero requires an in-window cost counter and no positive tokens in that model scope; session-count-only and lookback-only rows cannot establish that zero. Efficiency retains the existing per-user/model report eligibility, including its zero policy without observation metadata. Effort/agent model scopes without an in-window cost or token sample are omitted; pre-window samples still establish counter baselines. Current token samples with zero increments and no cost sample remain unavailable coverage; they mark a mixed Effort/agent subtotal partial rather than establish zero. The model endpoint retains its legacy rows and strict values. Its observation fields count folded series with current cost samples or any current cost/token samples, respectively, not raw exports. They use the existing raw/rollup window boundaries. Cost omits explicitly historical-only model rows before computing current model shares. The Claude detail Cost page uses these subtotals, discloses partial chart totals, lists unavailable chart scopes, and exports cost status for its tables. Summary-derived averages retain partial labels; the API's efficiency $/LOC and $/commit remain null when any included model lacks a usable report, while Cost divides the known subtotal and labels the result partial (ADR-013). Executive and other strict consumers retain their existing report selection. These fields do not recover an unavailable model's sessions or establish complete collection.

/api/cost/by-model-daily judges report availability per session inside ClickHouse (costByModelDailySql in queries.js) and returns one row per day, group, model (including empty models); modelCostTrend.js maps the counts. A session report is usable when it is positive, or zero with known zero tokens or no token samples. reported_cost is the sum of usable reports and null when no session is usable; reported_partial, reported_unavailable (unusable sessions), reported_reasons and reported_all_unavailable disclose the exclusions. report_missing counts sessions with no report sample or an invalid (negative or non-finite) report; report_zero_with_tokens counts zero reports beside positive or unknown tokens; together they equal reported_unavailable. A session with no in-window cost or token sample, such as a baseline-only first bucket, is omitted as no data rather than reported as a known $0. Minute buckets are clock-aligned and only buckets starting at or after from are returned, so an unaligned from drops the partial first minute and any increment reported in it (a pre-existing detail boundary; the coding-client views split that minute instead). A report-only session adds 0 to the canonical token sums; observed_tokens excludes it and tokens_partial is the coverage signal (all report-only yields null). observed_tokens and tokens_partial follow ADR-014; computed cost prices the summed tokens of each model, which equals the sum of per-session estimates, and is null for unpriced models. More than 50,000 day, group, model rows return 400.

The Cost UI reorders the returned agent subset by reported spend and shows at most 15; this is not a fleet-wide top-15 query by reported spend. Its computed comparison is opt-in. Efficiency ratios are null when reports are unpriced or denominators are zero, independently of server rate coverage. Token-tier dollars are estimates under the configured cache-write TTL, not an exact allocation of reported spend.

Adoption

Path Fields and grain
/api/adoption/levels Object total_members, mau, wau, dau; nonempty emails on session-counter rows before to. Members covers retained history; active windows are 30, 7 and 1 days.
/api/adoption/timeseries Daily t, dau, wau, mau, stickiness; UTC day unions over trailing 1/7/30 days. stickiness is a percentage (0-100), not a fraction.

Chat

POST /api/chat takes JSON history and returns SSE, not a JSON row array:

{"messages":[{"role":"user","content":"Summarize reported spend for the last day."}]}

It returns 503 unless auth is configured (or CHAT_ALLOW_INSECURE=1) and assertReadonlySession() has confirmed a readonly ClickHouse session. The probe runs at startup and every ten minutes; unknown status is fail-closed. Per-process IP limits return 429 before streaming. SSE events are status (message, optional sql), thinking (text), text (text), chart, done ({}), or error (message). Once streaming starts, failures are SSE events on HTTP 200. Successful streams use Cache-Control: no-cache. Concurrent chat overload returns 503 before SSE without consuming an IP rate slot. chart carries a fixed bar|stacked_bar|line template, title, category/time column x, x_type, one to three series (key, label), unit, and at most 50 data rows selected from a stored query result. source, coverage, cost_bases, partial, truncated and optional effective_range describe its evidence. Null values remain unavailable. The dashboard web validates each event again, renders the template without executing model-generated code and excludes chart arrays from subsequent model history. See chat limits and runtime admission.

Insights

GET /api/insights/daily?period=YYYY-MM-DD reads stored daily AI insights (Claude Code only; 404 when Claude is disabled). It returns object {enabled:false} when the feature is disabled or misconfigured. period must be one of the last seven UTC days, otherwise 400; the default is the latest stored period, else the previous day. A stored period returns enabled, status: "ready", period, generatedAt, model, empty (no sessions, no model call), summary, findings[{user, pseudonym, title, detail, metric}], caveats, org (period totals and the seven-day daily-average baseline), users[{pseudonym, user, ...aggregates}], usage{inputTokens, outputTokens, estimatedCostUsd} and available (stored periods). Otherwise it returns {enabled:true, period, status, available} with status pending (the previous day before success or exhausted attempts) or failed. user is the raw email mapped from the stored pseudonym; the UI masks it. Model text is untrusted plain text and can be wrong; the card's numbers come from the stored aggregates. Operations: daily insights.

Health and config

Path Response
/healthz HTTP 200 {ok:boolean} from ClickHouse ping, even when false.
/readyz {ready:boolean}, HTTP 200 when ready, 503 when draining or ping fails.
/api/health/data {status, latest, ageMinutes, staleAfterMinutes}; status is ok, stale or unknown. HTTP 200 only for ok, 503 otherwise.
/api/config {piiMask, teamMask, pricing, schema, groupMode, defaultRangeDays, rangeCapDays}; reads in-memory configuration/probe snapshots and the in-memory price catalog status, with no ClickHouse query. teamMask comes from a per-replica cache; when it is older than 30 seconds the handler starts one DynamoDB read, waits at most 500 ms for it, then answers from the cache while a slower read finishes in the background.

Freshness uses the maximum enabled-source timestamp (Claude raw metrics, Codex logs) over seven days, memoized for 30 seconds; one fresh source can mask another's outage. latest is ISO 8601 or null; ageMinutes is an integer or null. DATA_STALE_MINUTES defaults to 360 and must be positive and below 10,080. Missing or failed measurement is unknown, not healthy. Process shutdown and manifest probes are described in runtime.

Config fields:

  • piiMask: true for case-insensitive PII_MASK_ENABLED=1 or true; otherwise false. Terraform defaults masking on. The frontend masks unless this response explicitly says false. Ordinary API payloads still contain raw identities.
  • teamMask: effective team-name display masking: an administrator override saved through PUT /api/settings/display, else the deployment default TEAM_MASK_ENABLED (1/true, 0/false; unset follows piiMask). Each replica caches the override for 30 seconds, then rereads it (waiting at most 500 ms); a store outage keeps the last known value, and before a replica's first successful read the answer is true (masked). The frontend masks team names unless this says false and falls back to piiMask when the field is absent. Team values in API payloads stay raw.
  • pricing: {cacheWriteTtl, overriddenModels, priceList}. TTL is 1h by default or 5m when configured; invalid values fail startup. Override names come from PRICING_JSON. Rates are not exposed. Cost displays this TTL as an assumption in computed diagnostics.
  • pricing.priceList: {source, fetchedAt, models} for the active AWS Price List layer (ADR-017 amendment). source is runtime (latest successful in-memory refresh), snapshot (the image's awsPriceSnapshot.json) or code (neither; code tables only). fetchedAt is the layer's ISO 8601 fetch time or null; models lists the model keys the layer prices, sorted. Operator overrides (PRICING_JSON, CODEX_PRICING_JSON) still win per model, and code tables price models the layer lacks. Metadata only: no rates. Each replica refreshes independently, so two replicas can briefly report different layers. The Cost page names the source in its computed-cost help.
  • schema.segmentAwareSeriesKey: true, false or null from up to 2,000 newest cost datapoints within 24 hours. Mixed, absent or failed evidence is null; this does not certify old data or rollup rebuild completion.
  • schema.migrations: sorted distinct ledger versions; null means undetermined/missing ledger, while [] means a readable empty ledger.
  • schema.projectColumns: true after SELECT ProjectName, Entrypoint FROM claude_code.otel_logs LIMIT 0 succeeds; numeric server errors yield false, transport or other undetermined failures yield null. This checks logs only, not metric/trace columns or all migration-005 steps. All schema probes refresh every ten minutes.
  • groupMode: ab or single; defaultRangeDays defaults 2 and rangeCapDays defaults 90. Invalid startup settings are rejected, including a cap below the default range.

Errors and operational limits

Status Contract
400 Wrapped route validation: {error, detail} for invalid range/interval or excessive span. The detail does not echo submitted values.
401 Missing/invalid Basic credentials or an OIDC session that cannot be renewed.
403 OIDC permission/group or unsafe-request Origin denied.
429 Chat's per-process IP limit.
500 Wrapped query failure: {error:"internal error", id:"<uuid>"}. The full exception is logged under the ID, not returned.
503 Not-ready/freshness states, unmet chat gates, or exhausted runtime admission. Overload JSON has code:"capacity_exhausted" and Retry-After: 1; other wrapped query failures remain 500.
504 The shared data-query load exceeded its cooperative 25-second budget: {error, code:"query_timeout", id:"<uuid>"} and Retry-After: 5. A warning logs the ID, registered route and code without filters or SQL. The localized message suggests a smaller range or retry. Timeout results are not cached as empty or zero data.

After authentication, JSON API middleware sets no-store; the server's bounded promise cache is separate. CloudFront configuration declares zero error-cache TTL for dashboard 403/500/502/503/504 without rewriting status or body. CloudFront can cache these errors under other cache policies or positive origin cache directives; verify the actual policy, error TTL and response headers. AWS error processing documents that managed CachingDisabled disables cached 5xx errors; 400/403 caching requires positive max-age or s-maxage. The explicit zero-TTL entries protect this deployment contract if its policy changes. There is no general data-route rate limit or pagination contract. Named row caps above are part of the returned subset. See API implementation for cache and warmer behavior, and security for SQL and identity boundaries.

For an authenticated local port-forward, set BASIC_AUTH_USER to your authorized username. curl prompts for the password instead of placing it in the command arguments:

curl --fail-with-body --get http://localhost:8080/api/cost/by-model \
  --user "$BASIC_AUTH_USER" \
  --data-urlencode 'from=2026-09-01T00:00:00Z' \
  --data-urlencode 'to=2026-09-03T00:00:00Z' \
  --data-urlencode 'group=bedrock'

Outside a local port-forward, use the deployment's authorized HTTPS origin.