# OpenAPI description of the Capture Public API (the `public-api` edge function).
#
# This file is the machine-readable counterpart to api-reference.html and is
# deployed alongside it, so customers can import it straight into Postman, Bruno,
# Insomnia, or generate a client from it.
#
# Kept at OpenAPI 3.0.3 rather than 3.1 on purpose: Postman handles both, but
# Bruno's importer only reliably supports 3.0.x, and nothing here needs 3.1.
#
# tests/openapi_spec_test.ts asserts this file stays in sync with the routes
# registered in index.ts and the rate-limit defaults in rateLimit.ts. If you add,
# remove, or rename a route, update this file in the same change.

openapi: 3.0.3

info:
  title: Capture Public API
  version: '1.0.0'
  description: |
    A customer-facing, read-only REST API for the Matter **Capture** platform.

    Each customer is issued an **API key** that grants read access to the data of a
    **single organisation**. Every request is automatically scoped to that
    organisation, so a key can never read another organisation's data.

    All endpoints are `GET`, return JSON, and live under the `/v1` path prefix.

    ## Response envelope

    Every response uses a single JSON envelope. Success responses carry a `data`
    field; failures carry an `error` object with a stable `code`.

    ## Response headers

    Every response carries `X-Request-Id` (a per-request UUID, quote it in support
    requests), `Cache-Control: no-store`, `X-Content-Type-Options: nosniff` and
    `Strict-Transport-Security`. Authenticated responses additionally carry the
    `X-RateLimit-*` budget headers described under the 429 response.

    ## Pagination

    List endpoints use keyset (cursor) pagination: pass `limit` for page size and
    `cursor` to fetch the next page, using the `next_cursor` from the previous
    response. A `null` `next_cursor` means there are no more pages. Note that
    `/v1/events` is ordered newest-first and its cursor is a numeric `id`.

    Telemetry endpoints have no cursor. To read more than `limit` rows, page
    backward by setting `to` to the oldest `timestamp` you received, and
    de-duplicate on `(uid, timestamp)` because `to` is inclusive.
  contact:
    name: Matter
    url: https://matter.city
  x-ratelimit-defaults:
    # Asserted against rateLimit.ts by tests/openapi_spec_test.ts.
    per_minute: 1000
    per_day: 50000

servers:
  - url: https://api.matter.city
    description: Production

tags:
  - name: Health
    description: Unauthenticated liveness check.
  - name: Assets
    description: Assets and their telemetry.
  - name: Devices
    description: Devices and their most recent telemetry.
  - name: Events
    description: Events raised for assets and devices.
  - name: Organisations
    description: The organisation this API key is scoped to.
  - name: Tags
    description: Resolve tag ids returned on assets, devices and zones.
  - name: Zones
    description: Geographic zones.

security:
  - ApiKeyAuth: []
  - BearerAuth: []

