openapi: 3.0.3
info:
  title: ThingsFlow Public API
  version: 0.1.0
  description: |
    Initial public API contract for ThingsFlow control-plane, device provisioning,
    twin reads, telemetry reads, and ThingsBoard UI compatibility endpoints.
    Flow Core owns platform operations; ThingsFlow data plane owns high-volume telemetry
    ingress.
servers:
  - url: https://thingsflow.example.com
    description: Public deployment
  - url: http://localhost:8080
    description: Local flow-core development
tags:
  - name: Auth
  - name: Devices
  - name: Provisioning
  - name: Device Security
  - name: Twins
  - name: Twin Models
  - name: Entities Query
  - name: Telemetry
  - name: Topology
  - name: Health
security:
  - bearerAuth: []
paths:
  /api/auth/login:
    post:
      tags: [Auth]
      summary: Login with a platform user
      security: []
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/LoginRequest'
      responses:
        '200':
          description: JWT and refresh token.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/LoginResponse'
        '401':
          $ref: '#/components/responses/Unauthorized'
  /api/auth/user:
    get:
      tags: [Auth]
      summary: Read the current authenticated user
      responses:
        '200':
          description: Current user profile.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/UserInfo'
        '401':
          $ref: '#/components/responses/Unauthorized'
  /api/device:
    post:
      tags: [Devices]
      summary: Create a tenant device and default credentials
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/DeviceCreateRequest'
      responses:
        '200':
          description: Created device.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Device'
        '401':
          $ref: '#/components/responses/Unauthorized'
  /api/device/{deviceId}:
    get:
      tags: [Devices]
      summary: Read a tenant device
      parameters:
        - $ref: '#/components/parameters/DeviceId'
      responses:
        '200':
          description: Device.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Device'
        '404':
          $ref: '#/components/responses/NotFound'
    delete:
      tags: [Devices]
      summary: Delete a tenant device
      parameters:
        - $ref: '#/components/parameters/DeviceId'
      responses:
        '200':
          description: Deleted.
        '404':
          $ref: '#/components/responses/NotFound'
  /api/device/{deviceId}/credentials:
    get:
      tags: [Devices]
      summary: Read device credentials metadata and value when allowed
      parameters:
        - $ref: '#/components/parameters/DeviceId'
      responses:
        '200':
          description: Device credentials.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/DeviceCredentials'
        '404':
          $ref: '#/components/responses/NotFound'
  /api/device/credentials:
    post:
      tags: [Devices]
      summary: Create or rotate device credentials
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/DeviceCredentialsUpdate'
      responses:
        '200':
          description: Updated credentials.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/DeviceCredentials'
  /api/device/{deviceId}/security:
    get:
      tags: [Device Security]
      summary: Read device security status
      parameters:
        - $ref: '#/components/parameters/DeviceId'
      responses:
        '200':
          description: Current security status.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/DeviceSecurityStatus'
    post:
      tags: [Device Security]
      summary: Suspend or reactivate a device
      parameters:
        - $ref: '#/components/parameters/DeviceId'
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/DeviceSecurityUpdate'
      responses:
        '200':
          description: Updated security status.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/DeviceSecurityStatus'
  /api/device/{deviceId}/jwt:
    post:
      tags: [Provisioning]
      summary: Issue a fresh Device JWT for an existing device
      description: |
        Control-plane renewal endpoint for simulators, fleet portals, and
        automation. It requires normal user/API authentication and must not be
        treated as an unauthenticated telemetry endpoint. The returned token is
        used as the RMQTT username or as the HTTP telemetry bearer token.
      parameters:
        - $ref: '#/components/parameters/DeviceId'
      responses:
        '200':
          description: Short-lived Device JWT for the requested device.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/DeviceJwt'
  /api/v1/devices/me/jwt/refresh:
    post:
      tags: [Provisioning]
      summary: Refresh the current device JWT
      description: |
        Device-native renewal endpoint. The caller presents a still-valid
        Device JWT as a bearer token; Flow Core validates it, checks that the
        device is still active, and returns a fresh short-lived Device JWT.
      security:
        - deviceJwtAuth: []
      responses:
        '200':
          description: Fresh short-lived Device JWT for the authenticated device.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/DeviceJwt'
  /api/noauth/device-jwks:
    get:
      tags: [Provisioning]
      summary: Read the public JWKS used by MQTT/HTTP edges to validate device JWTs
      security: []
      responses:
        '200':
          description: JSON Web Key Set for provisioning-issued device JWTs.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/JWKS'
  /api/noauth/device-jwt-public.pem:
    get:
      tags: [Provisioning]
      summary: Read the ES256 public key PEM for brokers that validate JWTs from a PEM file
      security: []
      responses:
        '200':
          description: PEM-encoded public key for provisioning-issued device JWTs.
          content:
            application/x-pem-file:
              schema:
                type: string
  /.well-known/thingsflow-device-jwks.json:
    get:
      tags: [Provisioning]
      summary: Well-known alias for the device JWT JWKS
      security: []
      responses:
        '200':
          description: JSON Web Key Set for provisioning-issued device JWTs.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/JWKS'
  /.well-known/thingsflow-device-public.pem:
    get:
      tags: [Provisioning]
      summary: Well-known alias for the ES256 public key PEM
      security: []
      responses:
        '200':
          description: PEM-encoded public key for provisioning-issued device JWTs.
          content:
            application/x-pem-file:
              schema:
                type: string
  /api/v1/provision:
    post:
      tags: [Provisioning]
      summary: Self-provision a device through a provisioning profile
      description: |
        Device bootstrap endpoint. The device presents a provisioning profile
        key and secret; Flow Core creates or finds the device, returns the
        compatible ACCESS_TOKEN fields, and includes a native short-lived
        deviceJwt when Device JWT issuing is enabled.
      security: []
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/ProvisionRequest'
      responses:
        '200':
          description: Provisioning result.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ProvisionResponse'
  /api/v1/{token}/telemetry:
    post:
      tags: [Telemetry]
      summary: Publish device telemetry with a device access token (unavailable)
      deprecated: true
      description: |
        Not available. This TB-style compatibility path always returns 503 and
        directs callers to the HTTP ingest gateway, which matches only the exact
        path `/api/v1/telemetry` with a provisioning-issued device JWT
        (`Authorization: Bearer <deviceJwt.token>`). Kept documented so clients
        written against the classic contract get an explanation rather than a
        silent 404.
      security: []
      parameters:
        - name: token
          in: path
          required: true
          schema:
            type: string
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              additionalProperties: true
      responses:
        '503':
          description: |
            Always returned. Device ingest is handled by the HTTP ingest gateway.
  /api/v1/telemetry:
    post:
      tags: [Telemetry]
      summary: Publish native HTTP telemetry with a Device JWT
      description: |
        Native HTTP telemetry endpoint served by the `http-ingest` edge in the
        NATS event plane. Flow Core is not in this telemetry flow. Envoy validates
        the ES256 Device JWT with JWKS and Bento publishes accepted telemetry to
        NATS.
      security:
        - deviceJwtAuth: []
      requestBody:
        required: true
        content:
          application/json:
            schema:
              oneOf:
                - type: object
                  additionalProperties: true
                - type: array
                  items:
                    type: object
                    additionalProperties: true
      responses:
        '202':
          description: Telemetry accepted by the HTTP edge.
        '401':
          $ref: '#/components/responses/Unauthorized'
  /api/plugins/telemetry/{entityType}/{entityId}/values/timeseries:
    get:
      tags: [Telemetry]
      summary: Read latest or historical telemetry values
      parameters:
        - $ref: '#/components/parameters/EntityType'
        - $ref: '#/components/parameters/EntityId'
        - name: keys
          in: query
          schema:
            type: string
        - name: startTs
          in: query
          schema:
            type: integer
            format: int64
        - name: endTs
          in: query
          schema:
            type: integer
            format: int64
        - name: useStrictDataTypes
          in: query
          schema:
            type: boolean
      responses:
        '200':
          description: Timeseries map.
          content:
            application/json:
              schema:
                type: object
                additionalProperties:
                  type: array
                  items:
                    $ref: '#/components/schemas/TelemetryKv'
  /api/twins:
    get:
      tags: [Twins]
      summary: List tenant-scoped twins with cursor pagination and filters
      description: |
        GET /api/twins — returns a cursor-paginated page of twins scoped to the
        caller's tenant (a SYS_ADMIN may list across tenants). All filter
        predicates are applied at SQL level against `twin_registry`, so a
        cross-tenant row can never appear in a response. Filters combine with
        AND. Cursors are opaque, stateless, and tenant-bound: a cursor issued
        for one tenant is rejected with 400 when used by another.
        R6: the listing is policy-enforced — the caller must have READ on the
        tenant root (`thing:/<tenant>`) under the tenant's resolved policy
        document (`POLICY_ENFORCEMENT_ENABLED`, default true; SYS_ADMIN and the
        owning tenant pass via the default owner policy). TB classic routes are
        unchanged.
      parameters:
        - name: pageSize
          in: query
          required: false
          description: Page size, clamped into [1, 1000].
          schema:
            type: integer
            minimum: 1
            maximum: 1000
            default: 100
        - name: cursor
          in: query
          required: false
          description: Opaque keyset cursor returned in `nextPageLink`.
          schema:
            type: string
        - name: kind
          in: query
          required: false
          description: Restrict to one twin kind. Anything else is a 400.
          schema:
            type: string
            enum: [DEVICE, ASSET]
        - name: definition
          in: query
          required: false
          description: Case-insensitive substring match on the twin definition (e.g. thingsflow:device:energy_meter:1.0.0).
          schema:
            type: string
        - name: text
          in: query
          required: false
          description: Case-insensitive substring match on the twin name, type, or label.
          schema:
            type: string
        - name: relationType
          in: query
          required: false
          description: Existence filter — only twins participating in a relation of this type (tenant-scoped).
          schema:
            type: string
            example: Contains
        - name: relationDirection
          in: query
          required: false
          description: Direction of the relationType existence filter.
          schema:
            type: string
            enum: [FROM, TO]
            default: FROM
      responses:
        '200':
          description: A page of twins.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/TwinPage'
        '400':
          $ref: '#/components/responses/BadRequest'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/Forbidden'
        '422':
          $ref: '#/components/responses/UnprocessableEntity'
  /api/twins/{entityType}/{entityId}:
    get:
      tags: [Twins]
      summary: Read the native Flow Thing projection for a device or asset
      description: |
        Reads the twin projection. When `expand=relations(<depth>)` is present
        (1 <= depth <= 10), the response relations are replaced by the
        tenant-scoped expanded set: immediate relations are annotated with
        `depth` and embedded `state`, and reachable nodes beyond depth 1 are
        appended. Depth above the ceiling is a 400; exceeding the traversal
        node budget is a 422. Without `expand` the response is the plain twin
        projection.
        R6: reads are policy-enforced — the caller must have READ on the
        entity's `thing:/<tenant>/<type>/<id>` path under the twin's resolved
        policy document (`POLICY_ENFORCEMENT_ENABLED`, default true). The
        existing tenant isolation is preserved; SYS_ADMIN and the owning tenant
        pass via the default owner policy. TB classic routes are unchanged.
      parameters:
        - $ref: '#/components/parameters/EntityType'
        - $ref: '#/components/parameters/EntityId'
        - name: expand
          in: query
          required: false
          description: Graph expansion in `relations(<depth>)` form.
          schema:
            type: string
            pattern: 'relations\(\d+\)'
      responses:
        '200':
          description: Digital twin projection.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Twin'
        '400':
          $ref: '#/components/responses/BadRequest'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          description: Cross-tenant access denied.
        '404':
          $ref: '#/components/responses/NotFound'
        '422':
          $ref: '#/components/responses/UnprocessableEntity'
  /api/twins/{entityType}/{entityId}/model:
    put:
      tags: [Twin Models]
      summary: Re-point an entity to an active model version
      description: |
        R6: model writes are policy-enforced — the caller must have WRITE on the
        entity's `.../model` path under the twin's resolved policy document
        (`POLICY_ENFORCEMENT_ENABLED`, default true; SYS_ADMIN and the owning
        tenant pass via the default owner policy).
      parameters:
        - $ref: '#/components/parameters/EntityType'
        - $ref: '#/components/parameters/EntityId'
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/TwinModelReference'
      responses:
        '200':
          description: Current model pin. Repeating the same active pin is idempotent.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/TwinModelPin'
        '400':
          $ref: '#/components/responses/BadRequest'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/Forbidden'
        '404':
          $ref: '#/components/responses/NotFound'
        '405':
          $ref: '#/components/responses/MethodNotAllowed'
        '409':
          $ref: '#/components/responses/Conflict'
        '500':
          $ref: '#/components/responses/InternalError'
  /api/twins/{entityType}/{entityId}/attributes:
    put:
      tags: [Twins]
      summary: Save model-validated twin attributes
      description: |
        Persists model-validated twin attribute values. The write is validated
        against the entity's pinned twin model BEFORE any persistence: in reject
        mode a violation returns 400 and persists nothing; in warn mode the
        write persists and the violation is logged. Values are mirrored to
        `attribute_kv` (SERVER_SCOPE) and the twin-state KV under the same key
        namespace. Cross-tenant writes are denied with 403 unless the caller is
        a SYS_ADMIN.
        R6: attribute writes are policy-enforced — the caller must have WRITE on
        each written `thing:/<tenant>/<type>/<id>/attributes/<key>` path under
        the twin's resolved policy document (`POLICY_ENFORCEMENT_ENABLED`,
        default true). The existing tenant isolation + model validation are
        preserved; SYS_ADMIN and the owning tenant pass via the default owner
        policy.
      parameters:
        - $ref: '#/components/parameters/EntityType'
        - $ref: '#/components/parameters/EntityId'
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/TwinAttributesUpdate'
      responses:
        '200':
          description: Twin attributes persisted through the shared write path.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/TwinWriteResult'
        '400':
          $ref: '#/components/responses/BadRequest'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/Forbidden'
        '404':
          $ref: '#/components/responses/NotFound'
        '500':
          $ref: '#/components/responses/InternalError'
    patch:
      tags: [Twins]
      summary: Save model-validated twin attributes (PATCH)
      description: |
        Same contract as PUT /api/twins/{entityType}/{entityId}/attributes
        (including R6 policy enforcement of each written attribute path).
      parameters:
        - $ref: '#/components/parameters/EntityType'
        - $ref: '#/components/parameters/EntityId'
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/TwinAttributesUpdate'
      responses:
        '200':
          description: Twin attributes persisted through the shared write path.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/TwinWriteResult'
        '400':
          $ref: '#/components/responses/BadRequest'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/Forbidden'
        '404':
          $ref: '#/components/responses/NotFound'
        '500':
          $ref: '#/components/responses/InternalError'
  /api/twins/{entityType}/{entityId}/features:
    put:
      tags: [Twins]
      summary: Save model-validated twin feature properties
      description: |
        Persists model-validated feature property values. Each named feature's
        `properties` (and optional `desiredProperties`) are validated against
        the pinned model's feature schema; a feature not declared by the model
        is rejected with 400 unless the model's `unknownKeys` policy is `allow`.
        On validation failure nothing is persisted. Persisted reported
        properties are keyed `feature.<name>.<property>` and desired properties
        `feature.<name>.desired.<property>` in `attribute_kv` (SERVER_SCOPE)
        and the twin-state KV so a write is observable on the next twin read
        (desired state is delivered to devices via MQTT retained — see the
        Device Delivery docs).
        R6: feature writes are policy-enforced — the caller must have WRITE on
        each written `thing:/<tenant>/<type>/<id>/features/<name>` path under
        the twin's resolved policy document (`POLICY_ENFORCEMENT_ENABLED`,
        default true; Ditto deep-revoke precedence, e.g. a revoke on one
        feature path overrides a grant on the feature root). The existing
        tenant isolation + model validation are preserved; SYS_ADMIN and the
        owning tenant pass via the default owner policy.
      parameters:
        - $ref: '#/components/parameters/EntityType'
        - $ref: '#/components/parameters/EntityId'
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/TwinFeaturesUpdate'
      responses:
        '200':
          description: Twin feature properties persisted.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/TwinWriteResult'
        '400':
          $ref: '#/components/responses/BadRequest'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/Forbidden'
        '404':
          $ref: '#/components/responses/NotFound'
        '500':
          $ref: '#/components/responses/InternalError'
    patch:
      tags: [Twins]
      summary: Save model-validated twin feature properties (PATCH)
      description: |
        Same contract as PUT /api/twins/{entityType}/{entityId}/features
        (including R6 policy enforcement of each written feature path).
      parameters:
        - $ref: '#/components/parameters/EntityType'
        - $ref: '#/components/parameters/EntityId'
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/TwinFeaturesUpdate'
      responses:
        '200':
          description: Twin feature properties persisted.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/TwinWriteResult'
        '400':
          $ref: '#/components/responses/BadRequest'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/Forbidden'
        '404':
          $ref: '#/components/responses/NotFound'
        '500':
          $ref: '#/components/responses/InternalError'
  /api/entitiesQuery/find:
    post:
      tags: [Entities Query]
      summary: Resolve entities for a widget entity query
      description: |
        TB-compatible universal entity query engine. Supports the classic filter
        types (singleEntity, entityList, deviceType, entityType, assetType,
        apiUsageState) plus `relationsQuery`: a tenant-scoped graph walk from a
        root entity reusing the Wave 1 recursive CTE (`ExpandWithCTE`). The
        response is shaped like the WebSocket ENTITY_DATA relationsQuery branch
        — `{entityId, level, latest}` where `latest` embeds the requested
        ENTITY_FIELD and ATTRIBUTE values. A foreign-tenant root is denied with
        403; maxLevel above the fixed ceiling is a 400; exceeding the traversal
        node budget is a 422.
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/EntityQueryRequest'
      responses:
        '200':
          description: Matched entities in the WS-proven {entityId, level, latest} shape.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/EntityQueryResult'
        '400':
          $ref: '#/components/responses/BadRequest'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/Forbidden'
        '422':
          $ref: '#/components/responses/UnprocessableEntity'
  /api/twin-models:
    get:
      tags: [Twin Models]
      summary: List tenant-scoped model versions
      parameters:
        - name: page
          in: query
          schema:
            type: integer
            minimum: 0
            default: 0
        - name: pageSize
          in: query
          schema:
            type: integer
            minimum: 1
            maximum: 1000
            default: 100
        - name: kind
          in: query
          schema:
            type: string
            enum: [DEVICE, ASSET]
        - name: latest
          in: query
          schema:
            type: boolean
            default: true
        - name: includeDeprecated
          in: query
          schema:
            type: boolean
            default: false
      responses:
        '200':
          description: Page of model versions.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/TwinModelPage'
        '400':
          $ref: '#/components/responses/BadRequest'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/Forbidden'
        '405':
          $ref: '#/components/responses/MethodNotAllowed'
        '500':
          $ref: '#/components/responses/InternalError'
    post:
      tags: [Twin Models]
      summary: Create an immutable tenant-scoped model version
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/TwinModelDefinition'
      responses:
        '201':
          description: Created model version.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/TwinModelCatalogEntry'
        '400':
          $ref: '#/components/responses/BadRequest'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/Forbidden'
        '405':
          $ref: '#/components/responses/MethodNotAllowed'
        '409':
          $ref: '#/components/responses/Conflict'
        '500':
          $ref: '#/components/responses/InternalError'
  /api/twin-models/{modelId}/{version}:
    parameters:
      - $ref: '#/components/parameters/ModelId'
      - $ref: '#/components/parameters/ModelVersion'
    get:
      tags: [Twin Models]
      summary: Read one tenant-scoped model version
      responses:
        '200':
          description: Model version.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/TwinModelCatalogEntry'
        '400':
          $ref: '#/components/responses/BadRequest'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/Forbidden'
        '404':
          $ref: '#/components/responses/NotFound'
        '405':
          $ref: '#/components/responses/MethodNotAllowed'
        '500':
          $ref: '#/components/responses/InternalError'
    delete:
      tags: [Twin Models]
      summary: Soft-deprecate one model version
      description: Idempotent. Existing entity pins remain valid.
      responses:
        '200':
          description: Deprecated model version.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/TwinModelCatalogEntry'
        '400':
          $ref: '#/components/responses/BadRequest'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/Forbidden'
        '404':
          $ref: '#/components/responses/NotFound'
        '405':
          $ref: '#/components/responses/MethodNotAllowed'
        '500':
          $ref: '#/components/responses/InternalError'
  /api/policies:
    get:
      tags: [Policies]
      summary: List tenant-scoped policy versions
      description: |
        Returns the tenant's policy catalog. With `latest=true` each policy_id
        yields only its newest non-deprecated version. Deprecated versions are
        hidden unless `includeDeprecated=true`.
      parameters:
        - name: page
          in: query
          schema:
            type: integer
            minimum: 0
            default: 0
        - name: pageSize
          in: query
          schema:
            type: integer
            minimum: 1
            maximum: 1000
            default: 100
        - name: latest
          in: query
          schema:
            type: boolean
            default: true
        - name: includeDeprecated
          in: query
          schema:
            type: boolean
            default: false
      responses:
        '200':
          description: Page of policy versions.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/PolicyPage'
        '400':
          $ref: '#/components/responses/BadRequest'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/Forbidden'
        '405':
          $ref: '#/components/responses/MethodNotAllowed'
        '500':
          $ref: '#/components/responses/InternalError'
    post:
      tags: [Policies]
      summary: Create an immutable tenant-scoped policy version
      description: |
        The authored document carries subjects, `thing:/...` resources, and
        Ditto-style grant/revoke entries. The normalized derived schema is
        persisted beside it for the twin API enforcer.
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/PolicyDefinition'
      responses:
        '201':
          description: Created policy version.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/PolicyCatalogEntry'
        '400':
          $ref: '#/components/responses/BadRequest'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/Forbidden'
        '405':
          $ref: '#/components/responses/MethodNotAllowed'
        '409':
          $ref: '#/components/responses/Conflict'
        '500':
          $ref: '#/components/responses/InternalError'
  /api/policies/{policyId}/{version}:
    parameters:
      - name: policyId
        in: path
        required: true
        schema:
          type: string
        description: Canonical lower-snake policy id.
      - name: version
        in: path
        required: true
        schema:
          type: string
        description: Canonical 3-part semantic version.
    get:
      tags: [Policies]
      summary: Read one tenant-scoped policy version
      responses:
        '200':
          description: Policy version.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/PolicyCatalogEntry'
        '400':
          $ref: '#/components/responses/BadRequest'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/Forbidden'
        '404':
          $ref: '#/components/responses/NotFound'
        '405':
          $ref: '#/components/responses/MethodNotAllowed'
        '500':
          $ref: '#/components/responses/InternalError'
    delete:
      tags: [Policies]
      summary: Soft-deprecate one policy version
      description: Idempotent. Existing twin references remain valid.
      responses:
        '200':
          description: Deprecated policy version.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/PolicyCatalogEntry'
        '400':
          $ref: '#/components/responses/BadRequest'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/Forbidden'
        '404':
          $ref: '#/components/responses/NotFound'
        '405':
          $ref: '#/components/responses/MethodNotAllowed'
        '500':
          $ref: '#/components/responses/InternalError'
  /api/relation:
    post:
      tags: [Topology]
      summary: Create a ThingsBoard-compatible relation mirrored to topology
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/Relation'
      responses:
        '200':
          description: Relation accepted.
  /api/relations:
    get:
      tags: [Topology]
      summary: List ThingsBoard-compatible relations from topology
      parameters:
        - name: fromId
          in: query
          schema:
            type: string
            format: uuid
        - name: fromType
          in: query
          schema:
            type: string
        - name: toId
          in: query
          schema:
            type: string
            format: uuid
        - name: toType
          in: query
          schema:
            type: string
        - name: relationType
          in: query
          schema:
            type: string
      responses:
        '200':
          description: Relation list.
          content:
            application/json:
              schema:
                type: array
                items:
                  $ref: '#/components/schemas/Relation'
  /health:
    get:
      tags: [Health]
      summary: Liveness probe
      security: []
      responses:
        '200':
          description: Process is alive.
  /ready:
    get:
      tags: [Health]
      summary: Readiness probe
      security: []
      responses:
        '200':
          description: Dependencies are ready.
  /metrics:
    get:
      tags: [Health]
      summary: Prometheus metrics
      security: []
      responses:
        '200':
          description: Metrics text.
          content:
            text/plain:
              schema:
                type: string
