> ## Documentation Index
> Fetch the complete documentation index at: https://docs.veristar.app/llms.txt
> Use this file to discover all available pages before exploring further.

# Web totals per UTC hour, read live from Umami

> Reads Umami's own `/stats` for every active site of one tenant, both
bands and every hour of the range, through the control plane's
Umami service account, and returns the sums per hour and band. Only
aggregates leave the control plane. The current hour is read up to
the moment of the request and has `complete: false`. Every hour of
the range is present for both bands, zeros included. A key for all
tenants must name `tenant`; a scoped key reads its own tenant.




## OpenAPI

````yaml /reference/openapi/metrics-v1.yaml get /web/hourly
openapi: 3.1.0
info:
  title: Veristar machine metrics API
  version: 1.0.0
  summary: Read-only aggregate web and API metrics for downstream machine consumers.
  description: |
    The contract of `/api/metrics/v1/` (plan phase 12,
    docs/reference/control-plane-api.md section 9). Every request carries an
    operator API key as `Authorization: Bearer sa_op_<40 base62 characters>`.
    Console session cookies are never accepted here, and keys are accepted
    nowhere else.

    Conventions shared by every route:

    * Only `GET` is defined. Every answer carries `Cache-Control: no-store`
      and `X-Request-Id`.
    * A query parameter that the route does not list, or one given twice, is
      400 `invalid_query`.
    * Days are UTC days `yyyy-mm-dd`; hours are RFC 3339 UTC hour starts such
      as `2026-11-12T09:00:00Z`. Ranges include both ends.
    * A key scoped to one tenant reads only that tenant. Naming another
      tenant is 403 `scope_forbidden`. A key for all tenants never sees
      internal tenants (the monitoring probe); naming one is 404.
    * List routes return at most `limit` items (1 to 500, default 500) and an
      opaque `next_cursor`, `null` on the last page. A cursor is bound to the
      route and every other query parameter; with any other combination it
      is 400 `invalid_cursor`. The range a cursor carries is checked again on
      every page with the limits of a first page; a cursor that breaks them
      is 400 `invalid_cursor`.
    * Each key may make 10 requests per second per control plane instance
      (token bucket, burst 20). Above that the answer is 429 `rate_limited`
      with `Retry-After` in seconds.
    * Percentiles come from summed histogram buckets and are never averaged.
      A percentile in the open bucket above 10 s has `*_seconds: null` and
      `*_over_10s: true`.
  license:
    name: Apache-2.0
    identifier: Apache-2.0
servers:
  - url: /api/metrics/v1
security:
  - operatorKey: []
paths:
  /web/hourly:
    get:
      summary: Web totals per UTC hour, read live from Umami
      description: |
        Reads Umami's own `/stats` for every active site of one tenant, both
        bands and every hour of the range, through the control plane's
        Umami service account, and returns the sums per hour and band. Only
        aggregates leave the control plane. The current hour is read up to
        the moment of the request and has `complete: false`. Every hour of
        the range is present for both bands, zeros included. A key for all
        tenants must name `tenant`; a scoped key reads its own tenant.
      operationId: webHourly
      parameters:
        - $ref: '#/components/parameters/tenant'
        - name: from
          in: query
          required: false
          description: First hour (included). Default `to` minus 23 hours.
          schema:
            $ref: '#/components/schemas/Hour'
        - name: to
          in: query
          required: false
          description: >-
            Last hour (included), not after the current hour. Default the
            current hour. At most 24 hours in one request.
          schema:
            $ref: '#/components/schemas/Hour'
      responses:
        '200':
          description: Every hour of the range for both bands.
          headers:
            X-Request-Id:
              $ref: '#/components/headers/X-Request-Id'
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/WebHourly'
        '400':
          $ref: '#/components/responses/BadRequest'
        '401':
          $ref: '#/components/responses/Unauthenticated'
        '403':
          $ref: '#/components/responses/Forbidden'
        '404':
          $ref: '#/components/responses/NotFound'
        '429':
          $ref: '#/components/responses/RateLimited'
        '500':
          $ref: '#/components/responses/Internal'
        '503':
          $ref: '#/components/responses/Unavailable'
