openapi: 3.1.0
info:
  title: Inventory API
  version: 1.15.0
  description: >-
    Organization-scoped API for configurable inventory structures, localized
    content, indoor 3D room scans, location-aware stock, cycle counts,
    assignments, CSV exchange, typed custom fields, label setups, media, AI,
    outgoing integration webhooks, and data quality. User-bound credentials may
    select one of their memberships with X-Organization-ID. Standalone API
    tokens are pinned to their issuing organization.
  license:
    name: MIT License
    identifier: MIT
servers:
  - url: /api/v1
security:
  - bearerAuth: []
tags:
  - name: Authentication
    description: Native sign-in, token revocation, and scope discovery.
  - name: Organizations
    description: Membership discovery, active-organization selection, creation, and administration.
  - name: Access control
    description: Session-administered roles, granular permissions, and conditional inventory-item access rules.
  - name: Resources
    description: Inventory item creation, lookup, retrieval, and lifecycle.
  - name: Inventory structure
    description: Configurable inventory types, relationship types, and directed resource relationships.
  - name: Data exchange
    description: Idempotent CSV import and complete CSV export.
  - name: Custom fields
    description: Typed field definitions, including filtered record references, for inventory items and serialized stock units.
  - name: Content translations
    description: Canonical content languages and field-aware AI translations.
  - name: Label setups
    description: Reusable physical dimensions and positioned content for printed inventory labels.
  - name: Stock
    description: Stock health, configuration, movements, and serialized units.
  - name: Inventory cycles
    description: Recurring count policies, due-count queues, and stock reconciliation.
  - name: Assignments
    description: Checkout, assignment, reservation, return, and cancellation workflows.
  - name: Manufacturing
    description: Bills of materials and atomic assembly builds.
  - name: Purchasing
    description: Purchase orders, incoming quantities, and stock receipts.
  - name: Scan workflows
    description: Configurable scan actions and execution.
  - name: Media
    description: Resource images, metadata, and ordering.
  - name: Spatial rooms
    description: Versioned RoomPlan scenes, native AR world-map assets, and measured indoor item placements.
  - name: AI
    description: Visual inventory recognition, counting, item analysis, and generated covers.
  - name: Insights
    description: Workspace statistics and duplicate detection.
  - name: Notifications
    description: Session-scoped in-app inbox, anti-noise preferences, Web Push subscriptions, and preview-only channel tests.
  - name: Webhooks
    description: Session-administered outgoing integration events with durable at-least-once delivery, HMAC signatures, retry history, and manual replay.
