openapi: 3.1.0
info:
  title: SENS Platform API
  version: 0.1.0-draft
  summary: Draft API contract for the SENS Platform.
  description: |
    This OpenAPI document describes the currently available SENS Platform API
    surface. Product DTO schemas are included as draft components, but product
    paths are only added after the corresponding API behavior exists.
  x-status: draft
servers:
  - url: https://api.dev.iot-sens.schlossers.at
    description: Test environment — use this for interactive requests.
  - url: http://127.0.0.1:3000
    description: Local development.
  - url: https://api.iot-sens.schlossers.at
    description: Production — requests can change live customer data.
paths:
  /version:
    get:
      tags:
        - Diagnostics
      summary: Return the deployed platform version.
      description: |
        Returns the immutable platform version configured for the running
        platform-api instance. Kubernetes deployments set this value from the
        rendered image tag so smoke checks can verify the deployed release.
      operationId: getVersion
      responses:
        "200":
          description: The deployed platform version.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/VersionResponse"
  /test:
    get:
      tags:
        - Diagnostics
      summary: Exercise the platform API request path.
      description: |
        Emits a structured service log entry and returns a simple success
        response. This endpoint is intended for deployment smoke testing only.
      operationId: getTest
      responses:
        "200":
          description: The test endpoint was reached.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/TestResponse"
  /v1/auth/login:
    post:
      tags:
        - Authentication
      summary: Create a local password session.
      description: |
        Accepts a normalized username or email address and an unmodified
        password. On success, returns a short-lived access token and sets the
        rotating refresh token in a host-only HttpOnly cookie. Authentication
        responses are never cacheable.
      operationId: loginWithPassword
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: "#/components/schemas/AuthLoginRequest"
      responses:
        "200":
          $ref: "#/components/responses/AuthTokenResponse"
        "400":
          $ref: "#/components/responses/ErrorResponse"
        "401":
          $ref: "#/components/responses/ErrorResponse"
        "429":
          $ref: "#/components/responses/RateLimitedResponse"
        "503":
          $ref: "#/components/responses/ErrorResponse"
        "500":
          $ref: "#/components/responses/ErrorResponse"
  /v1/auth/refresh:
    post:
      tags:
        - Authentication
      summary: Rotate the current refresh token.
      description: |
        Requires the refresh cookie and an Origin header that exactly matches
        the configured allowlist. A successful rotation extends the session's
        seven-day inactivity deadline.
      operationId: refreshAccessToken
      security:
        - RefreshCookie: []
      responses:
        "200":
          $ref: "#/components/responses/AuthTokenResponse"
        "401":
          $ref: "#/components/responses/ErrorResponse"
        "403":
          $ref: "#/components/responses/ErrorResponse"
        "409":
          $ref: "#/components/responses/RefreshConflictResponse"
        "503":
          $ref: "#/components/responses/ErrorResponse"
        "500":
          $ref: "#/components/responses/ErrorResponse"
  /v1/auth/logout:
    post:
      tags:
        - Authentication
      summary: Revoke the refresh session and clear its cookie.
      description: Requires an exactly allowed Origin header.
      operationId: logoutCurrentRefreshSession
      security:
        - RefreshCookie: []
      responses:
        "204":
          description: The cookie was cleared and the matching session was revoked.
          headers:
            Cache-Control:
              schema:
                type: string
                const: no-store
        "403":
          $ref: "#/components/responses/ErrorResponse"
        "503":
          $ref: "#/components/responses/ErrorResponse"
        "500":
          $ref: "#/components/responses/ErrorResponse"
  /v1/auth/logout-all:
    post:
      tags:
        - Authentication
      summary: Revoke every session for the authenticated user.
      operationId: logoutAllSessions
      security:
        - AccessToken: []
      responses:
        "204":
          description: All user sessions were revoked.
          headers:
            Cache-Control:
              schema:
                type: string
                const: no-store
        "401":
          $ref: "#/components/responses/BearerAuthenticationErrorResponse"
        "503":
          $ref: "#/components/responses/ErrorResponse"
        "500":
          $ref: "#/components/responses/ErrorResponse"
  /v1/auth/me:
    get:
      tags:
        - Authentication
      summary: Return the current user, session, roles, and membership grants.
      operationId: getCurrentAuthentication
      security:
        - AccessToken: []
      responses:
        "200":
          description: Current authentication and authorization context.
          headers:
            Cache-Control:
              schema:
                type: string
                const: no-store
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/AuthMeResponse"
        "401":
          $ref: "#/components/responses/BearerAuthenticationErrorResponse"
        "503":
          $ref: "#/components/responses/ErrorResponse"
        "500":
          $ref: "#/components/responses/ErrorResponse"
  /v1/auth/sessions:
    get:
      tags:
        - Authentication
      summary: List the authenticated user's active sessions.
      operationId: listOwnAuthSessions
      security:
        - AccessToken: []
      responses:
        "200":
          description: At most ten active sessions, without network or user-agent data.
          headers:
            Cache-Control:
              schema:
                type: string
                const: no-store
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/AuthSessionsResponse"
        "401":
          $ref: "#/components/responses/BearerAuthenticationErrorResponse"
        "403":
          $ref: "#/components/responses/ErrorResponse"
        "503":
          $ref: "#/components/responses/ErrorResponse"
        "500":
          $ref: "#/components/responses/ErrorResponse"
  /v1/auth/sessions/{sessionId}:
    delete:
      tags:
        - Authentication
      summary: Revoke one session owned by the authenticated user.
      operationId: revokeOwnAuthSession
      security:
        - AccessToken: []
      parameters:
        - name: sessionId
          in: path
          required: true
          schema:
            $ref: "#/components/schemas/AuthSessionId"
      responses:
        "204":
          description: The session was revoked.
          headers:
            Cache-Control:
              schema:
                type: string
                const: no-store
        "400":
          $ref: "#/components/responses/ErrorResponse"
        "401":
          $ref: "#/components/responses/BearerAuthenticationErrorResponse"
        "403":
          $ref: "#/components/responses/ErrorResponse"
        "404":
          $ref: "#/components/responses/ErrorResponse"
        "503":
          $ref: "#/components/responses/ErrorResponse"
        "500":
          $ref: "#/components/responses/ErrorResponse"
  /v1/auth/change-password:
    post:
      tags:
        - Authentication
      summary: Replace the local password and every existing session.
      description: |
        Verifies the previous password, enforces the local password policy,
        revokes all sessions, and returns a new current session.
      operationId: changeOwnPassword
      security:
        - AccessToken: []
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: "#/components/schemas/ChangePasswordRequest"
      responses:
        "200":
          $ref: "#/components/responses/AuthTokenResponse"
        "400":
          $ref: "#/components/responses/ErrorResponse"
        "401":
          $ref: "#/components/responses/ErrorResponse"
        "429":
          $ref: "#/components/responses/RateLimitedResponse"
        "503":
          $ref: "#/components/responses/ErrorResponse"
        "500":
          $ref: "#/components/responses/ErrorResponse"
  /v1/tenants:
    get:
      tags: [Core administration]
      summary: List tenants visible to the authenticated actor.
      description: >-
        Returns Tenants in stable ascending creation order with the Tenant ID
        as a unique tie-breaker. Items and totalCount use the same search,
        status, and authorization predicates in one database snapshot. A cursor
        is bound to the caller, filters, and page size. Malformed, stale,
        oversized, or query-incompatible cursors fail with VALIDATION_FAILED.
      operationId: listTenants
      security: [{ AccessToken: [] }]
      parameters:
        - $ref: "#/components/parameters/StatusFilter"
        - $ref: "#/components/parameters/SearchFilter"
        - $ref: "#/components/parameters/Cursor"
        - $ref: "#/components/parameters/TenantPageLimit"
      responses:
        "200":
          description: Stable tenant cursor page.
          content:
            application/json:
              schema: { $ref: "#/components/schemas/TenantListResponse" }
        "400": { $ref: "#/components/responses/ErrorResponse" }
        default: { $ref: "#/components/responses/ErrorResponse" }
    post:
      tags: [Core administration]
      summary: Create a tenant as a platform administrator.
      operationId: createTenant
      security: [{ AccessToken: [] }]
      requestBody:
        required: true
        content:
          application/json:
            schema: { $ref: "#/components/schemas/CreateTenantRequest" }
      responses:
        "201":
          description: Tenant created.
          headers:
            Location:
              description: Canonical numeric Tenant URL.
              schema: { type: string, example: /v1/tenants/17 }
          content:
            application/json:
              schema: { $ref: "#/components/schemas/Tenant" }
        default: { $ref: "#/components/responses/ErrorResponse" }
  /v1/tenants/{tenantNumber}:
    parameters:
      - $ref: "#/components/parameters/TenantNumber"
    get:
      tags: [Core administration]
      summary: Read one visible tenant.
      operationId: getTenant
      security: [{ AccessToken: [] }]
      responses:
        "200":
          description: Tenant details.
          content:
            application/json:
              schema: { $ref: "#/components/schemas/Tenant" }
        default: { $ref: "#/components/responses/ErrorResponse" }
    patch:
      tags: [Core administration]
      summary: Update an authorized tenant.
      operationId: updateTenant
      security: [{ AccessToken: [] }]
      requestBody:
        required: true
        content:
          application/json:
            schema: { $ref: "#/components/schemas/UpdateTenantRequest" }
      responses:
        "200":
          description: Tenant updated.
          content:
            application/json:
              schema: { $ref: "#/components/schemas/Tenant" }
        default: { $ref: "#/components/responses/ErrorResponse" }
    delete:
      tags: [Core administration]
      summary: Soft-disable a tenant as a platform administrator.
      operationId: disableTenant
      security: [{ AccessToken: [] }]
      responses:
        "204": { description: Tenant disabled idempotently. }
        default: { $ref: "#/components/responses/ErrorResponse" }
  /v1/tenants/{tenantNumber}/organization-units:
    parameters:
      - $ref: "#/components/parameters/TenantNumber"
    get:
      tags: [Core administration]
      summary: List all accessible organization units and navigation ancestors.
      description: >-
        Compatibility collection ordered by createdAt and organizationUnitId.
        The API defaults to 50 rows. Its opaque cursor is bound to the actor,
        current platform roles and memberships, Tenant, normalized search,
        status filter, and page size. Changed inputs or a missing cursor
        position fail with VALIDATION_FAILED.
      operationId: listOrganizationUnits
      security: [{ AccessToken: [] }]
      parameters:
        - $ref: "#/components/parameters/StatusFilter"
        - $ref: "#/components/parameters/SearchFilter"
        - $ref: "#/components/parameters/Cursor"
        - $ref: "#/components/parameters/Limit"
      responses:
        "200":
          description: Stable organization-unit cursor page.
          content:
            application/json:
              schema:
                { $ref: "#/components/schemas/OrganizationUnitListResponse" }
        default: { $ref: "#/components/responses/ErrorResponse" }
    post:
      tags: [Core administration]
      summary: Create an organization unit in an administrable scope.
      operationId: createOrganizationUnit
      security: [{ AccessToken: [] }]
      requestBody:
        required: true
        content:
          application/json:
            schema:
              { $ref: "#/components/schemas/CreateOrganizationUnitRequest" }
      responses:
        "201":
          description: Organization unit created.
          headers:
            Location:
              description: Canonical flat organization-unit URL.
              schema:
                {
                  type: string,
                  example: /v1/organization-units/10000000-0000-4000-8000-000000000001
                }
          content:
            application/json:
              schema: { $ref: "#/components/schemas/OrganizationUnit" }
        default: { $ref: "#/components/responses/ErrorResponse" }
  /v1/tenants/{tenantNumber}/organization-units/roots:
    parameters:
      - $ref: "#/components/parameters/TenantNumber"
    get:
      tags: [Core administration]
      summary: List accessible root organization units and navigation ancestors.
      operationId: listOrganizationUnitRoots
      security: [{ AccessToken: [] }]
      parameters:
        - $ref: "#/components/parameters/StatusFilter"
        - $ref: "#/components/parameters/Cursor"
        - $ref: "#/components/parameters/Limit"
      responses:
        "200":
          description: Stable root organization-unit cursor page.
          content:
            application/json:
              schema:
                { $ref: "#/components/schemas/OrganizationUnitListResponse" }
        default: { $ref: "#/components/responses/ErrorResponse" }
  /v1/tenants/{tenantNumber}/organization-units/search:
    parameters:
      - $ref: "#/components/parameters/TenantNumber"
    get:
      tags: [Core administration]
      summary: Search the complete visible organization hierarchy.
      operationId: searchOrganizationUnits
      security: [{ AccessToken: [] }]
      parameters:
        - $ref: "#/components/parameters/OrganizationSearchQuery"
        - $ref: "#/components/parameters/StatusFilter"
        - $ref: "#/components/parameters/Cursor"
        - $ref: "#/components/parameters/Limit"
      responses:
        "200":
          description: Stable search page with each match's complete visible ancestor path.
          content:
            application/json:
              schema:
                { $ref: "#/components/schemas/OrganizationUnitSearchResponse" }
        default: { $ref: "#/components/responses/ErrorResponse" }
  /v1/organization-units/{organizationUnitId}/children:
    parameters:
      - $ref: "#/components/parameters/OrganizationUnitId"
    get:
      tags: [Core administration]
      summary: List the direct visible children of an organization unit.
      operationId: listOrganizationUnitChildren
      security: [{ AccessToken: [] }]
      parameters:
        - $ref: "#/components/parameters/StatusFilter"
        - $ref: "#/components/parameters/Cursor"
        - $ref: "#/components/parameters/Limit"
      responses:
        "200":
          description: Stable direct-child cursor page.
          content:
            application/json:
              schema:
                { $ref: "#/components/schemas/OrganizationUnitListResponse" }
        default: { $ref: "#/components/responses/ErrorResponse" }
  /v1/organization-units/{organizationUnitId}:
    parameters:
      - $ref: "#/components/parameters/OrganizationUnitId"
    get:
      tags: [Core administration]
      summary: Read an accessible unit or navigation ancestor.
      operationId: getOrganizationUnit
      security: [{ AccessToken: [] }]
      responses:
        "200":
          description: Organization unit details.
          content:
            application/json:
              schema: { $ref: "#/components/schemas/OrganizationUnit" }
        default: { $ref: "#/components/responses/ErrorResponse" }
    patch:
      tags: [Core administration]
      summary: Update, reparent, disable, or reactivate an organization unit.
      operationId: updateOrganizationUnit
      security: [{ AccessToken: [] }]
      requestBody:
        required: true
        content:
          application/json:
            schema:
              { $ref: "#/components/schemas/UpdateOrganizationUnitRequest" }
      responses:
        "200":
          description: Organization unit updated.
          content:
            application/json:
              schema: { $ref: "#/components/schemas/OrganizationUnit" }
        default: { $ref: "#/components/responses/ErrorResponse" }
    delete:
      tags: [Core administration]
      summary: Soft-disable an organization subtree.
      operationId: disableOrganizationUnit
      security: [{ AccessToken: [] }]
      responses:
        "204": { description: Organization unit disabled. }
        default: { $ref: "#/components/responses/ErrorResponse" }
  /v1/users:
    get:
      tags: [User administration]
      summary: List global users as a platform administrator.
      description: >-
        Returns an exact count for the same authorization scope and filters as
        the page. Rows use stable ascending (createdAt, userId) order. Cursors
        are bound to the actor, authorization scope, filters, and page size.
      operationId: listUsers
      security: [{ AccessToken: [] }]
      parameters:
        - $ref: "#/components/parameters/StatusFilter"
        - $ref: "#/components/parameters/SearchFilter"
        - $ref: "#/components/parameters/Cursor"
        - $ref: "#/components/parameters/IdentityPageLimit"
      responses:
        "200":
          description: Stable global-user cursor page.
          content:
            application/json:
              schema: { $ref: "#/components/schemas/UserAdminListResponse" }
        default: { $ref: "#/components/responses/ErrorResponse" }
    post:
      tags: [User administration]
      summary: Provision a global local-password user.
      operationId: createUser
      security: [{ AccessToken: [] }]
      requestBody:
        required: true
        content:
          application/json:
            schema: { $ref: "#/components/schemas/CreateUserRequest" }
      responses:
        "201":
          description: User and forced-change credential created atomically.
          content:
            application/json:
              schema: { $ref: "#/components/schemas/UserAdmin" }
        default: { $ref: "#/components/responses/ErrorResponse" }
  /v1/users/{userId}:
    parameters:
      - $ref: "#/components/parameters/UserId"
    get:
      tags: [User administration]
      summary: Read one global user.
      operationId: getUser
      security: [{ AccessToken: [] }]
      responses:
        "200":
          description: Global user details.
          content:
            application/json:
              schema: { $ref: "#/components/schemas/UserAdmin" }
        default: { $ref: "#/components/responses/ErrorResponse" }
    patch:
      tags: [User administration]
      summary: Update or reactivate a global user.
      operationId: updateUser
      security: [{ AccessToken: [] }]
      requestBody:
        required: true
        content:
          application/json:
            schema: { $ref: "#/components/schemas/UpdateUserRequest" }
      responses:
        "200":
          description: Global user updated.
          content:
            application/json:
              schema: { $ref: "#/components/schemas/UserAdmin" }
        default: { $ref: "#/components/responses/ErrorResponse" }
    delete:
      tags: [User administration]
      summary: Soft-disable a user and revoke all sessions.
      operationId: disableUser
      security: [{ AccessToken: [] }]
      responses:
        "204": { description: User disabled and sessions revoked. }
        default: { $ref: "#/components/responses/ErrorResponse" }
  /v1/users/{userId}/reset-password:
    parameters:
      - $ref: "#/components/parameters/UserId"
    post:
      tags: [User administration]
      summary: Set a temporary password and revoke all sessions.
      operationId: resetUserPassword
      security: [{ AccessToken: [] }]
      requestBody:
        required: true
        content:
          application/json:
            schema: { $ref: "#/components/schemas/ResetUserPasswordRequest" }
      responses:
        "204": { description: Temporary password set. }
        default: { $ref: "#/components/responses/ErrorResponse" }
  /v1/users/{userId}/platform-roles/platform_admin:
    parameters:
      - $ref: "#/components/parameters/UserId"
    put:
      tags: [User administration]
      summary: Grant the global platform administrator role.
      operationId: grantPlatformAdmin
      security: [{ AccessToken: [] }]
      responses:
        "204": { description: Platform role active idempotently. }
        default: { $ref: "#/components/responses/ErrorResponse" }
    delete:
      tags: [User administration]
      summary: Revoke the global platform administrator role.
      operationId: revokePlatformAdmin
      security: [{ AccessToken: [] }]
      responses:
        "204": { description: Platform role revoked. }
        default: { $ref: "#/components/responses/ErrorResponse" }
  /v1/tenants/{tenantNumber}/users:
    parameters:
      - $ref: "#/components/parameters/TenantNumber"
    get:
      tags: [User administration]
      summary: List users with membership history in a tenant.
      description: >-
        Returns an exact count for the same tenant authorization scope and
        filters as the page. Rows use stable ascending (createdAt, userId)
        order. Global roles and metadata are not exposed.
      operationId: listTenantUsers
      security: [{ AccessToken: [] }]
      parameters:
        - $ref: "#/components/parameters/StatusFilter"
        - $ref: "#/components/parameters/SearchFilter"
        - $ref: "#/components/parameters/Cursor"
        - $ref: "#/components/parameters/IdentityPageLimit"
      responses:
        "200":
          description: Tenant users without global role or metadata fields.
          content:
            application/json:
              schema: { $ref: "#/components/schemas/TenantUserListResponse" }
        default: { $ref: "#/components/responses/ErrorResponse" }
  /v1/tenants/{tenantNumber}/users/{userId}:
    parameters:
      - $ref: "#/components/parameters/TenantNumber"
      - $ref: "#/components/parameters/UserId"
    get:
      tags: [User administration]
      summary: Read a user with tenant membership history.
      operationId: getTenantUser
      security: [{ AccessToken: [] }]
      responses:
        "200":
          description: Tenant-scoped user details.
          content:
            application/json:
              schema: { $ref: "#/components/schemas/TenantUser" }
        default: { $ref: "#/components/responses/ErrorResponse" }
  /v1/tenants/{tenantNumber}/memberships:
    parameters:
      - $ref: "#/components/parameters/TenantNumber"
    get:
      tags: [Membership administration]
      summary: List tenant membership grants and their history.
      description: >-
        Returns an exact count for the same tenant authorization scope and
        filters as the page. Rows use stable ascending (createdAt,
        membershipId) order. Cursors are bound to the actor, authorization
        scope, filters, and page size.
      operationId: listMemberships
      security: [{ AccessToken: [] }]
      parameters:
        - $ref: "#/components/parameters/StatusFilter"
        - $ref: "#/components/parameters/SearchFilter"
        - $ref: "#/components/parameters/Cursor"
        - $ref: "#/components/parameters/IdentityPageLimit"
        - name: userId
          in: query
          schema: { $ref: "#/components/schemas/UserId" }
        - name: role
          in: query
          schema: { $ref: "#/components/schemas/TenantRole" }
      responses:
        "200":
          description: Stable membership cursor page.
          content:
            application/json:
              schema: { $ref: "#/components/schemas/MembershipListResponse" }
        default: { $ref: "#/components/responses/ErrorResponse" }
    post:
      tags: [Membership administration]
      summary: Create a tenant membership grant.
      operationId: createMembership
      security: [{ AccessToken: [] }]
      requestBody:
        required: true
        content:
          application/json:
            schema: { $ref: "#/components/schemas/CreateMembershipRequest" }
      responses:
        "201":
          description: Membership grant created.
          headers:
            Location:
              description: Canonical flat membership URL.
              schema:
                {
                  type: string,
                  example: /v1/memberships/10000000-0000-4000-8000-000000000001
                }
          content:
            application/json:
              schema: { $ref: "#/components/schemas/Membership" }
        default: { $ref: "#/components/responses/ErrorResponse" }
  /v1/memberships/{membershipId}:
    parameters:
      - $ref: "#/components/parameters/MembershipId"
    get:
      tags: [Membership administration]
      summary: Read one membership grant.
      operationId: getMembership
      security: [{ AccessToken: [] }]
      responses:
        "200":
          description: Membership details.
          content:
            application/json:
              schema: { $ref: "#/components/schemas/Membership" }
        default: { $ref: "#/components/responses/ErrorResponse" }
    patch:
      tags: [Membership administration]
      summary: Update, disable, or reactivate a membership grant.
      operationId: updateMembership
      security: [{ AccessToken: [] }]
      requestBody:
        required: true
        content:
          application/json:
            schema: { $ref: "#/components/schemas/UpdateMembershipRequest" }
      responses:
        "200":
          description: Membership grant updated.
          content:
            application/json:
              schema: { $ref: "#/components/schemas/Membership" }
        default: { $ref: "#/components/responses/ErrorResponse" }
    delete:
      tags: [Membership administration]
      summary: Soft-disable a membership grant.
      operationId: disableMembership
      security: [{ AccessToken: [] }]
      responses:
        "204": { description: Membership grant disabled. }
        default: { $ref: "#/components/responses/ErrorResponse" }
  /v1/audit-logs:
    get:
      tags: [Audit]
      summary: Query global audit events as a platform administrator.
      description: |
        Returns 25 metadata events at a time without an exact total. The first
        page fixes `to` as the upper investigation anchor. Continuations must
        reuse that anchor and every filter; refresh without a cursor creates a
        new anchor.
      operationId: listGlobalAuditEvents
      security: [{ AccessToken: [] }]
      parameters:
        - $ref: "#/components/parameters/AuditFrom"
        - $ref: "#/components/parameters/AuditTo"
        - $ref: "#/components/parameters/AuditEventTypeFilter"
        - $ref: "#/components/parameters/AuditActionFilter"
        - $ref: "#/components/parameters/AuditActorIdFilter"
        - $ref: "#/components/parameters/AuditEntityTypeFilter"
        - $ref: "#/components/parameters/AuditEntityIdFilter"
        - $ref: "#/components/parameters/AuditCorrelationFilter"
        - $ref: "#/components/parameters/AuditMethodFilter"
        - $ref: "#/components/parameters/AuditRouteFilter"
        - $ref: "#/components/parameters/AuditStatusCodeFilter"
        - $ref: "#/components/parameters/Cursor"
        - $ref: "#/components/parameters/TimelineLimit"
      responses:
        "200":
          description: Reverse-chronological audit event page.
          content:
            application/json:
              schema: { $ref: "#/components/schemas/AuditEventListResponse" }
        default: { $ref: "#/components/responses/ErrorResponse" }
  /v1/tenants/{tenantNumber}/audit-logs:
    parameters:
      - $ref: "#/components/parameters/TenantNumber"
    get:
      tags: [Audit]
      summary: Query safely tenant-scoped audit events.
      description: |
        Returns 25 tenant-scoped metadata events at a time without an exact
        total. The first page fixes `to`; every continuation keeps that anchor,
        Tenant scope, and all filters.
      operationId: listTenantAuditEvents
      security: [{ AccessToken: [] }]
      parameters:
        - $ref: "#/components/parameters/AuditFrom"
        - $ref: "#/components/parameters/AuditTo"
        - $ref: "#/components/parameters/AuditEventTypeFilter"
        - $ref: "#/components/parameters/AuditActionFilter"
        - $ref: "#/components/parameters/AuditActorIdFilter"
        - $ref: "#/components/parameters/AuditEntityTypeFilter"
        - $ref: "#/components/parameters/AuditEntityIdFilter"
        - $ref: "#/components/parameters/AuditCorrelationFilter"
        - $ref: "#/components/parameters/AuditMethodFilter"
        - $ref: "#/components/parameters/AuditRouteFilter"
        - $ref: "#/components/parameters/AuditStatusCodeFilter"
        - $ref: "#/components/parameters/Cursor"
        - $ref: "#/components/parameters/TimelineLimit"
      responses:
        "200":
          description: Reverse-chronological tenant audit event page.
          content:
            application/json:
              schema: { $ref: "#/components/schemas/AuditEventListResponse" }
        default: { $ref: "#/components/responses/ErrorResponse" }
  /v1/raw-ingestion-messages:
    get:
      tags: [Raw ingestion]
      summary: Query raw ingestion metadata as a platform administrator.
      description: |
        Returns 25 reverse-chronological metadata records at a time without an
        exact total. `to` is the fixed upper investigation anchor, and every
        continuation must reuse it and all active filters. Exact MQTT and sensor
        payloads are deliberately excluded and loaded only through the detail
        operation. Responses are not cacheable.
      operationId: listRawIngestionMessages
      security: [{ AccessToken: [] }]
      parameters:
        - $ref: "#/components/parameters/RawIngestionFrom"
        - $ref: "#/components/parameters/RawIngestionTo"
        - $ref: "#/components/parameters/RawIngestionProcessingStatusFilter"
        - $ref: "#/components/parameters/RawIngestionDevEuiFilter"
        - $ref: "#/components/parameters/RawIngestionCustomerFilter"
        - $ref: "#/components/parameters/RawIngestionTenantFilter"
        - $ref: "#/components/parameters/RawIngestionScopeFilter"
        - $ref: "#/components/parameters/Cursor"
        - $ref: "#/components/parameters/TimelineLimit"
      responses:
        "200":
          description: Reverse-chronological raw-ingestion metadata page.
          headers:
            Cache-Control:
              schema: { type: string, const: no-store }
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/RawIngestionMessageListResponse"
        default: { $ref: "#/components/responses/ErrorResponse" }
  /v1/raw-ingestion-messages/{rawIngestionMessageId}:
    parameters:
      - $ref: "#/components/parameters/RawIngestionMessageId"
    get:
      tags: [Raw ingestion]
      summary: Load one raw-ingestion message including exact payloads.
      description: |
        The receive timestamp is part of the hypertable key and is therefore
        required together with the message ID. Only platform administrators may
        access this sensitive payload response. Responses are not cacheable.
      operationId: getRawIngestionMessage
      security: [{ AccessToken: [] }]
      parameters:
        - $ref: "#/components/parameters/RawIngestionReceivedAt"
      responses:
        "200":
          description: Raw-ingestion message including exact transport bytes.
          headers:
            Cache-Control:
              schema: { type: string, const: no-store }
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/RawIngestionMessageDetail"
        default: { $ref: "#/components/responses/ErrorResponse" }
  /v1/tenants/{tenantNumber}/imports/iota-snapshot:
    parameters:
      - $ref: "#/components/parameters/TenantNumber"
    post:
      tags: [Administration]
      summary: Import a temporary create-only snapshot from IOTA.
      description: |
        Loads every tenant visible to the supplied IOTA credentials and creates
        the valid device types and devices in the active target tenant. Existing
        SENS records are never changed, deleted, reactivated, or moved. The
        credentials are used only for this request and are never persisted or
        included in logs or audit metadata. Only platform administrators may
        call this temporary V1 migration operation. Responses are not cacheable.
      operationId: importIotaSnapshot
      security: [{ AccessToken: [] }]
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: "#/components/schemas/IotaSnapshotImportRequest"
      responses:
        "200":
          description: The valid snapshot subset was persisted atomically.
          headers:
            Cache-Control:
              schema: { type: string, const: no-store }
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/IotaSnapshotImportResult"
        "400": { $ref: "#/components/responses/ErrorResponse" }
        "401":
          { $ref: "#/components/responses/BearerAuthenticationErrorResponse" }
        "403": { $ref: "#/components/responses/ErrorResponse" }
        "404": { $ref: "#/components/responses/ErrorResponse" }
        "409": { $ref: "#/components/responses/ErrorResponse" }
        "500": { $ref: "#/components/responses/ErrorResponse" }
        "502": { $ref: "#/components/responses/ErrorResponse" }
        "503": { $ref: "#/components/responses/ErrorResponse" }
        "504": { $ref: "#/components/responses/ErrorResponse" }
  /v1/tenants/{tenantNumber}/device-types:
    parameters:
      - $ref: "#/components/parameters/TenantNumber"
    get:
      tags: [Device types]
      summary: List tenant device types.
      description: >-
        Returns Device Types in stable ascending creation order with the Device
        Type ID as a unique tie-breaker. Items and totalCount use the same
        search, status filter, Tenant and organization visibility, and database
        snapshot. A cursor is bound to the caller, scope, filters, and page
        size. Malformed, stale, oversized, or query-incompatible cursors fail
        with VALIDATION_FAILED.
      operationId: listDeviceTypes
      security: [{ AccessToken: [] }]
      parameters:
        - $ref: "#/components/parameters/StatusFilter"
        - $ref: "#/components/parameters/SearchFilter"
        - $ref: "#/components/parameters/Cursor"
        - $ref: "#/components/parameters/DevicePageLimit"
      responses:
        "200":
          description: Stable device-type cursor page.
          content:
            application/json:
              schema: { $ref: "#/components/schemas/DeviceTypeListResponse" }
        default: { $ref: "#/components/responses/ErrorResponse" }
    post:
      tags: [Device types]
      summary: Create a device type with an immutable key.
      operationId: createDeviceType
      security: [{ AccessToken: [] }]
      requestBody:
        required: true
        content:
          application/json:
            schema: { $ref: "#/components/schemas/CreateDeviceTypeRequest" }
      responses:
        "201":
          description: Device type created.
          headers:
            Location:
              description: Canonical flat Device-Type URL.
              schema:
                {
                  type: string,
                  example: /v1/device-types/10000000-0000-4000-8000-000000000001
                }
          content:
            application/json:
              schema: { $ref: "#/components/schemas/DeviceType" }
        default: { $ref: "#/components/responses/ErrorResponse" }
  /v1/device-types/{deviceTypeId}:
    parameters:
      - $ref: "#/components/parameters/DeviceTypeId"
    get:
      tags: [Device types]
      summary: Read one tenant device type.
      operationId: getDeviceType
      security: [{ AccessToken: [] }]
      responses:
        "200":
          description: Device type details and its uniquely associated telemetry flow.
          content:
            application/json:
              schema: { $ref: "#/components/schemas/DeviceType" }
        default: { $ref: "#/components/responses/ErrorResponse" }
    patch:
      tags: [Device types]
      summary: Update a device type without changing its key.
      operationId: updateDeviceType
      security: [{ AccessToken: [] }]
      requestBody:
        required: true
        content:
          application/json:
            schema: { $ref: "#/components/schemas/UpdateDeviceTypeRequest" }
      responses:
        "200":
          description: Device type updated.
          content:
            application/json:
              schema: { $ref: "#/components/schemas/DeviceType" }
        default: { $ref: "#/components/responses/ErrorResponse" }
  /v1/device-types/{deviceTypeId}/fields:
    parameters:
      - $ref: "#/components/parameters/DeviceTypeId"
    get:
      tags: [Device types]
      summary: List the authoritative data-field contract of a Device Type.
      operationId: listDeviceTypeFields
      security: [{ AccessToken: [] }]
      responses:
        "200":
          description: Active and deprecated field definitions.
          content:
            application/json:
              schema:
                type: array
                maxItems: 100
                items: { $ref: "#/components/schemas/DeviceFieldDefinition" }
        default: { $ref: "#/components/responses/ErrorResponse" }
  /v1/device-types/{deviceTypeId}/fields/{key}:
    parameters:
      - $ref: "#/components/parameters/DeviceTypeId"
      - name: key
        in: path
        required: true
        schema: { type: string, pattern: "^[a-z][a-z0-9._-]{0,119}$" }
    put:
      tags: [Device types]
      summary: Create or compatibly update one Device Type data field.
      operationId: putDeviceTypeField
      security: [{ AccessToken: [] }]
      parameters:
        - name: If-Match
          in: header
          description: Numeric ETag required when the field already exists.
          schema: { type: string, pattern: '^(?:W/)?"[1-9][0-9]*"$' }
      requestBody:
        required: true
        content:
          application/json:
            schema:
              { $ref: "#/components/schemas/PutDeviceFieldDefinitionRequest" }
      responses:
        "200":
          description: Field stored. The returned numeric ETag is its new lock version.
          headers:
            ETag: { schema: { type: string } }
          content:
            application/json:
              schema: { $ref: "#/components/schemas/DeviceFieldDefinition" }
        "409": { $ref: "#/components/responses/ErrorResponse" }
        "422": { $ref: "#/components/responses/ErrorResponse" }
        default: { $ref: "#/components/responses/ErrorResponse" }
  /v1/device-types/{deviceTypeId}/fields/{key}/deprecate:
    parameters:
      - $ref: "#/components/parameters/DeviceTypeId"
      - name: key
        in: path
        required: true
        schema: { type: string, pattern: "^[a-z][a-z0-9._-]{0,119}$" }
    post:
      tags: [Device types]
      summary: Deprecate a data field without deleting its history.
      operationId: deprecateDeviceTypeField
      security: [{ AccessToken: [] }]
      parameters:
        - name: If-Match
          in: header
          required: true
          schema: { type: string, pattern: '^(?:W/)?"[1-9][0-9]*"$' }
      responses:
        "200":
          description: Field deprecated.
          headers:
            ETag: { schema: { type: string } }
          content:
            application/json:
              schema: { $ref: "#/components/schemas/DeviceFieldDefinition" }
        "409": { $ref: "#/components/responses/ErrorResponse" }
        default: { $ref: "#/components/responses/ErrorResponse" }
  /v1/tenants/{tenantNumber}/devices:
    parameters:
      - $ref: "#/components/parameters/TenantNumber"
    get:
      tags: [Devices]
      summary: List devices visible in the caller's organization scope.
      description: >-
        Returns devices in stable ascending creation order with the Device ID as
        a unique tie-breaker. Items and totalCount use the same filters,
        organization visibility, and database snapshot. A cursor is bound to the
        caller, scope, filters, and page size. Malformed, stale, oversized, or
        query-incompatible cursors fail with VALIDATION_FAILED.
      operationId: listDevices
      security: [{ AccessToken: [] }]
      parameters:
        - name: status
          in: query
          schema: { $ref: "#/components/schemas/DeviceStatus" }
        - $ref: "#/components/parameters/SearchFilter"
        - name: deviceTypeId
          in: query
          schema: { $ref: "#/components/schemas/DeviceTypeId" }
        - name: organizationUnitId
          in: query
          schema: { $ref: "#/components/schemas/OrganizationUnitId" }
        - $ref: "#/components/parameters/Cursor"
        - $ref: "#/components/parameters/DevicePageLimit"
      responses:
        "200":
          description: Scope-filtered stable device cursor page.
          content:
            application/json:
              schema: { $ref: "#/components/schemas/DeviceListResponse" }
        "400": { $ref: "#/components/responses/ErrorResponse" }
        default: { $ref: "#/components/responses/ErrorResponse" }
    post:
      tags: [Devices]
      summary: Create a device, optionally with its initial organization assignment.
      operationId: createDevice
      security: [{ AccessToken: [] }]
      requestBody:
        required: true
        content:
          application/json:
            schema: { $ref: "#/components/schemas/CreateDeviceRequest" }
      responses:
        "201":
          description: Device created.
          headers:
            Location:
              description: Canonical flat Device URL.
              schema:
                {
                  type: string,
                  example: /v1/devices/10000000-0000-4000-8000-000000000001
                }
          content:
            application/json:
              schema: { $ref: "#/components/schemas/Device" }
        default: { $ref: "#/components/responses/ErrorResponse" }
  /v1/devices/{deviceId}:
    parameters:
      - $ref: "#/components/parameters/DeviceId"
    get:
      tags: [Devices]
      summary: Read one scope-visible device.
      operationId: getDevice
      security: [{ AccessToken: [] }]
      responses:
        "200":
          description: Device with its current visible assignment.
          content:
            application/json:
              schema: { $ref: "#/components/schemas/Device" }
        default: { $ref: "#/components/responses/ErrorResponse" }
    patch:
      tags: [Devices]
      summary: Update the controlled device lifecycle or mutable fields.
      operationId: updateDevice
      security: [{ AccessToken: [] }]
      requestBody:
        required: true
        content:
          application/json:
            schema: { $ref: "#/components/schemas/UpdateDeviceRequest" }
      responses:
        "200":
          description: Device updated.
          content:
            application/json:
              schema: { $ref: "#/components/schemas/Device" }
        default: { $ref: "#/components/responses/ErrorResponse" }
  /v1/devices/{deviceId}/current-values:
    parameters:
      - $ref: "#/components/parameters/DeviceId"
    get:
      tags: [Devices]
      summary: Read the current value snapshot for all active fields of a visible Device.
      description: Requires telemetry:read and applies normal Device organization visibility.
      operationId: getDeviceCurrentValues
      security: [{ AccessToken: [] }]
      responses:
        "200":
          description: Every active field is returned; currentValue is null until first storage.
          content:
            application/json:
              schema:
                { $ref: "#/components/schemas/DeviceCurrentValuesResponse" }
        "404": { $ref: "#/components/responses/ErrorResponse" }
        default: { $ref: "#/components/responses/ErrorResponse" }
  /v1/devices/{deviceId}/measurements:
    parameters:
      - $ref: "#/components/parameters/DeviceId"
    get:
      tags: [Devices]
      summary: Read bounded historical measurements for one visible Device.
      description: |
        Requires `telemetry:read`. Device visibility follows the current
        inventory scope; every returned measurement is additionally filtered
        by its historical organization assignment against the caller's current
        effective grants. Results are ordered by `observedAt` and
        `measurementId`, both descending. `from` is inclusive and `to` is
        exclusive. The synchronous range may not exceed exactly 30 days.
      operationId: getDeviceMeasurements
      security: [{ AccessToken: [] }]
      parameters:
        - name: from
          in: query
          required: true
          description: Inclusive beginning of the time window.
          schema: { $ref: "#/components/schemas/IsoDateTimeString" }
        - name: to
          in: query
          required: true
          description: Exclusive end of the time window.
          schema: { $ref: "#/components/schemas/IsoDateTimeString" }
        - name: measurementKey
          in: query
          description: Optional repeated measurement-key filter; at most 25 values.
          style: form
          explode: true
          schema:
            type: array
            maxItems: 25
            items:
              type: string
              pattern: "^[a-z0-9][a-z0-9_-]{0,63}$"
        - name: limit
          in: query
          schema: { type: integer, minimum: 1, maximum: 5000, default: 5000 }
        - name: cursor
          in: query
          description: Opaque cursor for the stable descending result order.
          schema: { type: string, maxLength: 512 }
      responses:
        "200":
          description: Bounded, scope-filtered measurement page.
          content:
            application/json:
              schema: { $ref: "#/components/schemas/TelemetryQueryResponseDto" }
        "400": { $ref: "#/components/responses/ErrorResponse" }
        "404": { $ref: "#/components/responses/ErrorResponse" }
        default: { $ref: "#/components/responses/ErrorResponse" }
  /v1/devices/{deviceId}/organization-assignments:
    parameters:
      - $ref: "#/components/parameters/DeviceId"
    get:
      tags: [Devices]
      summary: List assignment periods visible in the caller's organization scope.
      description: |
        Returns 25 reverse-chronological periods at a time without an exact
        total. The first request fixes an upper anchor; continuations retain it,
        the Device, Tenant, and caller's current Organization visibility.
      operationId: listDeviceAssignments
      security: [{ AccessToken: [] }]
      parameters:
        - $ref: "#/components/parameters/Cursor"
        - $ref: "#/components/parameters/TimelineLimit"
      responses:
        "200":
          description: Filtered append-only assignment history.
          content:
            application/json:
              schema:
                { $ref: "#/components/schemas/DeviceAssignmentListResponse" }
        default: { $ref: "#/components/responses/ErrorResponse" }
  /v1/devices/{deviceId}/organization-assignment:
    parameters:
      - $ref: "#/components/parameters/DeviceId"
    put:
      tags: [Devices]
      summary: Assign or immediately move a device to one organization unit.
      operationId: setDeviceAssignment
      security: [{ AccessToken: [] }]
      requestBody:
        required: true
        content:
          application/json:
            schema: { $ref: "#/components/schemas/SetDeviceAssignmentRequest" }
      responses:
        "200":
          description: Device with its current assignment.
          content:
            application/json:
              schema: { $ref: "#/components/schemas/Device" }
        default: { $ref: "#/components/responses/ErrorResponse" }
    delete:
      tags: [Devices]
      summary: End the current assignment as a platform or tenant administrator.
      operationId: clearDeviceAssignment
      security: [{ AccessToken: [] }]
      responses:
        "204": { description: Current assignment ended or already absent. }
        default: { $ref: "#/components/responses/ErrorResponse" }
  /v1/flow-blocks:
    get:
      tags: [Telemetry flows]
      summary: List visible immutable block versions and their JSON Schemas.
      operationId: listFlowBlocks
      security: [{ AccessToken: [] }]
      parameters:
        - name: tenantNumber
          in: query
          description: Required for tenant administrators; omission requires platform administration.
          schema: { $ref: "#/components/schemas/TenantNumber" }
        - $ref: "#/components/parameters/SearchFilter"
        - $ref: "#/components/parameters/Cursor"
        - $ref: "#/components/parameters/SelectorPageLimit"
      responses:
        "200":
          description: Built-ins, released platform blocks and, with tenant context, released tenant blocks.
          content:
            application/json:
              schema: { $ref: "#/components/schemas/FlowBlockLibraryResponse" }
        default: { $ref: "#/components/responses/ErrorResponse" }
    post:
      tags: [Telemetry flows]
      summary: Create a global custom-script block as a platform administrator.
      operationId: createPlatformFlowBlock
      security: [{ AccessToken: [] }]
      requestBody:
        required: true
        content:
          application/json:
            schema:
              { $ref: "#/components/schemas/CreateCustomFlowBlockRequest" }
      responses:
        "201": { description: Global custom block created. }
        default: { $ref: "#/components/responses/ErrorResponse" }
  /v1/flow-block-versions/{versionId}:
    parameters:
      - {
          name: versionId,
          in: path,
          required: true,
          schema: { type: string, format: uuid }
        }
    get:
      tags: [Telemetry flows]
      summary: Resolve one immutable released or deprecated Custom-block version for a pinned Flow graph.
      operationId: getFlowBlockLibrarySelection
      security: [{ AccessToken: [] }]
      parameters:
        - name: tenantNumber
          in: query
          description: Tenant context for resolving a tenant-owned pinned version; omission resolves platform assets only.
          schema: { $ref: "#/components/schemas/TenantNumber" }
      responses:
        "200":
          description: Exact immutable released or deprecated block selection and its JSON Schemas.
          content:
            application/json:
              schema:
                {
                  $ref: "#/components/schemas/FlowBlockLibrarySelectionResponse"
                }
        default: { $ref: "#/components/responses/ErrorResponse" }
  /v1/flow-blocks/{blockId}/versions:
    parameters:
      - {
          name: blockId,
          in: path,
          required: true,
          schema: { type: string, format: uuid }
        }
    get:
      tags: [Telemetry flows]
      summary: List versions of one visible global or tenant custom block without compiled artifacts.
      operationId: listFlowBlockVersions
      security: [{ AccessToken: [] }]
      parameters:
        - $ref: "#/components/parameters/Cursor"
        - $ref: "#/components/parameters/ProgressivePageLimit"
      responses:
        "200":
          description: Progressive version metadata without an exact total.
          content:
            application/json:
              schema:
                {
                  $ref: "#/components/schemas/CustomFlowBlockVersionListResponse"
                }
        default: { $ref: "#/components/responses/ErrorResponse" }
    post:
      tags: [Telemetry flows]
      summary: Create an immutable draft for the addressed global or tenant custom block.
      operationId: createFlowBlockVersion
      security: [{ AccessToken: [] }]
      requestBody:
        required: true
        content:
          application/json:
            schema:
              {
                $ref: "#/components/schemas/CreateCustomFlowBlockVersionRequest"
              }
      responses:
        "201": { description: Draft version created. }
        default: { $ref: "#/components/responses/ErrorResponse" }
  /v1/flow-blocks/library:
    get:
      tags: [Telemetry flows]
      summary: List global custom-script blocks as a platform administrator.
      operationId: listPlatformFlowBlocks
      security: [{ AccessToken: [] }]
      parameters:
        - $ref: "#/components/parameters/SearchFilter"
        - $ref: "#/components/parameters/Cursor"
        - $ref: "#/components/parameters/FlowResourcePageLimit"
      responses:
        "200":
          description: Counted global custom blocks without source or artifacts.
          content:
            application/json:
              schema:
                { $ref: "#/components/schemas/CustomFlowBlockListResponse" }
        default: { $ref: "#/components/responses/ErrorResponse" }
  /v1/flow-blocks/{blockId}/versions/{versionId}:
    parameters:
      - {
          name: blockId,
          in: path,
          required: true,
          schema: { type: string, format: uuid }
        }
      - {
          name: versionId,
          in: path,
          required: true,
          schema: { type: string, format: uuid }
        }
    get:
      tags: [Telemetry flows]
      summary: Read one custom-block version including source but excluding its artifact.
      operationId: getFlowBlockVersion
      security: [{ AccessToken: [] }]
      responses:
        "200": { description: Custom-block version detail. }
        default: { $ref: "#/components/responses/ErrorResponse" }
  /v1/flow-blocks/{blockId}/versions/{versionId}/release:
    parameters:
      - {
          name: blockId,
          in: path,
          required: true,
          schema: { type: string, format: uuid }
        }
      - {
          name: versionId,
          in: path,
          required: true,
          schema: { type: string, format: uuid }
        }
    post:
      tags: [Telemetry flows]
      summary: Run technical compile and sandbox gates, then directly release the draft.
      operationId: releaseFlowBlockVersion
      security: [{ AccessToken: [] }]
      responses:
        "200": { description: Released immutable version. }
        "422": { $ref: "#/components/responses/ErrorResponse" }
        "503": { $ref: "#/components/responses/ErrorResponse" }
        default: { $ref: "#/components/responses/ErrorResponse" }
  /v1/flow-blocks/{blockId}/versions/{versionId}/deprecate:
    parameters:
      - {
          name: blockId,
          in: path,
          required: true,
          schema: { type: string, format: uuid }
        }
      - {
          name: versionId,
          in: path,
          required: true,
          schema: { type: string, format: uuid }
        }
    post:
      tags: [Telemetry flows]
      summary: Deprecate a released block version for new flow releases.
      operationId: deprecateFlowBlockVersion
      security: [{ AccessToken: [] }]
      responses:
        "200": { description: Deprecated immutable version. }
        default: { $ref: "#/components/responses/ErrorResponse" }
  /v1/tenants/{tenantNumber}/flow-blocks:
    parameters: [{ $ref: "#/components/parameters/TenantNumber" }]
    get:
      tags: [Telemetry flows]
      summary: List tenant-owned custom-script blocks as a platform or tenant administrator.
      operationId: listTenantFlowBlocks
      security: [{ AccessToken: [] }]
      parameters:
        - $ref: "#/components/parameters/SearchFilter"
        - $ref: "#/components/parameters/Cursor"
        - $ref: "#/components/parameters/FlowResourcePageLimit"
      responses:
        "200":
          description: Counted Tenant custom blocks without sources or artifacts.
          content:
            application/json:
              schema:
                { $ref: "#/components/schemas/CustomFlowBlockListResponse" }
        default: { $ref: "#/components/responses/ErrorResponse" }
    post:
      tags: [Telemetry flows]
      summary: Create a tenant-owned custom-script block as a platform or tenant administrator.
      operationId: createTenantFlowBlock
      security: [{ AccessToken: [] }]
      requestBody:
        required: true
        content:
          application/json:
            schema:
              { $ref: "#/components/schemas/CreateCustomFlowBlockRequest" }
      responses:
        "201": { description: Tenant custom block created. }
        default: { $ref: "#/components/responses/ErrorResponse" }
  /v1/flow-templates:
    get:
      tags: [Telemetry flows]
      summary: List global flow templates.
      operationId: listFlowTemplates
      security: [{ AccessToken: [] }]
      parameters:
        - $ref: "#/components/parameters/SearchFilter"
        - $ref: "#/components/parameters/Cursor"
        - $ref: "#/components/parameters/FlowResourcePageLimit"
      responses:
        "200":
          description: Counted global flow templates.
          content:
            application/json:
              schema: { $ref: "#/components/schemas/FlowTemplateListResponse" }
        default: { $ref: "#/components/responses/ErrorResponse" }
    post:
      tags: [Telemetry flows]
      summary: Create a global flow template.
      operationId: createFlowTemplate
      security: [{ AccessToken: [] }]
      requestBody:
        required: true
        content:
          application/json:
            schema: { $ref: "#/components/schemas/CreateFlowTemplateRequest" }
      responses:
        "201": { description: Template created. }
        default: { $ref: "#/components/responses/ErrorResponse" }
  /v1/flow-templates/{templateId}/versions:
    parameters:
      - {
          name: templateId,
          in: path,
          required: true,
          schema: { type: string, format: uuid }
        }
    get:
      tags: [Telemetry flows]
      summary: List immutable global template versions.
      operationId: listFlowTemplateVersions
      security: [{ AccessToken: [] }]
      parameters:
        - $ref: "#/components/parameters/Cursor"
        - $ref: "#/components/parameters/ProgressivePageLimit"
      responses:
        "200":
          description: Progressive template versions without an exact total.
          content:
            application/json:
              schema:
                { $ref: "#/components/schemas/FlowTemplateVersionListResponse" }
        default: { $ref: "#/components/responses/ErrorResponse" }
    post:
      tags: [Telemetry flows]
      summary: Create a global template draft from a flow graph.
      operationId: createFlowTemplateVersion
      security: [{ AccessToken: [] }]
      requestBody:
        required: true
        content:
          application/json:
            schema:
              { $ref: "#/components/schemas/CreateFlowTemplateVersionRequest" }
      responses:
        "201": { description: Template draft created. }
        default: { $ref: "#/components/responses/ErrorResponse" }
  /v1/flow-templates/{templateId}/versions/{versionId}/validate:
    parameters:
      - {
          name: templateId,
          in: path,
          required: true,
          schema: { type: string, format: uuid }
        }
      - {
          name: versionId,
          in: path,
          required: true,
          schema: { type: string, format: uuid }
        }
    post:
      tags: [Telemetry flows]
      summary: Validate a template and reject tenant-block dependencies.
      operationId: validateFlowTemplateVersion
      security: [{ AccessToken: [] }]
      responses:
        "200": { description: Validation result. }
        default: { $ref: "#/components/responses/ErrorResponse" }
  /v1/flow-templates/{templateId}/versions/{versionId}/release:
    parameters:
      - {
          name: templateId,
          in: path,
          required: true,
          schema: { type: string, format: uuid }
        }
      - {
          name: versionId,
          in: path,
          required: true,
          schema: { type: string, format: uuid }
        }
    post:
      tags: [Telemetry flows]
      summary: Release an immutable template version.
      operationId: releaseFlowTemplateVersion
      security: [{ AccessToken: [] }]
      responses:
        "200": { description: Released template version. }
        default: { $ref: "#/components/responses/ErrorResponse" }
  /v1/flow-templates/{templateId}/versions/{versionId}/deprecate:
    parameters:
      - {
          name: templateId,
          in: path,
          required: true,
          schema: { type: string, format: uuid }
        }
      - {
          name: versionId,
          in: path,
          required: true,
          schema: { type: string, format: uuid }
        }
    post:
      tags: [Telemetry flows]
      summary: Deprecate a template for future copies.
      operationId: deprecateFlowTemplateVersion
      security: [{ AccessToken: [] }]
      responses:
        "200": { description: Deprecated template version. }
        default: { $ref: "#/components/responses/ErrorResponse" }
  /v1/tenants/{tenantNumber}/flows/from-template:
    parameters: [{ $ref: "#/components/parameters/TenantNumber" }]
    post:
      tags: [Telemetry flows]
      summary: Copy a released platform template into an independent tenant draft.
      operationId: instantiateFlowTemplate
      security: [{ AccessToken: [] }]
      requestBody:
        required: true
        content:
          application/json:
            schema:
              { $ref: "#/components/schemas/InstantiateFlowTemplateRequest" }
      responses:
        "201": { description: Tenant flow and draft created. }
        "409": { $ref: "#/components/responses/ErrorResponse" }
        "422": { $ref: "#/components/responses/ErrorResponse" }
        default: { $ref: "#/components/responses/ErrorResponse" }
  /v1/tenants/{tenantNumber}/flows:
    parameters: [{ $ref: "#/components/parameters/TenantNumber" }]
    get:
      tags: [Telemetry flows]
      summary: List tenant-owned telemetry flows.
      operationId: listFlows
      security: [{ AccessToken: [] }]
      parameters:
        - name: deviceTypeId
          in: query
          schema: { $ref: "#/components/schemas/DeviceTypeId" }
        - $ref: "#/components/parameters/SearchFilter"
        - $ref: "#/components/parameters/Cursor"
        - $ref: "#/components/parameters/DevicePageLimit"
      responses:
        "200":
          description: Counted Flow cursor page from one authorized database snapshot.
          content:
            application/json:
              schema: { $ref: "#/components/schemas/FlowListResponse" }
        default: { $ref: "#/components/responses/ErrorResponse" }
  /v1/device-types/{deviceTypeId}/flow:
    parameters:
      - $ref: "#/components/parameters/DeviceTypeId"
    post:
      tags: [Telemetry flows]
      summary: Atomically create the single telemetry Flow and initial V2 draft for a Device Type.
      operationId: createFlow
      security: [{ AccessToken: [] }]
      requestBody:
        required: true
        content:
          application/json:
            schema: { $ref: "#/components/schemas/CreateFlowRequest" }
      responses:
        "201":
          description: Flow created.
          content:
            application/json:
              schema: { $ref: "#/components/schemas/Flow" }
        "409": { $ref: "#/components/responses/ErrorResponse" }
        default: { $ref: "#/components/responses/ErrorResponse" }
  /v1/flows/{flowId}:
    parameters:
      - {
          name: flowId,
          in: path,
          required: true,
          schema: { type: string, format: uuid }
        }
    get:
      tags: [Telemetry flows]
      summary: Read one authorized telemetry Flow for exact selector restoration.
      operationId: getFlow
      security: [{ AccessToken: [] }]
      responses:
        "200":
          description: Flow detail.
          content:
            application/json:
              schema: { $ref: "#/components/schemas/Flow" }
        default: { $ref: "#/components/responses/ErrorResponse" }
  /v1/flows/{flowId}/versions:
    parameters:
      - {
          name: flowId,
          in: path,
          required: true,
          schema: { type: string, format: uuid }
        }
    get:
      tags: [Telemetry flows]
      summary: List immutable flow versions.
      operationId: listFlowVersions
      security: [{ AccessToken: [] }]
      parameters:
        - $ref: "#/components/parameters/Cursor"
        - $ref: "#/components/parameters/TimelineLimit"
      responses:
        "200":
          description: Progressive reverse-chronological Flow-version page without an exact total.
          content:
            application/json:
              schema: { $ref: "#/components/schemas/FlowVersionListResponse" }
        default: { $ref: "#/components/responses/ErrorResponse" }
    post:
      tags: [Telemetry flows]
      summary: Create an immutable draft flow version.
      operationId: createFlowVersion
      security: [{ AccessToken: [] }]
      requestBody:
        required: true
        content:
          application/json:
            schema: { $ref: "#/components/schemas/CreateFlowVersionRequest" }
      responses:
        "201":
          description: Draft created.
          content:
            application/json:
              schema: { $ref: "#/components/schemas/FlowVersion" }
        default: { $ref: "#/components/responses/ErrorResponse" }
  /v1/flows/{flowId}/versions/{flowVersionId}/validate:
    parameters:
      - $ref: "#/components/parameters/FlowId"
      - $ref: "#/components/parameters/FlowVersionId"
    post:
      tags: [Telemetry flows]
      summary: Compile and validate a draft flow version.
      operationId: validateFlowVersion
      security: [{ AccessToken: [] }]
      responses:
        "200": { description: Graph validation result. }
        default: { $ref: "#/components/responses/ErrorResponse" }
  /v1/flows/{flowId}/versions/{flowVersionId}/release:
    parameters:
      - $ref: "#/components/parameters/FlowId"
      - $ref: "#/components/parameters/FlowVersionId"
    post:
      tags: [Telemetry flows]
      summary: Release a valid draft directly without four-eyes approval.
      operationId: releaseFlowVersion
      security: [{ AccessToken: [] }]
      responses:
        "200": { description: Released immutable flow version. }
        default: { $ref: "#/components/responses/ErrorResponse" }
  /v1/flows/{flowId}/active-version:
    parameters:
      - $ref: "#/components/parameters/FlowId"
    put:
      tags: [Telemetry flows]
      summary: Atomically activate or roll back to a released flow version.
      operationId: setActiveFlowVersion
      security: [{ AccessToken: [] }]
      requestBody:
        required: true
        content:
          application/json:
            schema: { $ref: "#/components/schemas/SetActiveFlowVersionRequest" }
      responses:
        "200": { description: Flow with updated active pointer. }
        default: { $ref: "#/components/responses/ErrorResponse" }
  /v1/flows/{flowId}/fixtures:
    parameters:
      - $ref: "#/components/parameters/FlowId"
    get:
      tags: [Telemetry flows]
      summary: List materialized test fixtures for a tenant flow.
      operationId: listFlowFixtures
      security: [{ AccessToken: [] }]
      parameters:
        - $ref: "#/components/parameters/SearchFilter"
        - $ref: "#/components/parameters/Cursor"
        - $ref: "#/components/parameters/FlowResourcePageLimit"
      responses:
        "200":
          description: Counted fixtures including materialized input and assertions.
          content:
            application/json:
              schema:
                { $ref: "#/components/schemas/FlowTestFixtureListResponse" }
        default: { $ref: "#/components/responses/ErrorResponse" }
    post:
      tags: [Telemetry flows]
      summary: Save a materialized fixture; Raw inputs require explicit retention confirmation.
      operationId: createFlowFixture
      security: [{ AccessToken: [] }]
      requestBody:
        required: true
        content:
          application/json:
            schema: { $ref: "#/components/schemas/CreateFlowFixtureRequest" }
      responses:
        "201": { description: Fixture snapshot created. }
        default: { $ref: "#/components/responses/ErrorResponse" }
  /v1/flows/{flowId}/fixtures/{fixtureId}:
    parameters:
      - $ref: "#/components/parameters/FlowId"
      - {
          name: fixtureId,
          in: path,
          required: true,
          schema: { type: string, format: uuid }
        }
      - { name: If-Match, in: header, required: true, schema: { type: string } }
    put:
      tags: [Telemetry flows]
      summary: Replace one fixture with optimistic concurrency.
      operationId: updateFlowFixture
      security: [{ AccessToken: [] }]
      requestBody:
        required: true
        content:
          application/json:
            schema: { $ref: "#/components/schemas/CreateFlowFixtureRequest" }
      responses:
        "200": { description: Fixture updated. }
        default: { $ref: "#/components/responses/ErrorResponse" }
    delete:
      tags: [Telemetry flows]
      summary: Delete one fixture with optimistic concurrency.
      operationId: deleteFlowFixture
      security: [{ AccessToken: [] }]
      responses:
        "204": { description: Fixture deleted. }
        default: { $ref: "#/components/responses/ErrorResponse" }
  /v1/flows/{flowId}/test-runs:
    parameters:
      - $ref: "#/components/parameters/FlowId"
    post:
      tags: [Telemetry flows]
      summary: Execute a synchronous, side-effect-free preview against Raw, manual or fixture input.
      operationId: runFlowTest
      security: [{ AccessToken: [] }]
      requestBody:
        required: true
        content:
          application/json:
            schema: { $ref: "#/components/schemas/FlowTestRunRequest" }
      responses:
        "200":
          {
            description: Node trace,
            measurements and non-executed action previews.
          }
        default: { $ref: "#/components/responses/ErrorResponse" }
  /v1/flows/{flowId}/parameter-values:
    parameters:
      - $ref: "#/components/parameters/FlowId"
    get:
      tags: [Telemetry flows]
      summary: List tenant, device-type and device overrides for one flow.
      operationId: listFlowParameterValues
      security: [{ AccessToken: [] }]
      parameters:
        - $ref: "#/components/parameters/SearchFilter"
        - $ref: "#/components/parameters/Cursor"
        - $ref: "#/components/parameters/FlowResourcePageLimit"
      responses:
        "200":
          description: Counted explicit parameter override rows.
          content:
            application/json:
              schema:
                { $ref: "#/components/schemas/FlowParameterValueListResponse" }
        default: { $ref: "#/components/responses/ErrorResponse" }
  /v1/flows/{flowId}/parameter-values/{scopeType}/{scopeId}/{parameterKey}:
    parameters:
      - $ref: "#/components/parameters/FlowId"
      - {
          name: scopeType,
          in: path,
          required: true,
          schema: { type: string, enum: [tenant, device_type, device] }
        }
      - {
          name: scopeId,
          in: path,
          required: true,
          schema: { type: string, format: uuid }
        }
      - {
          name: parameterKey,
          in: path,
          required: true,
          schema: { type: string }
        }
    put:
      tags: [Telemetry flows]
      summary: Create or replace one context-specific parameter value.
      operationId: putFlowParameterValue
      security: [{ AccessToken: [] }]
      requestBody:
        required: true
        content:
          application/json:
            schema:
              { $ref: "#/components/schemas/PutFlowParameterValueRequest" }
      responses:
        "200": { description: Parameter override stored. }
        default: { $ref: "#/components/responses/ErrorResponse" }
    delete:
      tags: [Telemetry flows]
      summary: Delete one context override and expose the next fallback.
      operationId: deleteFlowParameterValue
      security: [{ AccessToken: [] }]
      parameters:
        - {
            name: If-Match,
            in: header,
            required: true,
            schema: { type: string }
          }
      responses:
        "204": { description: Override deleted. }
        default: { $ref: "#/components/responses/ErrorResponse" }
  /v1/flows/{flowId}/resolved-parameters:
    parameters:
      - $ref: "#/components/parameters/FlowId"
      - {
          name: deviceId,
          in: query,
          required: true,
          schema: { $ref: "#/components/schemas/DeviceId" }
        }
    get:
      tags: [Telemetry flows]
      summary: Resolve Device, Device Type, Tenant and default values for diagnostics.
      operationId: resolveFlowParameters
      security: [{ AccessToken: [] }]
      responses:
        "200": { description: Effective parameter map. }
        default: { $ref: "#/components/responses/ErrorResponse" }
  /v1/raw-ingestion-messages/{rawIngestionMessageId}/replays:
    parameters:
      - $ref: "#/components/parameters/RawIngestionMessageId"
      - {
          name: receivedAt,
          in: query,
          required: true,
          description: Exact receive timestamp forming the second part of the hypertable key.,
          schema: { $ref: "#/components/schemas/IsoDateTimeString" }
        }
      - {
          name: Idempotency-Key,
          in: header,
          required: true,
          schema: { type: string, minLength: 1, maxLength: 200 }
        }
    post:
      tags: [Telemetry flows]
      summary: Queue an idempotent replay pinned to a released or deprecated version.
      operationId: createFlowReplay
      security: [{ AccessToken: [] }]
      requestBody:
        required: false
        content:
          application/json:
            schema:
              type: object
              additionalProperties: false
              properties:
                flowVersionId: { $ref: "#/components/schemas/FlowVersionId" }
      responses:
        "202":
          {
            description: Queued replay execution; repeated idempotency keys return the same execution.
          }
        default: { $ref: "#/components/responses/ErrorResponse" }
  /v1/tenants/{tenantNumber}/flow-executions:
    parameters: [{ $ref: "#/components/parameters/TenantNumber" }]
    get:
      tags: [Telemetry flows]
      summary: List live and replay execution diagnostics.
      description: |
        Returns reverse-chronological pages without an exact total. The first
        request fixes `to` as the investigation anchor. Continuations reuse
        that anchor, Tenant scope, every filter, and the requested page size.
        The API defaults to 25 and accepts 1 through 100; the Web Console
        requests 25 per explicit load.
      operationId: listFlowExecutions
      security: [{ AccessToken: [] }]
      parameters:
        - $ref: "#/components/parameters/FlowInvestigationFrom"
        - $ref: "#/components/parameters/FlowInvestigationTo"
        - $ref: "#/components/parameters/FlowInvestigationFlowFilter"
        - $ref: "#/components/parameters/FlowInvestigationOrganizationFilter"
        - $ref: "#/components/parameters/FlowExecutionStatusFilter"
        - $ref: "#/components/parameters/Cursor"
        - $ref: "#/components/parameters/TimelineLimit"
      responses:
        "200":
          description: Anchored progressive Flow Execution page.
          content:
            application/json:
              schema: { $ref: "#/components/schemas/FlowExecutionListResponse" }
        default: { $ref: "#/components/responses/ErrorResponse" }
  /v1/flow-executions/{executionId}:
    parameters:
      - {
          name: executionId,
          in: path,
          required: true,
          schema: { type: string, format: uuid }
        }
    get:
      tags: [Telemetry flows]
      summary: Read one tenant-isolated execution and its pinned context.
      operationId: getFlowExecution
      security: [{ AccessToken: [] }]
      responses:
        "200": { description: Execution detail. }
        default: { $ref: "#/components/responses/ErrorResponse" }
  /v1/tenants/{tenantNumber}/email-templates:
    parameters: [{ $ref: "#/components/parameters/TenantNumber" }]
    get:
      tags: [Flow actions]
      summary: List logical tenant E-mail templates and active pointers.
      operationId: listEmailTemplates
      security: [{ AccessToken: [] }]
      parameters:
        - $ref: "#/components/parameters/SearchFilter"
        - $ref: "#/components/parameters/Cursor"
        - $ref: "#/components/parameters/FlowResourcePageLimit"
      responses:
        "200":
          description: Counted Tenant E-mail templates.
          content:
            application/json:
              schema: { $ref: "#/components/schemas/EmailTemplateListResponse" }
        default: { $ref: "#/components/responses/ErrorResponse" }
    post:
      tags: [Flow actions]
      summary: Create a logical tenant E-mail template.
      operationId: createEmailTemplate
      security: [{ AccessToken: [] }]
      requestBody:
        required: true
        content:
          application/json:
            schema: { $ref: "#/components/schemas/CreateEmailTemplateRequest" }
      responses:
        "201": { description: E-mail template created. }
        default: { $ref: "#/components/responses/ErrorResponse" }
  /v1/email-templates/{templateId}/versions:
    parameters:
      - {
          name: templateId,
          in: path,
          required: true,
          schema: { type: string, format: uuid }
        }
    get:
      tags: [Flow actions]
      summary: List immutable E-mail template versions.
      operationId: listEmailTemplateVersions
      security: [{ AccessToken: [] }]
      parameters:
        - $ref: "#/components/parameters/Cursor"
        - $ref: "#/components/parameters/ProgressivePageLimit"
      responses:
        "200":
          description: Progressive E-mail template versions without an exact total.
          content:
            application/json:
              schema:
                {
                  $ref: "#/components/schemas/EmailTemplateVersionListResponse"
                }
        default: { $ref: "#/components/responses/ErrorResponse" }
    post:
      tags: [Flow actions]
      summary: Create and parse-check an E-mail template draft.
      operationId: createEmailTemplateVersion
      security: [{ AccessToken: [] }]
      requestBody:
        required: true
        content:
          application/json:
            schema:
              { $ref: "#/components/schemas/CreateEmailTemplateVersionRequest" }
      responses:
        "201": { description: E-mail template draft created. }
        default: { $ref: "#/components/responses/ErrorResponse" }
  /v1/email-templates/{templateId}/versions/{versionId}/release:
    parameters:
      - {
          name: templateId,
          in: path,
          required: true,
          schema: { type: string, format: uuid }
        }
      - {
          name: versionId,
          in: path,
          required: true,
          schema: { type: string, format: uuid }
        }
    post:
      tags: [Flow actions]
      summary: Release an immutable E-mail template version.
      operationId: releaseEmailTemplateVersion
      security: [{ AccessToken: [] }]
      responses:
        "200": { description: Released template version. }
        default: { $ref: "#/components/responses/ErrorResponse" }
  /v1/email-templates/{templateId}/versions/{versionId}/deprecate:
    parameters:
      - {
          name: templateId,
          in: path,
          required: true,
          schema: { type: string, format: uuid }
        }
      - {
          name: versionId,
          in: path,
          required: true,
          schema: { type: string, format: uuid }
        }
    post:
      tags: [Flow actions]
      summary: Deprecate an E-mail template version for future activation.
      operationId: deprecateEmailTemplateVersion
      security: [{ AccessToken: [] }]
      responses:
        "200": { description: Deprecated template version. }
        default: { $ref: "#/components/responses/ErrorResponse" }
  /v1/email-templates/{templateId}/active-version:
    parameters:
      - {
          name: templateId,
          in: path,
          required: true,
          schema: { type: string, format: uuid }
        }
      - { name: If-Match, in: header, required: true, schema: { type: string } }
    put:
      tags: [Flow actions]
      summary: Move the active template pointer with optimistic concurrency.
      operationId: setActiveEmailTemplateVersion
      security: [{ AccessToken: [] }]
      requestBody:
        required: true
        content:
          application/json:
            schema:
              {
                $ref: "#/components/schemas/SetActiveEmailTemplateVersionRequest"
              }
      responses:
        "200": { description: Updated logical template. }
        default: { $ref: "#/components/responses/ErrorResponse" }
  /v1/tenants/{tenantNumber}/notification-recipient-bindings:
    parameters: [{ $ref: "#/components/parameters/TenantNumber" }]
    get:
      tags: [Flow actions]
      summary: List explicit recipient and disabled bindings.
      operationId: listNotificationRecipientBindings
      security: [{ AccessToken: [] }]
      parameters:
        - $ref: "#/components/parameters/SearchFilter"
        - $ref: "#/components/parameters/Cursor"
        - $ref: "#/components/parameters/FlowResourcePageLimit"
      responses:
        "200":
          description: Counted recipient bindings.
          content:
            application/json:
              schema:
                {
                  $ref: "#/components/schemas/NotificationRecipientBindingListResponse"
                }
        default: { $ref: "#/components/responses/ErrorResponse" }
  /v1/tenants/{tenantNumber}/notification-recipient-bindings/{bindingKey}/{scopeType}/{scopeId}:
    parameters:
      - $ref: "#/components/parameters/TenantNumber"
      - { name: bindingKey, in: path, required: true, schema: { type: string } }
      - {
          name: scopeType,
          in: path,
          required: true,
          schema: { type: string, enum: [tenant, device_type, device] }
        }
      - {
          name: scopeId,
          in: path,
          required: true,
          schema: { type: string, format: uuid }
        }
    get:
      tags: [Flow actions]
      summary: Read one exact tenant-scoped recipient binding for optimistic editing.
      operationId: getNotificationRecipientBinding
      security: [{ AccessToken: [] }]
      responses:
        "200":
          description: Exact recipient binding including its current lock version.
          content:
            application/json:
              schema:
                { $ref: "#/components/schemas/NotificationRecipientBinding" }
        default: { $ref: "#/components/responses/ErrorResponse" }
    put:
      tags: [Flow actions]
      summary: Store a recipients or disabled binding; the most specific row replaces fallback.
      operationId: putNotificationRecipientBinding
      security: [{ AccessToken: [] }]
      requestBody:
        required: true
        content:
          application/json:
            schema:
              {
                $ref: "#/components/schemas/PutNotificationRecipientBindingRequest"
              }
      responses:
        "200": { description: Binding stored. }
        default: { $ref: "#/components/responses/ErrorResponse" }
    delete:
      tags: [Flow actions]
      summary: Delete one binding with optimistic concurrency.
      operationId: deleteNotificationRecipientBinding
      security: [{ AccessToken: [] }]
      parameters:
        - {
            name: If-Match,
            in: header,
            required: true,
            schema: { type: string }
          }
      responses:
        "204": { description: Binding deleted. }
        default: { $ref: "#/components/responses/ErrorResponse" }
  /v1/tenants/{tenantNumber}/flow-action-control:
    parameters: [{ $ref: "#/components/parameters/TenantNumber" }]
    get:
      tags: [Flow actions]
      summary: Read the tenant Action kill-switch.
      operationId: getFlowActionControl
      security: [{ AccessToken: [] }]
      responses:
        "200": { description: Tenant Action control with ETag. }
        default: { $ref: "#/components/responses/ErrorResponse" }
    put:
      tags: [Flow actions]
      summary: Pause or resume tenant Actions without deleting jobs.
      operationId: putFlowActionControl
      security: [{ AccessToken: [] }]
      parameters:
        - {
            name: If-Match,
            in: header,
            required: true,
            schema: { type: string }
          }
      requestBody:
        required: true
        content:
          application/json:
            schema: { $ref: "#/components/schemas/PutFlowActionControlRequest" }
      responses:
        "200": { description: Updated control; resume re-enqueues paused jobs. }
        default: { $ref: "#/components/responses/ErrorResponse" }
  /v1/tenants/{tenantNumber}/flow-dead-letters:
    parameters: [{ $ref: "#/components/parameters/TenantNumber" }]
    get:
      tags: [Flow actions]
      summary: List persistent flow and Action Dead Letters.
      description: |
        Returns reverse-chronological pages without an exact total. The first
        request fixes `to`; continuations keep the anchor, Tenant scope, every
        filter, and the requested page size. The API defaults to 25 and accepts
        1 through 100; the Web Console requests 25 per explicit load.
      operationId: listFlowDeadLetters
      security: [{ AccessToken: [] }]
      parameters:
        - $ref: "#/components/parameters/FlowInvestigationFrom"
        - $ref: "#/components/parameters/FlowInvestigationTo"
        - $ref: "#/components/parameters/FlowInvestigationFlowFilter"
        - $ref: "#/components/parameters/FlowInvestigationOrganizationFilter"
        - $ref: "#/components/parameters/FlowDeadLetterStatusFilter"
        - $ref: "#/components/parameters/Cursor"
        - $ref: "#/components/parameters/TimelineLimit"
      responses:
        "200":
          description: Anchored progressive Tenant-isolated Dead Letter page.
          content:
            application/json:
              schema:
                { $ref: "#/components/schemas/FlowDeadLetterListResponse" }
        default: { $ref: "#/components/responses/ErrorResponse" }
  /v1/flow-dead-letters/{deadLetterId}:
    parameters:
      - {
          name: deadLetterId,
          in: path,
          required: true,
          schema: { type: string, format: uuid }
        }
    get:
      tags: [Flow actions]
      summary: Read one persistent Dead Letter.
      operationId: getFlowDeadLetter
      security: [{ AccessToken: [] }]
      responses:
        "200": { description: Dead Letter detail. }
        default: { $ref: "#/components/responses/ErrorResponse" }
  /v1/flow-dead-letters/{deadLetterId}/redrive:
    parameters:
      - {
          name: deadLetterId,
          in: path,
          required: true,
          schema: { type: string, format: uuid }
        }
      - { name: If-Match, in: header, required: true, schema: { type: string } }
    post:
      tags: [Flow actions]
      summary: Manually redrive an open Dead Letter while preserving pinned data.
      operationId: redriveFlowDeadLetter
      security: [{ AccessToken: [] }]
      requestBody:
        required: true
        content:
          application/json:
            schema:
              { $ref: "#/components/schemas/RedriveFlowDeadLetterRequest" }
      responses:
        "200":
          {
            description: Dead Letter marked redriven and an outbox event created.
          }
        default: { $ref: "#/components/responses/ErrorResponse" }
  /healthz:
    get:
      tags:
        - Diagnostics
      summary: Return the service health state.
      operationId: getHealth
      responses:
        "200":
          description: The service process is alive.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/HealthResponse"
  /readyz:
    get:
      tags:
        - Diagnostics
      summary: Return service readiness and dependency checks.
      operationId: getReadiness
      responses:
        "200":
          description: The service is ready to receive traffic.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ReadinessResponse"
        "503":
          description: The service is alive but not ready to receive traffic.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ReadinessResponse"
  /metrics:
    get:
      tags:
        - Diagnostics
      summary: Return Prometheus metrics for the service.
      operationId: getMetrics
      responses:
        "200":
          description: Prometheus text exposition.
          content:
            text/plain:
              schema:
                type: string