components:
  parameters:
    tenant:
      name: tenant
      in: query
      required: false
      description: >-
        One tenant id. A scoped key defaults to its own tenant; a key for all
        tenants reads every non-internal tenant when absent.
      schema:
        $ref: '#/components/schemas/ID'
  schemas:
    Hour:
      type: string
      description: An RFC 3339 UTC hour start.
      pattern: ^[0-9]{4}-[0-9]{2}-[0-9]{2}T[0-9]{2}:00:00Z$
    WebHourly:
      type: object
      additionalProperties: false
      required:
        - tenant_id
        - from
        - to
        - read_at
        - items
      properties:
        tenant_id:
          $ref: '#/components/schemas/ID'
        from:
          $ref: '#/components/schemas/Hour'
        to:
          $ref: '#/components/schemas/Hour'
        read_at:
          $ref: '#/components/schemas/Time'
        items:
          type: array
          items:
            $ref: '#/components/schemas/WebHour'
    ID:
      type: string
      pattern: ^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$
    Time:
      type: string
      description: RFC 3339 in UTC with at most millisecond digits.
      pattern: ^[0-9]{4}-[0-9]{2}-[0-9]{2}T[0-9]{2}:[0-9]{2}:[0-9]{2}(\.[0-9]{1,3})?Z$
    WebHour:
      type: object
      additionalProperties: false
      required:
        - tenant_id
        - hour
        - band
        - complete
        - sites
        - pageviews
        - visitors
        - visits
        - bounces
        - totaltime
      allOf:
        - $ref: '#/components/schemas/WebTotals'
      properties:
        tenant_id:
          $ref: '#/components/schemas/ID'
        hour:
          $ref: '#/components/schemas/Hour'
        band:
          $ref: '#/components/schemas/Band'
        complete:
          type: boolean
          description: False for the current hour, which was read before it ended.
        sites: true
        pageviews: true
        visitors: true
        visits: true
        bounces: true
        totaltime: true
    Error:
      type: object
      additionalProperties: false
      required:
        - error
      properties:
        error:
          type: object
          additionalProperties: false
          required:
            - code
            - message
            - request_id
          properties:
            code:
              type: string
              enum:
                - invalid_query
                - invalid_cursor
                - unauthenticated
                - scope_forbidden
                - not_found
                - method_not_allowed
                - rate_limited
                - internal
                - unavailable
            message:
              type: string
            request_id:
              type: string
    WebTotals:
      type: object
      required:
        - sites
        - pageviews
        - visitors
        - visits
        - bounces
        - totaltime
      properties:
        sites:
          description: Sites the numbers are summed over.
          allOf:
            - $ref: '#/components/schemas/Count'
        pageviews:
          $ref: '#/components/schemas/Count'
        visitors:
          $ref: '#/components/schemas/Count'
        visits:
          $ref: '#/components/schemas/Count'
        bounces:
          $ref: '#/components/schemas/Count'
        totaltime:
          description: Seconds summed over visits.
          allOf:
            - $ref: '#/components/schemas/Count'
    Band:
      type: string
      enum:
        - primary
        - quarantine
    Count:
      type: integer
      minimum: 0
  headers:
    X-Request-Id:
      description: The id of the request, also in the error envelope and the server log.
      schema:
        type: string
    Retry-After:
      description: Seconds to wait before the next request.
      required: true
      schema:
        type: string
        pattern: ^[1-9][0-9]*$
  responses:
    BadRequest:
      description: >-
        A malformed or unknown query parameter (`invalid_query`) or cursor
        (`invalid_cursor`).
      headers:
        X-Request-Id:
          $ref: '#/components/headers/X-Request-Id'
      content:
        application/json:
          schema:
            $ref: '#/components/schemas/Error'
    Unauthenticated:
      description: >-
        No key, a malformed or unknown key, or an expired or revoked key
        (`unauthenticated`).
      headers:
        X-Request-Id:
          $ref: '#/components/headers/X-Request-Id'
        WWW-Authenticate:
          required: true
          schema:
            type: string
      content:
        application/json:
          schema:
            $ref: '#/components/schemas/Error'
    Forbidden:
      description: The key is scoped to another tenant (`scope_forbidden`).
      headers:
        X-Request-Id:
          $ref: '#/components/headers/X-Request-Id'
      content:
        application/json:
          schema:
            $ref: '#/components/schemas/Error'
    NotFound:
      description: >-
        Unknown path, or a tenant that does not exist or is internal
        (`not_found`).
      headers:
        X-Request-Id:
          $ref: '#/components/headers/X-Request-Id'
      content:
        application/json:
          schema:
            $ref: '#/components/schemas/Error'
    RateLimited:
      description: The key's token bucket is empty (`rate_limited`).
      headers:
        X-Request-Id:
          $ref: '#/components/headers/X-Request-Id'
        Retry-After:
          $ref: '#/components/headers/Retry-After'
      content:
        application/json:
          schema:
            $ref: '#/components/schemas/Error'
    Internal:
      description: Anything unexpected (`internal`); details only in the server log.
      headers:
        X-Request-Id:
          $ref: '#/components/headers/X-Request-Id'
      content:
        application/json:
          schema:
            $ref: '#/components/schemas/Error'
    Unavailable:
      description: Umami could not be read (`unavailable`).
      headers:
        X-Request-Id:
          $ref: '#/components/headers/X-Request-Id'
        Retry-After:
          $ref: '#/components/headers/Retry-After'
      content:
        application/json:
          schema:
            $ref: '#/components/schemas/Error'
  securitySchemes:
    operatorKey:
      type: http
      scheme: bearer
      description: An operator API key, `sa_op_` followed by 40 base62 characters.

````

This documentation is built and hosted on [Mintlify](https://mintlify.com), a developer documentation platform.