components:
  securitySchemes:
    bearerAuth:
      type: http
      scheme: bearer
      bearerFormat: JWT
      description: "Use the ThingsBoard-compatible `X-Authorization: Bearer <jwt>` header in clients."
    deviceJwtAuth:
      type: http
      scheme: bearer
      bearerFormat: JWT
      description: "Use `Authorization: Bearer <deviceJwt.token>` with native MQTT/HTTP edge credentials."
  parameters:
    DeviceId:
      name: deviceId
      in: path
      required: true
      schema:
        type: string
        format: uuid
    EntityId:
      name: entityId
      in: path
      required: true
      schema:
        type: string
        format: uuid
    EntityType:
      name: entityType
      in: path
      required: true
      schema:
        type: string
        enum: [DEVICE, ASSET]
    ModelId:
      name: modelId
      in: path
      required: true
      schema:
        type: string
        minLength: 1
        maxLength: 255
        pattern: '^[a-z0-9_]+$'
    ModelVersion:
      name: version
      in: path
      required: true
      schema:
        type: string
        pattern: '^(0|[1-9][0-9]{0,9})\.(0|[1-9][0-9]{0,9})\.(0|[1-9][0-9]{0,9})$'
  responses:
    BadRequest:
      description: Malformed JSON, parameters, kind, version, or unsupported model keyword.
      content:
        application/json:
          schema:
            $ref: '#/components/schemas/ErrorEnvelope'
    Unauthorized:
      description: Missing or invalid credentials.
      content:
        application/json:
          schema:
            $ref: '#/components/schemas/ErrorEnvelope'
    Forbidden:
      description: The caller is not permitted to access the target tenant.
      content:
        application/json:
          schema:
            $ref: '#/components/schemas/ErrorEnvelope'
    NotFound:
      description: Entity was not found in the caller tenant.
      content:
        application/json:
          schema:
            $ref: '#/components/schemas/ErrorEnvelope'
    MethodNotAllowed:
      description: The resource exists but does not support this HTTP method.
      content:
        application/json:
          schema:
            $ref: '#/components/schemas/ErrorEnvelope'
    Conflict:
      description: The immutable version already exists or the requested model is deprecated.
      content:
        application/json:
          schema:
            $ref: '#/components/schemas/ErrorEnvelope'
    InternalError:
      description: The catalog operation failed in the database.
      content:
        application/json:
          schema:
            $ref: '#/components/schemas/ErrorEnvelope'
    UnprocessableEntity:
      description: The request is well-formed but the traversal node budget was exceeded.
      content:
        application/json:
          schema:
            $ref: '#/components/schemas/ErrorEnvelope'
  schemas:
    ErrorEnvelope:
      type: object
      required: [status, message, errorCode, timestamp]
      properties:
        status:
          type: integer
        message:
          type: string
        errorCode:
          type: integer
        timestamp:
          type: integer
          format: int64
    TwinModelReference:
      type: object
      required: [modelId, version]
      additionalProperties: false
      properties:
        modelId:
          type: string
          minLength: 1
          maxLength: 255
          pattern: '^[a-z0-9_]+$'
        version:
          type: string
          pattern: '^(0|[1-9][0-9]{0,9})\.(0|[1-9][0-9]{0,9})\.(0|[1-9][0-9]{0,9})$'
    TwinModelPin:
      type: object
      required: [entityType, entityId, modelId, version, definition]
      properties:
        entityType:
          type: string
          enum: [DEVICE, ASSET]
        entityId:
          type: string
          format: uuid
        modelId:
          type: string
        version:
          type: string
          pattern: '^(0|[1-9][0-9]{0,9})\.(0|[1-9][0-9]{0,9})\.(0|[1-9][0-9]{0,9})$'
        definition:
          type: string
          example: thingsflow:device:energy_meter:1.0.0
    TwinModelDefinition:
      type: object
      required: [modelId, version, kind]
      properties:
        modelId:
          type: string
          minLength: 1
          maxLength: 255
        version:
          type: string
          pattern: '^(0|[1-9][0-9]{0,9})\.(0|[1-9][0-9]{0,9})\.(0|[1-9][0-9]{0,9})$'
        kind:
          type: string
          enum: [DEVICE, ASSET]
        displayName:
          type: string
        unknownKeys:
          type: string
          enum: [allow, reject]
          default: allow
        enforcementMode:
          type: string
          enum: [warn, reject]
          default: warn
        attributes:
          type: object
          additionalProperties: true
          default: {}
        features:
          type: object
          additionalProperties: true
          default: {}
        relationships:
          type: object
          additionalProperties: true
          default: {}
      additionalProperties: true
    TwinModelCatalogEntry:
      allOf:
        - $ref: '#/components/schemas/TwinModelDefinition'
        - type: object
          required: [deprecated, createdTime, updatedTime]
          properties:
            deprecated:
              type: boolean
            createdTime:
              type: integer
              format: int64
            updatedTime:
              type: integer
              format: int64
    TwinModelPage:
      type: object
      required: [data, totalElements, totalPages, hasNext, page]
      properties:
        data:
          type: array
          items:
            $ref: '#/components/schemas/TwinModelCatalogEntry'
        totalElements:
          type: integer
        totalPages:
          type: integer
        hasNext:
          type: boolean
        page:
          type: integer
    PolicyEntry:
      type: object
      required: [resource, actions]
      properties:
        resource:
          type: string
          description: A thing:/... resource path (path-level grant/revoke target).
        actions:
          type: array
          items:
            type: string
            enum: [READ, WRITE, DELETE, '*']
    PolicyDefinition:
      type: object
      required: [policyId, version]
      properties:
        policyId:
          type: string
          minLength: 1
          maxLength: 255
          pattern: '^[a-z0-9_]+$'
        version:
          type: string
          pattern: '^(0|[1-9][0-9]{0,9})\.(0|[1-9][0-9]{0,9})\.(0|[1-9][0-9]{0,9})$'
        kind:
          type: string
          enum: [TWIN]
          default: TWIN
        subjects:
          type: array
          description: 'Subjects the policy binds: tenant:<id>, user:<id>, role:<name>, or *.'
          items:
            type: string
        resources:
          type: array
          description: Declared thing:/... resource scope.
          items:
            type: string
        grants:
          type: array
          description: Path-level grants (a shallower grant is overridden by a deeper revoke).
          items:
            $ref: '#/components/schemas/PolicyEntry'
        revokes:
          type: array
          description: Path-level revokes (deep revoke wins).
          items:
            $ref: '#/components/schemas/PolicyEntry'
      additionalProperties: true
    PolicyCatalogEntry:
      allOf:
        - $ref: '#/components/schemas/PolicyDefinition'
        - type: object
          required: [deprecated, createdTime, updatedTime]
          properties:
            deprecated:
              type: boolean
            createdTime:
              type: integer
              format: int64
            updatedTime:
              type: integer
              format: int64
    PolicyPage:
      type: object
      required: [data, totalElements, totalPages, hasNext, page]
      properties:
        data:
          type: array
          items:
            $ref: '#/components/schemas/PolicyCatalogEntry'
        totalElements:
          type: integer
        totalPages:
          type: integer
        hasNext:
          type: boolean
        page:
          type: integer
    LoginRequest:
      type: object
      required: [username, password]
      properties:
        username:
          type: string
          format: email
        password:
          type: string
    LoginResponse:
      type: object
      required: [token, refreshToken]
      properties:
        token:
          type: string
        refreshToken:
          type: string
    UserInfo:
      type: object
      properties:
        id:
          $ref: '#/components/schemas/EntityIdObject'
        tenantId:
          $ref: '#/components/schemas/EntityIdObject'
        email:
          type: string
        authority:
          type: string
    EntityIdObject:
      type: object
      required: [id]
      properties:
        id:
          type: string
          format: uuid
        entityType:
          type: string
    DeviceCreateRequest:
      type: object
      required: [name]
      properties:
        name:
          type: string
        type:
          type: string
        label:
          type: string
    Device:
      type: object
      properties:
        id:
          $ref: '#/components/schemas/EntityIdObject'
        tenantId:
          $ref: '#/components/schemas/EntityIdObject'
        name:
          type: string
        type:
          type: string
        label:
          type: string
    DeviceCredentials:
      type: object
      properties:
        id:
          type: string
          format: uuid
        deviceId:
          $ref: '#/components/schemas/EntityIdObject'
        credentialsType:
          type: string
          enum: [ACCESS_TOKEN, X509_CERTIFICATE, MQTT_BASIC]
        credentialsId:
          type: string
    DeviceCredentialsUpdate:
      type: object
      required: [deviceId, credentialsType]
      properties:
        deviceId:
          $ref: '#/components/schemas/EntityIdObject'
        credentialsType:
          type: string
          enum: [ACCESS_TOKEN, X509_CERTIFICATE, MQTT_BASIC]
        credentialsId:
          type: string
    DeviceSecurityUpdate:
      type: object
      required: [securityStatus]
      properties:
        securityStatus:
          type: string
          enum: [ACTIVE, SUSPENDED]
    DeviceSecurityStatus:
      type: object
      required: [deviceId, securityStatus]
      properties:
        deviceId:
          type: string
          format: uuid
        securityStatus:
          type: string
          enum: [ACTIVE, SUSPENDED]
    ProvisionRequest:
      type: object
      required: [deviceName, provisionDeviceKey, provisionDeviceSecret]
      properties:
        deviceName:
          type: string
        provisionDeviceKey:
          type: string
        provisionDeviceSecret:
          type: string
        token:
          type: string
    ProvisionResponse:
      type: object
      required: [status]
      properties:
        status:
          type: string
          enum: [SUCCESS, FAILURE]
        credentialsType:
          type: string
          description: Classic ThingsBoard credential type. Successful provisioning keeps ACCESS_TOKEN for compatibility.
        credentialsValue:
          type: string
          description: Classic ThingsBoard device access token.
        deviceId:
          type: string
          description: Provisioned ThingsFlow device id and JWT subject.
        tenantId:
          type: string
          description: Tenant that owns the provisioned device.
        deviceJwt:
          $ref: '#/components/schemas/DeviceJwt'
        errorMsg:
          type: string
    DeviceJwt:
      type: object
      required: [token, tokenType, expiresAt, issuer, audience, subject, mqttIdentity, mqttUsername]
      properties:
        token:
          type: string
          description: ES256 bearer JWT signed by Flow Core for native device MQTT and HTTP telemetry.
        tokenType:
          type: string
          enum: [Bearer]
        expiresAt:
          type: integer
          format: int64
        issuer:
          type: string
        audience:
          type: string
        subject:
          type: string
          description: Canonical device id.
        mqttIdentity:
          type: string
          description: Topic-safe device identity used by the MQTT edge guard.
        mqttUsername:
          type: string
          description: MQTT username form returned for clients that expect the API contract.
    JWKS:
      type: object
      required: [keys]
      properties:
        keys:
          type: array
          items:
            type: object
            additionalProperties: true
    TelemetryKv:
      type: object
      properties:
        ts:
          type: integer
          format: int64
        value:
          oneOf:
            - type: string
            - type: number
            - type: boolean
            - type: object
    Relation:
      type: object
      required: [from, to, type]
      properties:
        from:
          $ref: '#/components/schemas/EntityRef'
        to:
          $ref: '#/components/schemas/EntityRef'
        type:
          type: string
          example: Contains
        typeGroup:
          type: string
          default: COMMON
    EntityRef:
      type: object
      required: [entityType, id]
      properties:
        entityType:
          type: string
          enum: [DEVICE, ASSET, CUSTOMER, ENTITY_VIEW, TENANT]
        id:
          type: string
          format: uuid
    Twin:
      type: object
      required: [thingId, policyId, definition, entity, attributes, features, relations]
      properties:
        thingId:
          type: string
        policyId:
          type: string
        definition:
          type: string
        entity:
          $ref: '#/components/schemas/EntityRef'
        attributes:
          type: object
          additionalProperties: true
        features:
          type: object
          additionalProperties: true
        relations:
          type: array
          items:
            type: object
            additionalProperties: true
    TwinWriteResult:
      type: object
      required: [persisted]
      properties:
        persisted:
          type: boolean
    TwinAttributesUpdate:
      type: object
      required: [attributes]
      properties:
        attributes:
          type: object
          additionalProperties: true
          description: Attribute values validated against the pinned model's attribute declarations.
    TwinFeaturesUpdate:
      type: object
      required: [features]
      properties:
        features:
          type: object
          additionalProperties:
            type: object
            properties:
              properties:
                type: object
                additionalProperties: true
                description: Reported feature property values, persisted as feature.<name>.<property>.
              desiredProperties:
                type: object
                additionalProperties: true
                description: Desired feature property values, persisted as feature.<name>.desired.<property> and surfaced on the twin read (R5).
    TwinPage:
      type: object
      required: [data, nextPageLink, hasNext, totalElements]
      properties:
        data:
          type: array
          items:
            $ref: '#/components/schemas/Twin'
        nextPageLink:
          type: string
          nullable: true
          description: Opaque cursor for the next page, or null when hasNext is false.
        hasNext:
          type: boolean
        totalElements:
          type: integer