components:
  parameters:
    TenantNumber:
      name: tenantNumber
      in: path
      required: true
      schema: { $ref: "#/components/schemas/TenantNumber" }
    OrganizationUnitId:
      name: organizationUnitId
      in: path
      required: true
      schema: { $ref: "#/components/schemas/OrganizationUnitId" }
    UserId:
      name: userId
      in: path
      required: true
      schema: { $ref: "#/components/schemas/UserId" }
    MembershipId:
      name: membershipId
      in: path
      required: true
      schema: { $ref: "#/components/schemas/MembershipId" }
    DeviceId:
      name: deviceId
      in: path
      required: true
      schema: { $ref: "#/components/schemas/DeviceId" }
    DeviceTypeId:
      name: deviceTypeId
      in: path
      required: true
      schema: { $ref: "#/components/schemas/DeviceTypeId" }
    FlowId:
      name: flowId
      in: path
      required: true
      schema: { $ref: "#/components/schemas/FlowId" }
    FlowVersionId:
      name: flowVersionId
      in: path
      required: true
      schema: { $ref: "#/components/schemas/FlowVersionId" }
    StatusFilter:
      name: status
      in: query
      schema: { $ref: "#/components/schemas/AdministrationStatus" }
    SearchFilter:
      name: q
      in: query
      schema: { type: string, maxLength: 100 }
    OrganizationSearchQuery:
      name: q
      in: query
      required: true
      schema: { type: string, minLength: 1, maxLength: 100 }
    Cursor:
      name: cursor
      in: query
      description: Opaque continuation token; reuse the original filters and scope.
      schema: { type: string, maxLength: 512 }
    Limit:
      name: limit
      in: query
      schema: { type: integer, minimum: 1, maximum: 100, default: 50 }
    TimelineLimit:
      name: limit
      in: query
      description: Number of timeline records to return; defaults to 25, accepts 1 through 100, and is bound into continuation cursors.
      schema: { type: integer, minimum: 1, maximum: 100, default: 25 }
    FlowInvestigationFrom:
      name: from
      in: query
      description: Optional inclusive lower timestamp for the investigation.
      schema: { $ref: "#/components/schemas/IsoDateTimeString" }
    FlowInvestigationTo:
      name: to
      in: query
      description: Fixed exclusive upper anchor; defaults to current server time and continuations reuse it.
      schema: { $ref: "#/components/schemas/IsoDateTimeString" }
    FlowInvestigationFlowFilter:
      name: flowId
      in: query
      schema: { $ref: "#/components/schemas/FlowId" }
    FlowInvestigationOrganizationFilter:
      name: organizationUnitId
      in: query
      description: Historical Organization assignment at the Execution timestamp.
      schema: { $ref: "#/components/schemas/OrganizationUnitId" }
    FlowExecutionStatusFilter:
      name: status
      in: query
      schema:
        type: string
        enum:
          [queued, running, succeeded, failed, dead_lettered, skipped_no_flow]
    FlowDeadLetterStatusFilter:
      name: status
      in: query
      schema: { type: string, enum: [open, redriven, resolved] }
    DevicePageLimit:
      name: limit
      in: query
      schema: { type: integer, enum: [25, 50, 100], default: 25 }
    TenantPageLimit:
      name: limit
      in: query
      schema: { type: integer, enum: [25, 50, 100], default: 25 }
    IdentityPageLimit:
      name: limit
      in: query
      schema: { type: integer, enum: [25, 50, 100], default: 25 }
    FlowResourcePageLimit:
      name: limit
      in: query
      description: Counted Flow administration page size.
      schema: { type: integer, enum: [25, 50, 100], default: 25 }
    ProgressivePageLimit:
      name: limit
      in: query
      description: Number of immutable history entries to return without an exact total.
      schema: { type: integer, minimum: 1, maximum: 100, default: 25 }
    SelectorPageLimit:
      name: limit
      in: query
      description: Fixed released Custom-block selector page size.
      schema: { type: integer, enum: [50], default: 50 }
    AuditFrom:
      name: from
      in: query
      description: Both time bounds are required together and limited to 31 days.
      schema: { $ref: "#/components/schemas/IsoDateTimeString" }
    AuditTo:
      name: to
      in: query
      description: Fixed upper investigation anchor; continuations must reuse it.
      schema: { $ref: "#/components/schemas/IsoDateTimeString" }
    AuditEventTypeFilter:
      name: eventType
      in: query
      schema: { $ref: "#/components/schemas/AuditEventType" }
    AuditActionFilter:
      name: action
      in: query
      schema: { type: string, maxLength: 128 }
    AuditActorIdFilter:
      name: actorId
      in: query
      schema: { type: string, maxLength: 128 }
    AuditEntityTypeFilter:
      name: entityType
      in: query
      schema: { type: string, maxLength: 128 }
    AuditEntityIdFilter:
      name: entityId
      in: query
      schema: { type: string, maxLength: 128 }
    AuditCorrelationFilter:
      name: correlationId
      in: query
      schema: { type: string, maxLength: 128 }
    AuditMethodFilter:
      name: method
      in: query
      schema: { type: string, maxLength: 16 }
    AuditRouteFilter:
      name: route
      in: query
      schema: { type: string, maxLength: 256 }
    AuditStatusCodeFilter:
      name: statusCode
      in: query
      schema: { type: integer, minimum: 100, maximum: 599 }
    RawIngestionMessageId:
      name: rawIngestionMessageId
      in: path
      required: true
      schema: { $ref: "#/components/schemas/RawIngestionMessageId" }
    RawIngestionFrom:
      name: from
      in: query
      description: Defaults to 24 hours before `to`; the bounded range may not exceed 42 days.
      schema: { $ref: "#/components/schemas/IsoDateTimeString" }
    RawIngestionTo:
      name: to
      in: query
      description: Fixed upper investigation anchor; defaults to current server time and continuations reuse it.
      schema: { $ref: "#/components/schemas/IsoDateTimeString" }
    RawIngestionReceivedAt:
      name: receivedAt
      in: query
      required: true
      description: Exact local receive timestamp from the list item.
      schema: { $ref: "#/components/schemas/IsoDateTimeString" }
    RawIngestionProcessingStatusFilter:
      name: processingStatus
      in: query
      schema: { $ref: "#/components/schemas/RawIngestionProcessingStatus" }
    RawIngestionDevEuiFilter:
      name: devEui
      in: query
      schema: { $ref: "#/components/schemas/DevEui" }
    RawIngestionCustomerFilter:
      name: customer
      in: query
      description: Exact case-normalized Netmore topic customer.
      schema: { type: string, minLength: 1, maxLength: 200 }
    RawIngestionTenantFilter:
      name: tenantNumber
      in: query
      description: Trusted registry-derived tenant scope, never the Netmore customer value.
      schema: { $ref: "#/components/schemas/TenantNumber" }
    RawIngestionScopeFilter:
      name: scope
      in: query
      schema:
        type: string
        enum: [all, unassigned]
  securitySchemes:
    AccessToken:
      type: http
      scheme: bearer
      bearerFormat: JWT
      description: Short-lived HS256 access JWT sent only in the Authorization header.
    RefreshCookie:
      type: apiKey
      in: cookie
      name: __Host-sens_refresh
      description: |
        Host-only HttpOnly refresh cookie used in test and production. Local
        plain-HTTP development uses the cookie name sens_refresh without Secure.
  responses:
    AuthTokenResponse:
      description: A short-lived access token and rotated refresh cookie.
      headers:
        Cache-Control:
          schema:
            type: string
            const: no-store
        Set-Cookie:
          description: Host-only HttpOnly refresh cookie; never contains an access token.
          schema:
            type: string
      content:
        application/json:
          schema:
            $ref: "#/components/schemas/AuthTokenResponse"
    ErrorResponse:
      description: A safe platform error response.
      headers:
        Cache-Control:
          schema:
            type: string
            const: no-store
      content:
        application/json:
          schema:
            $ref: "#/components/schemas/HttpErrorResponse"
    BearerAuthenticationErrorResponse:
      description: Bearer authentication is missing, invalid, expired, or revoked.
      headers:
        Cache-Control:
          schema:
            type: string
            const: no-store
        WWW-Authenticate:
          schema:
            type: string
            const: Bearer
      content:
        application/json:
          schema:
            $ref: "#/components/schemas/HttpErrorResponse"
    RateLimitedResponse:
      description: Login or password-check capacity is temporarily limited.
      headers:
        Cache-Control:
          schema:
            type: string
            const: no-store
        Retry-After:
          schema:
            type: integer
            minimum: 1
      content:
        application/json:
          schema:
            $ref: "#/components/schemas/HttpErrorResponse"
    RefreshConflictResponse:
      description: The same refresh token is already rotating in a short race window.
      headers:
        Cache-Control:
          schema:
            type: string
            const: no-store
        Retry-After:
          schema:
            type: integer
            const: 1
      content:
        application/json:
          schema:
            $ref: "#/components/schemas/HttpErrorResponse"
  schemas:
    TenantId:
      type: string
      format: uuid
    TenantNumber:
      type: integer
      format: int32
      minimum: 1
      maximum: 2147483647
    OrganizationUnitId:
      type: string
      format: uuid
    UserId:
      type: string
      format: uuid
    AuthSessionId:
      type: string
      format: uuid
    MembershipId:
      type: string
      format: uuid
    DeviceId:
      type: string
      format: uuid
    GatewayId:
      type: string
      minLength: 1
    DeviceTypeId:
      type: string
      format: uuid
    DeviceFieldDefinitionId:
      type: string
      format: uuid
    FlowId:
      type: string
      format: uuid
    FlowVersionId:
      type: string
      format: uuid
    DeviceAssignmentId:
      type: string
      format: uuid
    RawIngestionMessageId:
      type: string
      format: uuid
    MeasurementId:
      type: string
      minLength: 1
    ExportJobId:
      type: string
      minLength: 1
    AuditLogId:
      type: string
      format: uuid
    EventId:
      type: string
      minLength: 1
    CorrelationId:
      type: string
      minLength: 1
    IsoDateTimeString:
      type: string
      format: date-time
    DevEui:
      type: string
      minLength: 16
      maxLength: 16
      pattern: "^[A-F0-9]{16}$"
    IotaSnapshotImportRequest:
      type: object
      additionalProperties: false
      required: [baseUrl, username, password]
      properties:
        baseUrl:
          type: string
          format: uri
          maxLength: 300
          pattern: "^https://commonapi-[a-z0-9](?:[a-z0-9-]*[a-z0-9])?\\.xaas-a0a0\\.cloud/api$"
        username:
          type: string
          minLength: 1
          maxLength: 200
        password:
          type: string
          format: password
          writeOnly: true
          minLength: 1
          maxLength: 500
    IotaSnapshotImportIssueCode:
      type: string
      enum:
        - invalid_device_type_code
        - invalid_dev_eui
        - missing_device_type
        - duplicate_source_dev_eui
        - device_exists_in_other_tenant
        - existing_device_mismatch
        - disabled_existing_device_type
        - asset_excluded
        - incomplete_source_record
    IotaSnapshotImportIssue:
      type: object
      additionalProperties: false
      required: [entity, code, sourceIdentifier]
      properties:
        entity:
          type: string
          enum: [device_type, device]
        code:
          $ref: "#/components/schemas/IotaSnapshotImportIssueCode"
        sourceIdentifier:
          type: string
          minLength: 1
          maxLength: 200
    IotaSnapshotImportEntityCounts:
      type: object
      additionalProperties: false
      required: [created, existing, skipped]
      properties:
        created: { type: integer, minimum: 0 }
        existing: { type: integer, minimum: 0 }
        skipped: { type: integer, minimum: 0 }
    IotaSnapshotImportSourceCounts:
      type: object
      additionalProperties: false
      required: [tenantCount, deviceTypeCount, deviceCount]
      properties:
        tenantCount: { type: integer, minimum: 0 }
        deviceTypeCount: { type: integer, minimum: 0 }
        deviceCount: { type: integer, minimum: 0 }
    IotaSnapshotImportResult:
      type: object
      additionalProperties: false
      required:
        - targetTenantId
        - completedAt
        - source
        - deviceTypes
        - devices
        - issues
      properties:
        targetTenantId: { $ref: "#/components/schemas/TenantId" }
        completedAt: { $ref: "#/components/schemas/IsoDateTimeString" }
        source: { $ref: "#/components/schemas/IotaSnapshotImportSourceCounts" }
        deviceTypes:
          $ref: "#/components/schemas/IotaSnapshotImportEntityCounts"
        devices:
          $ref: "#/components/schemas/IotaSnapshotImportEntityCounts"
        issues:
          type: array
          maxItems: 200
          items:
            $ref: "#/components/schemas/IotaSnapshotImportIssue"
    ContractMetadataValue:
      oneOf:
        - type: string
        - type: number
        - type: boolean
        - type: "null"
    ContractMetadata:
      type: object
      maxProperties: 50
      additionalProperties:
        $ref: "#/components/schemas/ContractMetadataValue"
    AdministrationStatus:
      type: string
      enum: [active, disabled]
    DeviceStatus:
      type: string
      enum: [active, disabled, retired]
    DeviceAssignment:
      type: object
      additionalProperties: false
      required:
        - deviceAssignmentId
        - deviceId
        - tenantId
        - organizationUnitId
        - validFrom
        - validTo
        - assignedAt
      properties:
        deviceAssignmentId: { $ref: "#/components/schemas/DeviceAssignmentId" }
        deviceId: { $ref: "#/components/schemas/DeviceId" }
        tenantId: { $ref: "#/components/schemas/TenantId" }
        organizationUnitId: { $ref: "#/components/schemas/OrganizationUnitId" }
        validFrom: { $ref: "#/components/schemas/IsoDateTimeString" }
        validTo:
          oneOf:
            - { $ref: "#/components/schemas/IsoDateTimeString" }
            - { type: "null" }
        assignedAt: { $ref: "#/components/schemas/IsoDateTimeString" }
    Device:
      type: object
      additionalProperties: false
      required:
        - deviceId
        - tenantId
        - deviceTypeId
        - devEui
        - name
        - status
        - metadata
        - currentAssignment
        - createdAt
        - updatedAt
      properties:
        deviceId: { $ref: "#/components/schemas/DeviceId" }
        tenantId: { $ref: "#/components/schemas/TenantId" }
        deviceTypeId: { $ref: "#/components/schemas/DeviceTypeId" }
        devEui: { $ref: "#/components/schemas/DevEui" }
        name: { type: string, minLength: 1, maxLength: 200 }
        status: { $ref: "#/components/schemas/DeviceStatus" }
        metadata: { $ref: "#/components/schemas/ContractMetadata" }
        currentAssignment:
          oneOf:
            - { $ref: "#/components/schemas/DeviceAssignment" }
            - { type: "null" }
        createdAt: { $ref: "#/components/schemas/IsoDateTimeString" }
        updatedAt: { $ref: "#/components/schemas/IsoDateTimeString" }
    DeviceListItem:
      type: object
      additionalProperties: false
      required:
        - deviceId
        - tenantId
        - deviceTypeId
        - devEui
        - name
        - status
        - metadata
        - currentAssignment
        - createdAt
        - updatedAt
        - currentOrganizationName
        - deviceTypeName
      properties:
        deviceId: { $ref: "#/components/schemas/DeviceId" }
        tenantId: { $ref: "#/components/schemas/TenantId" }
        deviceTypeId: { $ref: "#/components/schemas/DeviceTypeId" }
        devEui: { $ref: "#/components/schemas/DevEui" }
        name: { type: string, minLength: 1, maxLength: 200 }
        status: { $ref: "#/components/schemas/DeviceStatus" }
        metadata: { $ref: "#/components/schemas/ContractMetadata" }
        currentAssignment:
          oneOf:
            - { $ref: "#/components/schemas/DeviceAssignment" }
            - { type: "null" }
        createdAt: { $ref: "#/components/schemas/IsoDateTimeString" }
        updatedAt: { $ref: "#/components/schemas/IsoDateTimeString" }
        currentOrganizationName:
          oneOf:
            - { type: string, minLength: 1, maxLength: 200 }
            - { type: "null" }
        deviceTypeName: { type: string, minLength: 1, maxLength: 200 }
    CreateDeviceRequest:
      type: object
      additionalProperties: false
      required: [deviceTypeId, devEui, name]
      properties:
        deviceTypeId: { $ref: "#/components/schemas/DeviceTypeId" }
        devEui: { $ref: "#/components/schemas/DevEui" }
        name: { type: string, minLength: 1, maxLength: 200 }
        organizationUnitId: { $ref: "#/components/schemas/OrganizationUnitId" }
        metadata: { $ref: "#/components/schemas/ContractMetadata" }
    UpdateDeviceRequest:
      type: object
      additionalProperties: false
      minProperties: 1
      properties:
        deviceTypeId: { $ref: "#/components/schemas/DeviceTypeId" }
        name: { type: string, minLength: 1, maxLength: 200 }
        status: { $ref: "#/components/schemas/DeviceStatus" }
        metadata: { $ref: "#/components/schemas/ContractMetadata" }
    SetDeviceAssignmentRequest:
      type: object
      additionalProperties: false
      required: [organizationUnitId]
      properties:
        organizationUnitId: { $ref: "#/components/schemas/OrganizationUnitId" }
    DeviceListResponse:
      type: object
      additionalProperties: false
      required: [items, totalCount]
      properties:
        items:
          type: array
          items: { $ref: "#/components/schemas/DeviceListItem" }
        nextCursor: { type: string }
        totalCount: { type: integer, minimum: 0 }
    DeviceAssignmentListResponse:
      type: object
      additionalProperties: false
      required: [items]
      properties:
        items:
          type: array
          items: { $ref: "#/components/schemas/DeviceAssignment" }
        nextCursor: { type: string }
    DeviceType:
      type: object
      additionalProperties: false
      required:
        - deviceTypeId
        - tenantId
        - key
        - name
        - status
        - flowId
        - activeFlowVersionNumber
        - deviceCount
        - activeDeviceCount
        - metadata
        - createdAt
        - updatedAt
      properties:
        deviceTypeId: { $ref: "#/components/schemas/DeviceTypeId" }
        tenantId: { $ref: "#/components/schemas/TenantId" }
        key: { type: string, pattern: "^[a-z0-9][a-z0-9_-]{0,63}$" }
        name: { type: string, minLength: 1, maxLength: 200 }
        status: { $ref: "#/components/schemas/AdministrationStatus" }
        flowId:
          oneOf:
            - { $ref: "#/components/schemas/FlowId" }
            - { type: "null" }
        activeFlowVersionNumber:
          type: [integer, "null"]
          minimum: 1
        deviceCount:
          type: integer
          minimum: 0
          description: Count within the caller's visible device scope.
        activeDeviceCount:
          type: integer
          minimum: 0
          description: Active count within the caller's visible device scope.
        metadata: { $ref: "#/components/schemas/ContractMetadata" }
        createdAt: { $ref: "#/components/schemas/IsoDateTimeString" }
        updatedAt: { $ref: "#/components/schemas/IsoDateTimeString" }
    CreateDeviceTypeRequest:
      type: object
      additionalProperties: false
      required: [key, name]
      properties:
        key: { type: string, pattern: "^[a-z0-9][a-z0-9_-]{0,63}$" }
        name: { type: string, minLength: 1, maxLength: 200 }
        metadata: { $ref: "#/components/schemas/ContractMetadata" }
    UpdateDeviceTypeRequest:
      type: object
      additionalProperties: false
      minProperties: 1
      properties:
        name: { type: string, minLength: 1, maxLength: 200 }
        status: { $ref: "#/components/schemas/AdministrationStatus" }
        metadata: { $ref: "#/components/schemas/ContractMetadata" }
    DeviceFieldStorageTarget:
      type: string
      enum: [current, timeseries]
    DeviceFieldMeasurementType:
      type: string
      enum: [gauge, counter, status, event]
    DeviceFieldSchemaRef:
      type: string
      enum:
        - sens://schemas/number/v1
        - sens://schemas/string/v1
        - sens://schemas/boolean/v1
        - sens://schemas/json/v1
    DeviceFieldDefinition:
      type: object
      additionalProperties: false
      required:
        - deviceFieldDefinitionId
        - tenantId
        - deviceTypeId
        - key
        - name
        - description
        - schemaRef
        - unit
        - storageTargets
        - measurementType
        - status
        - lockVersion
        - createdAt
        - updatedAt
        - deprecatedAt
      properties:
        deviceFieldDefinitionId: { type: string, format: uuid }
        tenantId: { $ref: "#/components/schemas/TenantId" }
        deviceTypeId: { $ref: "#/components/schemas/DeviceTypeId" }
        key: { type: string, pattern: "^[a-z][a-z0-9._-]{0,119}$" }
        name: { type: string, minLength: 1, maxLength: 200 }
        description: { type: string, maxLength: 500 }
        schemaRef: { $ref: "#/components/schemas/DeviceFieldSchemaRef" }
        unit: { type: [string, "null"], minLength: 1, maxLength: 40 }
        storageTargets:
          type: array
          minItems: 1
          maxItems: 2
          uniqueItems: true
          items: { $ref: "#/components/schemas/DeviceFieldStorageTarget" }
        measurementType:
          oneOf:
            - { $ref: "#/components/schemas/DeviceFieldMeasurementType" }
            - { type: "null" }
        status: { type: string, enum: [active, deprecated] }
        lockVersion: { type: integer, minimum: 1 }
        createdAt: { $ref: "#/components/schemas/IsoDateTimeString" }
        updatedAt: { $ref: "#/components/schemas/IsoDateTimeString" }
        deprecatedAt:
          oneOf:
            - { $ref: "#/components/schemas/IsoDateTimeString" }
            - { type: "null" }
    PutDeviceFieldDefinitionRequest:
      type: object
      additionalProperties: false
      required: [name, description, schemaRef, storageTargets]
      properties:
        name: { type: string, minLength: 1, maxLength: 200 }
        description: { type: string, maxLength: 500 }
        schemaRef: { $ref: "#/components/schemas/DeviceFieldSchemaRef" }
        unit: { type: [string, "null"], minLength: 1, maxLength: 40 }
        storageTargets:
          type: array
          minItems: 1
          maxItems: 2
          uniqueItems: true
          items: { $ref: "#/components/schemas/DeviceFieldStorageTarget" }
        measurementType:
          oneOf:
            - { $ref: "#/components/schemas/DeviceFieldMeasurementType" }
            - { type: "null" }
    DeviceCurrentValue:
      type: object
      additionalProperties: false
      required:
        - value
        - observedAt
        - rawReceivedAt
        - recordedAt
        - sourceFlowExecutionId
        - sourceRawIngestionMessageId
      properties:
        value: {}
        observedAt: { $ref: "#/components/schemas/IsoDateTimeString" }
        rawReceivedAt: { $ref: "#/components/schemas/IsoDateTimeString" }
        recordedAt: { $ref: "#/components/schemas/IsoDateTimeString" }
        sourceFlowExecutionId: { type: [string, "null"], format: uuid }
        sourceRawIngestionMessageId: { type: [string, "null"], format: uuid }
    DeviceCurrentValuesResponse:
      type: object
      additionalProperties: false
      required: [tenantId, deviceId, deviceTypeId, fields]
      properties:
        tenantId: { $ref: "#/components/schemas/TenantId" }
        deviceId: { $ref: "#/components/schemas/DeviceId" }
        deviceTypeId: { $ref: "#/components/schemas/DeviceTypeId" }
        fields:
          type: array
          maxItems: 100
          items:
            type: object
            additionalProperties: false
            required: [definition, currentValue]
            properties:
              definition: { $ref: "#/components/schemas/DeviceFieldDefinition" }
              currentValue:
                oneOf:
                  - { $ref: "#/components/schemas/DeviceCurrentValue" }
                  - { type: "null" }
    DeviceTypeListResponse:
      type: object
      additionalProperties: false
      required: [items, totalCount]
      properties:
        items:
          type: array
          items: { $ref: "#/components/schemas/DeviceType" }
        nextCursor: { type: string }
        totalCount: { type: integer, minimum: 0 }
    FlowVersionStatus:
      type: string
      enum: [draft, released, deprecated]
    Flow:
      type: object
      additionalProperties: false
      required:
        - flowId
        - tenantId
        - deviceTypeId
        - deviceTypeName
        - key
        - name
        - description
        - activeVersionId
        - activeVersionNumber
        - createdAt
        - updatedAt
      properties:
        flowId: { $ref: "#/components/schemas/FlowId" }
        tenantId: { $ref: "#/components/schemas/TenantId" }
        deviceTypeId: { $ref: "#/components/schemas/DeviceTypeId" }
        deviceTypeName: { type: string, minLength: 1, maxLength: 200 }
        key: { type: string, pattern: "^[a-z0-9][a-z0-9_-]{0,63}$" }
        name: { type: string, minLength: 1, maxLength: 200 }
        description: { type: string, maxLength: 1000 }
        activeVersionId:
          oneOf:
            - { $ref: "#/components/schemas/FlowVersionId" }
            - { type: "null" }
        activeVersionNumber: { type: [integer, "null"], minimum: 1 }
        createdAt: { $ref: "#/components/schemas/IsoDateTimeString" }
        updatedAt: { $ref: "#/components/schemas/IsoDateTimeString" }
    DeviceOutputFieldSnapshotV1:
      type: object
      additionalProperties: false
      required:
        - deviceFieldDefinitionId
        - key
        - name
        - schemaRef
        - unit
        - storageTargets
        - measurementType
        - definitionLockVersion
        - status
      properties:
        deviceFieldDefinitionId: { type: string, format: uuid }
        key: { type: string, pattern: "^[a-z][a-z0-9._-]{0,119}$" }
        name: { type: string }
        schemaRef: { type: string, format: uri }
        unit: { type: [string, "null"] }
        storageTargets:
          type: array
          minItems: 1
          maxItems: 2
          uniqueItems: true
          items: { $ref: "#/components/schemas/DeviceFieldStorageTarget" }
        measurementType:
          oneOf:
            - { $ref: "#/components/schemas/DeviceFieldMeasurementType" }
            - { type: "null" }
        definitionLockVersion: { type: integer, minimum: 1 }
        status: { type: string, enum: [active, deprecated] }
    FlowBlockNodeV2:
      type: object
      additionalProperties: false
      required: [kind, nodeId, block, name, configuration, position]
      properties:
        kind: { type: string, const: block }
        nodeId: { type: string, minLength: 1, maxLength: 120 }
        block: { type: object }
        name: { type: string, minLength: 1, maxLength: 200 }
        configuration: { type: object }
        position:
          type: object
          additionalProperties: false
          required: [x, y]
          properties:
            x: { type: number }
            y: { type: number }
    FlowStorageOutputNodeV2:
      type: object
      additionalProperties: false
      required: [kind, nodeId, name, target, position, fields]
      properties:
        kind: { type: string, const: storage_output }
        nodeId: { type: string, enum: [current-output, timeseries-output] }
        name: { type: string }
        target: { $ref: "#/components/schemas/DeviceFieldStorageTarget" }
        position:
          type: object
          additionalProperties: false
          required: [x, y]
          properties:
            x: { type: number }
            y: { type: number }
        fields:
          type: array
          maxItems: 100
          items: { $ref: "#/components/schemas/DeviceOutputFieldSnapshotV1" }
    FlowGraph:
      type: object
      additionalProperties: false
      description: Contains exactly one current-output and one timeseries-output system node.
      required: [schemaVersion, deviceTypeId, nodes, edges, parameters]
      properties:
        schemaVersion: { type: string, const: flow-graph.v2 }
        deviceTypeId: { $ref: "#/components/schemas/DeviceTypeId" }
        nodes:
          type: array
          minItems: 2
          maxItems: 102
          items:
            oneOf:
              - $ref: "#/components/schemas/FlowBlockNodeV2"
              - $ref: "#/components/schemas/FlowStorageOutputNodeV2"
        edges:
          type: array
          items: { type: object }
        parameters:
          type: array
          items: { type: object }
    FlowVersion:
      type: object
      additionalProperties: false
      required:
        - flowVersionId
        - flowId
        - tenantId
        - versionNumber
        - status
        - graph
        - checksum
        - releasedAt
        - createdAt
      properties:
        flowVersionId: { $ref: "#/components/schemas/FlowVersionId" }
        flowId: { $ref: "#/components/schemas/FlowId" }
        tenantId: { $ref: "#/components/schemas/TenantId" }
        versionNumber: { type: integer, minimum: 1 }
        status: { $ref: "#/components/schemas/FlowVersionStatus" }
        graph: { $ref: "#/components/schemas/FlowGraph" }
        checksum: { type: [string, "null"] }
        releasedAt:
          oneOf:
            - { $ref: "#/components/schemas/IsoDateTimeString" }
            - { type: "null" }
        createdAt: { $ref: "#/components/schemas/IsoDateTimeString" }
    CreateFlowRequest:
      type: object
      additionalProperties: false
      required: [key, name]
      properties:
        key: { type: string, pattern: "^[a-z0-9][a-z0-9_-]{0,63}$" }
        name: { type: string, minLength: 1, maxLength: 200 }
        description: { type: string, maxLength: 1000 }
    CreateFlowVersionRequest:
      type: object
      additionalProperties: false
      required: [graph]
      properties:
        graph: { $ref: "#/components/schemas/FlowGraph" }
    SetActiveFlowVersionRequest:
      type: object
      additionalProperties: false
      required: [flowVersionId, expectedActiveVersionId]
      properties:
        flowVersionId: { $ref: "#/components/schemas/FlowVersionId" }
        expectedActiveVersionId:
          oneOf:
            - { $ref: "#/components/schemas/FlowVersionId" }
            - { type: "null" }
    FlowListResponse:
      type: object
      additionalProperties: false
      required: [items, totalCount]
      properties:
        items:
          type: array
          items: { $ref: "#/components/schemas/Flow" }
        nextCursor: { type: string }
        totalCount: { type: integer, minimum: 0 }
    FlowVersionListResponse:
      type: object
      additionalProperties: false
      required: [items]
      properties:
        items:
          type: array
          items: { $ref: "#/components/schemas/FlowVersion" }
        nextCursor: { type: string }
    FlowBlockLibraryResponse:
      type: object
      additionalProperties: false
      required: [items, schemas]
      properties:
        nextCursor: { type: string }
        items:
          type: array
          items:
            type: object
            required: [reference, manifest, scope]
            properties:
              reference: { type: object }
              manifest: { type: object }
              scope: { type: string, enum: [builtin, platform, tenant] }
        schemas:
          type: array
          items:
            type: object
            required: [$id, $schema]
            properties:
              $id: { type: string, format: uri }
              $schema:
                {
                  type: string,
                  const: "https://json-schema.org/draft/2020-12/schema"
                }
    FlowBlockLibrarySelectionResponse:
      type: object
      additionalProperties: false
      required: [item, schemas]
      properties:
        item:
          type: object
          required: [reference, manifest, scope]
          properties:
            reference: { type: object }
            manifest: { type: object }
            scope: { type: string, enum: [platform, tenant] }
        schemas:
          type: array
          items:
            type: object
            required: [$id, $schema]
            properties:
              $id: { type: string, format: uri }
              $schema:
                {
                  type: string,
                  const: "https://json-schema.org/draft/2020-12/schema"
                }
    CustomFlowBlockListResponse:
      $ref: "#/components/schemas/CountedFlowResourcePage"
    CustomFlowBlockVersionListResponse:
      $ref: "#/components/schemas/ProgressiveFlowResourcePage"
    FlowTemplateListResponse:
      $ref: "#/components/schemas/CountedFlowResourcePage"
    FlowTemplateVersionListResponse:
      $ref: "#/components/schemas/ProgressiveFlowResourcePage"
    FlowTestFixtureListResponse:
      $ref: "#/components/schemas/CountedFlowResourcePage"
    FlowParameterValueListResponse:
      $ref: "#/components/schemas/CountedFlowResourcePage"
    EmailTemplateListResponse:
      $ref: "#/components/schemas/CountedFlowResourcePage"
    EmailTemplateVersionListResponse:
      $ref: "#/components/schemas/ProgressiveFlowResourcePage"
    NotificationRecipientBindingListResponse:
      $ref: "#/components/schemas/CountedFlowResourcePage"
    NotificationRecipientBinding:
      type: object
      additionalProperties: false
      required: [bindingKey, scopeType, scopeId, mode, recipients, lockVersion]
      properties:
        bindingKey: { type: string }
        scopeType: { type: string, enum: [tenant, device_type, device] }
        scopeId: { type: string, format: uuid }
        mode: { type: string, enum: [recipients, disabled] }
        recipients:
          type: array
          items: { type: string, format: email }
        lockVersion: { type: integer, minimum: 1 }
    CountedFlowResourcePage:
      type: object
      additionalProperties: false
      required: [items, totalCount]
      properties:
        items:
          type: array
          items: { type: object }
        nextCursor: { type: string }
        totalCount: { type: integer, minimum: 0 }
    ProgressiveFlowResourcePage:
      type: object
      additionalProperties: false
      required: [items]
      properties:
        items:
          type: array
          items: { type: object }
        nextCursor: { type: string }
    FlowExecutionListResponse:
      type: object
      additionalProperties: false
      required: [items]
      properties:
        items:
          type: array
          items: { $ref: "#/components/schemas/FlowExecutionSummary" }
        nextCursor: { type: string }
    FlowExecutionSummary:
      type: object
      additionalProperties: true
      required:
        [
          flowExecutionId,
          tenantId,
          flowId,
          flowVersionId,
          mode,
          status,
          createdAt,
          finishedAt
        ]
      properties:
        flowExecutionId: { type: string, format: uuid }
        tenantId: { type: string, format: uuid }
        flowId: { type: [string, "null"], format: uuid }
        flowVersionId: { type: [string, "null"], format: uuid }
        mode: { type: string, enum: [live, preview, replay] }
        status:
          {
            type: string,
            enum:
              [
                queued,
                running,
                succeeded,
                failed,
                dead_lettered,
                skipped_no_flow
              ]
          }
        createdAt: { $ref: "#/components/schemas/IsoDateTimeString" }
        finishedAt:
          oneOf:
            - { $ref: "#/components/schemas/IsoDateTimeString" }
            - { type: "null" }
    FlowDeadLetterListResponse:
      type: object
      additionalProperties: false
      required: [items]
      properties:
        items:
          type: array
          items: { $ref: "#/components/schemas/FlowDeadLetter" }
        nextCursor: { type: string }
    FlowDeadLetter:
      type: object
      additionalProperties: true
      required:
        [
          flowDeadLetterId,
          tenantId,
          sourceKind,
          flowExecutionId,
          flowActionId,
          errorCode,
          errorMessage,
          attemptCount,
          status,
          redriveCount,
          lockVersion,
          createdAt
        ]
      properties:
        flowDeadLetterId: { type: string, format: uuid }
        tenantId: { type: string, format: uuid }
        sourceKind: { type: string, enum: [flow_execution, flow_action] }
        flowExecutionId: { type: string, format: uuid }
        flowActionId: { type: [string, "null"], format: uuid }
        errorCode: { type: string }
        errorMessage: { type: string, maxLength: 1000 }
        attemptCount: { type: integer, minimum: 1 }
        status: { type: string, enum: [open, redriven, resolved] }
        redriveCount: { type: integer, minimum: 0 }
        lockVersion: { type: integer, minimum: 1 }
        createdAt: { $ref: "#/components/schemas/IsoDateTimeString" }
    CreateCustomFlowBlockRequest:
      type: object
      additionalProperties: false
      required: [key, name]
      properties:
        key: { type: string, pattern: "^[a-z0-9][a-z0-9._-]{0,127}$" }
        name: { type: string, minLength: 1, maxLength: 200 }
        description: { type: string, maxLength: 500 }
    CreateCustomFlowBlockVersionRequest:
      type: object
      additionalProperties: false
      required: [name, description, category, inputs, outputs, source]
      properties:
        name: { type: string, minLength: 1, maxLength: 200 }
        description: { type: string, maxLength: 500 }
        category: { type: string, minLength: 1, maxLength: 80 }
        inputs:
          type: array
          maxItems: 32
          items:
            type: object
            required: [key, schemaRef, required, cardinality]
            properties:
              key: { type: string }
              schemaRef: { type: string, format: uri }
              required: { type: boolean }
              cardinality: { type: string, const: one }
        outputs:
          type: array
          minItems: 1
          maxItems: 32
          items:
            type: object
            additionalProperties: false
            required: [key, required]
            oneOf:
              - required: [schema]
              - required: [schemaRef]
            properties:
              key: { type: string }
              required: { type: boolean }
              schema: { type: object }
              schemaRef: { type: string, format: uri }
        parameterSchema: { type: object }
        source: { type: string, minLength: 1, maxLength: 65536 }
    CreateFlowTemplateRequest:
      type: object
      additionalProperties: false
      required: [key, name]
      properties:
        key: { type: string }
        name: { type: string }
        description: { type: string }
    DeviceOutputFieldRequirementV1:
      type: object
      additionalProperties: false
      required: [key, name, schemaRef, unit, storageTargets, measurementType]
      properties:
        key: { type: string, pattern: "^[a-z][a-z0-9._-]{0,119}$" }
        name: { type: string, minLength: 1, maxLength: 200 }
        schemaRef: { type: string, format: uri }
        unit: { type: [string, "null"] }
        storageTargets:
          type: array
          minItems: 1
          maxItems: 2
          uniqueItems: true
          items: { $ref: "#/components/schemas/DeviceFieldStorageTarget" }
        measurementType:
          oneOf:
            - { $ref: "#/components/schemas/DeviceFieldMeasurementType" }
            - { type: "null" }
    FlowTemplateStorageOutputNodeV2:
      type: object
      additionalProperties: false
      required: [kind, nodeId, name, target, position, fields]
      properties:
        kind: { type: string, const: storage_output }
        nodeId: { type: string, enum: [current-output, timeseries-output] }
        name: { type: string }
        target: { $ref: "#/components/schemas/DeviceFieldStorageTarget" }
        position:
          type: object
          required: [x, y]
          additionalProperties: false
          properties:
            x: { type: number }
            y: { type: number }
        fields:
          type: array
          maxItems: 100
          items: { $ref: "#/components/schemas/DeviceOutputFieldRequirementV1" }
    FlowTemplateGraphV2:
      type: object
      additionalProperties: false
      required: [schemaVersion, nodes, edges, parameters]
      properties:
        schemaVersion: { type: string, const: flow-template-graph.v2 }
        nodes:
          type: array
          minItems: 2
          maxItems: 102
          items:
            oneOf:
              - $ref: "#/components/schemas/FlowBlockNodeV2"
              - $ref: "#/components/schemas/FlowTemplateStorageOutputNodeV2"
        edges:
          type: array
          items: { type: object }
        parameters:
          type: array
          items: { type: object }
    CreateFlowTemplateVersionRequest:
      type: object
      additionalProperties: false
      required: [graph]
      properties:
        graph: { $ref: "#/components/schemas/FlowTemplateGraphV2" }
    InstantiateFlowTemplateRequest:
      type: object
      additionalProperties: false
      required: [flowTemplateVersionId, deviceTypeId, key, name]
      properties:
        flowTemplateVersionId: { type: string, format: uuid }
        deviceTypeId: { $ref: "#/components/schemas/DeviceTypeId" }
        key: { type: string }
        name: { type: string }
        description: { type: string }
    FlowExecutionInput:
      type: object
      additionalProperties: false
      required:
        [
          mode,
          rawPayload,
          observedAt,
          rawReceivedAt,
          tenantId,
          deviceId,
          deviceTypeId,
          correlationId,
          connectionData,
          parameters
        ]
      properties:
        mode: { type: string, enum: [live, preview, replay] }
        rawPayload:
          type: object
          required: [encoding, data]
          properties:
            encoding: { type: string, enum: [base64, hex, utf8] }
            data: { type: string }
        observedAt:
          allOf: [{ $ref: "#/components/schemas/IsoDateTimeString" }]
          description: Normalized Netmore timestamp used by both storage targets.
        rawReceivedAt:
          allOf: [{ $ref: "#/components/schemas/IsoDateTimeString" }]
          description: Local MQTT/raw receipt timestamp used for provenance and tie-breaking.
        tenantId: { $ref: "#/components/schemas/TenantId" }
        deviceId: { $ref: "#/components/schemas/DeviceId" }
        deviceTypeId: { $ref: "#/components/schemas/DeviceTypeId" }
        correlationId: { $ref: "#/components/schemas/CorrelationId" }
        connectionData: { type: object }
        parameters: { type: object }
    RawFlowTestInput:
      type: object
      additionalProperties: false
      required: [rawIngestionMessageId, receivedAt]
      properties:
        rawIngestionMessageId: { type: string, format: uuid }
        receivedAt: { $ref: "#/components/schemas/IsoDateTimeString" }
    CreateFlowFixtureRequest:
      type: object
      additionalProperties: false
      required: [name]
      oneOf:
        - required: [input]
        - required: [raw, confirmRawRetentionExtension]
      properties:
        name: { type: string, minLength: 1, maxLength: 200 }
        input: { $ref: "#/components/schemas/FlowExecutionInput" }
        raw: { $ref: "#/components/schemas/RawFlowTestInput" }
        confirmRawRetentionExtension: { type: boolean, const: true }
        assertions:
          type: array
          items:
            type: object
            required: [path, expected]
            properties:
              path: { type: string }
              expected: {}
    FlowTestRunRequest:
      type: object
      additionalProperties: false
      oneOf:
        - required: [flowVersionId]
        - required: [graph]
      anyOf:
        - required: [fixtureId]
        - required: [raw]
        - required: [manualInput]
      properties:
        flowVersionId: { $ref: "#/components/schemas/FlowVersionId" }
        graph: { $ref: "#/components/schemas/FlowGraph" }
        fixtureId: { type: string, format: uuid }
        raw: { $ref: "#/components/schemas/RawFlowTestInput" }
        manualInput: { $ref: "#/components/schemas/FlowExecutionInput" }
    PutFlowParameterValueRequest:
      type: object
      additionalProperties: false
      required: [value]
      properties:
        value: {}
        expectedVersion: { type: integer, minimum: 1 }
    CreateEmailTemplateRequest:
      type: object
      additionalProperties: false
      required: [key, name]
      properties:
        key: { type: string }
        name: { type: string }
    CreateEmailTemplateVersionRequest:
      type: object
      additionalProperties: false
      required: [subjectTemplate, textTemplate]
      properties:
        subjectTemplate: { type: string, maxLength: 500 }
        textTemplate: { type: string, maxLength: 65536 }
        htmlTemplate: { type: string, maxLength: 131072 }
    SetActiveEmailTemplateVersionRequest:
      type: object
      additionalProperties: false
      required: [emailTemplateVersionId]
      properties:
        emailTemplateVersionId: { type: string, format: uuid }
        expectedVersion: { type: integer, minimum: 1 }
    PutNotificationRecipientBindingRequest:
      type: object
      additionalProperties: false
      required: [mode]
      properties:
        mode: { type: string, enum: [recipients, disabled] }
        recipients:
          type: array
          uniqueItems: true
          items: { type: string, format: email }
        expectedVersion: { type: integer, minimum: 1 }
    PutFlowActionControlRequest:
      type: object
      additionalProperties: false
      required: [enabled, expectedVersion]
      properties:
        enabled: { type: boolean }
        expectedVersion: { type: integer, minimum: 1 }
    RedriveFlowDeadLetterRequest:
      type: object
      additionalProperties: false
      required: [expectedVersion]
      properties:
        expectedVersion: { type: integer, minimum: 1 }
        confirmPossibleDuplicateEmail: { type: boolean }
    OrganizationScopeRelation:
      type: string
      enum: [accessible, ancestor]
    Tenant:
      type: object
      additionalProperties: false
      required:
        [
          tenantId,
          tenantNumber,
          key,
          name,
          status,
          metadata,
          createdAt,
          updatedAt
        ]
      properties:
        tenantId: { $ref: "#/components/schemas/TenantId" }
        tenantNumber: { $ref: "#/components/schemas/TenantNumber" }
        key: { type: string, pattern: "^[a-z0-9][a-z0-9_-]{0,63}$" }
        name: { type: string, minLength: 1, maxLength: 200 }
        status: { $ref: "#/components/schemas/AdministrationStatus" }
        metadata: { $ref: "#/components/schemas/ContractMetadata" }
        createdAt: { $ref: "#/components/schemas/IsoDateTimeString" }
        updatedAt: { $ref: "#/components/schemas/IsoDateTimeString" }
    CreateTenantRequest:
      type: object
      additionalProperties: false
      required: [key, name]
      properties:
        key: { type: string, pattern: "^[a-z0-9][a-z0-9_-]{0,63}$" }
        name: { type: string, minLength: 1, maxLength: 200 }
        metadata: { $ref: "#/components/schemas/ContractMetadata" }
    UpdateTenantRequest:
      type: object
      additionalProperties: false
      minProperties: 1
      properties:
        key: { type: string, pattern: "^[a-z0-9][a-z0-9_-]{0,63}$" }
        name: { type: string, minLength: 1, maxLength: 200 }
        status: { $ref: "#/components/schemas/AdministrationStatus" }
        metadata: { $ref: "#/components/schemas/ContractMetadata" }
    TenantListResponse:
      type: object
      additionalProperties: false
      required: [items, totalCount]
      properties:
        items:
          type: array
          items: { $ref: "#/components/schemas/Tenant" }
        nextCursor: { type: string }
        totalCount: { type: integer, minimum: 0 }
    OrganizationUnit:
      type: object
      additionalProperties: false
      required:
        - organizationUnitId
        - tenantId
        - parentOrganizationUnitId
        - key
        - name
        - status
        - effectiveStatus
        - scopeRelation
        - hasChildren
        - metadata
        - createdAt
        - updatedAt
      properties:
        organizationUnitId: { $ref: "#/components/schemas/OrganizationUnitId" }
        tenantId: { $ref: "#/components/schemas/TenantId" }
        parentOrganizationUnitId:
          oneOf:
            - $ref: "#/components/schemas/OrganizationUnitId"
            - type: "null"
        key: { type: string, pattern: "^[a-z0-9][a-z0-9_-]{0,63}$" }
        name: { type: string, minLength: 1, maxLength: 200 }
        status: { $ref: "#/components/schemas/AdministrationStatus" }
        effectiveStatus: { $ref: "#/components/schemas/AdministrationStatus" }
        scopeRelation:
          { $ref: "#/components/schemas/OrganizationScopeRelation" }
        hasChildren:
          type: boolean
          description: Whether at least one direct child is visible to the current actor.
        metadata: { $ref: "#/components/schemas/ContractMetadata" }
        createdAt: { $ref: "#/components/schemas/IsoDateTimeString" }
        updatedAt: { $ref: "#/components/schemas/IsoDateTimeString" }
    CreateOrganizationUnitRequest:
      type: object
      additionalProperties: false
      required: [key, name]
      properties:
        key: { type: string, pattern: "^[a-z0-9][a-z0-9_-]{0,63}$" }
        name: { type: string, minLength: 1, maxLength: 200 }
        parentOrganizationUnitId:
          oneOf:
            - $ref: "#/components/schemas/OrganizationUnitId"
            - type: "null"
        metadata: { $ref: "#/components/schemas/ContractMetadata" }
    UpdateOrganizationUnitRequest:
      type: object
      additionalProperties: false
      minProperties: 1
      properties:
        key: { type: string, pattern: "^[a-z0-9][a-z0-9_-]{0,63}$" }
        name: { type: string, minLength: 1, maxLength: 200 }
        parentOrganizationUnitId:
          oneOf:
            - $ref: "#/components/schemas/OrganizationUnitId"
            - type: "null"
        status: { $ref: "#/components/schemas/AdministrationStatus" }
        metadata: { $ref: "#/components/schemas/ContractMetadata" }
    OrganizationUnitListResponse:
      type: object
      additionalProperties: false
      required: [items]
      properties:
        items:
          type: array
          items: { $ref: "#/components/schemas/OrganizationUnit" }
        nextCursor: { type: string }
    OrganizationUnitPathSegment:
      type: object
      additionalProperties: false
      required: [organizationUnitId, key, name, scopeRelation]
      properties:
        organizationUnitId: { $ref: "#/components/schemas/OrganizationUnitId" }
        key: { type: string, pattern: "^[a-z0-9][a-z0-9_-]{0,63}$" }
        name: { type: string, minLength: 1, maxLength: 200 }
        scopeRelation:
          { $ref: "#/components/schemas/OrganizationScopeRelation" }
    OrganizationUnitSearchResult:
      type: object
      additionalProperties: false
      required: [organizationUnit, visibleAncestorPath]
      properties:
        organizationUnit: { $ref: "#/components/schemas/OrganizationUnit" }
        visibleAncestorPath:
          type: array
          items: { $ref: "#/components/schemas/OrganizationUnitPathSegment" }
    OrganizationUnitSearchResponse:
      type: object
      additionalProperties: false
      required: [items]
      properties:
        items:
          type: array
          items: { $ref: "#/components/schemas/OrganizationUnitSearchResult" }
        nextCursor: { type: string }
    UserAdmin:
      type: object
      additionalProperties: false
      required:
        - userId
        - username
        - email
        - displayName
        - status
        - metadata
        - platformRoles
        - isProtected
        - createdAt
        - updatedAt
      properties:
        userId: { $ref: "#/components/schemas/UserId" }
        username:
          oneOf:
            - type: string
            - type: "null"
        email: { type: string, format: email }
        displayName: { type: string, minLength: 1, maxLength: 200 }
        status: { $ref: "#/components/schemas/AdministrationStatus" }
        metadata: { $ref: "#/components/schemas/ContractMetadata" }
        platformRoles:
          type: array
          items: { $ref: "#/components/schemas/PlatformRole" }
        isProtected: { type: boolean }
        createdAt: { $ref: "#/components/schemas/IsoDateTimeString" }
        updatedAt: { $ref: "#/components/schemas/IsoDateTimeString" }
    CreateUserRequest:
      type: object
      additionalProperties: false
      required: [username, email, displayName, temporaryPassword]
      properties:
        username: { type: string, minLength: 3, maxLength: 64 }
        email: { type: string, format: email }
        displayName: { type: string, minLength: 1, maxLength: 200 }
        temporaryPassword:
          type: string
          minLength: 15
          maxLength: 128
          writeOnly: true
        metadata: { $ref: "#/components/schemas/ContractMetadata" }
    UpdateUserRequest:
      type: object
      additionalProperties: false
      minProperties: 1
      properties:
        username: { type: string, minLength: 3, maxLength: 64 }
        email: { type: string, format: email }
        displayName: { type: string, minLength: 1, maxLength: 200 }
        status: { $ref: "#/components/schemas/AdministrationStatus" }
        metadata: { $ref: "#/components/schemas/ContractMetadata" }
    ResetUserPasswordRequest:
      type: object
      additionalProperties: false
      required: [temporaryPassword]
      properties:
        temporaryPassword:
          type: string
          minLength: 15
          maxLength: 128
          writeOnly: true
    UserAdminListResponse:
      type: object
      additionalProperties: false
      required: [items, totalCount]
      properties:
        items:
          type: array
          items: { $ref: "#/components/schemas/UserAdmin" }
        nextCursor: { type: string }
        totalCount: { type: integer, minimum: 0 }
    Membership:
      type: object
      additionalProperties: false
      required:
        - membershipId
        - tenantId
        - userId
        - organizationUnitId
        - role
        - includeDescendants
        - status
        - createdAt
        - updatedAt
      properties:
        membershipId: { $ref: "#/components/schemas/MembershipId" }
        tenantId: { $ref: "#/components/schemas/TenantId" }
        userId: { $ref: "#/components/schemas/UserId" }
        organizationUnitId:
          oneOf:
            - $ref: "#/components/schemas/OrganizationUnitId"
            - type: "null"
        role: { $ref: "#/components/schemas/TenantRole" }
        includeDescendants: { type: boolean }
        status: { $ref: "#/components/schemas/AdministrationStatus" }
        createdAt: { $ref: "#/components/schemas/IsoDateTimeString" }
        updatedAt: { $ref: "#/components/schemas/IsoDateTimeString" }
    MembershipListItem:
      type: object
      additionalProperties: false
      required:
        - membershipId
        - tenantId
        - userId
        - organizationUnitId
        - role
        - includeDescendants
        - status
        - createdAt
        - updatedAt
        - userDisplayName
        - userEmail
        - organizationUnitName
      properties:
        membershipId: { $ref: "#/components/schemas/MembershipId" }
        tenantId: { $ref: "#/components/schemas/TenantId" }
        userId: { $ref: "#/components/schemas/UserId" }
        organizationUnitId:
          oneOf:
            - $ref: "#/components/schemas/OrganizationUnitId"
            - type: "null"
        role: { $ref: "#/components/schemas/TenantRole" }
        includeDescendants: { type: boolean }
        status: { $ref: "#/components/schemas/AdministrationStatus" }
        createdAt: { $ref: "#/components/schemas/IsoDateTimeString" }
        updatedAt: { $ref: "#/components/schemas/IsoDateTimeString" }
        userDisplayName: { type: string, minLength: 1, maxLength: 200 }
        userEmail: { type: string, format: email }
        organizationUnitName:
          oneOf:
            - { type: string, minLength: 1, maxLength: 200 }
            - { type: "null" }
    CreateMembershipRequest:
      type: object
      additionalProperties: false
      required: [userId, role]
      properties:
        userId: { $ref: "#/components/schemas/UserId" }
        organizationUnitId:
          oneOf:
            - $ref: "#/components/schemas/OrganizationUnitId"
            - type: "null"
        role: { $ref: "#/components/schemas/TenantRole" }
        includeDescendants: { type: boolean }
    UpdateMembershipRequest:
      type: object
      additionalProperties: false
      minProperties: 1
      properties:
        organizationUnitId:
          oneOf:
            - $ref: "#/components/schemas/OrganizationUnitId"
            - type: "null"
        role: { $ref: "#/components/schemas/TenantRole" }
        includeDescendants: { type: boolean }
        status: { $ref: "#/components/schemas/AdministrationStatus" }
    MembershipListResponse:
      type: object
      additionalProperties: false
      required: [items, totalCount]
      properties:
        items:
          type: array
          items: { $ref: "#/components/schemas/MembershipListItem" }
        nextCursor: { type: string }
        totalCount: { type: integer, minimum: 0 }
    TenantUser:
      type: object
      additionalProperties: false
      required:
        - userId
        - username
        - email
        - displayName
        - status
        - memberships
        - createdAt
        - updatedAt
      properties:
        userId: { $ref: "#/components/schemas/UserId" }
        username:
          oneOf:
            - type: string
            - type: "null"
        email: { type: string, format: email }
        displayName: { type: string }
        status: { $ref: "#/components/schemas/AdministrationStatus" }
        memberships:
          type: array
          items: { $ref: "#/components/schemas/Membership" }
        createdAt: { $ref: "#/components/schemas/IsoDateTimeString" }
        updatedAt: { $ref: "#/components/schemas/IsoDateTimeString" }
    TenantUserListResponse:
      type: object
      additionalProperties: false
      required: [items, totalCount]
      properties:
        items:
          type: array
          items: { $ref: "#/components/schemas/TenantUser" }
        nextCursor: { type: string }
        totalCount: { type: integer, minimum: 0 }
    AuditEventType:
      type: string
      enum: [security_event, entity_change, api_request]
    AuditOutcome:
      type: string
      enum: [success, client_error, server_error, incomplete]
    AuditEvent:
      type: object
      additionalProperties: false
      required:
        - auditLogId
        - eventType
        - tenantId
        - actorType
        - actorId
        - sessionId
        - action
        - entityType
        - entityId
        - correlationId
        - occurredAt
        - httpMethod
        - httpRoute
        - httpStatusCode
        - requestStartedAt
        - requestCompletedAt
        - durationMs
        - outcome
        - metadata
      properties:
        auditLogId: { $ref: "#/components/schemas/AuditLogId" }
        eventType: { $ref: "#/components/schemas/AuditEventType" }
        tenantId:
          oneOf:
            - $ref: "#/components/schemas/TenantId"
            - type: "null"
        actorType: { $ref: "#/components/schemas/AuditActorType" }
        actorId: { type: string }
        sessionId:
          oneOf:
            - type: string
            - type: "null"
        action: { type: string }
        entityType: { type: string }
        entityId:
          oneOf:
            - type: string
            - type: "null"
        correlationId:
          oneOf:
            - type: string
            - type: "null"
        occurredAt: { $ref: "#/components/schemas/IsoDateTimeString" }
        httpMethod:
          oneOf:
            - type: string
            - type: "null"
        httpRoute:
          oneOf:
            - type: string
            - type: "null"
        httpStatusCode:
          oneOf:
            - type: integer
              minimum: 100
              maximum: 599
            - type: "null"
        requestStartedAt:
          oneOf:
            - $ref: "#/components/schemas/IsoDateTimeString"
            - type: "null"
        requestCompletedAt:
          oneOf:
            - $ref: "#/components/schemas/IsoDateTimeString"
            - type: "null"
        durationMs:
          oneOf:
            - type: integer
              minimum: 0
            - type: "null"
        outcome: { $ref: "#/components/schemas/AuditOutcome" }
        metadata: { $ref: "#/components/schemas/ContractMetadata" }
    AuditEventListResponse:
      type: object
      additionalProperties: false
      required: [items]
      properties:
        items:
          type: array
          items: { $ref: "#/components/schemas/AuditEvent" }
        nextCursor: { type: string }
    RawIngestionProcessingStatus:
      type: string
      enum: [accepted, duplicate, unknown_device, malformed, stored]
    NullableRawPayload:
      oneOf:
        - $ref: "#/components/schemas/RawPayload"
        - type: "null"
    RawIngestionMessageSummary:
      type: object
      required:
        - rawIngestionMessageId
        - receivedAt
        - recordedAt
        - eventId
        - correlationId
        - tenantId
        - deviceId
        - devEui
        - processingStatus
        - parseErrorCode
        - mqttTopic
        - mqttQos
        - mqttRetained
        - mqttDuplicate
        - mqttReceivedAt
        - mqttMessageId
        - networkServerProvider
        - networkServerServiceProvider
        - networkServerCustomer
        - networkServerReceivedAt
        - networkServerFrameCounter
        - transportPayloadSizeBytes
        - rawPayloadEncoding
        - rawPayloadDataLength
      properties:
        rawIngestionMessageId:
          { $ref: "#/components/schemas/RawIngestionMessageId" }
        receivedAt: { $ref: "#/components/schemas/IsoDateTimeString" }
        recordedAt: { $ref: "#/components/schemas/IsoDateTimeString" }
        eventId: { type: string }
        correlationId: { type: string }
        tenantId:
          oneOf:
            - $ref: "#/components/schemas/TenantId"
            - type: "null"
        deviceId:
          oneOf:
            - $ref: "#/components/schemas/DeviceId"
            - type: "null"
        devEui:
          oneOf:
            - $ref: "#/components/schemas/DevEui"
            - type: "null"
        processingStatus:
          { $ref: "#/components/schemas/RawIngestionProcessingStatus" }
        parseErrorCode:
          oneOf:
            - type: string
            - type: "null"
        mqttTopic: { type: string }
        mqttQos: { $ref: "#/components/schemas/MqttQosLevel" }
        mqttRetained: { type: boolean }
        mqttDuplicate: { type: boolean }
        mqttReceivedAt: { $ref: "#/components/schemas/IsoDateTimeString" }
        mqttMessageId:
          oneOf:
            - type: string
            - type: "null"
        networkServerProvider: { type: string }
        networkServerServiceProvider:
          oneOf:
            - type: string
            - type: "null"
        networkServerCustomer:
          oneOf:
            - type: string
            - type: "null"
        networkServerReceivedAt:
          oneOf:
            - $ref: "#/components/schemas/IsoDateTimeString"
            - type: "null"
        networkServerFrameCounter:
          oneOf:
            - type: integer
              minimum: 0
            - type: "null"
        transportPayloadSizeBytes:
          oneOf:
            - type: integer
              minimum: 0
            - type: "null"
        rawPayloadEncoding:
          oneOf:
            - $ref: "#/components/schemas/RawPayloadEncoding"
            - type: "null"
        rawPayloadDataLength:
          oneOf:
            - type: integer
              minimum: 0
            - type: "null"
    RawIngestionMessageDetail:
      allOf:
        - $ref: "#/components/schemas/RawIngestionMessageSummary"
        - type: object
          required:
            [
              schemaVersion,
              source,
              idempotencyKey,
              transportPayload,
              rawPayload
            ]
          properties:
            schemaVersion: { type: string }
            source: { type: string }
            idempotencyKey: { type: string }
            transportPayload:
              { $ref: "#/components/schemas/NullableRawPayload" }
            rawPayload: { $ref: "#/components/schemas/NullableRawPayload" }
    RawIngestionMessageListResponse:
      type: object
      additionalProperties: false
      required: [items]
      properties:
        items:
          type: array
          items: { $ref: "#/components/schemas/RawIngestionMessageSummary" }
        nextCursor: { type: string }
    OrganizationScope:
      type: object
      required:
        - organizationUnitIds
        - includeDescendants
      properties:
        organizationUnitIds:
          type: array
          items:
            $ref: "#/components/schemas/OrganizationUnitId"
        includeDescendants:
          type: boolean
    ActorType:
      type: string
      enum:
        - user
        - service
    AuditActorType:
      type: string
      enum:
        - anonymous
        - user
        - service
    ActorContext:
      type: object
      required:
        - actorType
        - actorId
      properties:
        actorType:
          $ref: "#/components/schemas/ActorType"
        actorId:
          type: string
          minLength: 1
        tenantId:
          $ref: "#/components/schemas/TenantId"
        organizationScope:
          $ref: "#/components/schemas/OrganizationScope"
    VersionResponse:
      type: object
      required:
        - version
      properties:
        version:
          type: string
          minLength: 1
          pattern: '^(0|[1-9][0-9]*)\.(0|[1-9][0-9]*)\.(0|[1-9][0-9]*)(?:-(?:local\.0|alpha\.[1-9][0-9]*\.[1-9][0-9]*|rc\.[1-9][0-9]*))?$'
          description: |
            Immutable deployed Platform SemVer. The release channel is tied to
            the runtime environment: `-local.0` locally, `-alpha.RUN.ATTEMPT`
            in test, `-rc.NUMBER` in staging, and no pre-release in Production.
          examples:
            - 0.1.2-local.0
            - 0.1.2-alpha.2.1
            - 0.1.2-rc.1
            - 0.1.2
    TestResponse:
      type: object
      required:
        - success
      properties:
        success:
          type: boolean
          const: true
    HealthStatus:
      type: string
      enum:
        - ok
    HealthResponse:
      type: object
      required:
        - status
        - service
        - timestamp
        - correlationId
      properties:
        status:
          $ref: "#/components/schemas/HealthStatus"
        service:
          type: string
          minLength: 1
        timestamp:
          $ref: "#/components/schemas/IsoDateTimeString"
        correlationId:
          $ref: "#/components/schemas/CorrelationId"
    ReadinessStatus:
      type: string
      enum:
        - ready
        - not_ready
    ReadinessCheckStatus:
      type: string
      enum:
        - ok
        - failed
    ReadinessCheckResult:
      type: object
      required:
        - name
        - status
      properties:
        name:
          type: string
          minLength: 1
        status:
          $ref: "#/components/schemas/ReadinessCheckStatus"
        message:
          type: string
    ReadinessResponse:
      type: object
      required:
        - status
        - service
        - timestamp
        - correlationId
        - checks
      properties:
        status:
          $ref: "#/components/schemas/ReadinessStatus"
        service:
          type: string
          minLength: 1
        timestamp:
          $ref: "#/components/schemas/IsoDateTimeString"
        correlationId:
          $ref: "#/components/schemas/CorrelationId"
        checks:
          type: array
          items:
            $ref: "#/components/schemas/ReadinessCheckResult"
    PlatformRole:
      type: string
      enum:
        - platform_admin
    TenantRole:
      type: string
      enum:
        - tenant_admin
        - org_admin
        - viewer
    AuthenticationMethod:
      type: string
      enum:
        - password
        - oidc
    MembershipGrant:
      type: object
      required:
        - membershipId
        - tenantId
        - role
        - includeDescendants
      properties:
        membershipId:
          $ref: "#/components/schemas/MembershipId"
        tenantId:
          $ref: "#/components/schemas/TenantId"
        organizationUnitId:
          $ref: "#/components/schemas/OrganizationUnitId"
        role:
          $ref: "#/components/schemas/TenantRole"
        includeDescendants:
          type: boolean
    AuthLoginRequest:
      type: object
      additionalProperties: false
      required:
        - identifier
        - password
      properties:
        identifier:
          type: string
          minLength: 1
          maxLength: 320
        password:
          type: string
          maxLength: 128
          writeOnly: true
    AuthTokenResponse:
      type: object
      additionalProperties: false
      required:
        - accessToken
        - tokenType
        - expiresInSeconds
        - refreshIdleExpiresAt
        - passwordChangeRequired
      properties:
        accessToken:
          type: string
        tokenType:
          type: string
          const: Bearer
        expiresInSeconds:
          type: integer
          minimum: 60
          maximum: 3600
          default: 1800
        refreshIdleExpiresAt:
          $ref: "#/components/schemas/IsoDateTimeString"
        passwordChangeRequired:
          type: boolean
    AuthMeResponse:
      type: object
      additionalProperties: false
      required:
        - userId
        - username
        - email
        - displayName
        - sessionId
        - authenticationMethod
        - platformRoles
        - memberships
        - passwordChangeRequired
      properties:
        userId:
          $ref: "#/components/schemas/UserId"
        username:
          type: string
        email:
          type: string
        displayName:
          type: string
        sessionId:
          $ref: "#/components/schemas/AuthSessionId"
        authenticationMethod:
          $ref: "#/components/schemas/AuthenticationMethod"
        platformRoles:
          type: array
          items:
            $ref: "#/components/schemas/PlatformRole"
        memberships:
          type: array
          items:
            $ref: "#/components/schemas/MembershipGrant"
        passwordChangeRequired:
          type: boolean
        analytics:
          $ref: "#/components/schemas/ProductAnalyticsIdentity"
    ProductAnalyticsIdentity:
      type: object
      additionalProperties: false
      description: >-
        Optional pseudonymous identity for explicitly consented product
        analytics. Present only when the server-side integration is enabled.
      required: [schemaVersion, subjectId, tenantGroupIds]
      properties:
        schemaVersion:
          type: integer
          const: 1
        subjectId:
          type: string
          pattern: "^[A-Za-z0-9_-]{43}$"
        tenantGroupIds:
          type: object
          propertyNames:
            type: string
            format: uuid
          additionalProperties:
            type: string
            pattern: "^[A-Za-z0-9_-]{43}$"
    AuthSession:
      type: object
      additionalProperties: false
      required:
        - sessionId
        - authenticationMethod
        - current
        - createdAt
        - lastSeenAt
        - idleExpiresAt
      properties:
        sessionId:
          $ref: "#/components/schemas/AuthSessionId"
        authenticationMethod:
          $ref: "#/components/schemas/AuthenticationMethod"
        current:
          type: boolean
        createdAt:
          $ref: "#/components/schemas/IsoDateTimeString"
        lastSeenAt:
          $ref: "#/components/schemas/IsoDateTimeString"
        idleExpiresAt:
          $ref: "#/components/schemas/IsoDateTimeString"
    AuthSessionsResponse:
      type: object
      additionalProperties: false
      required:
        - sessions
      properties:
        sessions:
          type: array
          maxItems: 10
          items:
            $ref: "#/components/schemas/AuthSession"
    ChangePasswordRequest:
      type: object
      additionalProperties: false
      required:
        - currentPassword
        - newPassword
      properties:
        currentPassword:
          type: string
          maxLength: 128
          writeOnly: true
        newPassword:
          type: string
          minLength: 15
          maxLength: 128
          writeOnly: true
    PlatformErrorCategory:
      type: string
      enum:
        - validation
        - authentication
        - authorization
        - not_found
        - conflict
        - dependency_failure
        - flow_failure
        - rate_limit
        - internal
    PlatformErrorCode:
      type: string
      enum:
        - VALIDATION_FAILED
        - AUTHENTICATION_FAILED
        - AUTHORIZATION_FAILED
        - RESOURCE_NOT_FOUND
        - CONFLICT
        - DEPENDENCY_FAILURE
        - FLOW_FAILURE
        - RATE_LIMITED
        - INTERNAL_ERROR
    PlatformErrorDetailValue:
      $ref: "#/components/schemas/ContractMetadataValue"
    PlatformError:
      type: object
      required:
        - code
        - category
        - message
        - correlationId
      properties:
        code:
          $ref: "#/components/schemas/PlatformErrorCode"
        category:
          $ref: "#/components/schemas/PlatformErrorCategory"
        message:
          type: string
          description: Safe client-facing diagnostic message.
        correlationId:
          $ref: "#/components/schemas/CorrelationId"
          description: Correlation ID for operational investigation.
        details:
          type: object
          additionalProperties:
            $ref: "#/components/schemas/PlatformErrorDetailValue"
          description: Safe bounded diagnostic details.
    HttpErrorResponse:
      type: object
      required:
        - error
      properties:
        error:
          $ref: "#/components/schemas/PlatformError"
    EventSource:
      type: string
      enum:
        - platform-api
        - mqtt-ingestion-worker
        - telemetry-worker
    RawPayloadEncoding:
      type: string
      enum:
        - base64
        - hex
        - utf8
    MqttQosLevel:
      type: integer
      enum:
        - 0
        - 1
        - 2
    RawPayload:
      type: object
      required:
        - encoding
        - data
      properties:
        encoding:
          $ref: "#/components/schemas/RawPayloadEncoding"
        data:
          type: string
    RawIngestionMqttEnvelope:
      type: object
      required:
        - topic
        - qos
        - retained
        - duplicate
        - receivedAt
      properties:
        topic:
          type: string
          minLength: 1
        qos:
          $ref: "#/components/schemas/MqttQosLevel"
        retained:
          type: boolean
        duplicate:
          type: boolean
        receivedAt:
          $ref: "#/components/schemas/IsoDateTimeString"
        messageId:
          type: string
    RawIngestionGatewayMetadata:
      type: object
      properties:
        gatewayId:
          $ref: "#/components/schemas/GatewayId"
        externalGatewayId:
          type: string
        receivedAt:
          $ref: "#/components/schemas/IsoDateTimeString"
        rssi:
          type: number
        snr:
          type: number
        metadata:
          $ref: "#/components/schemas/ContractMetadata"
    RawIngestionNetworkServerMetadata:
      type: object
      required:
        - provider
        - serviceProvider
        - customer
        - format
      properties:
        provider:
          type: string
          minLength: 1
        serviceProvider:
          type: string
          minLength: 1
        customer:
          type: string
          minLength: 1
        format:
          type: string
          minLength: 1
        messageId:
          type: string
        receivedAt:
          $ref: "#/components/schemas/IsoDateTimeString"
        frameCounter:
          type: integer
          minimum: 0
        gateways:
          type: array
          items:
            $ref: "#/components/schemas/RawIngestionGatewayMetadata"
        metadata:
          $ref: "#/components/schemas/ContractMetadata"
    RawIngestionEventV1:
      type: object
      required:
        - schemaVersion
        - eventId
        - occurredAt
        - source
        - correlationId
        - tenantId
        - idempotencyKey
        - mqtt
        - networkServer
        - transportPayload
        - parseStatus
      properties:
        schemaVersion:
          type: string
          const: raw-ingestion.v1
        eventId:
          $ref: "#/components/schemas/EventId"
        occurredAt:
          $ref: "#/components/schemas/IsoDateTimeString"
        source:
          $ref: "#/components/schemas/EventSource"
        correlationId:
          $ref: "#/components/schemas/CorrelationId"
        tenantId:
          oneOf:
            - $ref: "#/components/schemas/TenantId"
            - type: "null"
        idempotencyKey:
          type: string
          minLength: 1
        mqtt:
          $ref: "#/components/schemas/RawIngestionMqttEnvelope"
        networkServer:
          $ref: "#/components/schemas/RawIngestionNetworkServerMetadata"
        transportPayload:
          $ref: "#/components/schemas/RawPayload"
        parseStatus:
          type: string
          enum:
            - parsed
            - malformed
        parseErrorCode:
          type: string
          minLength: 1
        devEui:
          $ref: "#/components/schemas/DevEui"
        rawPayload:
          $ref: "#/components/schemas/RawPayload"
        metadata:
          $ref: "#/components/schemas/ContractMetadata"
    MeasurementValueType:
      type: string
      enum:
        - gauge
        - counter
        - status
        - event
    MeasurementValue:
      oneOf:
        - type: string
        - type: number
        - type: boolean
    TelemetryMeasurementDto:
      type: object
      required:
        - measurementId
        - tenantId
        - deviceId
        - measurementKey
        - value
        - valueType
        - observedAt
        - recordedAt
      properties:
        measurementId:
          $ref: "#/components/schemas/MeasurementId"
        tenantId:
          $ref: "#/components/schemas/TenantId"
        deviceId:
          $ref: "#/components/schemas/DeviceId"
        deviceTypeId:
          $ref: "#/components/schemas/DeviceTypeId"
        deviceFieldDefinitionId:
          $ref: "#/components/schemas/DeviceFieldDefinitionId"
        organizationUnitId:
          $ref: "#/components/schemas/OrganizationUnitId"
        measurementKey:
          type: string
          minLength: 1
          maxLength: 64
        value:
          $ref: "#/components/schemas/MeasurementValue"
        valueType:
          $ref: "#/components/schemas/MeasurementValueType"
        unit:
          type: string
          maxLength: 40
        observedAt:
          $ref: "#/components/schemas/IsoDateTimeString"
        recordedAt:
          $ref: "#/components/schemas/IsoDateTimeString"
        metadata:
          $ref: "#/components/schemas/ContractMetadata"
    TelemetryQueryRequestDto:
      type: object
      required:
        - tenantId
        - from
        - to
      properties:
        tenantId:
          $ref: "#/components/schemas/TenantId"
        organizationScope:
          $ref: "#/components/schemas/OrganizationScope"
        deviceIds:
          type: array
          items:
            $ref: "#/components/schemas/DeviceId"
        measurementKeys:
          type: array
          maxItems: 25
          items:
            type: string
            minLength: 1
            maxLength: 64
        from:
          $ref: "#/components/schemas/IsoDateTimeString"
        to:
          $ref: "#/components/schemas/IsoDateTimeString"
        limit:
          type: integer
          minimum: 1
          maximum: 5000
        cursor:
          type: string
    TelemetryQueryResponseDto:
      type: object
      required:
        - measurements
      properties:
        measurements:
          type: array
          maxItems: 5000
          items:
            $ref: "#/components/schemas/TelemetryMeasurementDto"
        nextCursor:
          type: string
          minLength: 1
          maxLength: 512
    ExportFormat:
      type: string
      enum:
        - csv
    ExportJobStatus:
      type: string
      enum:
        - queued
        - processing
        - completed
        - failed
        - expired
    CreateTelemetryExportRequestDto:
      type: object
      required:
        - tenantId
        - from
        - to
        - format
      properties:
        tenantId:
          $ref: "#/components/schemas/TenantId"
        organizationScope:
          $ref: "#/components/schemas/OrganizationScope"
        deviceIds:
          type: array
          items:
            $ref: "#/components/schemas/DeviceId"
        measurementKeys:
          type: array
          items:
            type: string
            minLength: 1
        from:
          $ref: "#/components/schemas/IsoDateTimeString"
        to:
          $ref: "#/components/schemas/IsoDateTimeString"
        format:
          $ref: "#/components/schemas/ExportFormat"
    ExportJobResultDto:
      type: object
      required:
        - fileName
        - contentType
        - sizeBytes
        - expiresAt
      properties:
        fileName:
          type: string
          minLength: 1
        contentType:
          type: string
          const: text/csv
        sizeBytes:
          type: integer
          minimum: 0
        expiresAt:
          $ref: "#/components/schemas/IsoDateTimeString"
    ExportJobFailureDto:
      type: object
      required:
        - code
        - message
      properties:
        code:
          type: string
          minLength: 1
        message:
          type: string
        details:
          $ref: "#/components/schemas/ContractMetadata"
    ExportJobDto:
      type: object
      required:
        - exportJobId
        - tenantId
        - status
        - format
        - requestedBy
        - parameters
        - requestedAt
        - updatedAt
      properties:
        exportJobId:
          $ref: "#/components/schemas/ExportJobId"
        tenantId:
          $ref: "#/components/schemas/TenantId"
        status:
          $ref: "#/components/schemas/ExportJobStatus"
        format:
          $ref: "#/components/schemas/ExportFormat"
        requestedBy:
          $ref: "#/components/schemas/ActorContext"
        parameters:
          $ref: "#/components/schemas/CreateTelemetryExportRequestDto"
        requestedAt:
          $ref: "#/components/schemas/IsoDateTimeString"
        updatedAt:
          $ref: "#/components/schemas/IsoDateTimeString"
        completedAt:
          $ref: "#/components/schemas/IsoDateTimeString"
        result:
          $ref: "#/components/schemas/ExportJobResultDto"
        failure:
          $ref: "#/components/schemas/ExportJobFailureDto"
tags:
  - name: Diagnostics
    description: Early deployment and smoke-test endpoints.
  - name: Authentication
    description: User and API-client authentication flows.
  - name: Tenants
    description: Tenant administration and isolation boundaries.
  - name: Organizations
    description: Flexible organization trees inside a tenant.
  - name: Devices
    description: Tenant- and organization-scoped device inventory and assignment history.
  - name: Device types
    description: Tenant device types, field contracts, and their unique telemetry flow.
  - name: Telemetry flows
    description: Typed flow graphs, immutable versions, validation, release, and activation.
  - name: Flow actions
    description: Tenant E-mail templates, recipients, controls, and Dead Letter operations.
  - name: Raw ingestion
    description: Platform-admin-only inspection of bounded raw-ingestion metadata and payloads.
