openapi: 3.1.0
info:
  title: SoundBase Live Report API
  version: 1.2.0
  summary: Read-only JSON access to published live frequency coordination reports.
  description: |
    ## Overview

    This API returns the same published coordination report data shown in the
    SoundBase **Published Coord Data** web view. Use it to integrate live
    frequency lists and zone/group structure into external tools, show
    automation, or custom dashboards.

    **Spec file:** `liveReportApi_1.2.0.yaml`

    ## Requirements

    - **SoundBase Pro** subscription
    - An **API token** created in SoundBase: *My Settings → Security → API Tokens*
    - Header **`X-Live-Report-Api-Version`** on every request (`1.2.0`, `1.1.0`, or `1.0.1`)
    - The report's **publication URL** components (`projectId` and `publication` keys)
    - The **publication password**, when the report owner has password protection enabled

    ## API versioning

    Every request must include the API version header:

    - `X-Live-Report-Api-Version: 1.2.0` (current)
    - `X-Live-Report-Api-Version: 1.1.0` or `1.0.1` (also accepted; `1.2.0`
      additionally guarantees the `sites` array in the response)

    The current value matches the version in this spec filename
    (`liveReportApi_{version}`). Unsupported versions return `400` with
    `API_VERSION_MISMATCH`.

    ## Authentication

    Every request must include a Pro API token using either:

    - `Authorization: Bearer sb_live_<prefix>_<secret>`
    - `X-API-Key: sb_live_<prefix>_<secret>`

    Tokens are shown **once** at creation. Store them securely. Revoked tokens
    and tokens belonging to users without an active Pro entitlement are rejected
    with `401`.

    ## Identifying a report

    Published reports use the same `publication` query parameter as the web UI:

    | Scope | `publication` value | Example |
    |-------|---------------------|---------|
    | Site | `{sitePublishKey}` | `a1b2c3d4` |
    | Zone | `{sitePublishKey}-{zonePublishKey}` | `a1b2c3d4-e5f6g7h8` |
    | Group | `{sitePublishKey}-{zonePublishKey}-{groupPublishKey}` | `a1b2c3d4-e5f6g7h8-i9j0k1l2` |
    | Custom report | `{customReportPublishKey}` | `x9y8z7w6` |

    When `publication` matches a custom report publish key for the project, the
    API returns the selection-scoped sites, zones, groups, and frequencies for
    that custom report. Otherwise it resolves as a site/zone/group publication.

    Copy these keys from the published report URL:
    `https://my.soundbase.app/publisher/coord/{projectId}?publication=...`
    or, for custom reports in the web UI,
    `https://my.soundbase.app/publisher/coord/{projectId}?customReport=...`
    (use the `customReport` value as the API `publication` parameter).

    ## Publication password

    When a report is password-protected, pass the password using either:

    - Header: `X-Publication-Password: your-password` *(recommended)*
    - Query: `?password=your-password`

    ## Rate limiting

    Requests are rate-limited per client IP address. The default limit is
    **1000 requests per minute** (configurable on the server via `RATE_LIMIT_MAX`
    and `RATE_LIMIT_TIME_WINDOW`).

    When the limit is exceeded, the API returns **`429 Too Many Requests`**
    with a JSON body (`statusCode`, `error`, `message`) — not the `ApiError`
    shape used for application errors. Response headers include:

    - `x-ratelimit-limit` — maximum requests allowed in the window
    - `x-ratelimit-remaining` — requests remaining in the current window
    - `x-ratelimit-reset` — seconds until the window resets
    - `retry-after` — seconds to wait before retrying (present when limited)

    ## Date serialization

    MongoDB `Date` values in the response are serialized as strings:

    `{{timestamp}}<milliseconds-since-epoch>`

    Strip the `{{timestamp}}` prefix and pass the remainder to
    `new Date(Number(...))`. This applies to group time-of-use fields
    (`startDateTime`, `endDateTime`, and `timeRanges[].start` / `.end`).

    ## Payload size

    The hosting sync service enforces a maximum HTTP payload size of **50 MB**
    per request or response. This endpoint is `GET`-only and does not accept a
    request body; large coordination reports may produce sizable JSON responses.
    Responses support `gzip`, `deflate`, and `br` compression when requested via
    `Accept-Encoding`.

    ## Interactive documentation

    Browse this spec with examples at
    [/live-report/docs/](/live-report/docs/).
  contact:
    name: SoundBase Support
    url: https://soundbase.app