paths:
  /v1/health:
    get:
      operationId: getHealth
      tags: [Health]
      summary: Health check
      description: Unauthenticated liveness check. Does not require an API key.
      security: []
      responses:
        '200':
          description: Service is up.
          content:
            application/json:
              schema:
                type: object
                properties:
                  data:
                    type: object
                    properties:
                      status:
                        type: string
                        example: ok
                      timestamp:
                        type: string
                        format: date-time
                        example: '2026-06-17T00:37:00.000Z'
        '500':
          $ref: '#/components/responses/InternalError'

  /v1/assets:
    get:
      operationId: listAssets
      tags: [Assets]
      summary: List assets
      description: List the assets in your organisation. Keyset-paginated, ascending by `id`.
      parameters:
        - $ref: '#/components/parameters/Limit'
        - $ref: '#/components/parameters/Cursor'
      responses:
        '200':
          description: A page of assets.
          content:
            application/json:
              schema:
                type: object
                properties:
                  data:
                    allOf:
                      - type: object
                        properties:
                          items:
                            type: array
                            items:
                              $ref: '#/components/schemas/Asset'
                      - $ref: '#/components/schemas/PageEnvelope'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '429':
          $ref: '#/components/responses/TooManyRequests'
        '500':
          $ref: '#/components/responses/InternalError'

  /v1/assets/{id}:
    get:
      operationId: getAsset
      tags: [Assets]
      summary: Get asset
      description: Fetch a single asset by its primary key, including detailed fields.
      parameters:
        - $ref: '#/components/parameters/AssetId'
      responses:
        '200':
          description: The asset.
          content:
            application/json:
              schema:
                type: object
                properties:
                  data:
                    $ref: '#/components/schemas/AssetDetail'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '404':
          $ref: '#/components/responses/NotFound'
        '429':
          $ref: '#/components/responses/TooManyRequests'
        '500':
          $ref: '#/components/responses/InternalError'

  /v1/assets/{id}/asset-data:
    get:
      operationId: getAssetData
      tags: [Assets]
      summary: Asset data (recommended)
      description: |
        **Recommended** way to retrieve telemetry for an asset. Returns readings
        stamped with this asset's ID (`device_data.asset_id`), newest-first —
        including history from devices that have since been swapped out, and
        concurrent readings when multiple devices report for the same asset.

        Each row identifies its source device via `uid` (IMEI); the response also
        lists the distinct devices present in the page via `device_ids` /
        `num_devices`. Each row's `measurement_settings` is the snapshot stored at
        ingest time, not the asset's current settings.

        This endpoint has no `cursor`; page backward with `to` to read more than
        `limit` rows.
      parameters:
        - $ref: '#/components/parameters/AssetId'
        - $ref: '#/components/parameters/From'
        - $ref: '#/components/parameters/To'
        - $ref: '#/components/parameters/TelemetryLimit'
      responses:
        '200':
          description: Telemetry rows keyed by `asset_id`, plus the distinct reporting devices in this page.
          content:
            application/json:
              schema:
                type: object
                properties:
                  data:
                    $ref: '#/components/schemas/AssetTelemetryPage'
        '400':
          $ref: '#/components/responses/BadRequest'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '429':
          $ref: '#/components/responses/TooManyRequests'
        '500':
          $ref: '#/components/responses/InternalError'

  /v1/assets/{id}/device-data:
    get:
      operationId: getAssetDeviceData
      tags: [Assets]
      summary: Asset telemetry
      description: |
        Telemetry for all devices attached to an asset, newest-first. Rows from
        different devices are interleaved and distinguished by each row's `uid`
        (the device IMEI). `limit` is global across the asset's devices, not
        per-device.

        Prefer `/v1/assets/{id}/asset-data` for retrieving asset readings,
        especially across device swaps and concurrent reporters.
      parameters:
        - $ref: '#/components/parameters/AssetId'
        - $ref: '#/components/parameters/From'
        - $ref: '#/components/parameters/To'
        - $ref: '#/components/parameters/TelemetryLimit'
      responses:
        '200':
          description: Telemetry rows plus the distinct devices present in this page.
          content:
            application/json:
              schema:
                type: object
                properties:
                  data:
                    $ref: '#/components/schemas/AssetTelemetryPage'
        '400':
          $ref: '#/components/responses/BadRequest'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '429':
          $ref: '#/components/responses/TooManyRequests'
        '500':
          $ref: '#/components/responses/InternalError'

  /v1/devices:
    get:
      operationId: listDevices
      tags: [Devices]
      summary: List devices
      description: List the devices in your organisation. Keyset-paginated, ascending by `id`.
      parameters:
        - $ref: '#/components/parameters/Limit'
        - $ref: '#/components/parameters/Cursor'
      responses:
        '200':
          description: A page of devices.
          content:
            application/json:
              schema:
                type: object
                properties:
                  data:
                    allOf:
                      - type: object
                        properties:
                          items:
                            type: array
                            items:
                              $ref: '#/components/schemas/Device'
                      - $ref: '#/components/schemas/PageEnvelope'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '429':
          $ref: '#/components/responses/TooManyRequests'
        '500':
          $ref: '#/components/responses/InternalError'

  /v1/devices/imei/{imei}:
    get:
      operationId: getDeviceByImei
      tags: [Devices]
      summary: Get device by IMEI
      description: Fetch a single device by its IMEI.
      parameters:
        - $ref: '#/components/parameters/Imei'
      responses:
        '200':
          description: The device.
          content:
            application/json:
              schema:
                type: object
                properties:
                  data:
                    $ref: '#/components/schemas/Device'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '404':
          $ref: '#/components/responses/NotFound'
        '429':
          $ref: '#/components/responses/TooManyRequests'
        '500':
          $ref: '#/components/responses/InternalError'

  /v1/devices/recent-data/{imei}:
    get:
      operationId: getDeviceRecentData
      tags: [Devices]
      summary: Recent telemetry
      description: |
        The most recent telemetry records for a single device, newest-first, keyed
        by IMEI (`device_data.uid`). Takes no time range — it always returns the
        latest records, so the backward-paging technique used by the asset
        telemetry endpoints does not apply here.
      parameters:
        - $ref: '#/components/parameters/Imei'
        - $ref: '#/components/parameters/TelemetryLimit'
      responses:
        '200':
          description: Telemetry rows for the device.
          content:
            application/json:
              schema:
                type: object
                properties:
                  data:
                    type: object
                    properties:
                      imei:
                        type: string
                        example: '352000000000001'
                      items:
                        type: array
                        items:
                          $ref: '#/components/schemas/DeviceDataRow'
                      meta:
                        $ref: '#/components/schemas/TelemetryMeta'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '404':
          $ref: '#/components/responses/NotFound'
        '429':
          $ref: '#/components/responses/TooManyRequests'
        '500':
          $ref: '#/components/responses/InternalError'

  /v1/devices/{id}:
    get:
      operationId: getDevice
      tags: [Devices]
      summary: Get device
      description: Fetch a single device by its primary key.
      parameters:
        - name: id
          in: path
          required: true
          description: The device's primary key.
          schema:
            type: string
          example: d9e8f7
      responses:
        '200':
          description: The device.
          content:
            application/json:
              schema:
                type: object
                properties:
                  data:
                    $ref: '#/components/schemas/Device'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '404':
          $ref: '#/components/responses/NotFound'
        '429':
          $ref: '#/components/responses/TooManyRequests'
        '500':
          $ref: '#/components/responses/InternalError'

  /v1/events:
    get:
      operationId: listEvents
      tags: [Events]
      summary: List events
      description: List events in your organisation, ordered newest-first (descending `id`).
      parameters:
        - $ref: '#/components/parameters/Limit'
        - name: cursor
          in: query
          required: false
          description: |
            The numeric `id` from `next_cursor`; the next page returns `id < cursor`.
            Unlike the other list endpoints, this cursor is an integer.
          schema:
            type: integer
            format: int64
          example: 90210
      responses:
        '200':
          description: A page of events.
          content:
            application/json:
              schema:
                type: object
                properties:
                  data:
                    allOf:
                      - type: object
                        properties:
                          items:
                            type: array
                            items:
                              $ref: '#/components/schemas/Event'
                      - $ref: '#/components/schemas/PageEnvelope'
        '400':
          description: The `cursor` was not a numeric event id.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorEnvelope'
              example:
                error:
                  code: bad_request
                  message: cursor must be a numeric event id
        '401':
          $ref: '#/components/responses/Unauthorized'
        '429':
          $ref: '#/components/responses/TooManyRequests'
        '500':
          $ref: '#/components/responses/InternalError'

  /v1/organisations:
    get:
      operationId: listOrganisations
      tags: [Organisations]
      summary: List organisations
      description: |
        Organisations accessible to the key. Since a key is scoped to a single
        organisation, this returns at most one row.
      responses:
        '200':
          description: The organisation(s) for this key.
          content:
            application/json:
              schema:
                type: object
                properties:
                  data:
                    type: object
                    properties:
                      items:
                        type: array
                        items:
                          $ref: '#/components/schemas/Organisation'
                      meta:
                        type: object
                        properties:
                          organisation_id:
                            type: string
                            example: org_123
        '401':
          $ref: '#/components/responses/Unauthorized'
        '429':
          $ref: '#/components/responses/TooManyRequests'
        '500':
          $ref: '#/components/responses/InternalError'

  /v1/tags/{id}:
    get:
      operationId: getTag
      tags: [Tags]
      summary: Get tag
      description: |
        Fetch a single tag by its primary key. The `tags` array returned on assets,
        devices and zones contains tag **ids** — use this endpoint to resolve one of
        those ids to its name and type.

        There is no list endpoint for tags, so resolve ids one at a time and cache
        the result client-side: tags change rarely, and each lookup counts against
        your rate limit.
      parameters:
        - name: id
          in: path
          required: true
          description: The tag's primary key, as it appears in a resource's `tags` array.
          schema:
            type: string
          example: tag_7f3a
      responses:
        '200':
          description: The tag.
          content:
            application/json:
              schema:
                type: object
                properties:
                  data:
                    $ref: '#/components/schemas/Tag'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '404':
          $ref: '#/components/responses/NotFound'
        '429':
          $ref: '#/components/responses/TooManyRequests'
        '500':
          $ref: '#/components/responses/InternalError'

  /v1/zones:
    get:
      operationId: listZones
      tags: [Zones]
      summary: List zones
      description: List the zones in your organisation. Keyset-paginated, ascending by `id`.
      parameters:
        - $ref: '#/components/parameters/Limit'
        - $ref: '#/components/parameters/Cursor'
      responses:
        '200':
          description: A page of zones.
          content:
            application/json:
              schema:
                type: object
                properties:
                  data:
                    allOf:
                      - type: object
                        properties:
                          items:
                            type: array
                            items:
                              $ref: '#/components/schemas/Zone'
                      - $ref: '#/components/schemas/PageEnvelope'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '429':
          $ref: '#/components/responses/TooManyRequests'
        '500':
          $ref: '#/components/responses/InternalError'