paths:
  /notifications:
    parameters:
      - { $ref: "#/components/parameters/OrganizationId" }
    get:
      tags: [Notifications]
      summary: List the current user's in-app notifications
      description: The inbox is always available. Events are deduplicated using the user's configured cooldown; external delivery is not required.
      security: [{ adminSession: [] }]
      parameters:
        - name: limit
          in: query
          schema: { type: integer, minimum: 1, maximum: 100, default: 30 }
        - name: unreadOnly
          in: query
          schema: { type: boolean, default: false }
      responses:
        "200":
          description: Notifications plus the complete unread count
          content:
            application/json:
              schema:
                type: object
                required: [notifications, unread]
                properties:
                  notifications:
                    type: array
                    items: { $ref: "#/components/schemas/Notification" }
                  unread: { type: integer, minimum: 0 }
        "403": { description: An authenticated browser session is required }
  /notifications/{id}:
    parameters:
      - { $ref: "#/components/parameters/OrganizationId" }
      - name: id
        in: path
        required: true
        schema: { type: string, format: uuid }
    patch:
      tags: [Notifications]
      summary: Mark one notification read
      security: [{ adminSession: [] }]
      responses:
        "200": { description: Notification marked read }
        "404": { description: Notification not found for this user }
  /notifications/read-all:
    parameters:
      - { $ref: "#/components/parameters/OrganizationId" }
    post:
      tags: [Notifications]
      summary: Mark every notification for the current user read
      security: [{ adminSession: [] }]
      responses:
        "200": { description: Number of updated inbox entries }
  /notifications/preferences:
    parameters:
      - { $ref: "#/components/parameters/OrganizationId" }
    get:
      tags: [Notifications]
      summary: Read personal anti-noise and channel preferences
      security: [{ adminSession: [] }]
      responses:
        "200":
          description: Preferences, redacted runtime channel state, and Push subscription count
    patch:
      tags: [Notifications]
      summary: Update personal notification preferences
      description: External channels default off and must be explicitly enabled. Destination URLs remain server environment variables and are never accepted by this endpoint.
      security: [{ adminSession: [] }]
      requestBody:
        required: true
        content:
          application/json:
            schema: { $ref: "#/components/schemas/NotificationPreferencePatch" }
      responses:
        "200": { description: Preferences updated }
        "422": { description: Invalid thresholds, cadence, fields, locale, or channel opt-ins }
  /notifications/push-subscriptions:
    parameters:
      - { $ref: "#/components/parameters/OrganizationId" }
    post:
      tags: [Notifications]
      summary: Subscribe the current browser to Web Push
      description: Subscription endpoint and key material are encrypted before storage using the deployment notification encryption key.
      security: [{ adminSession: [] }]
      responses:
        "201": { description: Encrypted subscription stored }
        "503": { description: VAPID or at-rest encryption is not configured }
    delete:
      tags: [Notifications]
      summary: Revoke the current browser's Web Push subscription
      security: [{ adminSession: [] }]
      responses:
        "200": { description: Subscription revoked when present }
  /notifications/test:
    parameters:
      - { $ref: "#/components/parameters/OrganizationId" }
    post:
      tags: [Notifications]
      summary: Preview one delivery channel without sending
      description: Dry-run only. This endpoint never invokes SMTP, Web Push, Slack, Teams, or generic webhook transports.
      security: [{ adminSession: [] }]
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              additionalProperties: false
              required: [channel]
              properties:
                channel: { type: string, enum: [email, push, slack, teams, webhook] }
      responses:
        "200": { description: Redacted sample payload with dryRun true }
  /notifications/run:
    parameters:
      - { $ref: "#/components/parameters/OrganizationId" }
    post:
      tags: [Notifications]
      summary: Run notification detection and due digest dispatch
      description: >-
        Invokes one deduplicated detector/dispatcher cycle. Authenticate with
        `Authorization: Bearer NOTIFICATION_CRON_SECRET`, or with an
        authenticated browser session that has `tokens.manage`. Safe retries
        rely on cooldown, inbox uniqueness, and dispatch dedupe keys.
      security:
        - cronSecret: []
        - adminSession: []
      responses:
        "200": { description: Detection and dispatch counters }
        "401": { description: Missing or invalid cron secret or browser session }
        "403": { description: Browser session lacks tokens.manage }
  /webhooks:
    parameters:
      - { $ref: "#/components/parameters/OrganizationId" }
    get:
      tags: [Webhooks]
      summary: List outgoing webhook endpoints
      description: Requires an authenticated browser session with `webhooks.manage`. Stored targets are redacted in responses, and API bearer tokens are not accepted.
      security: [{ adminSession: [] }]
      responses:
        "200":
          description: Configured webhook endpoints
          content:
            application/json:
              schema: { $ref: "#/components/schemas/WebhookListResponse" }
        "401": { description: An authenticated browser session is required }
        "403": { description: Browser session lacks webhooks.manage }
    post:
      tags: [Webhooks]
      summary: Create an outgoing webhook endpoint
      description: Requires `webhooks.manage`. The signing secret is returned only by this response; store it before leaving the page.
      security: [{ adminSession: [] }]
      requestBody:
        required: true
        content:
          application/json:
            schema: { $ref: "#/components/schemas/WebhookEndpointCreate" }
      responses:
        "201":
          description: Webhook endpoint created and its signing secret disclosed once
          content:
            application/json:
              schema: { $ref: "#/components/schemas/WebhookCreatedResponse" }
        "400": { description: Request body is not valid JSON }
        "401": { description: An authenticated browser session is required }
        "403": { description: Browser session lacks webhooks.manage }
        "422": { description: Invalid name, target URL, event subscription, or enabled state }
        "503": { description: Webhook at-rest encryption is not configured }
  /webhooks/{id}:
    parameters:
      - { $ref: "#/components/parameters/OrganizationId" }
      - { $ref: "#/components/parameters/WebhookId" }
    get:
      tags: [Webhooks]
      summary: Retrieve one outgoing webhook endpoint
      description: Requires an authenticated browser session with `webhooks.manage`. The target is redacted and the signing secret is never returned.
      security: [{ adminSession: [] }]
      responses:
        "200":
          description: Webhook endpoint
          content:
            application/json:
              schema: { $ref: "#/components/schemas/WebhookResponse" }
        "401": { description: An authenticated browser session is required }
        "403": { description: Browser session lacks webhooks.manage }
        "404": { description: Webhook endpoint not found }
        "422": { description: Invalid webhook endpoint identifier }
    patch:
      tags: [Webhooks]
      summary: Update an outgoing webhook endpoint
      description: Requires `webhooks.manage`. Submit at least one field. A new target must be supplied in full because stored targets are only returned in redacted form.
      security: [{ adminSession: [] }]
      requestBody:
        required: true
        content:
          application/json:
            schema: { $ref: "#/components/schemas/WebhookEndpointPatch" }
      responses:
        "200":
          description: Webhook endpoint updated
          content:
            application/json:
              schema: { $ref: "#/components/schemas/WebhookResponse" }
        "400": { description: Request body is not valid JSON }
        "401": { description: An authenticated browser session is required }
        "403": { description: Browser session lacks webhooks.manage }
        "404": { description: Webhook endpoint not found }
        "422": { description: Empty or invalid webhook endpoint update }
        "503": { description: Webhook at-rest encryption is not configured }
    delete:
      tags: [Webhooks]
      summary: Delete an outgoing webhook endpoint
      description: Requires an authenticated browser session with `webhooks.manage`.
      security: [{ adminSession: [] }]
      responses:
        "204": { description: Webhook endpoint deleted }
        "401": { description: An authenticated browser session is required }
        "403": { description: Browser session lacks webhooks.manage }
        "404": { description: Webhook endpoint not found }
        "422": { description: Invalid webhook endpoint identifier }
  /webhooks/{id}/test:
    parameters:
      - { $ref: "#/components/parameters/OrganizationId" }
      - { $ref: "#/components/parameters/WebhookId" }
    post:
      tags: [Webhooks]
      summary: Enqueue a real signed test delivery
      description: Requires `webhooks.manage`. Unlike notification channel previews, this creates an event and sends an actual HTTP request through the durable delivery queue.
      security: [{ adminSession: [] }]
      responses:
        "202":
          description: Test event and delivery accepted for asynchronous processing
          content:
            application/json:
              schema: { $ref: "#/components/schemas/WebhookTestResponse" }
        "401": { description: An authenticated browser session is required }
        "403": { description: Browser session lacks webhooks.manage }
        "404": { description: Webhook endpoint not found }
        "422": { description: Invalid webhook endpoint identifier }
        "503": { description: Webhook at-rest encryption is not configured }
  /webhooks/{id}/rotate-secret:
    parameters:
      - { $ref: "#/components/parameters/OrganizationId" }
      - { $ref: "#/components/parameters/WebhookId" }
    post:
      tags: [Webhooks]
      summary: Rotate a webhook signing secret
      description: Requires `webhooks.manage`. The replacement secret is returned only by this response and cannot be read again.
      security: [{ adminSession: [] }]
      responses:
        "200":
          description: Signing secret rotated and disclosed once
          content:
            application/json:
              schema: { $ref: "#/components/schemas/WebhookSecretResponse" }
        "401": { description: An authenticated browser session is required }
        "403": { description: Browser session lacks webhooks.manage }
        "404": { description: Webhook endpoint not found }
        "422": { description: Invalid webhook endpoint identifier }
        "503": { description: Webhook at-rest encryption is not configured }
  /webhooks/{id}/deliveries:
    parameters:
      - { $ref: "#/components/parameters/OrganizationId" }
      - { $ref: "#/components/parameters/WebhookId" }
    get:
      tags: [Webhooks]
      summary: List delivery attempts for one webhook endpoint
      description: Requires an authenticated browser session with `webhooks.manage`.
      security: [{ adminSession: [] }]
      parameters:
        - name: limit
          in: query
          description: Maximum number of recent deliveries returned; values above 100 are capped.
          schema: { type: integer, minimum: 1, default: 50 }
      responses:
        "200":
          description: Recent deliveries and their retry state
          content:
            application/json:
              schema: { $ref: "#/components/schemas/WebhookDeliveryListResponse" }
        "401": { description: An authenticated browser session is required }
        "403": { description: Browser session lacks webhooks.manage }
        "404": { description: Webhook endpoint not found }
        "422": { description: Invalid webhook endpoint identifier or delivery limit }
  /webhooks/{id}/deliveries/{deliveryId}/retry:
    parameters:
      - { $ref: "#/components/parameters/OrganizationId" }
      - { $ref: "#/components/parameters/WebhookId" }
      - { $ref: "#/components/parameters/WebhookDeliveryId" }
    post:
      tags: [Webhooks]
      summary: Manually retry a failed delivery
      description: Requires `webhooks.manage` and an enabled endpoint. The same event identity is retained so receivers can deduplicate an at-least-once replay.
      security: [{ adminSession: [] }]
      responses:
        "202":
          description: Delivery accepted for asynchronous retry
          content:
            application/json:
              schema: { $ref: "#/components/schemas/WebhookDeliveryResponse" }
        "401": { description: An authenticated browser session is required }
        "403": { description: Browser session lacks webhooks.manage }
        "404": { description: Enabled webhook endpoint or failed delivery not found }
        "422": { description: Invalid webhook endpoint or delivery identifier }
  /webhooks/run:
    parameters:
      - { $ref: "#/components/parameters/OrganizationId" }
    post:
      tags: [Webhooks]
      summary: Process due outgoing webhook deliveries
      description: >-
        Runs one bounded delivery cycle for deployments without the built-in
        worker. Authenticate only with `Authorization: Bearer
        WEBHOOK_CRON_SECRET`; browser sessions and API tokens are not accepted.
      security: [{ webhookCronSecret: [] }]
      parameters:
        - name: limit
          in: query
          description: Maximum number of due deliveries claimed during this bounded cycle.
          schema: { type: integer, minimum: 1, maximum: 100, default: 20 }
      responses:
        "200":
          description: Due deliveries processed
          content:
            application/json:
              schema: { $ref: "#/components/schemas/WebhookRunResponse" }
        "401": { description: Missing, invalid, or unconfigured webhook cron secret }
        "422": { description: Limit must be an integer between 1 and 100 }
  /spatial-structures:
    parameters:
      - { $ref: "#/components/parameters/OrganizationId" }
    get:
      tags: [Spatial rooms]
      summary: List buildings and their active indoor-scan coverage
      responses:
        "200":
          description: Structures with active room, floor, coordinate-space, and compatible-bounds counts
          content:
            application/json:
              schema:
                type: object
                required: [structures]
                properties:
                  structures:
                    type: array
                    items: { $ref: "#/components/schemas/SpatialStructureSummary" }
    post:
      tags: [Spatial rooms]
      summary: Create a building or other multi-room structure
      description: Requires write scope. A structure may also be created atomically by the first grouped room-scan upload.
      requestBody:
        required: true
        content:
          application/json:
            schema: { $ref: "#/components/schemas/SpatialStructureCreate" }
      responses:
        "201":
          description: Structure created
        "409": { description: The client-provided structure id already exists }
        "422": { description: Invalid structure or georeference }
  /spatial-structures/{structureId}:
    parameters:
      - { $ref: "#/components/parameters/OrganizationId" }
      - name: structureId
        in: path
        required: true
        schema: { type: string, format: uuid }
    get:
      tags: [Spatial rooms]
      summary: Retrieve active rooms grouped by floor and AR coordinate space
      responses:
        "200":
          description: Structure, coordinate spaces, floors, room scenes, and placements
        "404": { description: Spatial structure not found }
        "422": { description: Invalid spatial structure identifier }
    patch:
      tags: [Spatial rooms]
      summary: Update a structure's name or canonical map anchor
      requestBody:
        required: true
        content:
          application/json:
            schema: { $ref: "#/components/schemas/SpatialStructurePatch" }
      responses:
        "200": { description: Structure updated }
        "404": { description: Spatial structure not found }
        "422": { description: Invalid structure change or georeference }
  /room-scans:
    parameters:
      - { $ref: "#/components/parameters/OrganizationId" }
    get:
      tags: [Spatial rooms]
      summary: List active 3D room scans
      parameters:
        - name: includeSuperseded
          in: query
          schema: { type: boolean, default: false }
      responses:
        "200":
          description: Room scans ordered by capture time
          content:
            application/json:
              schema:
                type: object
                required: [scans]
                properties:
                  scans:
                    type: array
                    items: { $ref: "#/components/schemas/RoomScanSummary" }
        "401": { description: Missing or invalid session or bearer token }
        "403": { description: Authenticated identity does not have the read scope }
    post:
      tags: [Spatial rooms]
      summary: Upload a normalized RoomPlan scan and its native assets
      description: Requires write scope. The client-generated scan id makes exact retries idempotent. The referenced resource must have type `place`. RGB keyframes carry ARKit camera poses and intrinsics in the scan's coordinate space; each JPEG part name must match its metadata fileField.
      requestBody:
        required: true
        content:
          multipart/form-data:
            schema:
              type: object
              required: [id, roomResourceId, capturedAt, scene, worldMap, model]
              properties:
                id: { type: string, format: uuid }
                roomResourceId: { type: string, format: uuid }
                capturedAt: { type: string, format: date-time }
                deviceModel: { type: string, maxLength: 120 }
                structureId:
                  type: string
                  format: uuid
                  description: Groups this room into a building. The first upload may create this id.
                structureName: { type: string, minLength: 1, maxLength: 240 }
                coordinateSpaceId:
                  type: string
                  format: uuid
                  description: Shared by rooms captured or relocalized in the same ARKit world frame. Never reuse it for independently captured coordinate systems.
                floorIdentifier: { type: string, minLength: 1, maxLength: 120 }
                floorIndex: { type: integer, minimum: -1000, maximum: 10000 }
                roomIdentifier: { type: string, minLength: 1, maxLength: 120 }
                georeference:
                  type: string
                  contentMediaType: application/json
                  description: UTF-8 JSON encoding of SpatialGeoreference for this coordinate space.
                scene:
                  type: string
                  description: UTF-8 JSON encoding of the RoomScene schema.
                  contentMediaType: application/json
                worldMap:
                  type: string
                  format: binary
                  description: NSSecureCoding archive of the ARWorldMap used for later relocalization.
                model:
                  type: string
                  format: binary
                  description: Original RoomPlan USDZ mesh.
                structureModel:
                  type: string
                  format: binary
                  description: Optional combined multi-room USDZ model produced by StructureBuilder.
                guideImage: { type: string, format: binary }
                keyframes:
                  type: string
                  contentMediaType: application/json
                  description: UTF-8 JSON array of at most 32 RoomCameraKeyframeInput records. Each record references one JPEG multipart field named keyframe:<frame-id>.
                texturedMesh:
                  type: string
                  format: binary
                  description: Optional self-contained, bounded GLB v2 textured room mesh whose vertices are already in the scan's ARKit world frame in metres (model/gltf-binary, maximum 80 MB). External resources, unsafe accessor ranges/counts, oversized embedded textures, and unsupported required codecs are rejected.
                gaussianSplat:
                  type: string
                  format: binary
                  description: Optional bounded binary-little-endian, vertex-only PLY Gaussian Splat whose vertices are already in the scan's ARKit world frame in metres (maximum 80 MB).
      responses:
        "201":
          description: Scan created and made the active revision for its room
          content:
            application/json:
              schema: { $ref: "#/components/schemas/RoomScanUploadResult" }
        "200":
          description: Exact scan-id replay; no duplicate revision was created
          content:
            application/json:
              schema: { $ref: "#/components/schemas/RoomScanUploadResult" }
        "404": { description: Room resource not found }
        "409": { description: Scan id belongs to another room }
        "411": { description: Content-Length is required before buffering the multipart upload }
        "413": { description: Combined upload exceeds the configured room-scan limit }
        "415": { description: A keyframe or photorealistic derivative has an unsafe MIME type/extension pair }
        "422": { description: Invalid scene, identifier, date, or non-place resource }
  /room-scans/{scanId}:
    parameters:
      - { $ref: "#/components/parameters/OrganizationId" }
      - name: scanId
        in: path
        required: true
        schema: { type: string, format: uuid }
    get:
      tags: [Spatial rooms]
      summary: Retrieve a web-viewer scene with positioned inventory
      responses:
        "200":
          description: Normalized room scene, assets, and current placements attached to this revision
          content:
            application/json:
              schema:
                type: object
                required: [scene]
                properties:
                  scene: { $ref: "#/components/schemas/RoomSceneManifest" }
        "404": { description: Room scan not found }
        "422": { description: Invalid room scan identifier }
  /room-scans/{scanId}/layout:
    parameters:
      - { $ref: "#/components/parameters/OrganizationId" }
      - name: scanId
        in: path
        required: true
        schema: { type: string, format: uuid }
    patch:
      tags: [Spatial rooms]
      summary: Position one room inside its floor layout
      description: Requires spatial management permission. The transform is a column-major world-from-model matrix in metres. Set it to null to restore automatic floor arrangement.
      requestBody:
        required: true
        content:
          application/json:
            schema: { $ref: "#/components/schemas/RoomLayoutTransformPatch" }
      responses:
        "200": { description: Room layout position updated }
        "404": { description: Room scan not found }
        "422": { description: Invalid room scan identifier or layout transform }
  /room-scans/{scanId}/assets/{kind}:
    parameters:
      - { $ref: "#/components/parameters/OrganizationId" }
      - name: scanId
        in: path
        required: true
        schema: { type: string, format: uuid }
      - name: kind
        in: path
        required: true
        schema: { type: string, enum: [world_map, model_usdz, structure_model, guide_image, textured_mesh, gaussian_splat] }
    get:
      tags: [Spatial rooms]
      summary: Download an authenticated native room-scan asset
      responses:
        "200": { description: Binary asset returned as an attachment }
        "404": { description: Scan asset or stored bytes not found }
        "422": { description: Invalid room scan identifier }
    put:
      tags: [Spatial rooms]
      summary: Attach or replace a photorealistic room derivative
      description: Accepts raw GLB bytes for textured_mesh or raw bounded PLY bytes for gaussian_splat. Vertices must already use the scan's shared ARKit world frame (metres, right-handed, Y-up); no alignment transform is inferred. The web client renders PLY as a bounded point-splat preview; textured GLB is the full photorealistic path. This mutable enrichment does not change the idempotent identity of the original room capture. Content-Length is required before the server buffers the body.
      requestBody:
        required: true
        content:
          model/gltf-binary:
            schema: { type: string, format: binary, maxLength: 80000000 }
          application/octet-stream:
            schema: { type: string, format: binary, maxLength: 80000000 }
      responses:
        "200": { description: Existing derivative replaced }
        "201": { description: Derivative attached }
        "404": { description: Room scan or mutable asset kind not found }
        "413": { description: Asset exceeds its 80 MB limit }
        "411": { description: Content-Length is required }
        "415": { description: Content-Type does not match the asset kind }
        "422": { description: Invalid identifier or malformed GLB/PLY data }
  /room-scans/{scanId}/keyframes/{keyframeId}:
    parameters:
      - { $ref: "#/components/parameters/OrganizationId" }
      - name: scanId
        in: path
        required: true
        schema: { type: string, format: uuid }
      - name: keyframeId
        in: path
        required: true
        schema: { type: string, format: uuid }
    get:
      tags: [Spatial rooms]
      summary: Download an authenticated RGB localization keyframe
      responses:
        "200":
          description: Inline JPEG keyframe
          content:
            image/jpeg:
              schema: { type: string, format: binary }
        "404": { description: Keyframe or stored bytes not found }
        "422": { description: Invalid room scan or keyframe identifier }
  /room-scans/{scanId}/placements/{resourceId}:
    parameters:
      - { $ref: "#/components/parameters/OrganizationId" }
      - name: scanId
        in: path
        required: true
        schema: { type: string, format: uuid }
      - name: resourceId
        in: path
        required: true
        schema: { type: string, format: uuid }
    put:
      tags: [Spatial rooms]
      summary: Create or replace an item's current indoor 3D placement
      description: The scan must still be the active revision. Coordinates use its ARKit world frame in metres.
      requestBody:
        required: true
        content:
          application/json:
            schema: { $ref: "#/components/schemas/SpatialPlacementInput" }
      responses:
        "200": { description: Placement saved }
        "404": { description: Room scan or inventory item not found }
        "409": { description: Room scan was superseded and the item must be relocalized }
        "422": { description: Invalid identifier or placement }
    delete:
      tags: [Spatial rooms]
      summary: Remove an item's placement from this scan
      responses:
        "204": { description: Placement removed }
        "404": { description: Placement not found }
        "422": { description: Invalid identifier }
  /auth/login:
    post:
      tags: [Authentication]
      summary: Sign in a native client with a local account
      security: []
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [email, password]
              properties:
                email: { type: string, format: email, maxLength: 320 }
                password: { type: string, format: password, minLength: 1, maxLength: 72 }
                deviceName: { type: string, minLength: 1, maxLength: 80, default: iOS }
                organizationId:
                  type: string
                  format: uuid
                  description: Optional membership to select when signing in. The first membership is used when omitted.
      responses:
        "201":
          description: Signed in and issued a revocable user-bound bearer token
          content:
            application/json:
              schema: { $ref: "#/components/schemas/NativeLoginResponse" }
        "400": { description: Invalid login request }
        "401": { description: Email or password is incorrect }
        "429":
          description: Too many login attempts
          headers:
            Retry-After:
              schema: { type: integer, minimum: 1 }
  /auth/logout:
    parameters:
      - { $ref: "#/components/parameters/OrganizationId" }
    post:
      tags: [Authentication]
      summary: Revoke the current bearer token
      responses:
        "204": { description: Token revoked }
        "401": { description: Missing or invalid bearer token }
  /auth/capabilities:
    parameters:
      - { $ref: "#/components/parameters/OrganizationId" }
    get:
      tags: [Authentication]
      summary: Inspect the authenticated token's available scopes
      responses:
        "200":
          description: Token identity and capabilities
          content:
            application/json:
              schema:
                type: object
                required: [name, principal, scopes, role, roleName, permissions, organization, organizations]
                properties:
                  name: { type: string }
                  principal:
                    type: string
                    description: Opaque organization-scoped principal identifier for binding durable client state.
                  scopes:
                    type: array
                    items: { type: string, enum: [read, write, ai] }
                  role:
                    type: [string, "null"]
                    pattern: '^[a-z][a-z0-9_-]{0,63}$'
                    description: Stable role key for user-bound identities; null for standalone API tokens.
                  roleName:
                    type: [string, "null"]
                    description: Human-readable role name for user-bound identities; null for standalone API tokens.
                  permissions:
                    type: array
                    description: Granular workspace-wide permissions available through this token.
                    items: { $ref: "#/components/schemas/AppPermission" }
                  organization: { $ref: "#/components/schemas/OrganizationMembership" }
                  organizations:
                    type: array
                    items: { $ref: "#/components/schemas/OrganizationMembership" }
        "401": { description: Missing or invalid token }
  /organizations:
    parameters:
      - { $ref: "#/components/parameters/OrganizationId" }
    get:
      tags: [Organizations]
      summary: List organizations available to the current identity
      responses:
        "200":
          description: Memberships and the organization active for this request
          content:
            application/json:
              schema:
                type: object
                required: [organizations, activeOrganizationId]
                properties:
                  organizations:
                    type: array
                    items: { $ref: "#/components/schemas/OrganizationMembership" }
                  activeOrganizationId: { type: string, format: uuid }
        "401": { description: Missing or invalid authentication }
    post:
      tags: [Organizations]
      summary: Create an organization for the current browser user
      security: [{ adminSession: [] }]
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              additionalProperties: false
              required: [name]
              properties:
                name: { type: string, minLength: 1, maxLength: 160 }
      responses:
        "201":
          description: Organization created with the current user as admin
          content:
            application/json:
              schema:
                type: object
                required: [organization]
                properties:
                  organization: { $ref: "#/components/schemas/OrganizationMembership" }
        "403": { description: A browser session is required }
        "422": { description: Invalid organization name }
  /organizations/select:
    parameters:
      - { $ref: "#/components/parameters/OrganizationId" }
    post:
      tags: [Organizations]
      summary: Select one of the current user's organizations
      description: Browser sessions persist the choice in an HTTP-only cookie. Native clients should send X-Organization-ID on subsequent requests.
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              additionalProperties: false
              required: [organizationId]
              properties:
                organizationId: { type: string, format: uuid }
      responses:
        "200":
          description: Selected organization
          content:
            application/json:
              schema:
                type: object
                required: [organization]
                properties:
                  organization: { $ref: "#/components/schemas/OrganizationMembership" }
        "404": { description: Organization is not available to this identity }
  /organizations/{id}:
    parameters:
      - { $ref: "#/components/parameters/OrganizationId" }
      - name: id
        in: path
        required: true
        schema: { type: string, format: uuid }
    patch:
      tags: [Organizations]
      summary: Rename an organization
      security: [{ adminSession: [] }]
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              additionalProperties: false
              required: [name]
              properties:
                name: { type: string, minLength: 1, maxLength: 160 }
      responses:
        "200":
          description: Updated organization
          content:
            application/json:
              schema:
                type: object
                required: [organization]
                properties:
                  organization: { $ref: "#/components/schemas/OrganizationMembership" }
        "403": { description: Browser user lacks organization administration permission }
        "404": { description: Organization not found or not available to this user }
  /api/access/roles:
    parameters:
      - { $ref: "#/components/parameters/OrganizationId" }
    servers:
      - url: /
    get:
      tags: [Access control]
      summary: List roles, inventory access rules, and permission metadata
      description: Requires an authenticated browser session with the roles.manage permission. Rules grant additional item-specific permissions when every condition matches.
      security:
        - adminSession: []
      responses:
        "200":
          description: Roles, rules, and metadata used to administer granular access
          content:
            application/json:
              schema:
                type: object
                additionalProperties: false
                required: [roles, rules, permissionGroups, resourceRulePermissions]
                properties:
                  roles:
                    type: array
                    items: { $ref: "#/components/schemas/AccessRole" }
                  rules:
                    type: array
                    items: { $ref: "#/components/schemas/InventoryAccessRule" }
                  permissionGroups:
                    type: array
                    items: { $ref: "#/components/schemas/PermissionGroup" }
                  resourceRulePermissions:
                    type: array
                    items: { $ref: "#/components/schemas/InventoryRulePermission" }
        "401": { description: Missing or invalid authenticated session }
        "403": { description: A browser session with the roles.manage permission is required }
    post:
      tags: [Access control]
      summary: Create an access role
      description: Requires an authenticated browser session with the roles.manage permission. Role keys are stable identifiers used by users and conditional rules.
      security:
        - adminSession: []
      requestBody:
        required: true
        content:
          application/json:
            schema: { $ref: "#/components/schemas/AccessRoleInput" }
      responses:
        "201":
          description: Role created
          content:
            application/json:
              schema:
                type: object
                required: [role]
                properties:
                  role: { $ref: "#/components/schemas/AccessRole" }
        "400": { description: Request body is not valid JSON }
        "401": { description: Missing or invalid authenticated session }
        "403": { description: A browser session with the roles.manage permission is required }
        "409": { description: A role with the requested key already exists }
        "422": { description: Invalid role }
  /api/access/roles/{key}:
    servers:
      - url: /
    parameters:
      - { $ref: "#/components/parameters/OrganizationId" }
      - name: key
        in: path
        required: true
        description: Stable role key.
        schema: { $ref: "#/components/schemas/AccessRoleKey" }
    patch:
      tags: [Access control]
      summary: Update an access role
      description: Requires an authenticated browser session with the roles.manage permission. Changing permissions revokes user-bound API tokens for members of this role. The built-in Admin role must retain every permission.
      security:
        - adminSession: []
      requestBody:
        required: true
        content:
          application/json:
            schema: { $ref: "#/components/schemas/AccessRolePatch" }
      responses:
        "200":
          description: Role updated
          content:
            application/json:
              schema:
                type: object
                required: [role]
                properties:
                  role: { $ref: "#/components/schemas/AccessRole" }
        "400": { description: Request body is not valid JSON }
        "401": { description: Missing or invalid authenticated session }
        "403": { description: A browser session with the roles.manage permission is required }
        "404": { description: Role not found }
        "409": { description: The built-in Admin role would lose one or more permissions }
        "422": { description: Invalid or empty role update }
    delete:
      tags: [Access control]
      summary: Delete an access role
      description: Requires an authenticated browser session with the roles.manage permission. Built-in roles and roles assigned to users cannot be deleted; deleting a role also deletes its conditional inventory rules.
      security:
        - adminSession: []
      responses:
        "204": { description: Role and its conditional inventory rules deleted }
        "401": { description: Missing or invalid authenticated session }
        "403": { description: A browser session with the roles.manage permission is required }
        "404": { description: Role not found }
        "409": { description: Built-in role or role still assigned to one or more users }
  /api/access/rules:
    parameters:
      - { $ref: "#/components/parameters/OrganizationId" }
    servers:
      - url: /
    post:
      tags: [Access control]
      summary: Create a conditional inventory access rule
      description: Requires an authenticated browser session with the roles.manage permission. The rule grants its permissions to members of the selected role only for inventory items that match every condition.
      security:
        - adminSession: []
      requestBody:
        required: true
        content:
          application/json:
            schema: { $ref: "#/components/schemas/InventoryAccessRuleInput" }
      responses:
        "201":
          description: Inventory access rule created
          content:
            application/json:
              schema:
                type: object
                required: [rule]
                properties:
                  rule: { $ref: "#/components/schemas/InventoryAccessRule" }
        "400": { description: Request body is not valid JSON }
        "401": { description: Missing or invalid authenticated session }
        "403": { description: A browser session with the roles.manage permission is required }
        "422": { description: Invalid rule or selected role does not exist }
  /api/access/rules/{id}:
    servers:
      - url: /
    parameters:
      - { $ref: "#/components/parameters/OrganizationId" }
      - name: id
        in: path
        required: true
        description: Inventory access rule identifier.
        schema: { type: string, format: uuid }
    patch:
      tags: [Access control]
      summary: Update a conditional inventory access rule
      description: Requires an authenticated browser session with the roles.manage permission. Changing a rule revokes user-bound API tokens for its previous and current roles.
      security:
        - adminSession: []
      requestBody:
        required: true
        content:
          application/json:
            schema: { $ref: "#/components/schemas/InventoryAccessRulePatch" }
      responses:
        "200":
          description: Inventory access rule updated
          content:
            application/json:
              schema:
                type: object
                required: [rule]
                properties:
                  rule: { $ref: "#/components/schemas/InventoryAccessRule" }
        "400": { description: Invalid rule id or request body is not valid JSON }
        "401": { description: Missing or invalid authenticated session }
        "403": { description: A browser session with the roles.manage permission is required }
        "404": { description: Inventory access rule not found }
        "422": { description: Invalid or empty inventory access rule update }
    delete:
      tags: [Access control]
      summary: Delete a conditional inventory access rule
      description: Requires an authenticated browser session with the roles.manage permission. Deleting a rule revokes user-bound API tokens for its role.
      security:
        - adminSession: []
      responses:
        "204": { description: Inventory access rule deleted }
        "400": { description: Invalid inventory access rule id }
        "401": { description: Missing or invalid authenticated session }
        "403": { description: A browser session with the roles.manage permission is required }
        "404": { description: Inventory access rule not found }
  /custom-fields:
    parameters:
      - { $ref: "#/components/parameters/OrganizationId" }
    get:
      tags: [Custom fields]
      summary: List custom-field definitions
      description: Requires the read scope. Active definitions are returned by default, ordered by entity, position, and label.
      parameters:
        - name: entityType
          in: query
          description: Limit definitions to inventory items or serialized stock units.
          schema: { type: string, enum: [inventory, stock_unit] }
        - name: includeArchived
          in: query
          description: Include soft-deleted definitions.
          schema: { type: boolean, default: false }
      responses:
        "200":
          description: Matching custom-field definitions
          content:
            application/json:
              schema:
                type: object
                required: [definitions]
                properties:
                  definitions:
                    type: array
                    items: { $ref: "#/components/schemas/CustomFieldDefinition" }
        "401": { description: Missing or invalid session or bearer token }
        "403": { description: Authenticated identity does not have the read scope }
        "422": { description: Invalid query parameters }
    post:
      tags: [Custom fields]
      summary: Create a custom-field definition
      description: Requires an authenticated administrator session. Bearer tokens cannot create definitions. The key is generated from the label when omitted.
      security:
        - adminSession: []
      requestBody:
        required: true
        content:
          application/json:
            schema: { $ref: "#/components/schemas/CustomFieldDefinitionInput" }
      responses:
        "201":
          description: Custom-field definition created
          content:
            application/json:
              schema:
                type: object
                required: [definition]
                properties:
                  definition: { $ref: "#/components/schemas/CustomFieldDefinition" }
        "401": { description: Missing or invalid administrator session }
        "403": { description: A browser session with the admin role is required }
        "409": { description: The generated or supplied key already exists in this entity scope }
        "422": { description: Invalid custom-field definition }
  /custom-fields/{id}:
    parameters:
      - { $ref: "#/components/parameters/OrganizationId" }
      - { $ref: "#/components/parameters/CustomFieldId" }
    get:
      tags: [Custom fields]
      summary: Retrieve a custom-field definition
      description: Requires the read scope and can return either an active or archived definition.
      responses:
        "200":
          description: Custom-field definition
          content:
            application/json:
              schema:
                type: object
                required: [definition]
                properties:
                  definition: { $ref: "#/components/schemas/CustomFieldDefinition" }
        "401": { description: Missing or invalid session or bearer token }
        "403": { description: Authenticated identity does not have the read scope }
        "404": { description: Custom-field definition not found }
        "422": { description: Invalid definition id }
    patch:
      tags: [Custom fields]
      summary: Update a custom-field definition
      description: Requires an authenticated administrator session. Entity scope and key are immutable. Send the last observed revision to prevent overwriting a concurrent edit; a successful edit increments it.
      security:
        - adminSession: []
      requestBody:
        required: true
        content:
          application/json:
            schema: { $ref: "#/components/schemas/CustomFieldDefinitionPatch" }
      responses:
        "200":
          description: Custom-field definition updated
          content:
            application/json:
              schema:
                type: object
                required: [definition]
                properties:
                  definition: { $ref: "#/components/schemas/CustomFieldDefinition" }
        "401": { description: Missing or invalid administrator session }
        "403": { description: A browser session with the admin role is required }
        "404": { description: Custom-field definition not found }
        "409":
          description: Revision conflict; reload the definition before applying the update again
          content:
            application/json:
              schema:
                type: object
                required: [error, details]
                properties:
                  error: { type: string }
                  details:
                    type: object
                    required: [currentRevision]
                    properties:
                      currentRevision: { type: integer, minimum: 1 }
        "422": { description: Invalid or empty custom-field update }
    delete:
      tags: [Custom fields]
      summary: Archive a custom-field definition
      description: Requires an authenticated administrator session. Soft deletion increments the definition revision and preserves existing values stored under its key.
      security:
        - adminSession: []
      responses:
        "204": { description: Definition archived }
        "401": { description: Missing or invalid administrator session }
        "403": { description: A browser session with the admin role is required }
        "404": { description: Custom-field definition not found }
        "422": { description: Invalid definition id }
  /custom-fields/{id}/options:
    parameters:
      - { $ref: "#/components/parameters/OrganizationId" }
      - { $ref: "#/components/parameters/CustomFieldId" }
    get:
      tags: [Custom fields]
      summary: Search or resolve reference-field choices
      description: Requires the read scope. The definition must be an active reference field. Search results enforce its target type, category, and status filters; explicitly selected IDs are also resolved so previously saved values remain readable after filter changes.
      parameters:
        - name: q
          in: query
          description: Case-insensitive name, SKU, stock-unit code, or UUID search.
          schema: { type: string, maxLength: 120 }
        - name: selected
          in: query
          description: A saved target ID to resolve. Repeat for multiple IDs.
          schema:
            type: array
            maxItems: 100
            items: { type: string, format: uuid }
          style: form
          explode: true
        - name: limit
          in: query
          schema: { type: integer, minimum: 1, maximum: 50, default: 25 }
      responses:
        "200":
          description: Matching and explicitly selected reference options
          content:
            application/json:
              schema:
                type: object
                required: [options]
                properties:
                  options:
                    type: array
                    items: { $ref: "#/components/schemas/CustomFieldReferenceOption" }
        "401": { description: Missing or invalid session or bearer token }
        "403": { description: Authenticated identity does not have the read scope }
        "404": { description: Custom-field definition not found }
        "422": { description: Invalid query or the definition is not a reference field }
  /label-setups:
    parameters:
      - { $ref: "#/components/parameters/OrganizationId" }
    get:
      tags: [Label setups]
      summary: List reusable label setups
      description: Requires the read scope. Setups are ordered by name and contain the complete print layout.
      responses:
        "200":
          description: Label setups
          content:
            application/json:
              schema:
                type: object
                required: [labelSetups]
                properties:
                  labelSetups:
                    type: array
                    items: { $ref: "#/components/schemas/LabelSetup" }
        "401": { description: Missing or invalid session or bearer token }
        "403": { description: Authenticated identity does not have the read scope }
    post:
      tags: [Label setups]
      summary: Create a label setup
      description: Requires the write scope. Setup names are unique case-insensitively.
      requestBody:
        required: true
        content:
          application/json:
            schema: { $ref: "#/components/schemas/LabelSetupInput" }
      responses:
        "201":
          description: Label setup created
          content:
            application/json:
              schema:
                type: object
                required: [labelSetup]
                properties:
                  labelSetup: { $ref: "#/components/schemas/LabelSetup" }
        "400": { description: Request body is not valid JSON }
        "401": { description: Missing or invalid session or bearer token }
        "403": { description: Authenticated identity does not have the write scope }
        "409": { description: A label setup with that name already exists }
        "422": { description: Invalid label setup }
  /label-setups/{id}:
    parameters:
      - { $ref: "#/components/parameters/OrganizationId" }
      - name: id
        in: path
        required: true
        description: Label setup identifier.
        schema: { type: string, format: uuid }
    get:
      tags: [Label setups]
      summary: Retrieve a label setup
      description: Requires the read scope.
      responses:
        "200":
          description: Label setup
          content:
            application/json:
              schema:
                type: object
                required: [labelSetup]
                properties:
                  labelSetup: { $ref: "#/components/schemas/LabelSetup" }
        "401": { description: Missing or invalid session or bearer token }
        "403": { description: Authenticated identity does not have the read scope }
        "404": { description: Label setup not found }
        "422": { description: Invalid label setup id }
    patch:
      tags: [Label setups]
      summary: Update a label setup using its current revision
      description: Requires the write scope. Send the last observed revision to prevent overwriting a concurrent edit; a successful update increments it.
      requestBody:
        required: true
        content:
          application/json:
            schema: { $ref: "#/components/schemas/LabelSetupPatch" }
      responses:
        "200":
          description: Label setup updated
          content:
            application/json:
              schema:
                type: object
                required: [labelSetup]
                properties:
                  labelSetup: { $ref: "#/components/schemas/LabelSetup" }
        "400": { description: Request body is not valid JSON }
        "401": { description: Missing or invalid session or bearer token }
        "403": { description: Authenticated identity does not have the write scope }
        "404": { description: Label setup not found }
        "409": { description: Revision conflict or case-insensitive setup-name conflict }
        "422": { description: Invalid or empty label setup update }
    delete:
      tags: [Label setups]
      summary: Delete a label setup
      description: Requires the write scope and the last observed revision so a stale editor cannot delete newer work.
      parameters:
        - name: revision
          in: query
          required: true
          description: Current setup revision, preventing deletion from a stale editor.
          schema: { type: integer, minimum: 1 }
      responses:
        "204": { description: Label setup deleted }
        "401": { description: Missing or invalid session or bearer token }
        "403": { description: Authenticated identity does not have the write scope }
        "404": { description: Label setup not found }
        "409": { description: Setup revision changed; reload before deleting }
        "422": { description: Invalid label setup id or revision }
  /inventory-types:
    parameters:
      - { $ref: "#/components/parameters/OrganizationId" }
    get:
      tags: [Inventory structure]
      summary: List configured inventory types
      description: Requires the read scope. Active definitions are returned in configured display order. Administrators may include archived definitions.
      parameters:
        - name: includeArchived
          in: query
          description: Include archived definitions when authenticated as an administrator.
          schema: { type: boolean, default: false }
      responses:
        "200":
          description: Inventory type definitions
          content:
            application/json:
              schema:
                type: object
                required: [types]
                properties:
                  types: { type: array, items: { $ref: "#/components/schemas/InventoryTypeDefinition" } }
        "401": { description: Missing or invalid session or bearer token }
        "403": { description: Authenticated identity does not have the read scope }
    post:
      tags: [Inventory structure]
      summary: Create an inventory type
      description: Requires an authenticated administrator browser session. Type keys are stable identifiers used by inventory records.
      security:
        - adminSession: []
      requestBody:
        required: true
        content:
          application/json:
            schema: { $ref: "#/components/schemas/InventoryTypeInput" }
      responses:
        "201":
          description: Inventory type created
          content:
            application/json:
              schema:
                type: object
                required: [type]
                properties:
                  type: { $ref: "#/components/schemas/InventoryTypeDefinition" }
        "401": { description: Missing or invalid administrator session }
        "403": { description: A browser session with the admin role is required }
        "409": { description: The type key already exists }
        "422": { description: Invalid inventory type definition }
  /inventory-types/{key}:
    parameters:
      - { $ref: "#/components/parameters/OrganizationId" }
      - name: key
        in: path
        required: true
        schema: { $ref: "#/components/schemas/InventoryTypeKey" }
    patch:
      tags: [Inventory structure]
      summary: Update or restore an inventory type
      description: Requires an authenticated administrator browser session. The stable type key is immutable.
      security:
        - adminSession: []
      requestBody:
        required: true
        content:
          application/json:
            schema: { $ref: "#/components/schemas/InventoryTypePatch" }
      responses:
        "200":
          description: Inventory type updated
          content:
            application/json:
              schema:
                type: object
                required: [type]
                properties:
                  type: { $ref: "#/components/schemas/InventoryTypeDefinition" }
        "401": { description: Missing or invalid administrator session }
        "403": { description: A browser session with the admin role is required }
        "404": { description: Inventory type not found }
        "409": { description: The fallback type cannot be archived }
        "422": { description: Invalid or empty inventory type update }
    delete:
      tags: [Inventory structure]
      summary: Archive an inventory type
      description: Requires an authenticated administrator browser session. Existing inventory records retain their type; the fallback type cannot be archived.
      security:
        - adminSession: []
      responses:
        "200":
          description: Inventory type archived
          content:
            application/json:
              schema:
                type: object
                required: [type]
                properties:
                  type: { $ref: "#/components/schemas/InventoryTypeDefinition" }
        "401": { description: Missing or invalid administrator session }
        "403": { description: A browser session with the admin role is required }
        "404": { description: Inventory type not found }
        "409": { description: The fallback type cannot be archived }
        "422": { description: Invalid type key }
  /relation-types:
    parameters:
      - { $ref: "#/components/parameters/OrganizationId" }
    get:
      tags: [Inventory structure]
      summary: List configured relationship types
      description: Requires the read scope. Administrators may include archived definitions.
      parameters:
        - name: includeArchived
          in: query
          description: Include archived definitions when authenticated as an administrator.
          schema: { type: boolean, default: false }
      responses:
        "200":
          description: Relationship type definitions
          content:
            application/json:
              schema:
                type: object
                required: [relationTypes]
                properties:
                  relationTypes: { type: array, items: { $ref: "#/components/schemas/RelationTypeDefinition" } }
        "401": { description: Missing or invalid session or bearer token }
        "403": { description: Authenticated identity does not have the read scope }
    post:
      tags: [Inventory structure]
      summary: Create a relationship type
      description: Requires an authenticated administrator browser session.
      security:
        - adminSession: []
      requestBody:
        required: true
        content:
          application/json:
            schema: { $ref: "#/components/schemas/RelationTypeInput" }
      responses:
        "201":
          description: Relationship type created
          content:
            application/json:
              schema:
                type: object
                required: [relationType]
                properties:
                  relationType: { $ref: "#/components/schemas/RelationTypeDefinition" }
        "401": { description: Missing or invalid administrator session }
        "403": { description: A browser session with the admin role is required }
        "409": { description: The relationship type key already exists }
        "422": { description: Invalid relationship type definition }
  /relation-types/{key}:
    parameters:
      - { $ref: "#/components/parameters/OrganizationId" }
      - name: key
        in: path
        required: true
        schema: { $ref: "#/components/schemas/RelationTypeKey" }
    patch:
      tags: [Inventory structure]
      summary: Update, archive, or restore a relationship type
      description: Requires an authenticated administrator browser session. Built-in relationship types cannot be archived.
      security:
        - adminSession: []
      requestBody:
        required: true
        content:
          application/json:
            schema: { $ref: "#/components/schemas/RelationTypePatch" }
      responses:
        "200":
          description: Relationship type updated
          content:
            application/json:
              schema:
                type: object
                required: [relationType]
                properties:
                  relationType: { $ref: "#/components/schemas/RelationTypeDefinition" }
        "401": { description: Missing or invalid administrator session }
        "403": { description: A browser session with the admin role is required }
        "404": { description: Relationship type not found }
        "409": { description: A built-in relationship type cannot be archived }
        "422": { description: Invalid or empty relationship type update }
  /ai/recognize:
    parameters:
      - { $ref: "#/components/parameters/OrganizationId" }
    post:
      tags: [AI]
      summary: Match one photographed object to existing inventory items
      description: Requires direct inventory.read permission and the ai scope/ai.use permission. The uploaded image is transient and is not attached to an item. The server describes the dominant object, shortlists a bounded inventory catalog, and visually reranks up to five advisory matches. Clients must let the user review the result instead of treating it as an exact identifier.
      parameters:
        - { $ref: "#/components/parameters/RequiredIdempotencyKey" }
      requestBody:
        required: true
        content:
          multipart/form-data:
            schema:
              type: object
              additionalProperties: false
              required: [image]
              properties:
                image:
                  type: string
                  format: binary
                  description: Exactly one JPEG, PNG, WebP, AVIF, HEIC, or HEIF image.
      responses:
        "200":
          description: Ranked, advisory inventory matches; matches may be empty.
          content:
            application/json:
              schema: { $ref: "#/components/schemas/InventoryRecognitionResult" }
        "202": { description: An identical idempotent request is still processing; retry with the same image and key after Retry-After. }
        "400": { description: Malformed idempotency key or multipart request }
        "403": { description: Missing AI or direct inventory-read permission }
        "409": { description: Idempotency-Key was reused for another identity or image }
        "413": { description: Image exceeds the configured upload limit }
        "415": { description: Request is not multipart or the image type is unsupported }
        "422": { description: Missing, duplicate, empty, or unreadable image }
        "429": { description: AI recognition is disabled or the request limit was reached }
        "502": { description: Vision provider or structured-output error }
        "503": { description: Inventory catalog, retry protection, or shared rate limiter unavailable }
  /ai/count:
    parameters:
      - { $ref: "#/components/parameters/OrganizationId" }
    post:
      tags: [AI]
      summary: Count matching physical items in one transient image
      description: Requires the ai scope. A selectable server-side Replicate model analyzes the image; text-aware models use itemHint as their query. The result is advisory and does not mutate stock. Supply a stable Idempotency-Key when retrying the same upload so a lost start response cannot create another paid prediction.
      parameters:
        - { $ref: "#/components/parameters/RequiredIdempotencyKey" }
      requestBody:
        required: true
        content:
          multipart/form-data:
            schema:
              type: object
              required: [image]
              properties:
                image:
                  type: string
                  format: binary
                  description: One JPEG, PNG, WebP, AVIF, HEIC, or HEIF image.
                itemHint:
                  type: string
                  minLength: 1
                  maxLength: 240
                  description: Optional description of the object that should be counted.
                itemId:
                  type: string
                  format: uuid
                  description: Optional inventory item identifier used to scope idempotent retries.
                modelId:
                  type: string
                  enum: [grounding-dino, yolo-world, sam-2, sam-3]
                  description: Optional counting model. The server default is used when omitted.
      responses:
        "400": { description: Missing or malformed Idempotency-Key, or malformed multipart request }
        "200":
          description: Advisory visual count
          content:
            application/json:
              schema: { $ref: "#/components/schemas/InventoryCount" }
        "202":
          description: The Replicate prediction was created and is warming or processing; poll it with the signed jobToken.
          headers:
            Retry-After:
              schema: { type: integer, minimum: 1 }
          content:
            application/json:
              schema: { $ref: "#/components/schemas/InventoryCountJob" }
        "409":
          description: The same idempotent start is still being persisted, or Replicate may have accepted it without returning a job ID. Retry only with the same image and Idempotency-Key after Retry-After; the server keeps the attempt reserved long enough to prevent an overlapping paid prediction.
          headers:
            Retry-After:
              schema: { type: integer, minimum: 1 }
        "413": { description: Image exceeds the configured upload limit }
        "415": { description: Request is not multipart or the image type is unsupported }
        "422": { description: Invalid image, item hint, model, or provider detection limit }
        "429":
          description: AI request limit reached
          headers:
            Retry-After:
              schema: { type: integer, minimum: 1 }
        "502": { description: Replicate prediction creation or result-validation error }
        "503": { description: Replicate configuration, billing, authentication, capacity, or shared rate limiter unavailable }
        "504": { description: Replicate prediction exceeded the configured deadline }
  /ai/count/jobs:
    parameters:
      - { $ref: "#/components/parameters/OrganizationId" }
    post:
      tags: [AI]
      summary: Poll one signed Replicate count job
      description: Requires the ai scope and the same authenticated identity that created the job. Polling checks the existing prediction and does not start or bill another prediction.
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              additionalProperties: false
              required: [jobToken]
              properties:
                jobToken: { type: string, minLength: 1, maxLength: 4096 }
      responses:
        "200":
          description: Completed advisory visual count
          content:
            application/json:
              schema: { $ref: "#/components/schemas/InventoryCount" }
        "202":
          description: Prediction is still processing
          headers:
            Retry-After:
              schema: { type: integer, minimum: 1 }
          content:
            application/json:
              schema: { $ref: "#/components/schemas/InventoryCountJob" }
        "401": { description: Missing or invalid identity }
        "403": { description: Identity is missing the ai scope }
        "422": { description: Invalid, expired, or wrong-identity job token }
        "502":
          description: Replicate polling or result-validation error. terminal=true means the prediction itself has finished and a new count may be started; terminal=false means the same signed job should be resumed.
          content:
            application/json:
              schema:
                type: object
                required: [error, terminal]
                properties:
                  error: { type: string }
                  terminal: { type: boolean }
        "503": { description: Replicate authentication/configuration error, or a transient capacity/poll timeout marked with Retry-After }
        "504": { description: Prediction exceeded its deadline }
  /ai/count-models:
    parameters:
      - { $ref: "#/components/parameters/OrganizationId" }
    get:
      tags: [AI]
      summary: List Replicate counting models enabled for this deployment
      description: Requires the ai scope. The same catalog is consumed by the web app and iOS app.
      responses:
        "200":
          description: Available counting models and the deployment default
          content:
            application/json:
              schema:
                type: object
                required: [models, defaultModelId]
                properties:
                  models:
                    type: array
                    items: { $ref: "#/components/schemas/InventoryCountModel" }
                  defaultModelId:
                    type: string
                    enum: [grounding-dino, yolo-world, sam-2, sam-3]
        "401": { description: Missing or invalid token }
        "403": { description: Token is missing the ai scope }
  /ai/image-models:
    parameters:
      - { $ref: "#/components/parameters/OrganizationId" }
    get:
      tags: [AI]
      summary: List image-generation models enabled for this deployment
      description: Requires the ai scope. Only allowlisted models with configured provider credentials are returned.
      responses:
        "200":
          description: Available image-generation models and the deployment default
          content:
            application/json:
              schema:
                type: object
                required: [models, defaultModelId]
                properties:
                  models:
                    type: array
                    items: { $ref: "#/components/schemas/ImageGenerationModel" }
                  defaultModelId:
                    type: [string, "null"]
        "401": { description: Missing or invalid token }
        "403": { description: Token is missing the ai scope }
  /languages:
    parameters:
      - { $ref: "#/components/parameters/OrganizationId" }
    get:
      tags: [Content translations]
      summary: List active content languages
      responses:
        "200":
          description: Canonical and target languages
    post:
      tags: [Content translations]
      summary: Add a content language
      description: Requires an administrator browser session. The first language becomes the canonical default.
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [code, label]
              properties:
                code: { type: string, minLength: 2, maxLength: 35, examples: [de] }
                label: { type: string, minLength: 1, maxLength: 120, examples: [German] }
                isDefault: { type: boolean, default: false }
                autoTranslate: { type: boolean, default: true }
                instructions: { type: string, maxLength: 5000 }
                position: { type: integer, minimum: 0, maximum: 100000 }
      responses:
        "201": { description: Language added }
        "409": { description: Duplicate code or unsafe default-language change }
        "422": { description: Invalid language }
  /languages/{code}:
    parameters:
      - { $ref: "#/components/parameters/OrganizationId" }
      - { name: code, in: path, required: true, schema: { type: string, maxLength: 35 } }
    patch:
      tags: [Content translations]
      summary: Update a content language
      description: Requires an administrator browser session. The canonical default cannot change after translations exist.
      responses:
        "200": { description: Language updated }
        "404": { description: Language not found }
        "409": { description: Unsafe default-language change }
    delete:
      tags: [Content translations]
      summary: Archive a target content language
      description: Requires an administrator browser session. Saved translations are retained.
      responses:
        "200": { description: Language archived }
        "409": { description: The default language cannot be archived }
  /translations/regenerate:
    parameters:
      - { $ref: "#/components/parameters/OrganizationId" }
    post:
      tags: [Content translations, AI]
      summary: Queue a durable translation backfill
      description: Requires an administrator browser session. All selected resource-locale jobs are persisted before the response; processing continues in the background.
      requestBody:
        content:
          application/json:
            schema:
              type: object
              properties:
                languageCodes:
                  type: array
                  maxItems: 20
                  items: { type: string, maxLength: 35 }
                force: { type: boolean, default: false }
      responses:
        "200": { description: No target-language jobs were needed }
        "202": { description: Durable translation jobs queued }
  /resources:
    parameters:
      - { $ref: "#/components/parameters/OrganizationId" }
    get:
      tags: [Resources]
      summary: List inventory items
      parameters:
        - { name: q, in: query, schema: { type: string } }
        - { name: type, in: query, schema: { $ref: "#/components/schemas/InventoryTypeKey" } }
        - { name: status, in: query, schema: { type: string } }
        - { name: page, in: query, schema: { type: integer, minimum: 1 } }
        - { name: pageSize, in: query, schema: { type: integer, minimum: 1, maximum: 100 } }
        - { name: language, in: query, description: Active BCP 47 content locale. Missing or stale fields fall back individually to canonical content., schema: { type: string, maxLength: 35 } }
      responses:
        "200":
          description: Paginated inventory
          content:
            application/json:
              schema:
                type: object
                properties:
                  resources: { type: array, items: { $ref: "#/components/schemas/Resource" } }
                  pagination: { $ref: "#/components/schemas/Pagination" }
    post:
      tags: [Resources]
      summary: Create an inventory item
      parameters:
        - { $ref: "#/components/parameters/IdempotencyKey" }
      requestBody:
        required: true
        content:
          application/json:
            schema: { $ref: "#/components/schemas/ResourceInput" }
      responses:
        "200": { description: Replayed the original creation response }
        "201":
          description: Created
          content:
            application/json:
              schema:
                type: object
                properties:
                  resource: { $ref: "#/components/schemas/Resource" }
        "409": { description: Idempotency key was used with a different request }
  /resources/import:
    parameters:
      - { $ref: "#/components/parameters/OrganizationId" }
    post:
      tags: [Data exchange]
      summary: Import inventory items from UTF-8 CSV
      description: Creates up to 1,000 items from a raw CSV body of at most 5 MB. Rows are validated independently, existing items are never overwritten, and structured fields may contain JSON. The same Idempotency-Key and identical file can be retried safely.
      parameters:
        - { $ref: "#/components/parameters/RequiredIdempotencyKey" }
      requestBody:
        required: true
        content:
          text/csv:
            schema:
              type: string
              format: binary
              description: UTF-8 CSV with a required name header. See CsvImportResponse for row-level outcomes.
          application/csv:
            schema: { type: string, format: binary }
          application/vnd.ms-excel:
            schema: { type: string, format: binary }
          text/plain:
            schema: { type: string }
      responses:
        "200":
          description: Every row replayed from an earlier identical import
          content:
            application/json:
              schema: { $ref: "#/components/schemas/CsvImportResponse" }
        "201":
          description: Import completed without row errors and at least one row was created
          content:
            application/json:
              schema: { $ref: "#/components/schemas/CsvImportResponse" }
        "207":
          description: Import completed with one or more row-level errors
          content:
            application/json:
              schema: { $ref: "#/components/schemas/CsvImportResponse" }
        "400": { description: "Missing or invalid Idempotency-Key, malformed CSV, or invalid UTF-8" }
        "413": { description: "File exceeds 5 MB or contains more than 1,000 data rows" }
        "415": { description: Unsupported media type }
        "422": { description: Missing rows or invalid CSV headers }
  /resources/export:
    parameters:
      - { $ref: "#/components/parameters/OrganizationId" }
    get:
      tags: [Data exchange]
      summary: Export the complete inventory as CSV, Excel, or PDF
      description: Returns up to 100,000 inventory records. CSV remains the default and is BOM-prefixed with potential spreadsheet formulas escaped. Excel includes formatted, filterable inventory and variant sheets; PDF is a compact printable report.
      parameters:
        - in: query
          name: format
          schema: { type: string, enum: [csv, xlsx, pdf], default: csv }
          description: Download format. Omitting the parameter preserves the existing CSV response.
        - in: query
          name: lang
          schema: { type: string, enum: [en, de], default: en }
          description: Report language used for generated Excel and PDF labels.
      responses:
        "200":
          description: Inventory export in the requested format
          headers:
            Content-Disposition:
              schema: { type: string }
              description: Dated inventory filename with an extension matching the requested format.
          content:
            text/csv:
              schema: { type: string }
            application/vnd.openxmlformats-officedocument.spreadsheetml.sheet:
              schema: { type: string, format: binary }
            application/pdf:
              schema: { type: string, format: binary }
        "400": { description: "Unsupported export format" }
        "413": { description: "The workspace contains more than 100,000 records" }
  /resources/batch:
    parameters:
      - { $ref: "#/components/parameters/OrganizationId" }
    patch:
      tags: [Resources]
      summary: Apply shared fields and additional tags to multiple inventory items atomically
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [ids]
              properties:
                ids:
                  type: array
                  minItems: 1
                  maxItems: 100
                  items: { type: string, format: uuid }
                changes:
                  type: object
                  properties:
                    type: { $ref: "#/components/schemas/InventoryTypeKey" }
                    status: { type: string, enum: [available, in-use, maintenance, archived] }
                    location: { type: [string, "null"], maxLength: 240 }
                    priority: { type: integer, minimum: 1, maximum: 5 }
                addTags:
                  type: array
                  maxItems: 40
                  items: { type: string, minLength: 1, maxLength: 60 }
      responses:
        "200": { description: Selected inventory items updated }
        "404": { description: At least one selected inventory item no longer exists }
        "422": { description: Invalid or empty batch update }
  /resources/{id}:
    parameters:
      - { $ref: "#/components/parameters/OrganizationId" }
      - { $ref: "#/components/parameters/ResourceId" }
    get:
      tags: [Resources]
      summary: Retrieve an inventory item
      parameters:
        - name: language
          in: query
          description: Enabled BCP 47 content language. Missing or stale translated fields fall back to canonical content.
          schema: { type: string, maxLength: 35, examples: [de] }
      responses:
        "200":
          description: Inventory item
          content:
            application/json:
              schema:
                type: object
                properties:
                  resource: { $ref: "#/components/schemas/Resource" }
        "404": { $ref: "#/components/responses/NotFound" }
    patch:
      tags: [Resources]
      summary: Update an inventory item
      requestBody:
        required: true
        content:
          application/json:
            schema: { $ref: "#/components/schemas/ResourcePatch" }
      responses:
        "200": { description: Updated }
        "404": { $ref: "#/components/responses/NotFound" }
    delete:
      tags: [Resources]
      summary: Delete an inventory item and its local media
      responses:
        "204": { description: Deleted }
        "404": { $ref: "#/components/responses/NotFound" }
  /resources/{id}/variants:
    parameters:
      - { $ref: "#/components/parameters/OrganizationId" }
      - { $ref: "#/components/parameters/ResourceId" }
    get:
      tags: [Resources, Stock]
      summary: List optional bulk-stock variants for an inventory item
      responses:
        "200":
          description: Variants and the allocated/unallocated parent stock summary
          content:
            application/json:
              schema:
                type: object
                required: [variants, summary, trackingMode]
                properties:
                  variants: { type: array, items: { $ref: "#/components/schemas/ResourceVariant" } }
                  summary: { $ref: "#/components/schemas/ResourceVariantStockSummary" }
                  trackingMode: { type: string, enum: [bulk, serialized] }
        "404": { $ref: "#/components/responses/NotFound" }
    post:
      tags: [Resources, Stock]
      summary: Add a variant and optionally allocate existing parent stock
      description: Variants are bulk-only. `initialAllocation` moves existing unallocated parent stock into the variant without changing the parent total.
      requestBody:
        required: true
        content:
          application/json:
            schema: { $ref: "#/components/schemas/ResourceVariantInput" }
      responses:
        "201": { description: Variant created }
        "409": { description: Identifier conflict, serialized tracking, or insufficient unallocated stock }
        "422": { description: Invalid variant }
  /resources/{id}/variants/{variantId}:
    parameters:
      - { $ref: "#/components/parameters/OrganizationId" }
      - { $ref: "#/components/parameters/ResourceId" }
      - name: variantId
        in: path
        required: true
        schema: { type: string, format: uuid }
    patch:
      tags: [Resources]
      summary: Update variant identity or price fields
      description: Quantity cannot be patched; use a dated variant stock movement.
      requestBody:
        required: true
        content:
          application/json:
            schema: { $ref: "#/components/schemas/ResourceVariantPatch" }
      responses:
        "200": { description: Variant updated }
        "404": { $ref: "#/components/responses/NotFound" }
        "409": { description: SKU or barcode is already used }
    delete:
      tags: [Resources]
      summary: Delete an unused zero-stock variant
      responses:
        "204": { description: Variant deleted }
        "409": { description: Variant has stock or movement history }
  /resources/{id}/variants/{variantId}/stock/movements:
    parameters:
      - { $ref: "#/components/parameters/OrganizationId" }
      - { $ref: "#/components/parameters/ResourceId" }
      - name: variantId
        in: path
        required: true
        schema: { type: string, format: uuid }
    post:
      tags: [Stock]
      summary: Book an atomic parent and variant stock movement
      description: The signed delta updates parent and variant quantities together and appends one dated stock-ledger row.
      requestBody:
        required: true
        content:
          application/json:
            schema: { $ref: "#/components/schemas/ResourceVariantStockMovementInput" }
      responses:
        "201": { description: Parent and variant stock updated atomically }
        "404": { $ref: "#/components/responses/NotFound" }
        "409": { description: Insufficient stock, serialized tracking, or maximum quantity exceeded }
        "422": { description: Invalid stock movement }
  /resources/{id}/translations:
    parameters:
      - { $ref: "#/components/parameters/OrganizationId" }
      - { $ref: "#/components/parameters/ResourceId" }
    get:
      tags: [Content translations]
      summary: Inspect field-level translation freshness
      responses:
        "200": { description: Per-language current, stale, and missing fields }
        "404": { $ref: "#/components/responses/NotFound" }
    post:
      tags: [Content translations, AI]
      summary: Queue stale fields for background translation
      description: Requires ai scope. Omit languageCodes to queue every active target language. Jobs are coalesced per resource and locale and survive restarts.
      requestBody:
        content:
          application/json:
            schema:
              type: object
              properties:
                languageCodes:
                  type: array
                  maxItems: 20
                  items: { type: string, maxLength: 35 }
                force: { type: boolean, default: false }
      responses:
        "200": { description: No target-language jobs were needed }
        "202": { description: Durable translation jobs queued }
        "404": { $ref: "#/components/responses/NotFound" }
        "422": { description: Invalid or inactive target language }
  /resources/{id}/translations/{languageCode}:
    parameters:
      - { $ref: "#/components/parameters/OrganizationId" }
      - { $ref: "#/components/parameters/ResourceId" }
      - { name: languageCode, in: path, required: true, schema: { type: string, maxLength: 35 } }
    patch:
      tags: [Content translations]
      summary: Edit, approve, or unlock translated fields
      description: Requires write scope and the current locale-document revision. Human edits are protected from AI overwrite; accepting a suggestion keeps the field human-managed.
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              additionalProperties: false
              required: [expectedRevision, operations]
              properties:
                expectedRevision: { type: integer, minimum: 0 }
                operations:
                  type: array
                  minItems: 1
                  maxItems: 100
                  items:
                    type: object
                    required: [action, fieldKey]
                    properties:
                      action: { type: string, enum: [set, accept_suggestion, use_ai] }
                      fieldKey: { type: string, maxLength: 96 }
                      translatedText: { type: string, maxLength: 100000 }
      responses:
        "200": { description: Translation document updated }
        "404": { $ref: "#/components/responses/NotFound" }
        "409": { description: Translation revision changed or suggestion became stale }
        "422": { description: Invalid language, field, or operation }
  /resources/{id}/relations:
    parameters:
      - { $ref: "#/components/parameters/OrganizationId" }
      - { $ref: "#/components/parameters/ResourceId" }
    get:
      tags: [Inventory structure]
      summary: List relationships touching an inventory item
      description: Returns incoming and outgoing directed relationships with both endpoint summaries and the relationship definition.
      responses:
        "200":
          description: Resource relationships
          content:
            application/json:
              schema:
                type: object
                required: [relations]
                properties:
                  relations: { type: array, items: { $ref: "#/components/schemas/ResourceRelation" } }
        "422": { description: Invalid resource id }
    post:
      tags: [Inventory structure]
      summary: Create or pin a manual relationship
      description: One endpoint must match the resource id in the path. Containment relationships reject invalid parents, self-reference, and direct or indirect cycles. Creating an existing spatial relationship converts it to a manual relationship.
      requestBody:
        required: true
        content:
          application/json:
            schema: { $ref: "#/components/schemas/ResourceRelationInput" }
      responses:
        "201":
          description: Manual relationship created or pinned
          content:
            application/json:
              schema:
                type: object
                required: [relation]
                properties:
                  relation: { $ref: "#/components/schemas/ResourceRelationRecord" }
        "404": { description: One of the inventory items or the relationship type was not found }
        "409": { description: The containment relationship would create a cycle }
        "422": { description: Invalid relationship or a relationship type that disallows manual use }
  /relations/{relationId}:
    parameters:
      - { $ref: "#/components/parameters/OrganizationId" }
      - name: relationId
        in: path
        required: true
        schema: { type: string, format: uuid }
    delete:
      tags: [Inventory structure]
      summary: Remove a manual resource relationship
      description: Spatially derived relationships cannot be deleted directly. After deletion, spatial containment is recalculated.
      parameters:
        - name: resourceId
          in: query
          required: true
          description: One endpoint of the relationship, used to scope the deletion.
          schema: { type: string, format: uuid }
      responses:
        "204": { description: Manual relationship removed }
        "404": { description: Manual relationship not found }
        "422": { description: Invalid relationship or resource id }
  /resources/lookup:
    parameters:
      - { $ref: "#/components/parameters/OrganizationId" }
    get:
      tags: [Resources]
      summary: Resolve a scanned QR or barcode to an inventory item
      description: Accepts an item UUID, Inventory resource URL, item or variant SKU/barcode, or serial number. Variant matches include a `variant` object.
      parameters:
        - name: code
          in: query
          required: true
          schema: { type: string, minLength: 1, maxLength: 2048 }
      responses:
        "200":
          description: Matching inventory item
          content:
            application/json:
              schema:
                type: object
                required: [resource, matchedBy]
                properties:
                  resource: { $ref: "#/components/schemas/Resource" }
                  variant: { $ref: "#/components/schemas/ResourceVariant" }
                  matchedBy: { type: string, enum: [id, sku, barcode, serialNumber, variantSku, variantBarcode] }
        "404": { $ref: "#/components/responses/NotFound" }
        "409": { description: Serial number is ambiguous }
  /stock:
    parameters:
      - { $ref: "#/components/parameters/OrganizationId" }
    get:
      tags: [Stock]
      summary: Retrieve stock health and replenishment forecasts
      responses:
        "200":
          description: Stock overview
          content:
            application/json:
              schema:
                type: object
                required: [summary, items]
                properties:
                  summary:
                    type: object
                    properties:
                      trackedItems: { type: integer }
                      totalQuantity: { type: integer }
                      totalOnOrder: { type: integer }
                      incomingItems: { type: integer }
                      lowStockItems: { type: integer }
                      outOfStockItems: { type: integer }
                      predictedStockouts: { type: integer }
                      reorderItems: { type: integer }
                  items:
                    type: array
                    items: { $ref: "#/components/schemas/StockOverviewItem" }
  /stock/scan-workflows:
    parameters:
      - { $ref: "#/components/parameters/OrganizationId" }
    get:
      tags: [Scan workflows]
      summary: List configurable stock scan workflows
      responses:
        "200":
          description: Scan workflows
          content:
            application/json:
              schema:
                type: object
                required: [workflows]
                properties:
                  workflows:
                    type: array
                    items: { $ref: "#/components/schemas/ScanWorkflow" }
    post:
      tags: [Scan workflows]
      summary: Create a stock scan workflow
      requestBody:
        required: true
        content:
          application/json:
            schema: { $ref: "#/components/schemas/ScanWorkflowInput" }
      responses:
        "201":
          description: Workflow created
          content:
            application/json:
              schema:
                type: object
                required: [workflow]
                properties:
                  workflow: { $ref: "#/components/schemas/ScanWorkflow" }
        "409": { description: The target item does not use serialized tracking }
        "422": { description: Invalid workflow definition }
  /stock/scan-workflows/{workflowId}:
    parameters:
      - { $ref: "#/components/parameters/OrganizationId" }
      - name: workflowId
        in: path
        required: true
        schema: { type: string, format: uuid }
    get:
      tags: [Scan workflows]
      summary: Retrieve one stock scan workflow
      responses:
        "200":
          description: Scan workflow
          content:
            application/json:
              schema:
                type: object
                required: [workflow]
                properties:
                  workflow: { $ref: "#/components/schemas/ScanWorkflow" }
        "404": { description: Workflow not found }
    patch:
      tags: [Scan workflows]
      summary: Update a workflow using its current revision
      requestBody:
        required: true
        content:
          application/json:
            schema: { $ref: "#/components/schemas/ScanWorkflowPatch" }
      responses:
        "200":
          description: Workflow updated
          content:
            application/json:
              schema:
                type: object
                required: [workflow]
                properties:
                  workflow: { $ref: "#/components/schemas/ScanWorkflow" }
        "404": { description: Workflow not found }
        "409": { description: Revision conflict or incompatible target item }
        "422": { description: Invalid workflow update }
    delete:
      tags: [Scan workflows]
      summary: Delete a stock scan workflow
      parameters:
        - name: revision
          in: query
          required: true
          description: Current workflow revision, preventing deletion from a stale editor.
          schema: { type: integer, minimum: 1 }
      responses:
        "204": { description: Workflow deleted; immutable scan audit rows are preserved }
        "404": { description: Workflow not found }
        "409": { description: Workflow revision changed; reload before deleting }
        "422": { description: The workflow id or required single positive revision is invalid }
  /stock/scans/resolve:
    parameters:
      - { $ref: "#/components/parameters/OrganizationId" }
    post:
      tags: [Scan workflows]
      summary: Resolve and preview a scanned code without changing stock
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [workflowId, code]
              additionalProperties: false
              properties:
                workflowId: { type: string, format: uuid }
                code: { type: string, minLength: 1, maxLength: 2048 }
      responses:
        "200":
          description: Reviewed scan target and required operator inputs
          content:
            application/json:
              schema: { $ref: "#/components/schemas/StockScanResolution" }
        "404": { description: Workflow not found }
        "409": { description: Workflow disabled or target is not serialized }
        "422": { description: Invalid request or the code does not match the configured extractor }
  /stock/scans/execute:
    parameters:
      - { $ref: "#/components/parameters/OrganizationId" }
    post:
      tags: [Scan workflows]
      summary: Atomically apply a previously reviewed stock scan
      parameters:
        - { $ref: "#/components/parameters/IdempotencyKey" }
      requestBody:
        required: true
        content:
          application/json:
            schema: { $ref: "#/components/schemas/StockScanExecutionInput" }
      responses:
        "200":
          description: Replayed the original scan execution response
          content:
            application/json:
              schema: { $ref: "#/components/schemas/StockScanExecution" }
        "201":
          description: Unit, stock ledger, and immutable scan audit updated atomically
          content:
            application/json:
              schema: { $ref: "#/components/schemas/StockScanExecution" }
        "400": { description: Missing or invalid Idempotency-Key }
        "404": { description: Workflow or target unit not found }
        "409": { description: "Revision, stale resource or unit preview, idempotency, tracking-mode, or stock conflict" }
        "422": { description: Invalid code or operator input }
  /resources/{id}/stock:
    parameters:
      - { $ref: "#/components/parameters/OrganizationId" }
      - { $ref: "#/components/parameters/ResourceId" }
    get:
      tags: [Stock]
      summary: Retrieve one item's stock policy, forecast, units, and ledger
      responses:
        "200":
          description: Complete stock detail
          content:
            application/json:
              schema: { $ref: "#/components/schemas/StockDetail" }
        "404": { $ref: "#/components/responses/NotFound" }
  /resources/{id}/stock/locations:
    parameters:
      - { $ref: "#/components/parameters/OrganizationId" }
      - { $ref: "#/components/parameters/ResourceId" }
    get:
      tags: [Stock]
      summary: Retrieve stock balances by structured inventory location
      description: Bulk balances are derived from location-aware movements; serialized balances count available units at each location. The response also lists active inventory records whose types can contain stock.
      responses:
        "200":
          description: Per-location stock breakdown and selectable locations
          content:
            application/json:
              schema: { $ref: "#/components/schemas/StockLocationsResponse" }
        "404": { $ref: "#/components/responses/NotFound" }
        "422": { description: Invalid resource id }
  /inventory-counts/due:
    parameters:
      - { $ref: "#/components/parameters/OrganizationId" }
    get:
      tags: [Inventory cycles]
      summary: List inventory cycles that are currently due
      responses:
        "200":
          description: Due inventory counts ordered by due date
          content:
            application/json:
              schema:
                type: object
                required: [due]
                properties:
                  due: { type: array, items: { $ref: "#/components/schemas/DueInventoryCycle" } }
  /resources/{id}/inventory-cycle:
    parameters:
      - { $ref: "#/components/parameters/OrganizationId" }
      - { $ref: "#/components/parameters/ResourceId" }
    get:
      tags: [Inventory cycles]
      summary: Retrieve an item's inventory cycle and recent count history
      responses:
        "200":
          description: Inventory cycle, nullable policy, and up to 25 recent counts
          content:
            application/json:
              schema:
                type: object
                required: [cycle]
                properties:
                  cycle: { $ref: "#/components/schemas/InventoryCycle" }
        "404": { $ref: "#/components/responses/NotFound" }
        "422": { description: Invalid resource id }
    put:
      tags: [Inventory cycles]
      summary: Create or replace an item's recurring count policy
      requestBody:
        required: true
        content:
          application/json:
            schema: { $ref: "#/components/schemas/InventoryCyclePolicyInput" }
      responses:
        "200":
          description: Inventory cycle policy saved
          content:
            application/json:
              schema:
                type: object
                required: [policy]
                properties:
                  policy: { $ref: "#/components/schemas/InventoryCyclePolicy" }
        "404": { $ref: "#/components/responses/NotFound" }
        "422": { description: Invalid resource id or count interval }
  /resources/{id}/inventory-counts:
    parameters:
      - { $ref: "#/components/parameters/OrganizationId" }
      - { $ref: "#/components/parameters/ResourceId" }
    get:
      tags: [Inventory cycles]
      summary: List an item's recent inventory counts
      responses:
        "200":
          description: Up to 25 recent counts
          content:
            application/json:
              schema:
                type: object
                required: [counts]
                properties:
                  counts: { type: array, items: { $ref: "#/components/schemas/InventoryCountRecord" } }
        "404": { $ref: "#/components/responses/NotFound" }
        "422": { description: Invalid resource id }
    post:
      tags: [Inventory cycles]
      summary: Record an inventory count or serialized-unit review
      description: Bulk counts reconcile either the global balance or one structured location. Serialized items are reviewed through their individual units and may complete the cycle when the submitted whole-item count matches current serialized availability. Every completion appends an inventory-count movement and advances an existing cycle policy.
      parameters:
        - { $ref: "#/components/parameters/IdempotencyKey" }
      requestBody:
        required: true
        content:
          application/json:
            schema: { $ref: "#/components/schemas/InventoryCountInput" }
      responses:
        "200":
          description: Replayed an earlier count with the same Idempotency-Key
          content:
            application/json:
              schema: { $ref: "#/components/schemas/InventoryCountMutation" }
        "201":
          description: Count recorded and stock reconciled
          content:
            application/json:
              schema: { $ref: "#/components/schemas/InventoryCountMutation" }
        "404": { $ref: "#/components/responses/NotFound" }
        "409": { description: "Serialized count differs from current unit availability, conflicting idempotency key, or an invalid global count below assigned location stock" }
        "422": { description: Invalid count or structured location }
  /resources/{id}/assignments:
    parameters:
      - { $ref: "#/components/parameters/OrganizationId" }
      - { $ref: "#/components/parameters/ResourceId" }
    get:
      tags: [Assignments]
      summary: List checkout, assignment, and reservation records for an item
      responses:
        "200":
          description: Assignment history, current availability, and selectable serialized units
          content:
            application/json:
              schema: { $ref: "#/components/schemas/InventoryAssignmentList" }
        "404": { $ref: "#/components/responses/NotFound" }
        "422": { description: Invalid resource id }
    post:
      tags: [Assignments]
      summary: Check out, assign, or reserve inventory
      description: Atomically creates an active allocation, reduces available stock, updates a serialized unit when selected, and appends a stock movement.
      parameters:
        - { $ref: "#/components/parameters/RequiredIdempotencyKey" }
      requestBody:
        required: true
        content:
          application/json:
            schema: { $ref: "#/components/schemas/InventoryAssignmentInput" }
      responses:
        "200":
          description: Replayed the original assignment response
          content:
            application/json:
              schema: { $ref: "#/components/schemas/InventoryAssignmentMutation" }
        "201":
          description: Assignment created and stock allocated
          content:
            application/json:
              schema: { $ref: "#/components/schemas/InventoryAssignmentMutation" }
        "400": { description: Missing or invalid Idempotency-Key }
        "404": { description: Inventory item or serialized unit not found }
        "409": { description: "Insufficient stock, unavailable unit, or conflicting idempotency key" }
        "422": { description: "Invalid recipient, quantity, dates, or tracking-mode combination" }
  /assignments/{assignmentId}:
    parameters:
      - { $ref: "#/components/parameters/OrganizationId" }
      - name: assignmentId
        in: path
        required: true
        schema: { type: string, format: uuid }
    patch:
      tags: [Assignments]
      summary: Return or cancel an active assignment
      description: Atomically completes the assignment, restores stock when applicable, restores an assigned serialized unit to available, and appends a stock movement.
      parameters:
        - { $ref: "#/components/parameters/RequiredIdempotencyKey" }
      requestBody:
        required: true
        content:
          application/json:
            schema: { $ref: "#/components/schemas/InventoryAssignmentCompletion" }
      responses:
        "200":
          description: Assignment completed or an identical completion replayed
          content:
            application/json:
              schema: { $ref: "#/components/schemas/InventoryAssignmentMutation" }
        "400": { description: Missing or invalid Idempotency-Key }
        "404": { description: "Assignment, inventory item, or serialized unit not found" }
        "409": { description: "Assignment already completed, stock overflow, or conflicting idempotency key" }
        "422": { description: Invalid status or completion date }
  /resources/{id}/stock/config:
    parameters:
      - { $ref: "#/components/parameters/OrganizationId" }
      - { $ref: "#/components/parameters/ResourceId" }
    patch:
      tags: [Stock]
      summary: Update tracking mode and replenishment policy
      requestBody:
        required: true
        content:
          application/json:
            schema: { $ref: "#/components/schemas/StockConfigInput" }
      responses:
        "200": { description: Updated stock policy }
        "404": { $ref: "#/components/responses/NotFound" }
        "409": { description: Tracking mode transition would break serialized-unit integrity }
  /resources/{id}/bom:
    parameters:
      - { $ref: "#/components/parameters/OrganizationId" }
      - { $ref: "#/components/parameters/ResourceId" }
    get:
      tags: [Manufacturing]
      summary: Retrieve an item's bill of materials and current build capacity
      responses:
        "200":
          description: Bill of materials with component availability
          content:
            application/json:
              schema: { $ref: "#/components/schemas/BomDetail" }
        "404": { $ref: "#/components/responses/NotFound" }
    put:
      tags: [Manufacturing]
      summary: Replace an item's complete bill of materials
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [components]
              properties:
                components:
                  type: array
                  maxItems: 100
                  items: { $ref: "#/components/schemas/BomComponentInput" }
      responses:
        "200": { description: Bill of materials replaced }
        "409": { description: Circular dependency or concurrent referenced-resource conflict }
        "422": { description: "Invalid, duplicate, missing, or self-referencing component list" }
  /resources/{id}/stock/builds:
    parameters:
      - { $ref: "#/components/parameters/OrganizationId" }
      - { $ref: "#/components/parameters/ResourceId" }
    get:
      tags: [Manufacturing]
      summary: List completed assembly builds for an item
      parameters:
        - name: limit
          in: query
          schema: { type: integer, minimum: 1, maximum: 100, default: 50 }
      responses:
        "200":
          description: Completed builds with immutable component snapshots
          content:
            application/json:
              schema:
                type: object
                required: [builds]
                properties:
                  builds: { type: array, items: { $ref: "#/components/schemas/AssemblyBuild" } }
    post:
      tags: [Manufacturing]
      summary: Atomically consume components and create finished stock
      parameters:
        - { $ref: "#/components/parameters/RequiredIdempotencyKey" }
      requestBody:
        required: true
        content:
          application/json:
            schema: { $ref: "#/components/schemas/AssemblyBuildInput" }
      responses:
        "200": { description: Replayed the original assembly build }
        "201": { description: Components consumed and finished stock created atomically }
        "400": { description: Missing or invalid Idempotency-Key }
        "409": { description: "Component shortage, serialized-unit, or idempotency conflict" }
        "422": { description: Invalid build request }
  /resources/{id}/stock/movements:
    parameters:
      - { $ref: "#/components/parameters/OrganizationId" }
      - { $ref: "#/components/parameters/ResourceId" }
    get:
      tags: [Stock]
      summary: List the immutable dated stock ledger
      parameters:
        - name: limit
          in: query
          schema: { type: integer, minimum: 1, default: 100 }
        - name: before
          in: query
          description: Return movements that occurred before this timestamp.
          schema: { type: string, format: date-time }
      responses:
        "200":
          description: Stock movements
          content:
            application/json:
              schema:
                type: object
                required: [movements]
                properties:
                  movements: { type: array, items: { $ref: "#/components/schemas/StockMovement" } }
    post:
      tags: [Stock]
      summary: Book a bulk stock change or structured location transfer
      parameters:
        - { $ref: "#/components/parameters/IdempotencyKey" }
      requestBody:
        required: true
        content:
          application/json:
            schema: { $ref: "#/components/schemas/StockMovementInput" }
      responses:
        "200": { description: Replayed the original stock movement response }
        "201": { description: Movement booked and balance updated }
        "409": { description: "Insufficient stock, serialized mode, or conflicting idempotency key" }
        "422": { description: "Invalid quantity, movement direction, or structured location transfer" }
  /resources/{id}/stock/units:
    parameters:
      - { $ref: "#/components/parameters/OrganizationId" }
      - { $ref: "#/components/parameters/ResourceId" }
    get:
      tags: [Stock]
      summary: List individually identified stock units
      responses:
        "200":
          description: Serialized units
          content:
            application/json:
              schema:
                type: object
                required: [units]
                properties:
                  units: { type: array, items: { $ref: "#/components/schemas/StockUnit" } }
    post:
      tags: [Stock]
      summary: Create one or more individually identified stock units
      requestBody:
        required: true
        content:
          application/json:
            schema: { $ref: "#/components/schemas/StockUnitInput" }
      responses:
        "201": { description: Units created with audit entries }
        "409": { description: Duplicate code or incompatible tracking mode }
  /resources/{id}/stock/units/{unitId}:
    parameters:
      - { $ref: "#/components/parameters/OrganizationId" }
      - { $ref: "#/components/parameters/ResourceId" }
      - { name: unitId, in: path, required: true, schema: { type: string, format: uuid } }
    patch:
      tags: [Stock]
      summary: Change a unit's status, location, or metadata with an audit entry
      requestBody:
        required: true
        content:
          application/json:
            schema: { $ref: "#/components/schemas/StockUnitPatch" }
      responses:
        "200": { description: Unit updated and movement recorded }
        "404": { $ref: "#/components/responses/NotFound" }
  /purchase-orders:
    parameters:
      - { $ref: "#/components/parameters/OrganizationId" }
    get:
      tags: [Purchasing]
      summary: List purchase orders and their open incoming quantities
      parameters:
        - name: status
          in: query
          schema: { type: string, enum: [draft, ordered, partially-received, received, cancelled] }
        - name: limit
          in: query
          schema: { type: integer, minimum: 1, maximum: 100, default: 100 }
      responses:
        "200":
          description: Purchase orders
          content:
            application/json:
              schema:
                type: object
                required: [orders]
                properties:
                  orders: { type: array, items: { $ref: "#/components/schemas/PurchaseOrder" } }
    post:
      tags: [Purchasing]
      summary: Create a draft or ordered purchase order
      parameters:
        - { $ref: "#/components/parameters/RequiredIdempotencyKey" }
      requestBody:
        required: true
        content:
          application/json:
            schema: { $ref: "#/components/schemas/PurchaseOrderInput" }
      responses:
        "200": { description: Replayed the original purchase-order response }
        "201": { description: Purchase order created }
        "400": { description: Missing or invalid Idempotency-Key }
        "409": { description: Idempotency conflict or supported open-order total exceeded }
        "422": { description: "Invalid purchase order, duplicate line, or missing inventory item" }
  /purchase-orders/{orderId}:
    parameters:
      - { $ref: "#/components/parameters/OrganizationId" }
      - name: orderId
        in: path
        required: true
        schema: { type: string, format: uuid }
    get:
      tags: [Purchasing]
      summary: Retrieve one purchase order
      responses:
        "200":
          description: Purchase order
          content:
            application/json:
              schema:
                type: object
                required: [order]
                properties:
                  order: { $ref: "#/components/schemas/PurchaseOrder" }
        "404": { description: Purchase order not found }
    patch:
      tags: [Purchasing]
      summary: Update or cancel a purchase order
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              properties:
                reference: { type: [string, "null"], maxLength: 160 }
                supplier: { type: string, maxLength: 240 }
                status: { type: string, enum: [draft, ordered, cancelled] }
                orderedAt: { type: string, format: date-time }
                expectedAt: { type: [string, "null"], format: date-time }
                note: { type: string, maxLength: 20000 }
      responses:
        "200": { description: Purchase order updated }
        "409": { description: Invalid lifecycle transition }
  /purchase-orders/{orderId}/lines/{lineId}/receipts:
    parameters:
      - { $ref: "#/components/parameters/OrganizationId" }
      - name: orderId
        in: path
        required: true
        schema: { type: string, format: uuid }
      - name: lineId
        in: path
        required: true
        schema: { type: string, format: uuid }
    post:
      tags: [Purchasing]
      summary: Receive all or part of an ordered line into physical stock
      parameters:
        - { $ref: "#/components/parameters/RequiredIdempotencyKey" }
      requestBody:
        required: true
        content:
          application/json:
            schema: { $ref: "#/components/schemas/PurchaseReceiptInput" }
      responses:
        "200": { description: Replayed the original goods receipt }
        "201": { description: Receipt and physical stock movements created atomically }
        "400": { description: Missing or invalid Idempotency-Key }
        "409": { description: "Over-receipt, order-state, serialized-unit, or idempotency conflict" }
  /resources/{id}/media:
    parameters:
      - { $ref: "#/components/parameters/OrganizationId" }
      - { $ref: "#/components/parameters/ResourceId" }
    post:
      tags: [Media]
      summary: Upload up to 12 media files, including Apple USDZ object models
      parameters:
        - { $ref: "#/components/parameters/IdempotencyKey" }
      requestBody:
        required: true
        content:
          multipart/form-data:
            schema:
              type: object
              properties:
                files:
                  type: array
                  maxItems: 12
                  description: Images, videos, PDF documents, or .usdz models sent as model/vnd.usdz+zip.
                  items: { type: string, format: binary }
      responses:
        "200": { description: Replayed the existing upload batch }
        "201": { description: Uploaded }
        "413": { description: A file exceeds MAX_UPLOAD_MB, or a USDZ model exceeds MAX_USDZ_UPLOAD_MB }
        "415": { description: Unsupported media type, invalid USDZ filename, or structurally invalid USDZ package }
        "422": { description: A USDZ Object Capture model was uploaded without an existing or accompanying item image }
        "409": { description: Idempotency key belongs to another resource }
        "503": { description: The configured storage provider cannot store the accepted media type }
    patch:
      tags: [Media]
      summary: Reorder all media
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [order]
              properties:
                order: { type: array, items: { type: string, format: uuid } }
      responses:
        "200": { description: Reordered }
  /resources/{id}/media/{mediaId}:
    parameters:
      - { $ref: "#/components/parameters/OrganizationId" }
    delete:
      tags: [Media]
      summary: Remove one media file
      parameters:
        - { $ref: "#/components/parameters/ResourceId" }
        - { name: mediaId, in: path, required: true, schema: { type: string, format: uuid } }
      responses:
        "204": { description: Removed }
  /resources/{id}/analyze:
    parameters:
      - { $ref: "#/components/parameters/OrganizationId" }
    post:
      tags: [AI]
      summary: Generate structured item fields from the first three images
      parameters:
        - { $ref: "#/components/parameters/ResourceId" }
        - { $ref: "#/components/parameters/IdempotencyKey" }
      requestBody:
        content:
          application/json:
            schema:
              type: object
              properties:
                overwrite: { type: boolean, default: true }
      responses:
        "200": { description: Item enriched or completed response replayed }
        "202": { description: An operation with this key is still processing }
        "409": { description: Idempotency key belongs to another resource or payload }
        "429":
          description: AI request limit reached
          headers:
            Retry-After:
              schema: { type: integer, minimum: 1 }
        "502": { description: AI provider error }
        "503": { description: Shared AI rate limiter unavailable }
  /resources/{id}/cover:
    parameters:
      - { $ref: "#/components/parameters/OrganizationId" }
    post:
      tags: [AI]
      summary: Generate a square studio or transparent cover from an item image
      parameters:
        - { $ref: "#/components/parameters/ResourceId" }
        - { $ref: "#/components/parameters/IdempotencyKey" }
      requestBody:
        content:
          application/json:
            schema:
              type: object
              properties:
                sourceMediaId:
                  type: string
                  format: uuid
                  description: Image to use as the visual reference. Defaults to the first non-AI image, then falls back to the first image.
                prompt: { type: string, maxLength: 5000 }
                modelId:
                  type: string
                  maxLength: 240
                  description: An ID returned by GET /ai/image-models. Omit it to use the deployment default.
                transparentBackground:
                  type: boolean
                  default: false
                  description: When true, store a transparent PNG instead of an opaque JPEG.
                transparencyMethod:
                  type: string
                  enum: [greenscreen, difference-matting]
                  default: difference-matting
                  description: Greenscreen uses one generated chroma-key pass. Difference matting uses aligned white and black passes for higher-quality alpha, including translucent details and soft shadows.
      responses:
        "200": { description: Cover generated or completed response replayed }
        "202": { description: An operation with this key is still processing }
        "409": { description: Idempotency key belongs to another resource or payload }
        "422": { description: Invalid request or unavailable image model }
        "429":
          description: Image generation limit reached
          headers:
            Retry-After:
              schema: { type: integer, minimum: 1 }
        "502": { description: AI provider error }
        "503": { description: Shared AI rate limiter unavailable }
  /stats:
    parameters:
      - { $ref: "#/components/parameters/OrganizationId" }
    get:
      tags: [Insights]
      summary: Retrieve aggregate dashboard statistics
      responses:
        "200": { description: Statistics }
  /duplicates:
    parameters:
      - { $ref: "#/components/parameters/OrganizationId" }
    get:
      tags: [Insights]
      summary: Find likely duplicate item pairs
      responses:
        "200": { description: Ranked duplicate pairs }
    post:
      tags: [Insights]
      summary: Merge one duplicate into another
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [keepResourceId, removeResourceId]
              properties:
                keepResourceId: { type: string, format: uuid }
                removeResourceId: { type: string, format: uuid }
      responses:
        "200": { description: Merged resource }