servers:
  - url: https://prod-rest-sync.soundbase.services
    description: SoundBase production
  - url: http://localhost:5454
    description: Local development

tags:
  - name: Live Reports
    description: Fetch published live coordination report data.

paths:
  /live-report/{projectId}:
    get:
      operationId: getLiveReport
      tags:
        - Live Reports
      summary: Get published live coordination report data
      description: |
        Returns project, site, zone, group, and frequency data for a published
        coordination report (including custom reports when `publication` is a
        custom report publish key).

        The response mirrors the dataset assembled by the Published Coord Data
        web UI, including frequency hopping channels and child-frequency expansion.

        Requires `X-Live-Report-Api-Version: 1.2.0`.
      security:
        - bearerAuth: []
        - apiKeyAuth: []
      parameters:
        - $ref: '#/components/parameters/LiveReportApiVersion'
        - $ref: '#/components/parameters/ProjectId'
        - $ref: '#/components/parameters/Publication'
        - $ref: '#/components/parameters/PublicationPasswordQuery'
        - $ref: '#/components/parameters/PublicationPasswordHeader'
      responses:
        '200':
          description: Published report data
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/LiveReportPayload'
              examples:
                siteReport:
                  summary: Site-level report
                  value:
                    project:
                      _id: abc123project
                      name: Summer Tour 2026
                    site:
                      _id: site001
                      name: Main Stage
                      location:
                        name: Convention Center
                        lat: 40.7128
                        lon: -74.006
                    sites:
                      - _id: site001
                        name: Main Stage
                    zones:
                      - _id: zone001
                        name: Zone A
                        publishKey: e5f6g7h8
                        siteId: site001
                        projectId: abc123project
                        publicationEnabled: true
                    groups:
                      - _id: group001
                        name: Handhelds
                        publishKey: i9j0k1l2
                        publicationEnabled: true
                        zoneId: zone001
                        siteId: site001
                        projectId: abc123project
                        ordinal: 1
                        isFrequencyHopping: false
                        timeRanges:
                          - start: '{{timestamp}}1710000000000'
                            end: '{{timestamp}}1710086400000'
                    frequenciesByGroup:
                      group001:
                        - _id: freq001
                          name: HH 01
                          identifier: hh-01
                          freq: 470100000
                          groupId: group001
                          zoneId: zone001
                          siteId: site001
                          projectId: abc123project
                          ordinal: 1
                          isCoordinated: true
                          isDeployed: false
                          model:
                            manufacturer: Shure
                            model: ULXD
                            modelRangeName: G50
                            bandwidth: 200000
                          wasRecalced: true
                          recalcs:
                            - _id: K83eN27kZZFgE9RHY
                              instanceId: 8yq4SRSK6yZx6SyP9
                              freqId: freq001
                              previousValue: 471400000
                              newValue: '470500000'
                              groupId: group001
                              zoneId: zone001
                              siteId: site001
                              projectId: abc123project
                customReport:
                  summary: Custom report spanning multiple sites
                  description: |
                    For custom report publications, `site` carries the custom
                    report's id/name when selections span multiple sites, and
                    the `sites` array lists every site the report covers.
                  value:
                    project:
                      _id: abc123project
                      name: Summer Tour 2026
                    site:
                      _id: customReport001
                      name: FOH + Monitors
                    sites:
                      - _id: site001
                        name: Main Stage
                      - _id: site002
                        name: Second Stage
                    zones:
                      - _id: zone001
                        name: Zone A
                        publishKey: e5f6g7h8
                        siteId: site001
                        projectId: abc123project
                        publicationEnabled: true
                    groups:
                      - _id: group001
                        name: Handhelds
                        publishKey: i9j0k1l2
                        publicationEnabled: true
                        zoneId: zone001
                        siteId: site001
                        projectId: abc123project
                        ordinal: 1
                        isFrequencyHopping: false
                    frequenciesByGroup:
                      group001:
                        - _id: freq001
                          name: HH 01
                          identifier: hh-01
                          freq: 470100000
                          groupId: group001
                          zoneId: zone001
                          siteId: site001
                          projectId: abc123project
                          ordinal: 1
                          isCoordinated: true
                          isDeployed: false
        '400':
          description: Missing or invalid request parameters, or unsupported API version
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ApiError'
              examples:
                missingPublication:
                  value:
                    errorCode: INVALID_PARAMS
                    message: publication query param is required
                missingVersion:
                  value:
                    errorCode: INVALID_PARAMS
                    message: x-live-report-api-version header is required
                versionMismatch:
                  value:
                    errorCode: API_VERSION_MISMATCH
                    message: 'Unsupported API version. Expected one of: 1.0.1, 1.1.0, 1.2.0'
        '401':
          description: Missing, invalid, or revoked API token; or Pro subscription required
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ApiError'
              examples:
                unauthorized:
                  value:
                    errorCode: UNAUTHORIZED
                    message: API token is required
                proRequired:
                  value:
                    errorCode: pro-required
                    message: This feature requires a SoundBase Pro subscription
        '403':
          description: Publication disabled or incorrect publication password
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ApiError'
              examples:
                disabled:
                  value:
                    errorCode: PUBLICATION_DISABLED
                    message: This report has not been enabled for sharing
                wrongPassword:
                  value:
                    errorCode: INVALID_CREDENTIALS
                    message: Invalid or missing publication password
        '404':
          description: Project, site, zone, or group not found for the given keys
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ApiError'
              example:
                errorCode: NOT_FOUND
                message: Published project or site not found
        '429':
          description: Rate limit exceeded (per client IP)
          headers:
            x-ratelimit-limit:
              description: Maximum requests allowed in the current time window
              schema:
                type: integer
            x-ratelimit-remaining:
              description: Requests remaining in the current time window
              schema:
                type: integer
            x-ratelimit-reset:
              description: Seconds until the rate-limit window resets
              schema:
                type: integer
            retry-after:
              description: Seconds to wait before sending another request
              schema:
                type: integer
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/RateLimitError'
              example:
                statusCode: 429
                error: Too Many Requests
                message: Rate limit exceeded, retry in 1 minute