components:
  securitySchemes:
    ApiKeyAuth:
      type: apiKey
      in: header
      name: x-api-key
      description: |
        Your API key, sent as the `x-api-key` header. Keys are opaque 64-character
        hex tokens; only the SHA-256 hash is stored server-side. Treat the key like
        a password.
    BearerAuth:
      type: http
      scheme: bearer
      description: |
        The same API key sent as `Authorization: Bearer <key>`. Equivalent to
        `x-api-key` — supply one or the other, not both.

  parameters:
    Limit:
      name: limit
      in: query
      required: false
      description: Page size. Default `50`, maximum `200` (values above the maximum are clamped).
      schema:
        type: integer
        minimum: 1
        maximum: 200
        default: 50
    TelemetryLimit:
      name: limit
      in: query
      required: false
      description: |
        Number of telemetry rows. Default `20`, maximum `1000` (values above the
        maximum are clamped). Global across all devices reporting for the asset,
        not per-device.
      schema:
        type: integer
        minimum: 1
        maximum: 1000
        default: 20
    Cursor:
      name: cursor
      in: query
      required: false
      description: The `next_cursor` from the previous page. Omit for the first page.
      schema:
        type: string
    AssetId:
      name: id
      in: path
      required: true
      description: The asset's primary key. Matched against `device_data.asset_id` on the telemetry endpoints.
      schema:
        type: string
      example: a1b2c3
    Imei:
      name: imei
      in: path
      required: true
      description: The device IMEI (`device_data.uid`).
      schema:
        type: string
      example: '352000000000001'
    From:
      name: from
      in: query
      required: false
      description: |
        Start of the window, as either an ISO-8601 date-time string
        (`2026-06-01T00:00:00Z`) or Unix epoch milliseconds as a bare integer
        (`1748736000000`). The two forms may be mixed across `from` and `to`.

        Optional — omitting it leaves the bound open. Omitting both `from` and `to`
        returns the most recent records.
      schema:
        type: string
      example: '2026-06-01T00:00:00Z'
    To:
      name: to
      in: query
      required: false
      description: |
        End of the window, in the same formats as `from`. **Inclusive**, which is
        why backward paging should de-duplicate on `(uid, timestamp)`.

        When both bounds are given, `from` must not be later than `to` and the
        window must not exceed 31 days, or the request fails with `400 bad_request`.
      schema:
        type: string
      example: '1748736000000'

  headers:
    RequestId:
      description: Per-request UUID for correlating logs and support tickets.
      schema:
        type: string
        format: uuid
    RetryAfter:
      description: Seconds to wait before retrying — until the per-minute window resets.
      schema:
        type: integer
    RateLimitLimitMinute:
      description: Requests allowed per minute for this key.
      schema:
        type: integer
    RateLimitRemainingMinute:
      description: Remaining requests in the current minute window.
      schema:
        type: integer
    RateLimitResetMinute:
      description: When the per-minute window resets (HTTP date).
      schema:
        type: string
    RateLimitLimitDay:
      description: Requests allowed per UTC day for this key.
      schema:
        type: integer
    RateLimitRemainingDay:
      description: Remaining requests in the current UTC day.
      schema:
        type: integer
    RateLimitResetDay:
      description: When the daily window resets (HTTP date).
      schema:
        type: string

  responses:
    BadRequest:
      description: |
        Invalid query parameter — a malformed `from`/`to`, `from` later than `to`,
        or a window larger than 31 days.
      headers:
        X-Request-Id:
          $ref: '#/components/headers/RequestId'
      content:
        application/json:
          schema:
            $ref: '#/components/schemas/ErrorEnvelope'
          example:
            error:
              code: bad_request
              message: Time range exceeds the maximum allowed window of 31 days
    Unauthorized:
      description: Missing, invalid, inactive, or expired API key.
      headers:
        X-Request-Id:
          $ref: '#/components/headers/RequestId'
      content:
        application/json:
          schema:
            $ref: '#/components/schemas/ErrorEnvelope'
          example:
            error:
              code: unauthorized
              message: Invalid or expired API key
    NotFound:
      description: The resource does not exist, or is not in your organisation.
      headers:
        X-Request-Id:
          $ref: '#/components/headers/RequestId'
      content:
        application/json:
          schema:
            $ref: '#/components/schemas/ErrorEnvelope'
          example:
            error:
              code: not_found
              message: Asset a1b2c3 not found
    TooManyRequests:
      description: |
        Per-minute or per-day budget exhausted. Requests are throttled per API key
        using two fixed-window buckets — 1000 requests/minute and 50,000
        requests/day by default, both overridable per key. Telemetry endpoints cost
        5 units per request rather than 1.
      headers:
        Retry-After:
          $ref: '#/components/headers/RetryAfter'
        X-Request-Id:
          $ref: '#/components/headers/RequestId'
        X-RateLimit-Limit-Minute:
          $ref: '#/components/headers/RateLimitLimitMinute'
        X-RateLimit-Remaining-Minute:
          $ref: '#/components/headers/RateLimitRemainingMinute'
        X-RateLimit-Reset-Minute:
          $ref: '#/components/headers/RateLimitResetMinute'
        X-RateLimit-Limit-Day:
          $ref: '#/components/headers/RateLimitLimitDay'
        X-RateLimit-Remaining-Day:
          $ref: '#/components/headers/RateLimitRemainingDay'
        X-RateLimit-Reset-Day:
          $ref: '#/components/headers/RateLimitResetDay'
      content:
        application/json:
          schema:
            $ref: '#/components/schemas/ErrorEnvelope'
          example:
            error:
              code: rate_limit_exceeded
              message: Rate limit exceeded
    InternalError:
      description: Unexpected server error. Quote the `X-Request-Id` when reporting it.
      headers:
        X-Request-Id:
          $ref: '#/components/headers/RequestId'
      content:
        application/json:
          schema:
            $ref: '#/components/schemas/ErrorEnvelope'
          example:
            error:
              code: internal_error
              message: Internal server error

  schemas:
    ErrorEnvelope:
      type: object
      description: Failure envelope. Present on every non-2xx response.
      required: [error]
      properties:
        error:
          type: object
          required: [code, message]
          properties:
            code:
              type: string
              description: Stable machine-readable error code.
              enum:
                - bad_request
                - unauthorized
                - forbidden
                - not_found
                - method_not_allowed
                - unsupported_media_type
                - rate_limit_exceeded
                - internal_error
            message:
              type: string
              description: Human-readable explanation. Not intended for programmatic matching.
            details:
              description: |
                Optional extra context, of no fixed shape. Only populated in some
                cases — for example the underlying database error when the
                function runs with PUBLIC_API_DEBUG enabled.

    PageEnvelope:
      type: object
      description: Keyset-pagination fields shared by the list endpoints.
      properties:
        next_cursor:
          type: string
          nullable: true
          description: Pass as `?cursor=` to fetch the next page. `null` means there are no more pages.
        meta:
          type: object
          properties:
            limit:
              type: integer
              example: 50
            cursor:
              type: string
              nullable: true
            organisation_id:
              type: string
              example: org_123

    TelemetryMeta:
      type: object
      properties:
        limit:
          type: integer
          example: 20
        organisation_id:
          type: string
          example: org_123

    Asset:
      type: object
      description: Asset summary, as returned by the list endpoint.
      properties:
        id:
          type: string
          example: a1b2c3
        name:
          type: string
          nullable: true
          example: Bin 42
        asset_type:
          type: string
          nullable: true
          example: bin
        lat:
          type: number
          nullable: true
          example: -33.8688
        long:
          type: number
          nullable: true
          example: 151.2093
        status:
          type: string
          nullable: true
          example: active
        zone:
          type: string
          nullable: true
          example: cbd
        tags:
          type: array
          description: Tag ids. Resolve them via `/v1/tags/{id}`.
          items:
            type: string
          example: [tag_7f3a]
        address:
          type: string
          nullable: true
          example: 1 George St
        description:
          type: string
          nullable: true

    AssetDetail:
      description: Asset with the additional fields returned by the single-asset endpoint.
      allOf:
        - $ref: '#/components/schemas/Asset'
        - type: object
          properties:
            thresholds:
              type: object
              nullable: true
              additionalProperties: true
              example: { fill: 80 }
            measurement_settings:
              type: object
              nullable: true
              additionalProperties: true
            customer_data:
              type: object
              nullable: true
              additionalProperties: true
            created_at:
              type: string
              format: date-time
            updated_at:
              type: string
              format: date-time

    AssetTelemetryPage:
      type: object
      description: A page of telemetry for one asset, newest-first.
      properties:
        asset_id:
          type: string
          example: a1b2c3
        range:
          type: object
          description: Echoes the `from`/`to` you supplied, verbatim and un-normalised.
          properties:
            from:
              type: string
              nullable: true
            to:
              type: string
              nullable: true
        items:
          type: array
          items:
            $ref: '#/components/schemas/DeviceDataRow'
        num_devices:
          type: integer
          description: Count of distinct devices that reported in this page.
          example: 2
        device_ids:
          type: array
          description: Distinct device IMEIs present in this page.
          items:
            type: string
          example: [imei-A, imei-B]
        meta:
          $ref: '#/components/schemas/TelemetryMeta'

    DeviceDataRow:
      type: object
      description: |
        A single telemetry reading. Fields are populated according to the device's
        capabilities, so most are nullable.
      properties:
        uid:
          type: string
          description: IMEI of the reporting device.
          example: '352000000000001'
        asset_id:
          type: string
          nullable: true
          description: Only present on `/v1/assets/{id}/asset-data`.
        timestamp:
          type: integer
          format: int64
          description: Reading time as Unix epoch **milliseconds**.
          example: 1750000003000
        level:
          type: number
          nullable: true
          description: Fill level, percent.
          example: 10
        fill_status:
          type: string
          nullable: true
          example: low
        depth:
          type: number
          nullable: true
        temperature:
          type: number
          nullable: true
        tilt:
          type: number
          nullable: true
        debris:
          type: boolean
          nullable: true
        debris_level:
          type: number
          nullable: true
        light_ambient:
          type: number
          nullable: true
        light_clear:
          type: number
          nullable: true
        is_collection:
          type: boolean
          nullable: true
        location:
          type: object
          nullable: true
          additionalProperties: true
        network:
          type: object
          nullable: true
          additionalProperties: true
        measurement_settings:
          type: object
          nullable: true
          additionalProperties: true
          description: Snapshot taken at ingest time — not the asset's current settings.

    Device:
      type: object
      properties:
        id:
          type: string
          example: d9e8f7
        name:
          type: string
          nullable: true
          example: Sensor A
        IMEI:
          type: string
          description: Note the uppercase field name.
          example: '352000000000001'
        status:
          type: string
          nullable: true
          example: active
        parent_asset:
          type: string
          nullable: true
          description: Id of the asset this device is currently attached to.
          example: a1b2c3
        tags:
          type: array
          items:
            type: string
        description:
          type: string
          nullable: true
        created_at:
          type: string
          format: date-time
        updated_at:
          type: string
          format: date-time

    Event:
      type: object
      properties:
        id:
          type: integer
          format: int64
          description: Numeric primary key. Also used as the pagination cursor.
          example: 90210
        created_at:
          type: string
          format: date-time
        asset_id:
          type: string
          nullable: true
          example: a1b2c3
        device_id:
          type: string
          nullable: true
          example: d9e8f7
        type:
          type: string
          nullable: true
          example: fill_threshold
        status:
          type: string
          nullable: true
          example: open
        confidence:
          type: number
          nullable: true
          example: 0.92
        data:
          type: object
          nullable: true
          additionalProperties: true
          example: { level: 85 }

    Organisation:
      type: object
      properties:
        id:
          type: string
          example: org_123
        name:
          type: string
          example: City Council
        status:
          type: string
          nullable: true
          example: active
        description:
          type: string
          nullable: true
        domain:
          type: string
          nullable: true
          example: council.example
        lat:
          type: number
          nullable: true
        long:
          type: number
          nullable: true
        timezone:
          type: string
          nullable: true
          example: Australia/Sydney
        profile:
          type: object
          nullable: true
          additionalProperties: true

    Tag:
      type: object
      properties:
        id:
          type: string
          example: tag_7f3a
        name:
          type: string
          example: priority
        tag_type:
          type: string
          nullable: true
          example: operational
        description:
          type: string
          nullable: true
        created_at:
          type: string
          format: date-time

    Zone:
      type: object
      properties:
        id:
          type: string
          example: z1
        name:
          type: string
          example: CBD
        status:
          type: string
          nullable: true
          example: active
        description:
          type: string
          nullable: true
        city:
          type: string
          nullable: true
          example: Sydney
        country:
          type: string
          nullable: true
          example: AU
        state:
          type: string
          nullable: true
          example: NSW
        postal_code:
          type: string
          nullable: true
          example: '2000'
        parent_zone:
          type: string
          nullable: true
        is_sub_zone:
          type: boolean
          nullable: true
        tags:
          type: array
          items:
            type: string
        timezone:
          type: string
          nullable: true
          example: Australia/Sydney
        polygon_coordinates_map:
          type: object
          nullable: true
          additionalProperties: true
          description: Zone boundary geometry.