webhooks:
  inventoryEvent:
    post:
      tags: [Webhooks]
      summary: Receive an outgoing Inventory event
      security: []
      description: >-
        This operation documents the HTTP request Inventory sends to each
        subscribed target; it is not an endpoint hosted by Inventory. Delivery
        is at least once, so receivers must deduplicate by the stable event id.
        Return any 2xx status to acknowledge the exact request body.
      parameters:
        - name: X-Inventory-Event-Id
          in: header
          required: true
          description: Stable event UUID; use it as the receiver's deduplication key.
          schema: { type: string, format: uuid }
        - name: X-Inventory-Event-Type
          in: header
          required: true
          description: Event name, also present in the JSON envelope.
          schema: { $ref: "#/components/schemas/WebhookOutboundEventType" }
        - name: X-Inventory-Delivery-Id
          in: header
          required: true
          description: UUID of this endpoint-specific delivery record.
          schema: { type: string, format: uuid }
        - name: X-Inventory-Timestamp
          in: header
          required: true
          description: Unix timestamp also carried as `t` in the signature header.
          schema: { type: string, pattern: '^[0-9]+$' }
        - name: X-Inventory-Signature
          in: header
          required: true
          description: >-
            `t=<unix>,v1=<hex>` where `v1` is the lowercase hexadecimal
            HMAC-SHA256 of `<unix>.<exact raw request body>` using the endpoint
            signing secret. Verify the raw bytes before parsing JSON.
          schema:
            type: string
            pattern: '^t=[0-9]+,v1=[a-f0-9]{64}$'
          example: t=1786608000,v1=7d6f2dba7b8d83cd0c87bfce4c34f35621e96fe930a29d309926f7156bdb976c
      requestBody:
        required: true
        content:
          application/json:
            schema: { $ref: "#/components/schemas/WebhookEvent" }
      responses:
        2XX:
          description: Event acknowledged; Inventory marks the delivery successful.
        default:
          description: Non-2xx response; transient statuses may be retried according to the delivery policy.