components:
  securitySchemes:
    bearerAuth:
      type: http
      scheme: bearer
      description: |
        Pro API token in the form `sb_live_<prefix>_<secret>`.
        Create tokens in SoundBase under My Settings → Security → API Tokens.
    apiKeyAuth:
      type: apiKey
      in: header
      name: X-API-Key
      description: Alternative to Bearer auth; same `sb_live_<prefix>_<secret>` token value.

  parameters:
    LiveReportApiVersion:
      name: X-Live-Report-Api-Version
      in: header
      required: true
      description: |
        Live Report API contract version. Current spec is `1.2.0`
        (`liveReportApi_1.2.0.yaml`). `1.1.0` and `1.0.1` are also accepted;
        `1.2.0` additionally guarantees the `sites` array in the response.
      schema:
        type: string
        enum:
          - '1.0.1'
          - '1.1.0'
          - '1.2.0'
      example: '1.2.0'
    ProjectId:
      name: projectId
      in: path
      required: true
      description: SoundBase project ID from the published report URL.
      schema:
        type: string
      example: abc123project
    Publication:
      name: publication
      in: query
      required: true
      description: |
        Publication key from the report URL. Accepts site/zone/group keys
        (`siteKey`, `siteKey-zoneKey`, or `siteKey-zoneKey-groupKey`) or a
        custom report publish key. Custom report keys are looked up first.
      schema:
        type: string
      examples:
        site:
          value: a1b2c3d4
          summary: Site report
        zone:
          value: a1b2c3d4-e5f6g7h8
          summary: Zone report
        group:
          value: a1b2c3d4-e5f6g7h8-i9j0k1l2
          summary: Group report
        customReport:
          value: x9y8z7w6
          summary: Custom report
    PublicationPasswordQuery:
      name: password
      in: query
      required: false
      description: Publication password (alternative to `X-Publication-Password` header).
      schema:
        type: string
    PublicationPasswordHeader:
      name: X-Publication-Password
      in: header
      required: false
      description: Publication password when the report is password-protected.
      schema:
        type: string

  schemas:
    ApiError:
      type: object
      required:
        - errorCode
        - message
      properties:
        errorCode:
          type: string
          enum:
            - INVALID_PARAMS
            - API_VERSION_MISMATCH
            - UNAUTHORIZED
            - pro-required
            - INVALID_CREDENTIALS
            - PUBLICATION_DISABLED
            - NOT_FOUND
        message:
          type: string

    RateLimitError:
      type: object
      description: |
        Returned by the global rate limiter when too many requests are sent from
        the same client IP. Unlike application errors, this shape uses
        `statusCode` and `error` instead of `errorCode`.
      required:
        - statusCode
        - error
        - message
      properties:
        statusCode:
          type: integer
          const: 429
        error:
          type: string
          example: Too Many Requests
        message:
          type: string
          description: Human-readable message including retry guidance
          example: Rate limit exceeded, retry in 1 minute

    LiveReportPayload:
      type: object
      required:
        - project
        - site
        - sites
        - zones
        - groups
        - frequenciesByGroup
      properties:
        project:
          $ref: '#/components/schemas/ProjectSummary'
        site:
          $ref: '#/components/schemas/PublishedSite'
        sites:
          type: array
          items:
            $ref: '#/components/schemas/SiteSummary'
          description: |
            The sites this publication spans. A single entry for site/zone/
            group publications; for custom reports, every site the report's
            selections cover, sorted by name.
        zones:
          type: array
          items:
            $ref: '#/components/schemas/PublishedZone'
        groups:
          type: array
          items:
            $ref: '#/components/schemas/PublishedGroup'
        frequenciesByGroup:
          type: object
          additionalProperties:
            type: array
            items:
              $ref: '#/components/schemas/PublishedFrequency'
          description: Map of group ID to ordered frequency/channel rows for that group.

    ProjectSummary:
      type: object
      properties:
        _id:
          type: string
        name:
          type: string

    SiteSummary:
      type: object
      properties:
        _id:
          type: string
        name:
          type: string

    PublishedSite:
      type: object
      properties:
        _id:
          type: string
        name:
          type: string
        location:
          type: object
          nullable: true
          properties:
            name:
              type: string
            address:
              type: string
            lat:
              type: number
            lon:
              type: number
            country:
              type: string

    PublishedZone:
      type: object
      properties:
        _id:
          type: string
        name:
          type: string
        publishKey:
          type: string
        siteId:
          type: string
        projectId:
          type: string
        publicationEnabled:
          type: boolean

    SoundBaseTimestamp:
      type: string
      pattern: '^\{\{timestamp\}\}\d+$'
      description: |
        Date serialized as `{{timestamp}}<milliseconds-since-epoch>`.
        Parse by stripping the prefix and passing the remainder to
        `new Date(Number(...))`.
      examples:
        - '{{timestamp}}1710000000000'

    TimeOfUseRange:
      type: object
      properties:
        start:
          $ref: '#/components/schemas/SoundBaseTimestamp'
        end:
          $ref: '#/components/schemas/SoundBaseTimestamp'

    SpecteraChannel:
      type: object
      description: TV channel slot selected for a Spectera frequency-hopping group.
      properties:
        start:
          type: number
          description: Channel start frequency in MHz.
        stop:
          type: number
          description: Channel stop frequency in MHz.
        channel:
          type: string
          description: TV channel identifier.
        rfChannelId:
          type: number
          nullable: true
          description: RF block index (0-based) when assigned.

    PublishedGroup:
      type: object
      properties:
        _id:
          type: string
        name:
          type: string
        publishKey:
          type: string
        publicationEnabled:
          type: boolean
        zoneId:
          type: string
        siteId:
          type: string
        projectId:
          type: string
        ordinal:
          type: number
        color:
          type: string
          nullable: true
        startDateTime:
          allOf:
            - $ref: '#/components/schemas/SoundBaseTimestamp'
          nullable: true
          deprecated: true
          description: |
            Legacy single time-of-use window start. Prefer `timeRanges`.
            Omitted when the group uses `timeRanges` only.
        endDateTime:
          allOf:
            - $ref: '#/components/schemas/SoundBaseTimestamp'
          nullable: true
          deprecated: true
          description: |
            Legacy single time-of-use window end. Prefer `timeRanges`.
            Omitted when the group uses `timeRanges` only.
        timeRanges:
          type: array
          nullable: true
          items:
            $ref: '#/components/schemas/TimeOfUseRange'
          description: Scheduled time-of-use windows for this group.
        isFrequencyHopping:
          type: boolean
        startFreq:
          type: number
          nullable: true
          description: Start frequency in Hz (frequency hopping groups).
        stopFreq:
          type: number
          nullable: true
          description: Stop frequency in Hz (frequency hopping groups).
        channelCount:
          type: number
          nullable: true
          description: Configured channel count for frequency-hopping groups.
        deviceBrand:
          type: string
          nullable: true
        deviceModel:
          type: string
          nullable: true
        specteraChannels:
          type: array
          items:
            $ref: '#/components/schemas/SpecteraChannel'
          nullable: true

    PublishedFrequency:
      type: object
      description: Regular frequency, child frequency, or frequency-hopping channel row.
      properties:
        _id:
          type: string
        name:
          type: string
        identifier:
          type: string
          description: |
            Stable channel identifier assigned in SoundBase (for example
            `spectera-rf-01` for Spectera RF blocks). Omitted when not set on
            the source frequency.
        freq:
          type: number
          description: Center frequency in Hz (regular frequencies).
        groupId:
          type: string
        zoneId:
          type: string
        siteId:
          type: string
        projectId:
          type: string
        ordinal:
          type: number
        isCoordinated:
          type: boolean
        isDeployed:
          type: boolean
        isChildFreq:
          type: boolean
        isFrequencyHopping:
          type: boolean
        parentFreqId:
          type: string
        childOrdinal:
          type: number
        channelNumber:
          type: string
        frequencyRange:
          type: object
          properties:
            start:
              type: number
            stop:
              type: number
        model:
          type: object
          properties:
            _id:
              type: string
            manufacturer:
              type: string
            model:
              type: string
            modelRangeName:
              type: string
            mode:
              type: string
            bandwidth:
              type: number
              description: Channel bandwidth in Hz from the equipment model.
        recalcs:
          type: array
          description: Recalculation history for this frequency, oldest to newest.
          items:
            $ref: '#/components/schemas/RecalcRecord'
        wasRecalced:
          type: boolean
          description: True when this frequency has one or more recalc history entries.

    RecalcRecord:
      type: object
      description: |
        A single recalculation history entry for a frequency. Entries are sorted
        oldest to newest within the parent frequency's `recalcs` array.
      properties:
        _id:
          type: string
          description: Recalc record document ID.
        instanceId:
          type: string
          description: ID of the recalculation run this entry belongs to.
        freqId:
          type: string
          description: ID of the frequency that was recalculated.
        previousValue:
          $ref: '#/components/schemas/RecalcFrequencyValue'
        newValue:
          $ref: '#/components/schemas/RecalcFrequencyValue'
        groupId:
          type: string
        zoneId:
          type: string
        siteId:
          type: string
        projectId:
          type: string
        previousPreset:
          type: string
          nullable: true
          description: Device preset name before recalculation, when applicable.
        newPreset:
          type: string
          nullable: true
          description: Device preset name after recalculation, when applicable.

    RecalcFrequencyValue:
      description: |
        Frequency in Hz. MongoDB may return this field as a number or a
        decimal-string; clients should accept either form.
      oneOf:
        - type: number
        - type: string
      examples:
        - 471400000
        - '470500000'