components:
  securitySchemes:
    bearerAuth:
      type: http
      scheme: bearer
      bearerFormat: inv_ token
    adminSession:
      type: apiKey
      in: cookie
      name: authjs.session-token
      description: Auth.js browser session cookie; each operation declares its required administrative permission. HTTPS deployments use the secure-prefixed cookie variant.
    cronSecret:
      type: http
      scheme: bearer
      bearerFormat: NOTIFICATION_CRON_SECRET
      description: Deployment-level scheduler secret used only by the notification run endpoint.
    webhookCronSecret:
      type: http
      scheme: bearer
      bearerFormat: WEBHOOK_CRON_SECRET
      description: Deployment-level scheduler secret used only by the outgoing webhook run endpoint.
  parameters:
    OrganizationId:
      name: X-Organization-ID
      in: header
      required: false
      description: >-
        Organization membership selected for this request. User-bound
        credentials may select any active membership; standalone tokens may
        only repeat their pinned organization ID.
      schema: { type: string, format: uuid }
    IdempotencyKey:
      name: Idempotency-Key
      in: header
      required: false
      description: UUID identifying one queue operation; safe to reuse for retries.
      schema: { type: string, format: uuid }
    RequiredIdempotencyKey:
      name: Idempotency-Key
      in: header
      required: true
      description: UUID identifying this atomic operation; reuse it only to retry the exact same request.
      schema: { type: string, format: uuid }
    ResourceId:
      name: id
      in: path
      required: true
      schema: { type: string, format: uuid }
    CustomFieldId:
      name: id
      in: path
      required: true
      schema: { type: string, format: uuid }
    WebhookId:
      name: id
      in: path
      required: true
      schema: { type: string, format: uuid }
    WebhookDeliveryId:
      name: deliveryId
      in: path
      required: true
      schema: { type: string, format: uuid }
  responses:
    NotFound:
      description: Resource not found
  schemas:
    OrganizationMembership:
      type: object
      additionalProperties: false
      required: [id, name, slug, role, roleName]
      properties:
        id: { type: string, format: uuid }
        name: { type: string, minLength: 1, maxLength: 160 }
        slug: { type: string, minLength: 1, maxLength: 180 }
        role:
          type: [string, "null"]
          pattern: '^[a-z][a-z0-9_-]{0,63}$'
        roleName: { type: [string, "null"] }
        canManage:
          type: boolean
          description: Present on organization-list responses; true when the current identity can administer that active organization.
    NotificationEventType:
      type: string
      enum: [low_stock, expiry, maintenance, return_due]
    Notification:
      type: object
      required: [id, eventType, sourceKey, title, body, metadata, createdAt]
      properties:
        id: { type: string, format: uuid }
        eventType: { $ref: "#/components/schemas/NotificationEventType" }
        resourceId: { type: [string, "null"], format: uuid }
        assignmentId: { type: [string, "null"], format: uuid }
        sourceKey: { type: string }
        title: { type: string }
        body: { type: string }
        href: { type: [string, "null"] }
        metadata: { type: object, additionalProperties: true }
        readAt: { type: [string, "null"], format: date-time }
        createdAt: { type: string, format: date-time }
    NotificationPreferencePatch:
      type: object
      additionalProperties: false
      properties:
        enabledEventTypes:
          type: array
          uniqueItems: true
          maxItems: 4
          items: { $ref: "#/components/schemas/NotificationEventType" }
        frequency: { type: string, enum: [daily, immediate] }
        digestHour: { type: integer, minimum: 0, maximum: 23 }
        timezone: { type: string, maxLength: 80 }
        locale: { type: string, enum: [en, de] }
        cooldownHours: { type: integer, minimum: 1, maximum: 720 }
        lowStockThresholdPercent: { type: integer, minimum: 1, maximum: 500 }
        expiryWindowDays: { type: integer, minimum: 0, maximum: 3650 }
        expiryFieldKey: { type: string, maxLength: 64, pattern: '^[a-z][a-z0-9_]{0,63}$' }
        maintenanceWindowDays: { type: integer, minimum: 0, maximum: 3650 }
        maintenanceFieldKey: { type: string, maxLength: 64, pattern: '^[a-z][a-z0-9_]{0,63}$' }
        returnDueWindowDays: { type: integer, minimum: 0, maximum: 365 }
        emailEnabled: { type: boolean, default: false }
        pushEnabled: { type: boolean, default: false }
        slackEnabled: { type: boolean, default: false }
        teamsEnabled: { type: boolean, default: false }
        webhookEnabled: { type: boolean, default: false }
    WebhookEventType:
      type: string
      description: Stable integration event name selected when configuring an endpoint.
      enum:
        - inventory.resource.created
        - inventory.resource.updated
        - inventory.resource.deleted
        - inventory.resource.merged
        - inventory.stock.movement.created
    WebhookOutboundEventType:
      type: string
      description: Event name sent in a delivery. Test actions use a dedicated type that cannot be subscribed to.
      enum:
        - inventory.resource.created
        - inventory.resource.updated
        - inventory.resource.deleted
        - inventory.resource.merged
        - inventory.stock.movement.created
        - inventory.webhook.test
    WebhookEndpointCreate:
      type: object
      additionalProperties: false
      required: [name, url, eventTypes]
      properties:
        name: { type: string, minLength: 1, maxLength: 120 }
        url:
          type: string
          format: uri
          minLength: 1
          maxLength: 2048
          description: Absolute HTTPS delivery target. Private and local networks are blocked unless the deployment explicitly opts in.
        eventTypes:
          type: array
          minItems: 1
          maxItems: 5
          uniqueItems: true
          items: { $ref: "#/components/schemas/WebhookEventType" }
        enabled: { type: boolean, default: true }
    WebhookEndpointPatch:
      type: object
      additionalProperties: false
      minProperties: 1
      properties:
        name: { type: string, minLength: 1, maxLength: 120 }
        url:
          type: string
          format: uri
          minLength: 1
          maxLength: 2048
          description: Complete replacement HTTPS target; the current stored target cannot be recovered from its redacted response form.
        eventTypes:
          type: array
          minItems: 1
          maxItems: 5
          uniqueItems: true
          items: { $ref: "#/components/schemas/WebhookEventType" }
        enabled: { type: boolean }
    WebhookEndpoint:
      type: object
      additionalProperties: false
      required: [id, name, target, eventTypes, enabled, failureCount, lastSuccessAt, lastFailureAt, createdAt, updatedAt]
      properties:
        id: { type: string, format: uuid }
        name: { type: string }
        target:
          type: string
          description: Redacted delivery target. Query values and secret path segments are never returned.
          examples: [https://hooks.example.com/…]
        eventTypes:
          type: array
          uniqueItems: true
          items: { $ref: "#/components/schemas/WebhookEventType" }
        enabled: { type: boolean }
        failureCount: { type: integer, minimum: 0 }
        lastSuccessAt: { type: [string, "null"], format: date-time }
        lastFailureAt: { type: [string, "null"], format: date-time }
        createdAt: { type: string, format: date-time }
        updatedAt: { type: string, format: date-time }
    WebhookListResponse:
      type: object
      additionalProperties: false
      required: [webhooks]
      properties:
        webhooks:
          type: array
          items: { $ref: "#/components/schemas/WebhookEndpoint" }
    WebhookResponse:
      type: object
      additionalProperties: false
      required: [webhook]
      properties:
        webhook: { $ref: "#/components/schemas/WebhookEndpoint" }
    WebhookCreatedResponse:
      type: object
      additionalProperties: false
      required: [webhook, secret]
      properties:
        webhook: { $ref: "#/components/schemas/WebhookEndpoint" }
        secret:
          type: string
          description: Signing secret disclosed once. It is encrypted at rest and cannot be retrieved later.
    WebhookSecretResponse:
      type: object
      additionalProperties: false
      required: [secret]
      properties:
        secret:
          type: string
          description: Replacement signing secret disclosed once.
    WebhookTestResponse:
      type: object
      additionalProperties: false
      required: [eventId, deliveryId]
      properties:
        eventId: { type: string, format: uuid }
        deliveryId: { type: string, format: uuid }
    WebhookDeliveryStatus:
      type: string
      enum: [pending, processing, succeeded, failed]
    WebhookDeliveryEvent:
      type: object
      additionalProperties: false
      required: [id, type, occurredAt]
      properties:
        id: { type: string, format: uuid }
        type: { $ref: "#/components/schemas/WebhookOutboundEventType" }
        occurredAt: { type: string, format: date-time }
    WebhookDelivery:
      type: object
      additionalProperties: false
      required: [id, status, attempts, nextAttemptAt, httpStatus, error, deliveredAt, createdAt, event]
      properties:
        id: { type: string, format: uuid }
        status: { $ref: "#/components/schemas/WebhookDeliveryStatus" }
        attempts: { type: integer, minimum: 0 }
        nextAttemptAt: { type: string, format: date-time }
        httpStatus: { type: [integer, "null"], minimum: 100, maximum: 599 }
        error: { type: [string, "null"] }
        deliveredAt: { type: [string, "null"], format: date-time }
        createdAt: { type: string, format: date-time }
        event: { $ref: "#/components/schemas/WebhookDeliveryEvent" }
    WebhookDeliveryState:
      type: object
      additionalProperties: false
      description: Delivery state returned immediately after a manual retry; fetch the delivery list for its event summary.
      required: [id, status, attempts, nextAttemptAt, httpStatus, error, deliveredAt, createdAt]
      properties:
        id: { type: string, format: uuid }
        status: { $ref: "#/components/schemas/WebhookDeliveryStatus" }
        attempts: { type: integer, minimum: 0 }
        nextAttemptAt: { type: string, format: date-time }
        httpStatus: { type: [integer, "null"], minimum: 100, maximum: 599 }
        error: { type: [string, "null"] }
        deliveredAt: { type: [string, "null"], format: date-time }
        createdAt: { type: string, format: date-time }
    WebhookDeliveryListResponse:
      type: object
      additionalProperties: false
      required: [deliveries]
      properties:
        deliveries:
          type: array
          items: { $ref: "#/components/schemas/WebhookDelivery" }
    WebhookDeliveryResponse:
      type: object
      additionalProperties: false
      required: [delivery]
      properties:
        delivery: { $ref: "#/components/schemas/WebhookDeliveryState" }
    WebhookRunResponse:
      type: object
      additionalProperties: false
      required: [processed, results]
      properties:
        processed: { type: integer, minimum: 0 }
        results:
          type: array
          items: { type: string, enum: [succeeded, failed] }
    WebhookEvent:
      type: object
      additionalProperties: false
      required: [id, type, apiVersion, occurredAt, actor, data]
      properties:
        id:
          type: string
          format: uuid
          description: Stable event identifier used by receivers for deduplication across retries.
        type: { $ref: "#/components/schemas/WebhookOutboundEventType" }
        apiVersion: { type: string, const: "1" }
        occurredAt: { type: string, format: date-time }
        actor:
          type: [string, "null"]
          description: Identity responsible for the event when one is available.
        data:
          type: object
          additionalProperties: true
          description: >-
            Event-specific snapshot: create, update, and delete events include
            `resource`; updates also include `changedFields`; merges include `keptResource`,
            `removedResource`, `keptResourceId`, and `removedResourceId`; stock
            movement events include the immutable `movement` snapshot; test
            deliveries include `test: true` and a human-readable `message`.
    AccessRoleKey:
      type: string
      minLength: 1
      maxLength: 64
      pattern: '^[a-z][a-z0-9_-]{0,63}$'
      description: Stable lowercase identifier for a built-in or custom role.
      examples: [editor, warehouse_team]
    AppPermission:
      type: string
      description: Granular workspace-wide capability assigned to a role.
      enum:
        - inventory.read
        - inventory.create
        - inventory.update
        - inventory.delete
        - inventory.import
        - inventory.export
        - stock.read
        - stock.manage
        - assignments.read
        - assignments.manage
        - counts.read
        - counts.manage
        - spatial.read
        - spatial.manage
        - orders.read
        - orders.manage
        - workflows.read
        - workflows.manage
        - labels.read
        - labels.manage
        - ai.use
        - settings.inventory-types.manage
        - settings.custom-fields.manage
        - settings.languages.manage
        - users.manage
        - roles.manage
        - sharing.manage
        - tokens.manage
        - tokens.delegate
        - webhooks.manage
    InventoryRulePermission:
      type: string
      description: Permission that a conditional rule may grant for matching inventory items.
      enum:
        - inventory.update
        - inventory.delete
        - stock.manage
        - assignments.manage
        - counts.manage
        - spatial.manage
        - ai.use
    PermissionDefinition:
      type: object
      additionalProperties: false
      required: [key, label, description]
      properties:
        key: { $ref: "#/components/schemas/AppPermission" }
        label: { type: string }
        description: { type: string }
    PermissionGroup:
      type: object
      additionalProperties: false
      required: [key, label, description, permissions]
      properties:
        key: { type: string }
        label: { type: string }
        description: { type: string }
        permissions:
          type: array
          items: { $ref: "#/components/schemas/PermissionDefinition" }
    AccessRoleInput:
      type: object
      additionalProperties: false
      required: [key, name]
      properties:
        key: { $ref: "#/components/schemas/AccessRoleKey" }
        name: { type: string, minLength: 1, maxLength: 120 }
        description: { type: string, maxLength: 1000, default: "" }
        permissions:
          type: array
          maxItems: 30
          default: []
          items: { $ref: "#/components/schemas/AppPermission" }
    AccessRolePatch:
      type: object
      additionalProperties: false
      minProperties: 1
      properties:
        name: { type: string, minLength: 1, maxLength: 120 }
        description: { type: string, maxLength: 1000 }
        permissions:
          type: array
          maxItems: 30
          items: { $ref: "#/components/schemas/AppPermission" }
    AccessRole:
      type: object
      additionalProperties: false
      required: [key, name, description, permissions, isSystem]
      properties:
        key: { $ref: "#/components/schemas/AccessRoleKey" }
        name: { type: string, minLength: 1, maxLength: 120 }
        description: { type: string, maxLength: 1000 }
        permissions:
          type: array
          maxItems: 30
          items: { $ref: "#/components/schemas/AppPermission" }
        isSystem:
          type: boolean
          description: Whether the role is built in and cannot be deleted.
        memberCount:
          type: integer
          minimum: 0
          description: Number of active users assigned to the role; included in role-list and create responses.
        createdBy: { type: [string, "null"], maxLength: 320 }
        updatedBy: { type: [string, "null"], maxLength: 320 }
        createdAt: { type: string, format: date-time }
        updatedAt: { type: string, format: date-time }
    AccessRuleCondition:
      type: object
      additionalProperties: false
      required: [field, operator]
      description: One condition in a rule. Every condition in the same rule must match; comparisons are case-insensitive for strings.
      properties:
        field:
          type: string
          pattern: '^(id|name|type|status|sku|location|serialNumber|priority|tags|categories|createdBy|customFields\.[A-Za-z0-9_-]{1,120})$'
          description: Built-in inventory field or a custom field addressed as customFields.KEY.
          examples: [status, categories, customFields.department]
        operator:
          type: string
          enum: [equals, not_equals, contains, starts_with, exists, not_exists]
        value:
          description: Comparison value. Required except for exists and not_exists, where it is ignored.
          oneOf:
            - { type: string, maxLength: 500 }
            - { type: number }
            - { type: boolean }
            - { type: "null" }
    InventoryAccessRuleInput:
      type: object
      additionalProperties: false
      required: [name, roleKey, permissions, conditions]
      description: Additive item-specific access grant. Multiple enabled rules are ORed; all conditions inside this rule are ANDed.
      properties:
        name: { type: string, minLength: 1, maxLength: 160 }
        description: { type: string, maxLength: 1000, default: "" }
        roleKey: { $ref: "#/components/schemas/AccessRoleKey" }
        permissions:
          type: array
          minItems: 1
          maxItems: 7
          items: { $ref: "#/components/schemas/InventoryRulePermission" }
        conditions:
          type: array
          minItems: 1
          maxItems: 12
          description: Conditions combined with logical AND.
          items: { $ref: "#/components/schemas/AccessRuleCondition" }
        enabled: { type: boolean, default: true }
        priority:
          type: integer
          minimum: 0
          maximum: 10000
          default: 100
          description: Display and evaluation order; lower values come first and do not override other matching rules.
      examples:
        - name: Update XYZ items
          description: Let members of the ABC role update items whose name contains xyz.
          roleKey: abc
          permissions: [inventory.update]
          conditions:
            - field: name
              operator: contains
              value: xyz
          enabled: true
          priority: 100
    InventoryAccessRulePatch:
      type: object
      additionalProperties: false
      minProperties: 1
      properties:
        name: { type: string, minLength: 1, maxLength: 160 }
        description: { type: string, maxLength: 1000 }
        roleKey: { $ref: "#/components/schemas/AccessRoleKey" }
        permissions:
          type: array
          minItems: 1
          maxItems: 7
          items: { $ref: "#/components/schemas/InventoryRulePermission" }
        conditions:
          type: array
          minItems: 1
          maxItems: 12
          description: Conditions combined with logical AND.
          items: { $ref: "#/components/schemas/AccessRuleCondition" }
        enabled: { type: boolean }
        priority: { type: integer, minimum: 0, maximum: 10000 }
    InventoryAccessRule:
      type: object
      additionalProperties: false
      required: [id, name, description, roleKey, permissions, conditions, enabled, priority, createdAt, updatedAt]
      description: Additive permission grant for matching inventory items. Rules never deny permissions already granted by a role; any matching enabled rule may grant access.
      properties:
        id: { type: string, format: uuid }
        name: { type: string, minLength: 1, maxLength: 160 }
        description: { type: string, maxLength: 1000 }
        roleKey: { $ref: "#/components/schemas/AccessRoleKey" }
        permissions:
          type: array
          minItems: 1
          maxItems: 7
          items: { $ref: "#/components/schemas/InventoryRulePermission" }
        conditions:
          type: array
          minItems: 1
          maxItems: 12
          description: Conditions combined with logical AND.
          items: { $ref: "#/components/schemas/AccessRuleCondition" }
        enabled: { type: boolean }
        priority: { type: integer, minimum: 0, maximum: 10000 }
        createdBy: { type: [string, "null"], maxLength: 320 }
        updatedBy: { type: [string, "null"], maxLength: 320 }
        createdAt: { type: string, format: date-time }
        updatedAt: { type: string, format: date-time }
    SpatialVector3:
      type: array
      minItems: 3
      maxItems: 3
      items: { type: number, minimum: -10000, maximum: 10000 }
      description: XYZ values in metres.
    SpatialDimensions:
      type: array
      minItems: 3
      maxItems: 3
      items: { type: number, minimum: 0, maximum: 100 }
    SpatialQuaternion:
      type: array
      minItems: 4
      maxItems: 4
      items: { type: number, minimum: -1, maximum: 1 }
      description: Normalized XYZW quaternion.
    SpatialMatrix4:
      type: array
      minItems: 16
      maxItems: 16
      items: { type: number, minimum: -10000, maximum: 10000 }
      description: Column-major affine 4x4 matrix.
    SpatialReferencePoint:
      type: object
      additionalProperties: false
      required: [id, localPosition, latitude, longitude]
      properties:
        id: { type: string, minLength: 1, maxLength: 80 }
        label: { type: string, minLength: 1, maxLength: 120 }
        localPosition: { $ref: "#/components/schemas/SpatialVector3" }
        latitude: { type: number, minimum: -90, maximum: 90 }
        longitude: { type: number, minimum: -180, maximum: 180 }
        altitude: { type: number, minimum: -12000, maximum: 100000 }
    SpatialGeoreference:
      type: object
      additionalProperties: false
      required: [latitude, longitude, headingDegrees, capturedAt, source]
      description: Geographic anchor for one ARKit coordinate space. headingDegrees is the bearing of AR local -Z measured clockwise from true north; latitude/longitude/altitude identify localReferencePosition.
      properties:
        latitude: { type: number, minimum: -90, maximum: 90 }
        longitude: { type: number, minimum: -180, maximum: 180 }
        altitude: { type: number, minimum: -12000, maximum: 100000 }
        headingDegrees: { type: number, minimum: 0, exclusiveMaximum: 360 }
        horizontalAccuracy: { type: number, minimum: 0, maximum: 100000 }
        verticalAccuracy: { type: number, minimum: 0, maximum: 100000 }
        capturedAt: { type: string, format: date-time }
        source: { type: string, enum: [gps, manual, qr-marker, app-clip, other] }
        localReferencePosition:
          allOf:
            - { $ref: "#/components/schemas/SpatialVector3" }
          default: [0, 0, 0]
        referencePoints:
          type: array
          maxItems: 64
          items: { $ref: "#/components/schemas/SpatialReferencePoint" }
        entryMarkerCode: { type: string, minLength: 1, maxLength: 240 }
    SpatialStructureCreate:
      type: object
      additionalProperties: false
      required: [name]
      properties:
        id: { type: string, format: uuid }
        name: { type: string, minLength: 1, maxLength: 240 }
        description: { type: string, maxLength: 10000, default: "" }
        georeference:
          oneOf:
            - { $ref: "#/components/schemas/SpatialGeoreference" }
            - { type: "null" }
    SpatialStructurePatch:
      type: object
      additionalProperties: false
      minProperties: 1
      properties:
        name: { type: string, minLength: 1, maxLength: 240 }
        description: { type: string, maxLength: 10000 }
        georeference:
          oneOf:
            - { $ref: "#/components/schemas/SpatialGeoreference" }
            - { type: "null" }
    SpatialStructureSummary:
      type: object
      required: [id, name, description, floorCount, roomCount, activeScanCount, coordinateSpaceCount, bounds, createdAt, updatedAt]
      properties:
        id: { type: string, format: uuid }
        name: { type: string }
        description: { type: string }
        georeference:
          oneOf:
            - { $ref: "#/components/schemas/SpatialGeoreference" }
            - { type: "null" }
          description: Canonical building map marker, not necessarily the anchor for every room scan.
        floorCount: { type: integer, minimum: 0 }
        roomCount: { type: integer, minimum: 0 }
        activeScanCount: { type: integer, minimum: 0 }
        coordinateSpaceCount: { type: integer, minimum: 0 }
        bounds:
          oneOf:
            - type: object
              required: [min, max]
              properties:
                min: { $ref: "#/components/schemas/SpatialVector3" }
                max: { $ref: "#/components/schemas/SpatialVector3" }
            - { type: "null" }
          description: Local bounds only when every active scan shares one explicit coordinateSpaceId; otherwise null.
        boundsCoordinateSpaceId: { type: [string, "null"], format: uuid }
        boundsGeoreference:
          oneOf:
            - { $ref: "#/components/schemas/SpatialGeoreference" }
            - { type: "null" }
        createdAt: { type: string, format: date-time }
        updatedAt: { type: string, format: date-time }
    RoomSceneElement:
      type: object
      additionalProperties: false
      required: [id, category, dimensions, transform, confidence]
      properties:
        id: { type: string, format: uuid }
        category: { type: string, minLength: 1, maxLength: 80 }
        dimensions: { $ref: "#/components/schemas/SpatialDimensions" }
        transform: { $ref: "#/components/schemas/SpatialMatrix4" }
        polygonCorners:
          type: array
          minItems: 3
          maxItems: 1024
          description: Ordered RoomPlan boundary in the surface-local coordinate system; preferred over dimensions for non-rectangular floors.
          items: { $ref: "#/components/schemas/SpatialVector3" }
        confidence: { type: string, enum: [low, medium, high] }
    RoomScene:
      type: object
      additionalProperties: false
      required: [schemaVersion, coordinateSystem, units, matrixOrder, worldFromModel, webFromWorld, bounds, surfaces, objects]
      properties:
        schemaVersion: { type: integer, const: 1 }
        coordinateSystem: { type: string, const: arkit-right-handed-y-up }
        units: { type: string, const: meter }
        matrixOrder: { type: string, const: column-major }
        worldFromModel: { $ref: "#/components/schemas/SpatialMatrix4" }
        webFromWorld:
          allOf:
            - { $ref: "#/components/schemas/SpatialMatrix4" }
          description: Visualization transform shared by every scan with the same coordinateSpaceId.
        bounds:
          type: object
          additionalProperties: false
          required: [min, max]
          properties:
            min: { $ref: "#/components/schemas/SpatialVector3" }
            max: { $ref: "#/components/schemas/SpatialVector3" }
        surfaces:
          type: array
          maxItems: 4096
          items: { $ref: "#/components/schemas/RoomSceneElement" }
        objects:
          type: array
          maxItems: 2048
          items: { $ref: "#/components/schemas/RoomSceneElement" }
    RoomScanAsset:
      type: object
      required: [id, kind, name, mimeType, size, checksumSha256, url, createdAt]
      properties:
        id: { type: string, format: uuid }
        kind: { type: string, enum: [world_map, model_usdz, structure_model, guide_image, textured_mesh, gaussian_splat] }
        name: { type: string }
        mimeType: { type: string }
        size: { type: integer, minimum: 0 }
        checksumSha256: { type: string, pattern: "^[a-f0-9]{64}$" }
        url: { type: string }
        createdAt: { type: string, format: date-time }
    RoomScanSummary:
      type: object
      required: [id, roomResourceId, roomName, revision, status, capturedAt, createdAt, updatedAt, placementCount, assets, keyframeCount]
      properties:
        id: { type: string, format: uuid }
        roomResourceId: { type: string, format: uuid }
        roomName: { type: string }
        structureId: { type: [string, "null"], format: uuid }
        structureName: { type: [string, "null"] }
        coordinateSpaceId: { type: [string, "null"], format: uuid }
        floorIdentifier: { type: [string, "null"] }
        floorIndex: { type: [integer, "null"] }
        roomIdentifier: { type: [string, "null"] }
        layoutTransform:
          oneOf:
            - { $ref: "#/components/schemas/SpatialMatrix4" }
            - { type: "null" }
          description: Optional saved world-from-model transform used to arrange the room with the other rooms on its floor.
        georeference:
          oneOf:
            - { $ref: "#/components/schemas/SpatialGeoreference" }
            - { type: "null" }
        revision: { type: integer, minimum: 1 }
        status: { type: string, enum: [active, superseded] }
        capturedAt: { type: string, format: date-time }
        deviceModel: { type: [string, "null"] }
        createdAt: { type: string, format: date-time }
        updatedAt: { type: string, format: date-time }
        placementCount: { type: integer, minimum: 0 }
        keyframeCount:
          type: integer
          minimum: 0
          maximum: 32
          description: Number of calibrated reference photos. Full metadata is returned only by the room-scene detail endpoint.
        assets:
          type: array
          items: { $ref: "#/components/schemas/RoomScanAsset" }
    RoomScanUploadResult:
      type: object
      additionalProperties: false
      required: [id, replayed]
      properties:
        id: { type: string, format: uuid }
        replayed: { type: boolean }
    RoomLayoutTransformPatch:
      type: object
      additionalProperties: false
      required: [transform]
      properties:
        transform:
          oneOf:
            - { $ref: "#/components/schemas/SpatialMatrix4" }
            - { type: "null" }
    SpatialPlacementInput:
      type: object
      additionalProperties: false
      required: [position, orientation, confidence, method, capturedAt]
      properties:
        position: { $ref: "#/components/schemas/SpatialVector3" }
        orientation: { $ref: "#/components/schemas/SpatialQuaternion" }
        extent: { $ref: "#/components/schemas/SpatialDimensions" }
        confidence: { type: number, minimum: 0, maximum: 1 }
        method: { type: string, enum: [scene-depth, mesh-raycast, plane-raycast, manual] }
        anchorIdentifier: { type: string, format: uuid }
        capturedAt: { type: string, format: date-time }
        localizationEvidence:
          $ref: "#/components/schemas/PhotoLocalizationEvidence"
    PhotoLocalizationEvidence:
      type: object
      additionalProperties: false
      required: [matchedKeyframeId, distance, confidence]
      properties:
        matchedKeyframeId: { type: string, format: uuid }
        distance: { type: number, minimum: 0, maximum: 1000000 }
        confidence: { type: number, minimum: 0, maximum: 1 }
        cameraPositionError: { type: number, minimum: 0, maximum: 100 }
    RoomCameraKeyframeInput:
      type: object
      additionalProperties: false
      required: [id, fileField, capturedAt, timestamp, cameraTransform, intrinsics, width, height, orientation, quality]
      properties:
        id: { type: string, format: uuid }
        fileField:
          type: string
          description: Must exactly equal keyframe:<id> and name one JPEG multipart part.
        capturedAt: { type: string, format: date-time }
        timestamp: { type: number, minimum: 0, description: ARFrame session timestamp in seconds. }
        cameraTransform:
          allOf:
            - { $ref: "#/components/schemas/SpatialMatrix4" }
          description: Column-major worldFromCamera transform in the scan coordinate space.
        intrinsics:
          type: array
          minItems: 9
          maxItems: 9
          items: { type: number }
          description: Column-major ARKit 3x3 pinhole-camera matrix for the encoded native-raster pixel dimensions. Apply orientation only when displaying the JPEG.
        width: { type: integer, minimum: 1, maximum: 4096 }
        height: { type: integer, minimum: 1, maximum: 4096 }
        orientation: { type: string, enum: [up, up-mirrored, down, down-mirrored, left-mirrored, right, right-mirrored, left] }
        quality: { type: number, minimum: 0, maximum: 1 }
        featureDescriptor:
          type: [object, "null"]
          properties:
            format: { type: string, const: vision-feature-print-v1 }
            dataBase64: { type: string, maxLength: 65536 }
    RoomCameraKeyframe:
      allOf:
        - type: object
          additionalProperties: false
          required: [id, capturedAt, timestamp, cameraTransform, intrinsics, width, height, orientation, quality, mimeType, size, checksumSha256, url]
          properties:
            id: { type: string, format: uuid }
            capturedAt: { type: string, format: date-time }
            timestamp: { type: number, minimum: 0 }
            cameraTransform: { $ref: "#/components/schemas/SpatialMatrix4" }
            intrinsics:
              type: array
              minItems: 9
              maxItems: 9
              items: { type: number }
            width: { type: integer, minimum: 1, maximum: 4096 }
            height: { type: integer, minimum: 1, maximum: 4096 }
            orientation: { type: string, enum: [up, up-mirrored, down, down-mirrored, left-mirrored, right, right-mirrored, left] }
            quality: { type: number, minimum: 0, maximum: 1 }
            mimeType: { type: string, const: image/jpeg }
            size: { type: integer, minimum: 1, maximum: 6291456 }
            checksumSha256: { type: string, pattern: "^[a-f0-9]{64}$" }
            url: { type: string }
    RoomSceneManifest:
      type: object
      required: [room, scan, placements]
      properties:
        structureId: { type: [string, "null"], format: uuid }
        structureName: { type: [string, "null"] }
        coordinateSpaceId: { type: [string, "null"], format: uuid }
        floorIdentifier: { type: [string, "null"] }
        floorIndex: { type: [integer, "null"] }
        roomIdentifier: { type: [string, "null"] }
        georeference:
          oneOf:
            - { $ref: "#/components/schemas/SpatialGeoreference" }
            - { type: "null" }
        room:
          type: object
          required: [id, name]
          properties:
            id: { type: string, format: uuid }
            name: { type: string }
            description: { type: string }
        scan:
          type: object
          required: [id, revision, status, scene, capturedAt, assets, keyframes]
          properties:
            id: { type: string, format: uuid }
            structureId: { type: [string, "null"], format: uuid }
            structureName: { type: [string, "null"] }
            coordinateSpaceId: { type: [string, "null"], format: uuid }
            floorIdentifier: { type: [string, "null"] }
            floorIndex: { type: [integer, "null"] }
            roomIdentifier: { type: [string, "null"] }
            layoutTransform:
              oneOf:
                - { $ref: "#/components/schemas/SpatialMatrix4" }
                - { type: "null" }
              description: Optional saved world-from-model transform used to arrange the room with the other rooms on its floor.
            georeference:
              oneOf:
                - { $ref: "#/components/schemas/SpatialGeoreference" }
                - { type: "null" }
            revision: { type: integer, minimum: 1 }
            status: { type: string, enum: [active, superseded] }
            scene: { $ref: "#/components/schemas/RoomScene" }
            capturedAt: { type: string, format: date-time }
            deviceModel: { type: [string, "null"] }
            assets:
              type: array
              items: { $ref: "#/components/schemas/RoomScanAsset" }
            keyframes:
              type: array
              maxItems: 32
              items: { $ref: "#/components/schemas/RoomCameraKeyframe" }
        placements:
          type: array
          items:
            type: object
            required: [id, resource, position, orientation, confidence, method, capturedAt, updatedAt]
            properties:
              id: { type: string, format: uuid }
              resource:
                type: object
                required: [id, name, type, status]
                properties:
                  id: { type: string, format: uuid }
                  name: { type: string }
                  description: { type: string }
                  type: { type: string }
                  status: { type: string }
                  location: { type: [string, "null"] }
                  cover: { type: [object, "null"] }
              position: { $ref: "#/components/schemas/SpatialVector3" }
              orientation: { $ref: "#/components/schemas/SpatialQuaternion" }
              extent:
                oneOf:
                  - { $ref: "#/components/schemas/SpatialDimensions" }
                  - { type: "null" }
              confidence: { type: number, minimum: 0, maximum: 1 }
              method: { type: string, enum: [scene-depth, mesh-raycast, plane-raycast, manual] }
              anchorIdentifier: { type: [string, "null"], format: uuid }
              localizationEvidence:
                oneOf:
                  - { $ref: "#/components/schemas/PhotoLocalizationEvidence" }
                  - { type: "null" }
              capturedAt: { type: string, format: date-time }
              updatedAt: { type: string, format: date-time }
    DefinitionKey:
      type: string
      minLength: 1
      maxLength: 64
      pattern: "^[a-z][a-z0-9_-]{0,63}$"
    InventoryTypeKey:
      allOf:
        - { $ref: "#/components/schemas/DefinitionKey" }
      description: Stable key of an administrator-configurable inventory type.
    RelationTypeKey:
      allOf:
        - { $ref: "#/components/schemas/DefinitionKey" }
      description: Stable key of an administrator-configurable directed relationship type.
    InventoryTypeInput:
      type: object
      required: [key, label]
      additionalProperties: false
      properties:
        key: { $ref: "#/components/schemas/InventoryTypeKey" }
        label: { type: string, minLength: 1, maxLength: 120 }
        description: { type: string, maxLength: 5000, default: "" }
        color: { type: string, minLength: 1, maxLength: 32, default: "#635bff" }
        icon: { type: string, minLength: 1, maxLength: 80, default: box }
        canContain: { type: boolean, default: false, description: Records of this type can be used as parents and structured stock locations. }
        spatialContainment: { type: boolean, default: false, description: Automatically derive containment from map polygons. Requires canContain. }
        position: { type: integer, minimum: 0, maximum: 100000, default: 0 }
    InventoryTypePatch:
      type: object
      minProperties: 1
      additionalProperties: false
      properties:
        label: { type: string, minLength: 1, maxLength: 120 }
        description: { type: string, maxLength: 5000 }
        color: { type: string, minLength: 1, maxLength: 32 }
        icon: { type: string, minLength: 1, maxLength: 80 }
        canContain: { type: boolean }
        spatialContainment: { type: boolean }
        position: { type: integer, minimum: 0, maximum: 100000 }
        archived: { type: boolean, description: Archive when true or restore when false. }
    InventoryTypeDefinition:
      type: object
      required: [key, label, description, color, icon, canContain, spatialContainment, position, isSystem, createdBy, updatedBy, createdAt, updatedAt, archivedAt]
      properties:
        key: { $ref: "#/components/schemas/InventoryTypeKey" }
        label: { type: string }
        description: { type: string }
        color: { type: string }
        icon: { type: string }
        canContain: { type: boolean }
        spatialContainment: { type: boolean }
        position: { type: integer, minimum: 0 }
        isSystem: { type: boolean }
        createdBy: { type: [string, "null"] }
        updatedBy: { type: [string, "null"] }
        createdAt: { type: string, format: date-time }
        updatedAt: { type: string, format: date-time }
        archivedAt: { type: [string, "null"], format: date-time }
    RelationTypeInput:
      type: object
      required: [key, label, inverseLabel]
      additionalProperties: false
      properties:
        key: { $ref: "#/components/schemas/RelationTypeKey" }
        label: { type: string, minLength: 1, maxLength: 120 }
        inverseLabel: { type: string, minLength: 1, maxLength: 120 }
        description: { type: string, maxLength: 5000, default: "" }
        allowManual: { type: boolean, default: true }
        position: { type: integer, minimum: 0, maximum: 100000, default: 0 }
    RelationTypePatch:
      type: object
      minProperties: 1
      additionalProperties: false
      properties:
        label: { type: string, minLength: 1, maxLength: 120 }
        inverseLabel: { type: string, minLength: 1, maxLength: 120 }
        description: { type: string, maxLength: 5000 }
        allowManual: { type: boolean }
        position: { type: integer, minimum: 0, maximum: 100000 }
        archived: { type: boolean, description: Archive when true or restore when false. }
    RelationTypeDefinition:
      type: object
      required: [key, label, inverseLabel, description, allowManual, spatial, position, isSystem, createdBy, updatedBy, createdAt, updatedAt, archivedAt]
      properties:
        key: { $ref: "#/components/schemas/RelationTypeKey" }
        label: { type: string }
        inverseLabel: { type: string }
        description: { type: string }
        allowManual: { type: boolean }
        spatial: { type: boolean, description: Whether this built-in definition may be generated from geometry. }
        position: { type: integer, minimum: 0 }
        isSystem: { type: boolean }
        createdBy: { type: [string, "null"] }
        updatedBy: { type: [string, "null"] }
        createdAt: { type: string, format: date-time }
        updatedAt: { type: string, format: date-time }
        archivedAt: { type: [string, "null"], format: date-time }
    ResourceReference:
      type: object
      required: [id, name, type, status]
      properties:
        id: { type: string, format: uuid }
        name: { type: string }
        type: { $ref: "#/components/schemas/InventoryTypeKey" }
        status: { type: string, enum: [available, in-use, maintenance, archived] }
    ResourceRelationInput:
      type: object
      required: [sourceResourceId, targetResourceId, relationTypeKey]
      additionalProperties: false
      properties:
        sourceResourceId: { type: string, format: uuid, description: Directed parent or subject endpoint. }
        targetResourceId: { type: string, format: uuid, description: Directed child or object endpoint. }
        relationTypeKey: { $ref: "#/components/schemas/RelationTypeKey" }
    ResourceRelationRecord:
      type: object
      required: [id, sourceResourceId, targetResourceId, relationTypeKey, origin, sourceFeatureId, targetFeatureId, createdBy, createdAt]
      properties:
        id: { type: string, format: uuid }
        sourceResourceId: { type: string, format: uuid }
        targetResourceId: { type: string, format: uuid }
        relationTypeKey: { $ref: "#/components/schemas/RelationTypeKey" }
        origin: { type: string, enum: [manual, spatial] }
        sourceFeatureId: { type: [string, "null"] }
        targetFeatureId: { type: [string, "null"] }
        createdBy: { type: [string, "null"] }
        createdAt: { type: string, format: date-time }
    ResourceRelation:
      allOf:
        - { $ref: "#/components/schemas/ResourceRelationRecord" }
        - type: object
          required: [source, target, relationType]
          properties:
            source:
              oneOf:
                - { $ref: "#/components/schemas/ResourceReference" }
                - { type: "null" }
            target:
              oneOf:
                - { $ref: "#/components/schemas/ResourceReference" }
                - { type: "null" }
            relationType:
              oneOf:
                - { $ref: "#/components/schemas/RelationTypeDefinition" }
                - { type: "null" }
    CustomFieldOption:
      type: object
      required: [value, label]
      additionalProperties: false
      properties:
        value: { type: string, minLength: 1, maxLength: 120, description: Stable value stored in customFields. }
        label: { type: string, minLength: 1, maxLength: 120 }
        color: { type: string, minLength: 1, maxLength: 32 }
    CustomFieldReferenceOption:
      type: object
      required: [id, entityType, label, description, status]
      additionalProperties: false
      properties:
        id: { type: string, format: uuid }
        entityType: { type: string, enum: [inventory, stock_unit] }
        label: { type: string }
        description: { type: string }
        status: { type: string }
    CustomFieldValues:
      type: object
      maxProperties: 100
      description: Values keyed by stable custom-field definition keys. Types and required values are checked against active definitions applicable to the resource.
      additionalProperties:
        oneOf:
          - { type: string, maxLength: 20000 }
          - { type: number }
          - { type: boolean }
          - type: array
            maxItems: 100
            items: { type: string, maxLength: 120 }
    CustomFieldDefinitionInput:
      type: object
      required: [entityType, label, fieldType]
      additionalProperties: false
      properties:
        entityType:
          type: string
          enum: [inventory, stock_unit]
          description: Whether values live on inventory resources or serialized stock units.
        key:
          type: string
          pattern: "^[a-z][a-z0-9_]{0,63}$"
          description: Stable API/storage identifier. Generated from label when omitted and immutable after creation.
        label: { type: string, minLength: 1, maxLength: 120 }
        description: { type: string, maxLength: 5000, default: "" }
        placeholder: { type: string, maxLength: 240, default: "" }
        fieldType:
          type: string
          enum: [text, textarea, number, boolean, date, datetime, select, multi_select, reference, email, url]
        required: { type: boolean, default: false }
        minValue:
          type: [number, "null"]
          default: null
          description: Inclusive minimum for number fields; null for other field types.
        maxValue:
          type: [number, "null"]
          default: null
          description: Inclusive maximum for number fields; null for other field types.
        step:
          type: [number, "null"]
          exclusiveMinimum: 0
          default: null
          description: Positive increment for number fields; null for other field types.
        resourceTypes:
          type: array
          maxItems: 100
          uniqueItems: true
          description: Applicable inventory types. An empty array matches every type.
          items: { $ref: "#/components/schemas/InventoryTypeKey" }
          default: []
        categories:
          type: array
          maxItems: 40
          uniqueItems: true
          description: Applicable category names, matched case-insensitively. An empty array matches every category; when resourceTypes is also set, both targets must match.
          items: { type: string, minLength: 1, maxLength: 120 }
          default: []
        options:
          type: array
          maxItems: 100
          description: Required and non-empty for select and multi_select fields; empty for every other field type.
          items: { $ref: "#/components/schemas/CustomFieldOption" }
          default: []
        referenceEntityType:
          type: [string, "null"]
          enum: [inventory, stock_unit, null]
          default: null
          description: Required for reference fields and null for all other field types.
        referenceMultiple:
          type: boolean
          default: false
          description: Whether a reference field stores an array of target UUIDs instead of one UUID.
        referenceResourceTypes:
          type: array
          maxItems: 100
          uniqueItems: true
          items: { $ref: "#/components/schemas/InventoryTypeKey" }
          default: []
          description: Allowed target or parent inventory types; empty matches every type.
        referenceCategories:
          type: array
          maxItems: 40
          uniqueItems: true
          items: { type: string, minLength: 1, maxLength: 120 }
          default: []
          description: Allowed target or parent inventory categories; empty matches every category.
        referenceStatuses:
          type: array
          maxItems: 40
          uniqueItems: true
          items: { type: string, minLength: 1, maxLength: 32 }
          default: []
          description: Allowed target statuses; empty matches every status.
        position: { type: integer, minimum: 0, maximum: 100000, default: 0 }
    CustomFieldDefinitionPatch:
      type: object
      required: [revision]
      minProperties: 2
      additionalProperties: false
      description: The last observed revision plus a non-empty subset of mutable definition fields. Entity type and key cannot be changed.
      properties:
        revision: { type: integer, minimum: 1 }
        label: { type: string, minLength: 1, maxLength: 120 }
        description: { type: string, maxLength: 5000 }
        placeholder: { type: string, maxLength: 240 }
        fieldType: { type: string, enum: [text, textarea, number, boolean, date, datetime, select, multi_select, reference, email, url] }
        required: { type: boolean }
        minValue: { type: [number, "null"] }
        maxValue: { type: [number, "null"] }
        step: { type: [number, "null"], exclusiveMinimum: 0 }
        resourceTypes:
          type: array
          maxItems: 100
          uniqueItems: true
          items: { $ref: "#/components/schemas/InventoryTypeKey" }
        categories:
          type: array
          maxItems: 40
          uniqueItems: true
          items: { type: string, minLength: 1, maxLength: 120 }
        options:
          type: array
          maxItems: 100
          items: { $ref: "#/components/schemas/CustomFieldOption" }
        referenceEntityType: { type: [string, "null"], enum: [inventory, stock_unit, null] }
        referenceMultiple: { type: boolean }
        referenceResourceTypes:
          type: array
          maxItems: 100
          uniqueItems: true
          items: { $ref: "#/components/schemas/InventoryTypeKey" }
        referenceCategories:
          type: array
          maxItems: 40
          uniqueItems: true
          items: { type: string, minLength: 1, maxLength: 120 }
        referenceStatuses:
          type: array
          maxItems: 40
          uniqueItems: true
          items: { type: string, minLength: 1, maxLength: 32 }
        position: { type: integer, minimum: 0, maximum: 100000 }
    CustomFieldDefinition:
      type: object
      required: [id, entityType, key, label, description, placeholder, fieldType, required, minValue, maxValue, step, resourceTypes, categories, options, referenceEntityType, referenceMultiple, referenceResourceTypes, referenceCategories, referenceStatuses, position, revision, createdBy, updatedBy, createdAt, updatedAt, archivedAt]
      additionalProperties: false
      properties:
        id: { type: string, format: uuid }
        entityType: { type: string, enum: [inventory, stock_unit] }
        key: { type: string, pattern: "^[a-z][a-z0-9_]{0,63}$" }
        label: { type: string, minLength: 1, maxLength: 120 }
        description: { type: string, maxLength: 5000 }
        placeholder: { type: string, maxLength: 240 }
        fieldType: { type: string, enum: [text, textarea, number, boolean, date, datetime, select, multi_select, reference, email, url] }
        required: { type: boolean }
        minValue: { type: [number, "null"] }
        maxValue: { type: [number, "null"] }
        step: { type: [number, "null"], exclusiveMinimum: 0 }
        resourceTypes:
          type: array
          maxItems: 100
          uniqueItems: true
          items: { $ref: "#/components/schemas/InventoryTypeKey" }
        categories:
          type: array
          maxItems: 40
          uniqueItems: true
          items: { type: string, minLength: 1, maxLength: 120 }
        options:
          type: array
          maxItems: 100
          items: { $ref: "#/components/schemas/CustomFieldOption" }
        referenceEntityType: { type: [string, "null"], enum: [inventory, stock_unit, null] }
        referenceMultiple: { type: boolean }
        referenceResourceTypes:
          type: array
          maxItems: 100
          uniqueItems: true
          items: { $ref: "#/components/schemas/InventoryTypeKey" }
        referenceCategories:
          type: array
          maxItems: 40
          uniqueItems: true
          items: { type: string, minLength: 1, maxLength: 120 }
        referenceStatuses:
          type: array
          maxItems: 40
          uniqueItems: true
          items: { type: string, minLength: 1, maxLength: 32 }
        position: { type: integer, minimum: 0, maximum: 100000 }
        revision: { type: integer, minimum: 1, description: Incremented by every update and soft deletion. }
        createdBy: { type: [string, "null"], maxLength: 320 }
        updatedBy: { type: [string, "null"], maxLength: 320 }
        createdAt: { type: string, format: date-time }
        updatedAt: { type: string, format: date-time }
        archivedAt: { type: [string, "null"], format: date-time }
    LabelElement:
      oneOf:
        - type: object
          title: QR or barcode element
          required: [type, x, y, width, height, visible]
          additionalProperties: false
          properties:
            type: { type: string, enum: [qr, barcode] }
            x: { type: number, minimum: 0, maximum: 100 }
            y: { type: number, minimum: 0, maximum: 100 }
            width: { type: number, exclusiveMinimum: 0, maximum: 100 }
            height: { type: number, exclusiveMinimum: 0, maximum: 100 }
            visible: { type: boolean }
        - type: object
          title: Object image element
          required: [type, x, y, width, height, visible]
          additionalProperties: false
          properties:
            type: { type: string, const: image }
            x: { type: number, minimum: 0, maximum: 100 }
            y: { type: number, minimum: 0, maximum: 100 }
            width: { type: number, exclusiveMinimum: 0, maximum: 100 }
            height: { type: number, exclusiveMinimum: 0, maximum: 100 }
            visible: { type: boolean }
            fit: { type: string, enum: [cover, contain], default: cover }
        - type: object
          title: Text element
          required: [type, x, y, width, height, visible]
          additionalProperties: false
          properties:
            type: { type: string, enum: [name, identifier, url, location] }
            x: { type: number, minimum: 0, maximum: 100 }
            y: { type: number, minimum: 0, maximum: 100 }
            width: { type: number, exclusiveMinimum: 0, maximum: 100 }
            height: { type: number, exclusiveMinimum: 0, maximum: 100 }
            visible: { type: boolean }
            fontSizeMm: { type: number, exclusiveMinimum: 0, maximum: 100 }
            align: { type: string, enum: [left, center, right], default: left }
      description: A normalized percentage box. The element must fit entirely inside the label; options are restricted by element type.
    LabelElements:
      type: array
      maxItems: 7
      description: At most one element of each type. A visible object image may not overlap a visible QR code. The image element prints the resource cover image; QR and URL elements use the compact resource link.
      items: { $ref: "#/components/schemas/LabelElement" }
    LabelSetupInput:
      type: object
      required: [name, widthMm, heightMm, elements]
      additionalProperties: false
      properties:
        name: { type: string, minLength: 1, maxLength: 160 }
        widthMm: { type: number, exclusiveMinimum: 0, maximum: 1000 }
        heightMm: { type: number, exclusiveMinimum: 0, maximum: 1000 }
        elements: { $ref: "#/components/schemas/LabelElements" }
    LabelSetupPatch:
      type: object
      required: [revision]
      minProperties: 2
      additionalProperties: false
      description: The last observed revision plus at least one changed setup property.
      properties:
        revision: { type: integer, minimum: 1 }
        name: { type: string, minLength: 1, maxLength: 160 }
        widthMm: { type: number, exclusiveMinimum: 0, maximum: 1000 }
        heightMm: { type: number, exclusiveMinimum: 0, maximum: 1000 }
        elements: { $ref: "#/components/schemas/LabelElements" }
    LabelSetup:
      type: object
      required: [id, name, widthMm, heightMm, elements, revision, createdAt, updatedAt]
      additionalProperties: false
      properties:
        id: { type: string, format: uuid }
        name: { type: string, minLength: 1, maxLength: 160 }
        widthMm: { type: number, exclusiveMinimum: 0, maximum: 1000 }
        heightMm: { type: number, exclusiveMinimum: 0, maximum: 1000 }
        elements: { $ref: "#/components/schemas/LabelElements" }
        revision: { type: integer, minimum: 1, description: Incremented by every successful update. }
        createdAt: { type: string, format: date-time }
        updatedAt: { type: string, format: date-time }
    InventoryCount:
      type: object
      required: [count, confidence, detectedItem, isExact, explanation, warnings, markers, model]
      properties:
        count: { type: integer, minimum: 0, maximum: 1000000 }
        confidence: { type: number, minimum: 0, maximum: 1 }
        detectedItem: { type: string, minLength: 1, maxLength: 240 }
        isExact:
          type: boolean
          description: True only when every counted instance is individually visible without meaningful ambiguity. The current zero-shot SAM 3 provider returns false so clients require review.
        explanation: { type: string, minLength: 1, maxLength: 1000 }
        warnings:
          type: array
          maxItems: 10
          items: { type: string, minLength: 1, maxLength: 240 }
        markers:
          type: array
          description: One bounding-box center per counted item on a 0...1000 image grid, measured from the top-left corner of the orientation-corrected full image.
          items:
            type: object
            additionalProperties: false
            required: [x, y]
            properties:
              x: { type: integer, minimum: 0, maximum: 1000 }
              y: { type: integer, minimum: 0, maximum: 1000 }
        model: { type: string }
    InventoryRecognitionObservation:
      type: object
      additionalProperties: false
      required: [label, category, brand, model, color, material, visibleText, searchTerms, confidence]
      properties:
        label: { type: string, minLength: 1, maxLength: 160 }
        category: { type: string, minLength: 1, maxLength: 160 }
        brand: { type: [string, "null"], minLength: 1, maxLength: 160 }
        model: { type: [string, "null"], minLength: 1, maxLength: 160 }
        color: { type: [string, "null"], minLength: 1, maxLength: 120 }
        material: { type: [string, "null"], minLength: 1, maxLength: 120 }
        visibleText:
          type: array
          maxItems: 20
          items: { type: string, minLength: 1, maxLength: 160 }
        searchTerms:
          type: array
          minItems: 1
          maxItems: 30
          items: { type: string, minLength: 1, maxLength: 160 }
        confidence: { type: number, minimum: 0, maximum: 1 }
    InventoryRecognitionMatch:
      type: object
      additionalProperties: false
      required: [resourceId, confidence, reason, evidence, resource]
      properties:
        resourceId: { type: string, format: uuid }
        confidence: { type: number, minimum: 0, maximum: 1 }
        reason: { type: string, minLength: 1, maxLength: 600 }
        evidence:
          type: array
          maxItems: 8
          items: { type: string, minLength: 1, maxLength: 240 }
        resource: { $ref: "#/components/schemas/Resource" }
    InventoryRecognitionResult:
      type: object
      additionalProperties: false
      required: [detected, matches, isConfident, model, catalog]
      properties:
        detected:
          oneOf:
            - { $ref: "#/components/schemas/InventoryRecognitionObservation" }
            - { type: "null" }
        matches:
          type: array
          maxItems: 5
          items: { $ref: "#/components/schemas/InventoryRecognitionMatch" }
        isConfident:
          type: boolean
          description: True only when the leading provider score is strong and sufficiently separated from the runner-up; the match remains advisory.
        model: { type: [string, "null"] }
        catalog:
          type: object
          additionalProperties: false
          required: [considered, truncated]
          properties:
            considered: { type: integer, minimum: 0, maximum: 5000 }
            truncated:
              type: boolean
              description: True when metadata matches or the image-bearing visual fallback exceeded the bounded comparison catalog; clients should present the result as non-exhaustive.
    InventoryCountJob:
      type: object
      additionalProperties: false
      required: [status, jobToken, expiresAt]
      properties:
        status: { type: string, enum: [processing] }
        jobToken: { type: string, minLength: 1, maxLength: 4096 }
        expiresAt: { type: string, format: date-time }
        message: { type: string }
    ImageGenerationModel:
      type: object
      required: [id, provider, model, label]
      additionalProperties: false
      properties:
        id: { type: string, maxLength: 240, example: "google:gemini-3.1-flash-image" }
        provider: { type: string, enum: [openai, google] }
        model: { type: string, maxLength: 233, example: "gemini-3.1-flash-image" }
        label: { type: string, maxLength: 260, example: "Nano Banana 2" }
    InventoryCountModel:
      type: object
      required: [id, provider, model, label, description]
      additionalProperties: false
      properties:
        id:
          type: string
          enum: [grounding-dino, yolo-world, sam-2, sam-3]
        provider: { type: string, enum: [replicate] }
        model: { type: string, maxLength: 240 }
        label: { type: string, maxLength: 260 }
        description: { type: string, maxLength: 500 }
    BomComponentInput:
      type: object
      required: [resourceId, quantityPerAssembly]
      additionalProperties: false
      properties:
        resourceId: { type: string, format: uuid }
        variantId: { type: [string, "null"], format: uuid }
        variantDelta: { type: [integer, "null"] }
        variantBalanceAfter: { type: [integer, "null"], minimum: 0 }
        quantityPerAssembly: { type: integer, minimum: 1 }
        position: { type: integer, minimum: 0 }
        note: { type: string, maxLength: 20000 }
    BomComponent:
      allOf:
        - { $ref: "#/components/schemas/BomComponentInput" }
        - type: object
          required: [id, name, availableQuantity, trackingMode, availableUnits]
          properties:
            id: { type: string, format: uuid }
            name: { type: string }
            sku: { type: [string, "null"] }
            availableQuantity: { type: integer, minimum: 0 }
            trackingMode: { type: string, enum: [bulk, serialized] }
            availableUnits:
              type: array
              items:
                type: object
                required: [id, code, location]
                properties:
                  id: { type: string, format: uuid }
                  code: { type: string }
                  location: { type: [string, "null"] }
    BomDetail:
      type: object
      required: [resource, components, buildableQuantity]
      properties:
        resource:
          type: object
          required: [id, name, quantity, trackingMode]
          properties:
            id: { type: string, format: uuid }
            name: { type: string }
            quantity: { type: integer, minimum: 0 }
            trackingMode: { type: string, enum: [bulk, serialized] }
        components: { type: array, items: { $ref: "#/components/schemas/BomComponent" } }
        buildableQuantity: { type: integer, minimum: 0 }
    AssemblyBuildInput:
      type: object
      required: [quantity]
      additionalProperties: false
      properties:
        quantity: { type: integer, minimum: 1, maximum: 1000 }
        occurredAt: { type: string, format: date-time }
        location: { type: [string, "null"], maxLength: 240 }
        note: { type: string, maxLength: 20000 }
        componentUnitIds:
          type: object
          additionalProperties:
            type: array
            items: { type: string, format: uuid }
        outputUnitCodes:
          type: array
          items: { type: string, maxLength: 180 }
    AssemblyBuild:
      type: object
      required: [id, assemblyResourceId, quantity, occurredAt, createdAt, components, outputUnits]
      properties:
        id: { type: string, format: uuid }
        assemblyResourceId: { type: string, format: uuid }
        quantity: { type: integer, minimum: 1 }
        occurredAt: { type: string, format: date-time }
        location: { type: [string, "null"] }
        note: { type: string }
        createdBy: { type: [string, "null"] }
        createdAt: { type: string, format: date-time }
        components:
          type: array
          items:
            type: object
            required: [resourceId, name, sku, quantityPerAssembly, quantityConsumed, componentUnits, stockMovementIds, outputUnitIds]
            properties:
              resourceId: { type: [string, "null"], format: uuid }
              name: { type: string }
              sku: { type: [string, "null"] }
              quantityPerAssembly: { type: integer, minimum: 1 }
              quantityConsumed: { type: integer, minimum: 1 }
              componentUnits:
                type: array
                items:
                  type: object
                  required: [id, code, location, status, outputUnitId]
                  properties:
                    id: { type: string, format: uuid }
                    code: { type: string }
                    location: { type: [string, "null"] }
                    status: { type: string }
                    outputUnitId: { type: [string, "null"], format: uuid }
              stockMovementIds:
                type: array
                items: { type: string, format: uuid }
              outputUnitIds:
                type: array
                items: { type: string, format: uuid }
        outputUnits:
          type: array
          items: { $ref: "#/components/schemas/StockUnit" }
    PurchaseOrderLineInput:
      type: object
      required: [resourceId, orderedQuantity]
      additionalProperties: false
      properties:
        resourceId: { type: string, format: uuid }
        orderedQuantity: { type: integer, minimum: 1 }
        expectedAt: { type: [string, "null"], format: date-time }
        note: { type: string, maxLength: 20000 }
    PurchaseOrderInput:
      type: object
      required: [lines]
      additionalProperties: false
      properties:
        reference: { type: [string, "null"], maxLength: 160 }
        supplier: { type: string, maxLength: 240 }
        status: { type: string, enum: [draft, ordered] }
        orderedAt: { type: string, format: date-time }
        expectedAt: { type: [string, "null"], format: date-time }
        note: { type: string, maxLength: 20000 }
        lines:
          type: array
          minItems: 1
          maxItems: 100
          items: { $ref: "#/components/schemas/PurchaseOrderLineInput" }
    PurchaseOrderLine:
      allOf:
        - { $ref: "#/components/schemas/PurchaseOrderLineInput" }
        - type: object
          required: [id, resourceName, receivedQuantity, openQuantity, trackingMode]
          properties:
            id: { type: string, format: uuid }
            resourceName: { type: string }
            resourceSku: { type: [string, "null"] }
            receivedQuantity: { type: integer, minimum: 0 }
            openQuantity: { type: integer, minimum: 0 }
            trackingMode: { type: string, enum: [bulk, serialized] }
    PurchaseOrder:
      type: object
      required: [id, supplier, status, orderedAt, lines, totalOrdered, totalReceived, totalOpen]
      properties:
        id: { type: string, format: uuid }
        reference: { type: [string, "null"] }
        supplier: { type: string }
        status: { type: string, enum: [draft, ordered, partially-received, received, cancelled] }
        orderedAt: { type: string, format: date-time }
        expectedAt: { type: [string, "null"], format: date-time }
        note: { type: string }
        createdBy: { type: [string, "null"] }
        lines: { type: array, items: { $ref: "#/components/schemas/PurchaseOrderLine" } }
        totalOrdered: { type: integer, minimum: 0 }
        totalReceived: { type: integer, minimum: 0 }
        totalOpen: { type: integer, minimum: 0 }
    PurchaseReceiptInput:
      type: object
      required: [quantity]
      additionalProperties: false
      properties:
        quantity: { type: integer, minimum: 1, maximum: 1000 }
        receivedAt: { type: string, format: date-time }
        location: { type: [string, "null"], maxLength: 240 }
        note: { type: string, maxLength: 20000 }
        unitCodes:
          type: array
          items: { type: string, maxLength: 180 }
    NativeLoginResponse:
      type: object
      required: [token, user, scopes, expiresAt, organization, organizations]
      properties:
        token:
          type: string
          description: Opaque secret returned only once.
        user:
          type: object
          required: [id, name, email, role]
          properties:
            id: { type: string, format: uuid }
            name: { type: string }
            email: { type: string, format: email }
            role: { $ref: "#/components/schemas/AccessRoleKey" }
        scopes:
          type: array
          items: { type: string, enum: [read, write, ai] }
        expiresAt: { type: string, format: date-time }
        organization: { $ref: "#/components/schemas/OrganizationMembership" }
        organizations:
          type: array
          minItems: 1
          items: { $ref: "#/components/schemas/OrganizationMembership" }
    Pagination:
      type: object
      required: [page, pageSize, total, pages]
      properties:
        page: { type: integer }
        pageSize: { type: integer }
        total: { type: integer }
        pages: { type: integer }
    CsvImportResponse:
      type: object
      required: [importId, summary, rows]
      properties:
        importId: { type: string, format: uuid, description: The request Idempotency-Key. }
        summary:
          type: object
          required: [total, created, replayed, failed]
          properties:
            total: { type: integer, minimum: 1, maximum: 1000 }
            created: { type: integer, minimum: 0 }
            replayed: { type: integer, minimum: 0 }
            failed: { type: integer, minimum: 0 }
        rows:
          type: array
          minItems: 1
          maxItems: 1000
          items:
            type: object
            required: [line, status]
            properties:
              line: { type: integer, minimum: 2, description: One-based source line where the CSV row begins. }
              status: { type: string, enum: [created, replayed, error] }
              resource:
                type: object
                description: Present for created and replayed rows.
                required: [id, name, sku]
                properties:
                  id: { type: string, format: uuid }
                  name: { type: string }
                  sku: { type: [string, "null"] }
              error: { type: string, description: Present for rows with status error. }
              details:
                type: array
                items: { type: string }
    Media:
      type: object
      required: [id, resourceId, url, name, mimeType, kind, position, source]
      properties:
        id: { type: string, format: uuid }
        resourceId: { type: string, format: uuid }
        url: { type: string }
        name: { type: string }
        mimeType: { type: string }
        kind: { type: string, enum: [image, video, document, model, unknown] }
        position: { type: integer }
        source: { type: string, enum: [upload, ai] }
        altText: { type: string }
    ScanWorkflowExtraction:
      oneOf:
        - type: object
          required: [mode]
          additionalProperties: false
          properties:
            mode: { type: string, const: full }
        - type: object
          required: [mode, parameter]
          additionalProperties: false
          properties:
            mode: { type: string, const: url-query }
            parameter: { type: string, minLength: 1, maxLength: 80 }
            sourceOrigin:
              type: string
              maxLength: 300
              description: Optional exact URL origin; the server never fetches the URL.
            sourcePath: { type: string, pattern: "^/[^?#]*$", maxLength: 500 }
        - type: object
          required: [mode, prefix]
          additionalProperties: false
          properties:
            mode: { type: string, const: prefix }
            prefix: { type: string, minLength: 1, maxLength: 160 }
    ScanWorkflowFixedProperty:
      type: object
      required: [key, label, value]
      additionalProperties: false
      properties:
        key: { type: string, minLength: 1, maxLength: 80, pattern: "^[A-Za-z0-9_.-]+$" }
        label: { type: string, minLength: 1, maxLength: 120 }
        value: { type: string, maxLength: 240 }
    ScanWorkflowOption:
      type: object
      required: [value, label]
      additionalProperties: false
      properties:
        value: { type: string, minLength: 1, maxLength: 120 }
        label: { type: string, minLength: 1, maxLength: 120 }
        color: { type: string, minLength: 1, maxLength: 32 }
    ScanWorkflowInputField:
      type: object
      required: [key, label, required, options]
      additionalProperties: false
      properties:
        key: { type: string, minLength: 1, maxLength: 80, pattern: "^[A-Za-z0-9_.-]+$" }
        label: { type: string, minLength: 1, maxLength: 120 }
        required: { type: boolean }
        options:
          type: array
          minItems: 1
          maxItems: 40
          items: { $ref: "#/components/schemas/ScanWorkflowOption" }
    ScanWorkflowInput:
      type: object
      required: [name, resourceId, extraction, identifierPropertyKey]
      properties:
        name: { type: string, minLength: 1, maxLength: 160 }
        description: { type: string, maxLength: 5000, default: "" }
        enabled: { type: boolean, default: true }
        resourceId: { type: string, format: uuid }
        extraction: { $ref: "#/components/schemas/ScanWorkflowExtraction" }
        identifierPropertyKey: { type: string, minLength: 1, maxLength: 80, pattern: "^[A-Za-z0-9_.-]+$" }
        createMissingUnit: { type: boolean, default: false }
        unitStatus:
          oneOf:
            - { type: string, enum: [available, reserved, in-use, maintenance, consumed, lost, retired] }
            - { type: "null" }
        fixedProperties:
          type: array
          maxItems: 24
          items: { $ref: "#/components/schemas/ScanWorkflowFixedProperty" }
        inputFields:
          type: array
          maxItems: 12
          items: { $ref: "#/components/schemas/ScanWorkflowInputField" }
    ScanWorkflowPatch:
      type: object
      required: [revision]
      minProperties: 2
      additionalProperties: false
      description: Current revision plus at least one writable ScanWorkflowInput property.
      properties:
        revision: { type: integer, minimum: 1 }
        name: { type: string, minLength: 1, maxLength: 160 }
        description: { type: string, maxLength: 5000 }
        enabled: { type: boolean }
        resourceId: { type: string, format: uuid }
        extraction: { $ref: "#/components/schemas/ScanWorkflowExtraction" }
        identifierPropertyKey: { type: string, minLength: 1, maxLength: 80, pattern: "^[A-Za-z0-9_.-]+$" }
        createMissingUnit: { type: boolean }
        unitStatus:
          oneOf:
            - { type: string, enum: [available, reserved, in-use, maintenance, consumed, lost, retired] }
            - { type: "null" }
        fixedProperties:
          type: array
          maxItems: 24
          items: { $ref: "#/components/schemas/ScanWorkflowFixedProperty" }
        inputFields:
          type: array
          maxItems: 12
          items: { $ref: "#/components/schemas/ScanWorkflowInputField" }
    ScanWorkflow:
      allOf:
        - { $ref: "#/components/schemas/ScanWorkflowInput" }
        - type: object
          required: [id, name, description, enabled, resourceId, revision, extraction, identifierPropertyKey, createMissingUnit, unitStatus, fixedProperties, inputFields, createdBy, updatedBy, createdAt, updatedAt]
          properties:
            id: { type: string, format: uuid }
            revision: { type: integer, minimum: 1 }
            createdBy: { type: [string, "null"] }
            updatedBy: { type: [string, "null"] }
            createdAt: { type: string, format: date-time }
            updatedAt: { type: string, format: date-time }
    StockScanResolution:
      type: object
      required: [workflow, resource, identifier, unit, willCreate, fields, fixedProperties, metadataPreview, statusBefore, statusAfter, quantityBefore, quantityAfter, delta, expectedResourceUpdatedAt, expectedUnitId, expectedUnitUpdatedAt]
      additionalProperties: false
      properties:
        workflow: { $ref: "#/components/schemas/ScanWorkflow" }
        resource:
          type: object
          required: [id, name, quantity, trackingMode]
          properties:
            id: { type: string, format: uuid }
            name: { type: string }
            quantity: { type: integer, minimum: 0 }
            trackingMode: { type: string, const: serialized }
        identifier: { type: string, minLength: 1, maxLength: 180 }
        unit:
          oneOf:
            - { $ref: "#/components/schemas/StockUnit" }
            - { type: "null" }
        willCreate: { type: boolean }
        statusBefore:
          oneOf:
            - { type: string, enum: [available, reserved, in-use, maintenance, consumed, lost, retired] }
            - { type: "null" }
        statusAfter: { type: string, enum: [available, reserved, in-use, maintenance, consumed, lost, retired] }
        quantityBefore: { type: integer, minimum: 0 }
        quantityAfter: { type: integer, minimum: 0 }
        delta: { type: integer, minimum: -1, maximum: 1 }
        expectedResourceUpdatedAt:
          type: string
          format: date-time
          description: Resource version captured by resolve; execute rejects a stale quantity preview.
        expectedUnitId:
          description: Unit captured by resolve, or null when no matching unit existed. Both unit guard fields must be null or non-null together.
          oneOf:
            - { type: string, format: uuid }
            - { type: "null" }
        expectedUnitUpdatedAt:
          description: Unit version captured by resolve, or null when no matching unit existed. Both unit guard fields must be null or non-null together.
          oneOf:
            - { type: string, format: date-time }
            - { type: "null" }
        fields:
          type: array
          maxItems: 12
          items: { $ref: "#/components/schemas/ScanWorkflowInputField" }
        fixedProperties:
          type: array
          maxItems: 24
          items: { $ref: "#/components/schemas/ScanWorkflowFixedProperty" }
        metadataPreview: { type: object, additionalProperties: true }
    StockScanExecutionInput:
      type: object
      required: [workflowId, revision, code, expectedResourceUpdatedAt, expectedUnitId, expectedUnitUpdatedAt]
      additionalProperties: false
      properties:
        workflowId: { type: string, format: uuid }
        revision: { type: integer, minimum: 1 }
        code: { type: string, minLength: 1, maxLength: 2048 }
        expectedResourceUpdatedAt:
          type: string
          format: date-time
          description: Copy exactly from the resolve response.
        expectedUnitId:
          description: Copy exactly from resolve; must be null together with expectedUnitUpdatedAt or non-null together with it.
          oneOf:
            - { type: string, format: uuid }
            - { type: "null" }
        expectedUnitUpdatedAt:
          description: Copy exactly from resolve; must be null together with expectedUnitId or non-null together with it.
          oneOf:
            - { type: string, format: date-time }
            - { type: "null" }
        inputs:
          type: object
          default: {}
          description: Operator-provided workflow fields. May be omitted when the workflow has no input fields.
          maxProperties: 12
          propertyNames: { minLength: 1, maxLength: 80 }
          additionalProperties: { type: string, maxLength: 240 }
    StockScanExecution:
      type: object
      required: [workflowId, revision, resource, unit, movement, created, metadataBefore, metadataAfter]
      additionalProperties: false
      properties:
        workflowId: { type: string, format: uuid }
        revision: { type: integer, minimum: 1 }
        resource:
          type: object
          required: [id, name, quantity]
          properties:
            id: { type: string, format: uuid }
            name: { type: string }
            quantity: { type: integer, minimum: 0 }
        unit: { $ref: "#/components/schemas/StockUnit" }
        movement: { $ref: "#/components/schemas/StockMovement" }
        created: { type: boolean }
        metadataBefore:
          oneOf:
            - { type: object, additionalProperties: true }
            - { type: "null" }
        metadataAfter: { type: object, additionalProperties: true }
    StockLocationResource:
      type: object
      required: [id, name, type, status]
      properties:
        id: { type: string, format: uuid }
        name: { type: string }
        type: { $ref: "#/components/schemas/InventoryTypeKey" }
        status: { type: string, enum: [available, in-use, maintenance, archived] }
    StockLocationBalance:
      type: object
      required: [locationResourceId, name, type, quantity]
      properties:
        locationResourceId: { type: string, format: uuid }
        name: { type: string }
        type: { $ref: "#/components/schemas/InventoryTypeKey" }
        quantity: { type: integer, minimum: 0 }
    StockLocationBreakdown:
      type: object
      required: [resource, trackingMode, assignedQuantity, unassignedQuantity, locations]
      properties:
        resource:
          type: object
          required: [id, name, quantity, trackingMode]
          properties:
            id: { type: string, format: uuid }
            name: { type: string }
            quantity: { type: integer, minimum: 0 }
            trackingMode: { type: [string, "null"], enum: [bulk, serialized, null] }
        trackingMode: { type: string, enum: [bulk, serialized] }
        assignedQuantity: { type: integer, minimum: 0 }
        unassignedQuantity: { type: integer, minimum: 0 }
        locations:
          type: array
          items: { $ref: "#/components/schemas/StockLocationBalance" }
    StockLocationsResponse:
      type: object
      required: [breakdown, availableLocations]
      properties:
        breakdown: { $ref: "#/components/schemas/StockLocationBreakdown" }
        availableLocations:
          type: array
          items: { $ref: "#/components/schemas/StockLocationResource" }
    InventoryCyclePolicyInput:
      type: object
      required: [intervalDays]
      additionalProperties: false
      properties:
        intervalDays: { type: integer, minimum: 1, maximum: 3650 }
        enabled: { type: boolean, default: true }
    InventoryCyclePolicy:
      type: object
      required: [resourceId, intervalDays, enabled, nextDueAt, lastCompletedAt, createdBy, updatedBy, createdAt, updatedAt]
      properties:
        resourceId: { type: string, format: uuid }
        intervalDays: { type: integer, minimum: 1, maximum: 3650 }
        enabled: { type: boolean }
        nextDueAt: { type: string, format: date-time }
        lastCompletedAt: { type: [string, "null"], format: date-time }
        createdBy: { type: [string, "null"] }
        updatedBy: { type: [string, "null"] }
        createdAt: { type: string, format: date-time }
        updatedAt: { type: string, format: date-time }
    InventoryCountInput:
      type: object
      required: [countedQuantity]
      additionalProperties: false
      properties:
        countedQuantity: { type: integer, minimum: 0, maximum: 2000000000, description: For serialized tracking this must match current availability after individual units have been reviewed or corrected. }
        locationResourceId: { type: [string, "null"], format: uuid, description: Reconcile only this structured stock location when provided. }
        countedAt: { type: string, format: date-time }
        note: { type: string, maxLength: 20000 }
    InventoryCountRecord:
      type: object
      required: [id, resourceId, locationResourceId, expectedQuantity, countedQuantity, variance, countedAt, note, movementId, idempotencyKey, createdBy, createdAt]
      properties:
        id: { type: string, format: uuid }
        resourceId: { type: string, format: uuid }
        locationResourceId: { type: [string, "null"], format: uuid }
        expectedQuantity: { type: integer, minimum: 0 }
        countedQuantity: { type: integer, minimum: 0 }
        variance: { type: integer }
        countedAt: { type: string, format: date-time }
        note: { type: string }
        movementId: { type: [string, "null"], format: uuid }
        idempotencyKey: { type: [string, "null"], format: uuid }
        createdBy: { type: [string, "null"] }
        createdAt: { type: string, format: date-time }
    InventoryCountMutation:
      type: object
      required: [count, replayed]
      properties:
        count: { $ref: "#/components/schemas/InventoryCountRecord" }
        movementId: { type: string, format: uuid, description: Present when the count is newly recorded; replayed counts retain the movement id inside count. }
        replayed: { type: boolean }
    InventoryCycle:
      type: object
      required: [resource, policy, history]
      properties:
        resource:
          type: object
          required: [id, name, quantity, trackingMode]
          properties:
            id: { type: string, format: uuid }
            name: { type: string }
            quantity: { type: integer, minimum: 0 }
            trackingMode: { type: string, enum: [bulk, serialized] }
        policy:
          oneOf:
            - { $ref: "#/components/schemas/InventoryCyclePolicy" }
            - { type: "null" }
        history:
          type: array
          maxItems: 25
          items: { $ref: "#/components/schemas/InventoryCountRecord" }
    DueInventoryCycle:
      type: object
      required: [resourceId, name, type, quantity, intervalDays, nextDueAt, lastCompletedAt]
      properties:
        resourceId: { type: string, format: uuid }
        name: { type: string }
        type: { $ref: "#/components/schemas/InventoryTypeKey" }
        quantity: { type: integer, minimum: 0 }
        intervalDays: { type: integer, minimum: 1, maximum: 3650 }
        nextDueAt: { type: string, format: date-time }
        lastCompletedAt: { type: [string, "null"], format: date-time }
    InventoryAssignmentRecipientInput:
      oneOf:
        - type: object
          required: [type, userId]
          additionalProperties: false
          properties:
            type: { type: string, const: user }
            userId: { type: string, format: uuid }
        - type: object
          required: [type, resourceId]
          additionalProperties: false
          properties:
            type: { type: string, const: resource }
            resourceId: { type: string, format: uuid }
        - type: object
          required: [type, label]
          additionalProperties: false
          properties:
            type: { type: string, const: label }
            label: { type: string, minLength: 1, maxLength: 240 }
      discriminator:
        propertyName: type
    InventoryAssignmentInput:
      type: object
      required: [kind, recipient]
      additionalProperties: false
      properties:
        kind: { type: string, enum: [checkout, assignment, reservation] }
        quantity: { type: integer, minimum: 1, maximum: 2000000000, default: 1 }
        stockUnitId: { type: [string, "null"], format: uuid, description: Required for serialized stock and omitted for bulk stock. }
        recipient: { $ref: "#/components/schemas/InventoryAssignmentRecipientInput" }
        startsAt: { type: string, format: date-time }
        dueAt: { type: [string, "null"], format: date-time }
        note: { type: string, maxLength: 20000 }
    InventoryAssignmentCompletion:
      type: object
      required: [status]
      additionalProperties: false
      properties:
        status: { type: string, enum: [returned, cancelled] }
        completedAt: { type: string, format: date-time }
        note: { type: string, maxLength: 20000 }
    InventoryAssignment:
      type: object
      required: [id, resourceId, stockUnitId, kind, status, quantity, assignee, stockUnit, startsAt, dueAt, completedAt, note, createdBy, completedBy, createdAt, updatedAt]
      properties:
        id: { type: string, format: uuid }
        resourceId: { type: string, format: uuid }
        stockUnitId: { type: [string, "null"], format: uuid }
        kind: { type: string, enum: [checkout, assignment, reservation] }
        status: { type: string, enum: [active, returned, cancelled] }
        quantity: { type: integer, minimum: 1 }
        assignee:
          type: object
          required: [type, id, label, detail]
          properties:
            type: { type: string, enum: [user, resource, label] }
            id: { type: [string, "null"], format: uuid }
            label: { type: string }
            detail: { type: [string, "null"] }
        stockUnit:
          oneOf:
            - type: object
              required: [id, code, status]
              properties:
                id: { type: string, format: uuid }
                code: { type: string }
                status: { type: [string, "null"] }
            - { type: "null" }
        startsAt: { type: string, format: date-time }
        dueAt: { type: [string, "null"], format: date-time }
        completedAt: { type: [string, "null"], format: date-time }
        note: { type: string }
        createdBy: { type: [string, "null"] }
        completedBy: { type: [string, "null"] }
        createdAt: { type: string, format: date-time }
        updatedAt: { type: string, format: date-time }
    InventoryAssignmentMutation:
      type: object
      required: [assignment, resource, movement]
      properties:
        assignment: { $ref: "#/components/schemas/InventoryAssignment" }
        resource:
          type: object
          required: [id, name, quantity]
          properties:
            id: { type: string, format: uuid }
            name: { type: string }
            quantity: { type: integer, minimum: 0 }
        movement: { $ref: "#/components/schemas/StockMovement" }
    InventoryAssignmentList:
      type: object
      required: [resource, trackingMode, availability, availableUnits, assignments]
      properties:
        resource:
          type: object
          required: [id, name, quantity]
          properties:
            id: { type: string, format: uuid }
            name: { type: string }
            quantity: { type: integer, minimum: 0 }
        trackingMode: { type: string, enum: [bulk, serialized] }
        availability:
          type: object
          required: [availableQuantity, activeQuantity]
          properties:
            availableQuantity: { type: integer, minimum: 0 }
            activeQuantity: { type: integer, minimum: 0 }
        availableUnits:
          type: array
          maxItems: 500
          items:
            type: object
            required: [id, code, status, location]
            properties:
              id: { type: string, format: uuid }
              code: { type: string }
              status: { type: string, enum: [available] }
              location: { type: [string, "null"] }
        assignments:
          type: array
          maxItems: 200
          items: { $ref: "#/components/schemas/InventoryAssignment" }
    StockConfigInput:
      type: object
      properties:
        trackingMode: { type: string, enum: [bulk, serialized] }
        minimumStock: { type: integer, minimum: 0 }
        reorderQuantity: { type: integer, minimum: 0 }
        leadTimeDays: { type: integer, minimum: 0 }
        unitName: { type: string, minLength: 1, maxLength: 80 }
    StockConfig:
      allOf:
        - { $ref: "#/components/schemas/StockConfigInput" }
        - type: object
          required: [trackingMode, minimumStock, reorderQuantity, leadTimeDays, unitName]
    StockForecast:
      type: object
      required: [averageDailyUsage, daysUntilStockout, predictedStockoutAt, isBelowMinimum, suggestedReorderQuantity]
      properties:
        averageDailyUsage: { type: [number, "null"], minimum: 0 }
        daysUntilStockout: { type: [number, "null"], minimum: 0 }
        predictedStockoutAt: { type: [string, "null"], format: date-time }
        isBelowMinimum: { type: boolean }
        suggestedReorderQuantity: { type: integer, minimum: 0 }
    StockMovementInput:
      type: object
      required: [delta, type]
      additionalProperties: false
      properties:
        delta: { type: integer, minimum: -2000000000, maximum: 2000000000, description: Positive receipts and negative issues. Structured transfers require zero because the global balance does not change. }
        quantity: { type: integer, minimum: 0, maximum: 2000000000, description: Required and positive for a structured transfer; otherwise it must equal the absolute delta when provided. }
        type: { type: string, enum: [receipt, issue, adjustment, return, waste, transfer] }
        reason: { type: [string, "null"], maxLength: 240 }
        note: { type: string, maxLength: 20000 }
        location: { type: [string, "null"], maxLength: 240 }
        fromLocationResourceId: { type: [string, "null"], format: uuid, description: Structured source location for an issue or transfer. }
        toLocationResourceId: { type: [string, "null"], format: uuid, description: Structured destination location for a receipt or transfer. }
        occurredAt: { type: string, format: date-time }
    StockMovement:
      type: object
      required: [id, resourceId, delta, quantity, balanceAfter, fromLocationBalanceAfter, toLocationBalanceAfter, type, note, location, fromLocationResourceId, toLocationResourceId, occurredAt, createdAt, createdBy]
      properties:
        id: { type: string, format: uuid }
        resourceId: { type: string, format: uuid }
        unitId: { type: [string, "null"], format: uuid }
        assemblyBuildId: { type: [string, "null"], format: uuid }
        purchaseReceiptId: { type: [string, "null"], format: uuid }
        delta: { type: integer }
        quantity: { type: integer, minimum: 0 }
        balanceAfter: { type: integer, minimum: 0 }
        fromLocationBalanceAfter: { type: [integer, "null"], minimum: 0 }
        toLocationBalanceAfter: { type: [integer, "null"], minimum: 0 }
        type:
          type: string
          description: Includes client booking types plus system audit types such as opening_balance, inventory-count, assignment-checkout, assignment-return, unit-created, unit-status, and merge.
        reason: { type: [string, "null"] }
        note: { type: string }
        location: { type: [string, "null"] }
        fromLocationResourceId: { type: [string, "null"], format: uuid }
        toLocationResourceId: { type: [string, "null"], format: uuid }
        occurredAt: { type: string, format: date-time }
        createdAt: { type: string, format: date-time }
        createdBy: { type: [string, "null"] }
    StockUnitInput:
      type: object
      properties:
        count: { type: integer, minimum: 1, maximum: 100, default: 1 }
        code: { type: string, maxLength: 180 }
        codes: { type: array, maxItems: 100, items: { type: string, maxLength: 180 } }
        location: { type: [string, "null"], maxLength: 240 }
        locationResourceId: { type: [string, "null"], format: uuid }
        metadata: { type: object, additionalProperties: true }
        customFields: { $ref: "#/components/schemas/CustomFieldValues" }
        acquiredAt: { type: string, format: date-time }
    StockUnitPatch:
      type: object
      properties:
        status: { type: string, enum: [available, reserved, in-use, maintenance, consumed, lost, retired] }
        location: { type: [string, "null"], maxLength: 240 }
        locationResourceId: { type: [string, "null"], format: uuid }
        metadata: { type: object, additionalProperties: true }
        customFields: { $ref: "#/components/schemas/CustomFieldValues" }
        occurredAt: { type: string, format: date-time }
        reason: { type: [string, "null"], maxLength: 240 }
        note: { type: string, maxLength: 20000 }
    StockUnit:
      allOf:
        - { $ref: "#/components/schemas/StockUnitPatch" }
        - type: object
          required: [id, resourceId, code, status, location, locationResourceId, metadata, customFields, acquiredAt, lastMovedAt, createdAt, updatedAt]
          properties:
            id: { type: string, format: uuid }
            resourceId: { type: string, format: uuid }
            code: { type: string }
            acquiredAt: { type: string, format: date-time }
            lastMovedAt: { type: string, format: date-time }
            createdAt: { type: string, format: date-time }
            updatedAt: { type: string, format: date-time }
    StockOverviewItem:
      type: object
      required: [resourceId, name, type, quantity, onOrder, projectedQuantity, minimumStock, trackingMode, reorderSuggested, unitName]
      properties:
        resourceId: { type: string, format: uuid }
        name: { type: string }
        type: { type: string }
        quantity: { type: integer, minimum: 0 }
        onOrder: { type: integer, minimum: 0 }
        projectedQuantity: { type: integer, minimum: 0 }
        nextExpectedAt: { type: [string, "null"], format: date-time }
        minimumStock: { type: integer, minimum: 0 }
        trackingMode: { type: string, enum: [bulk, serialized] }
        averageDailyUsage: { type: [number, "null"], minimum: 0 }
        daysUntilStockout: { type: [number, "null"], minimum: 0 }
        predictedStockoutAt: { type: [string, "null"], format: date-time }
        reorderSuggested: { type: boolean }
        unitName: { type: string }
    StockDetail:
      type: object
      required: [resource, config, forecast, procurement, movements, units]
      properties:
        resource:
          type: object
          required: [id, name, type, categories, quantity]
          properties:
            id: { type: string, format: uuid }
            name: { type: string }
            type: { $ref: "#/components/schemas/InventoryTypeKey" }
            categories:
              type: array
              items:
                type: object
                required: [name]
                properties:
                  name: { type: string }
                  color: { type: string }
            quantity: { type: integer, minimum: 0 }
        config: { $ref: "#/components/schemas/StockConfig" }
        forecast: { $ref: "#/components/schemas/StockForecast" }
        procurement:
          type: object
          required: [onOrder, projectedQuantity, nextExpectedAt, openLines]
          properties:
            onOrder: { type: integer, minimum: 0 }
            projectedQuantity: { type: integer, minimum: 0 }
            nextExpectedAt: { type: [string, "null"], format: date-time }
            openLines:
              type: array
              items:
                type: object
                required: [lineId, orderId, supplier, orderedQuantity, receivedQuantity, openQuantity]
                properties:
                  lineId: { type: string, format: uuid }
                  orderId: { type: string, format: uuid }
                  reference: { type: [string, "null"] }
                  supplier: { type: string }
                  orderedQuantity: { type: integer, minimum: 1 }
                  receivedQuantity: { type: integer, minimum: 0 }
                  openQuantity: { type: integer, minimum: 1 }
                  expectedAt: { type: [string, "null"], format: date-time }
        movements: { type: array, maxItems: 100, items: { $ref: "#/components/schemas/StockMovement" } }
        units: { type: array, items: { $ref: "#/components/schemas/StockUnit" } }
    ResourceInput:
      type: object
      required: [name]
      properties:
        name: { type: string, minLength: 1, maxLength: 240 }
        description: { type: string, maxLength: 20000 }
        type: { $ref: "#/components/schemas/InventoryTypeKey" }
        status: { type: string, enum: [available, in-use, maintenance, archived] }
        sku: { type: [string, "null"], maxLength: 80 }
        barcode: { type: [string, "null"], maxLength: 180 }
        quantity: { type: integer, minimum: 0, description: Opening quantity; later changes use stock movements. }
        location: { type: [string, "null"] }
        serialNumber: { type: [string, "null"] }
        valueCents: { type: [integer, "null"], minimum: 0 }
        currency: { type: string, minLength: 3, maxLength: 3 }
        priority: { type: integer, minimum: 1, maximum: 5 }
        tags: { type: array, items: { type: string } }
        categories:
          type: array
          items:
            type: object
            required: [name]
            properties:
              name: { type: string }
              color: { type: string }
        relatedResourceIds:
          type: array
          maxItems: 100
          items: { type: string, format: uuid }
          description: Legacy undirected related-item ids. Prefer typed resource relationships for new integrations.
        gpsLatitude: { type: [number, "null"], minimum: -90, maximum: 90 }
        gpsLongitude: { type: [number, "null"], minimum: -180, maximum: 180 }
        gpsAltitude: { type: [number, "null"] }
        mapFeatures:
          type: array
          maxItems: 100
          items: { $ref: "#/components/schemas/ResourceMapFeature" }
        notes: { type: string }
        customFields: { $ref: "#/components/schemas/CustomFieldValues" }
    ResourcePatch:
      type: object
      description: Any subset of the writable resource fields.
      properties:
        name: { type: string, minLength: 1, maxLength: 240 }
        description: { type: string, maxLength: 20000 }
        type: { $ref: "#/components/schemas/InventoryTypeKey" }
        status: { type: string, enum: [available, in-use, maintenance, archived] }
        sku: { type: [string, "null"], maxLength: 80 }
        barcode: { type: [string, "null"], maxLength: 180 }
        location: { type: [string, "null"] }
        serialNumber: { type: [string, "null"] }
        valueCents: { type: [integer, "null"], minimum: 0 }
        currency: { type: string, minLength: 3, maxLength: 3 }
        priority: { type: integer, minimum: 1, maximum: 5 }
        tags: { type: array, items: { type: string } }
        categories:
          type: array
          items:
            type: object
            required: [name]
            properties:
              name: { type: string }
              color: { type: string }
        relatedResourceIds:
          type: array
          maxItems: 100
          items: { type: string, format: uuid }
          description: Legacy undirected related-item ids. Prefer typed resource relationships for new integrations.
        gpsLatitude: { type: [number, "null"], minimum: -90, maximum: 90 }
        gpsLongitude: { type: [number, "null"], minimum: -180, maximum: 180 }
        gpsAltitude: { type: [number, "null"] }
        mapFeatures:
          type: array
          maxItems: 100
          items: { $ref: "#/components/schemas/ResourceMapFeature" }
        notes: { type: string }
        customFields: { $ref: "#/components/schemas/CustomFieldValues" }
    ResourceVariantInput:
      type: object
      additionalProperties: false
      required: [name]
      properties:
        name: { type: string, minLength: 1, maxLength: 240 }
        sku: { type: [string, "null"], maxLength: 80 }
        barcode: { type: [string, "null"], maxLength: 180 }
        priceCents: { type: [integer, "null"], minimum: 0, maximum: 2000000000 }
        currency: { type: string, minLength: 3, maxLength: 3 }
        position: { type: integer, minimum: 0, maximum: 100000 }
        initialAllocation:
          type: integer
          minimum: 0
          maximum: 2000000000
          default: 0
    ResourceVariantPatch:
      type: object
      additionalProperties: false
      properties:
        name: { type: string, minLength: 1, maxLength: 240 }
        sku: { type: [string, "null"], maxLength: 80 }
        barcode: { type: [string, "null"], maxLength: 180 }
        priceCents: { type: [integer, "null"], minimum: 0, maximum: 2000000000 }
        currency: { type: string, minLength: 3, maxLength: 3 }
        position: { type: integer, minimum: 0, maximum: 100000 }
    ResourceVariant:
      type: object
      required: [id, resourceId, name, currency, quantity, position, createdAt, updatedAt]
      properties:
        id: { type: string, format: uuid }
        resourceId: { type: string, format: uuid }
        name: { type: string }
        sku: { type: [string, "null"] }
        barcode: { type: [string, "null"] }
        priceCents: { type: [integer, "null"], minimum: 0 }
        currency: { type: string }
        quantity: { type: integer, minimum: 0 }
        position: { type: integer, minimum: 0 }
        createdBy: { type: [string, "null"] }
        updatedBy: { type: [string, "null"] }
        createdAt: { type: string, format: date-time }
        updatedAt: { type: string, format: date-time }
    ResourceVariantStockSummary:
      type: object
      required: [totalQuantity, allocatedQuantity, unallocatedQuantity, variantCount]
      properties:
        totalQuantity: { type: integer, minimum: 0 }
        allocatedQuantity: { type: integer, minimum: 0 }
        unallocatedQuantity: { type: integer, minimum: 0 }
        variantCount: { type: integer, minimum: 0 }
    ResourceVariantStockMovementInput:
      type: object
      additionalProperties: false
      required: [delta]
      properties:
        delta: { type: integer, minimum: -2000000000, maximum: 2000000000, not: { const: 0 } }
        type: { type: string, enum: [receipt, issue, adjustment, return, waste], default: adjustment }
        reason: { type: [string, "null"], maxLength: 240 }
        note: { type: string, maxLength: 20000 }
        occurredAt: { type: string, format: date-time }
    ResourceMapFeature:
      type: object
      required: [id, type, layer, description, coordinates]
      properties:
        id: { type: string, minLength: 1, maxLength: 80 }
        type: { type: string, enum: [point, polygon] }
        layer: { type: string, minLength: 1, maxLength: 80 }
        description: { type: string, maxLength: 5000 }
        coordinates:
          description: A [longitude, latitude] pair for points or a closed ring of pairs for polygons.
          oneOf:
            - type: array
              minItems: 2
              maxItems: 2
              items: { type: number }
            - type: array
              minItems: 4
              maxItems: 500
              items:
                type: array
                minItems: 2
                maxItems: 2
                items: { type: number }
    Resource:
      allOf:
        - { $ref: "#/components/schemas/ResourceInput" }
        - type: object
          required: [id, customFields, createdAt, updatedAt, media]
          properties:
            id: { type: string, format: uuid }
            createdAt: { type: string, format: date-time }
            updatedAt: { type: string, format: date-time }
            media: { type: array, items: { $ref: "#/components/schemas/Media" } }
            cover:
              oneOf:
                - { $ref: "#/components/schemas/Media" }
                - { type: "null" }
