openapi: 3.1.0
info:
  title: Inventory API
  version: 1.22.0
  description: >-
    Organization-scoped API for configurable inventory structures, localized
    content, indoor 3D room scans, location-aware stock, cycle counts,
    assignments, internal requests, reservation planning, CSV exchange, typed custom fields, label setups, shared label printing, stock planning, 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: Contacts
    description: Organization-scoped customers and suppliers with inventory and stock-ledger assignments.
  - 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: Label printing
    description: Personal saved queues, shared Office printers, Windows agents and local QZ signing. Browser-session operations cannot use ordinary API tokens.
  - 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: Internal requests
    description: Multi-item inventory requests with approval, reservation, fulfillment, and calendar planning.
  - name: Manufacturing
    description: Bills of materials and atomic assembly builds.
  - name: Purchasing
    description: Purchase orders, incoming quantities, and stock receipts.
  - name: Orders
    description: Shared purchase, sales, and multi-item loan workflows with line-linked stock movements, outbound parcels, and shipment history.
  - 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, web-grounded enrichment, 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: Integrations
    description: Session-administered connections to external inventory and commerce systems.
  - 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 }
  /integrations/woocommerce:
    parameters:
      - { $ref: "#/components/parameters/OrganizationId" }
    get:
      tags: [Integrations]
      summary: Read the WooCommerce connection status
      description: Requires an authenticated browser session with `webhooks.manage`. Credentials are never returned.
      security: [{ adminSession: [] }]
      responses:
        "200":
          description: Current connection status and encryption readiness
          content:
            application/json:
              schema: { $ref: "#/components/schemas/WooCommerceConnectionStatusResponse" }
        "401": { description: An authenticated browser session is required }
        "403": { description: Browser session lacks webhooks.manage }
    put:
      tags: [Integrations]
      summary: Connect or replace a WooCommerce store
      description: Tests the credentials against WooCommerce REST API v3 before encrypting and storing them. Requires `webhooks.manage`.
      security: [{ adminSession: [] }]
      requestBody:
        required: true
        content:
          application/json:
            schema: { $ref: "#/components/schemas/WooCommerceConnectionInput" }
      responses:
        "200":
          description: Connection tested and stored
          content:
            application/json:
              schema: { $ref: "#/components/schemas/WooCommerceConnectionMutationResponse" }
        "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 store URL or credential format }
        "502": { description: WooCommerce rejected the request or returned an invalid response }
        "503": { description: Integration credential encryption is not configured }
    patch:
      tags: [Integrations]
      summary: Enable or disable WooCommerce order synchronization
      description: Creates or removes signed order.created and order.updated webhooks in WooCommerce. Enabling requires a public HTTPS AUTH_URL and a Consumer Key with write access.
      security: [{ adminSession: [] }]
      requestBody:
        required: true
        content:
          application/json:
            schema: { $ref: "#/components/schemas/WooCommerceSyncPatch" }
      responses:
        "200":
          description: Order synchronization state updated
          content:
            application/json:
              schema: { $ref: "#/components/schemas/WooCommerceConnectionStatusResponse" }
        "401": { description: An authenticated browser session is required }
        "403": { description: Browser session lacks webhooks.manage }
        "404": { description: No WooCommerce connection is configured }
        "422": { description: Invalid stock sync setting }
        "502": { description: WooCommerce webhook registration failed }
        "503": { description: Public callback URL or integration encryption is not configured }
    delete:
      tags: [Integrations]
      summary: Disconnect the WooCommerce store
      description: Permanently removes the encrypted credentials for the selected organization. Requires `webhooks.manage`.
      security: [{ adminSession: [] }]
      responses:
        "204": { description: "Connection removed, or no connection existed" }
        "401": { description: An authenticated browser session is required }
        "403": { description: Browser session lacks webhooks.manage }
  /integrations/woocommerce/test:
    parameters:
      - { $ref: "#/components/parameters/OrganizationId" }
    post:
      tags: [Integrations]
      summary: Test the saved WooCommerce connection
      description: Decrypts the saved credentials only in memory and verifies product read access against WooCommerce REST API v3. Requires `webhooks.manage`.
      security: [{ adminSession: [] }]
      responses:
        "200":
          description: Connection test succeeded and status was updated
          content:
            application/json:
              schema: { $ref: "#/components/schemas/WooCommerceConnectionMutationResponse" }
        "401": { description: An authenticated browser session is required }
        "403": { description: Browser session lacks webhooks.manage }
        "404": { description: No WooCommerce connection is configured }
        "502": { description: WooCommerce rejected the request or returned an invalid response }
        "503": { description: Integration credential encryption is not configured }
  /integrations/woocommerce/sync:
    parameters:
      - { $ref: "#/components/parameters/OrganizationId" }
    post:
      tags: [Integrations]
      summary: Import recent WooCommerce orders, reconcile one order, or retry issues
      description: Fetches canonical order and refund data from WooCommerce, projects the customer and sales order into Inventory, maps exact item or variant SKUs, and writes idempotent issue or return stock movements. Set window to last-7-days for a read-only WooCommerce import based on order creation time. Omitting both fields retries up to 20 recorded orders with issues.
      security: [{ adminSession: [] }]
      requestBody:
        required: false
        content:
          application/json:
            schema: { $ref: "#/components/schemas/WooCommerceManualSyncRequest" }
      responses:
        "200": { description: Reconciliation results }
        "401": { description: An authenticated browser session is required }
        "403": { description: Browser session lacks webhooks.manage }
        "404": { description: No WooCommerce connection is configured }
        "422": { description: Invalid order ID }
        "502": { description: WooCommerce order or refund retrieval failed }
  /integrations/woocommerce/webhook/{connectionId}:
    parameters:
      - name: connectionId
        in: path
        required: true
        schema: { type: string, format: uuid }
    post:
      tags: [Integrations]
      summary: Receive a signed WooCommerce order webhook
      description: Public callback registered automatically in WooCommerce. The exact raw body is authenticated with X-WC-Webhook-Signature and the registered webhook ID before canonical order data is fetched.
      security: []
      parameters:
        - name: X-WC-Webhook-Signature
          in: header
          required: true
          schema: { type: string }
        - name: X-WC-Webhook-Topic
          in: header
          required: true
          schema: { type: string, enum: [order.created, order.updated] }
        - name: X-WC-Webhook-ID
          in: header
          required: true
          schema: { type: integer }
        - name: X-WC-Webhook-Delivery-ID
          in: header
          required: false
          schema: { type: string, maxLength: 160 }
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [id]
              properties:
                id: { type: integer, minimum: 1 }
              additionalProperties: true
      responses:
        "200": { description: "Delivery authenticated and reconciled, or safely recognized as a duplicate" }
        "400": { description: Invalid JSON or unsupported topic }
        "401": { description: Invalid signature or unknown WooCommerce webhook ID }
        "410": { description: WooCommerce order synchronization is disabled }
        "413": { description: Payload exceeds one megabyte }
        "422": { description: Payload has no valid order ID }
        "502": { description: Canonical WooCommerce order retrieval failed and the delivery may be retried }
  /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 }
                slug:
                  type: string
                  minLength: 1
                  maxLength: 48
                  pattern: '^[a-z0-9]+(?:-[a-z0-9]+)*$'
                  description: Optional short Webapp URL slug; generated from the name when omitted.
      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, and regular-user organization creation must be enabled unless the user is a superadmin" }
        "409": { description: Requested organization slug is already in use }
        "422": { description: Invalid organization name or slug }
  /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: Update an organization's settings
      security: [{ adminSession: [] }]
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              additionalProperties: false
              minProperties: 1
              properties:
                name: { type: string, minLength: 1, maxLength: 160 }
                slug:
                  type: string
                  minLength: 1
                  maxLength: 48
                  pattern: '^[a-z0-9]+(?:-[a-z0-9]+)*$'
                allowNegativeStock:
                  type: boolean
                  description: Allow bulk stock balances to fall below zero.
      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 }
        "409": { description: Requested organization slug is already in use }
        "404": { description: Organization not found or not available to this user }
        "422": { description: Invalid organization name or slug }
  /admin/organizations:
    get:
      tags: [Organizations]
      summary: List every organization in the deployment
      description: Requires a browser session whose email is configured in `SUPERADMIN_EMAILS`.
      security: [{ superadminSession: [] }]
      responses:
        "200":
          description: Every organization in the deployment
          content:
            application/json:
              schema:
                type: object
                required: [organizations]
                properties:
                  organizations:
                    type: array
                    items: { $ref: "#/components/schemas/OrganizationSummary" }
        "401": { description: An authenticated browser session is required }
        "403": { description: The browser user is not a configured superadmin }
    post:
      tags: [Organizations]
      summary: Create an organization as a deployment superadmin
      description: Creates an administrator membership for the superadmin who creates the organization.
      security: [{ superadminSession: [] }]
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              additionalProperties: false
              required: [name]
              properties:
                name: { type: string, minLength: 1, maxLength: 160 }
                slug:
                  type: string
                  minLength: 1
                  maxLength: 48
                  pattern: '^[a-z0-9]+(?:-[a-z0-9]+)*$'
      responses:
        "201":
          description: Organization created
          content:
            application/json:
              schema:
                type: object
                required: [organization]
                properties:
                  organization: { $ref: "#/components/schemas/OrganizationSummary" }
        "401": { description: An authenticated browser session is required }
        "403": { description: The browser user is not a configured superadmin }
        "409": { description: Requested organization slug is already in use }
        "422": { description: Invalid organization name or slug }
  /admin/organizations/{id}:
    parameters:
      - name: id
        in: path
        required: true
        schema: { type: string, format: uuid }
    patch:
      tags: [Organizations]
      summary: Update any organization as a deployment superadmin
      security: [{ superadminSession: [] }]
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              additionalProperties: false
              minProperties: 1
              properties:
                name: { type: string, minLength: 1, maxLength: 160 }
                slug:
                  type: string
                  minLength: 1
                  maxLength: 48
                  pattern: '^[a-z0-9]+(?:-[a-z0-9]+)*$'
                allowNegativeStock: { type: boolean }
      responses:
        "200":
          description: Organization updated
          content:
            application/json:
              schema:
                type: object
                required: [organization]
                properties:
                  organization: { $ref: "#/components/schemas/OrganizationSummary" }
        "401": { description: An authenticated browser session is required }
        "403": { description: The browser user is not a configured superadmin }
        "404": { description: Organization not found }
        "409": { description: Requested organization slug is already in use }
        "422": { description: Invalid organization name or slug }
  /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 labels.read (read scope for standalone tokens). 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 labels.manage (write scope for standalone tokens). 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 labels.read (read scope for standalone tokens).
      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 labels.manage (write scope for standalone tokens). 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 labels.manage (write scope for standalone tokens) 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 }
  /contacts:
    parameters:
      - { $ref: "#/components/parameters/OrganizationId" }
    get:
      tags: [Contacts]
      summary: List customers and suppliers
      parameters:
        - name: q
          in: query
          schema: { type: string, maxLength: 240 }
        - name: role
          in: query
          schema: { type: string, enum: [customer, supplier] }
        - name: includeArchived
          in: query
          schema: { type: boolean, default: false }
      responses:
        "200":
          description: Contacts with linked inventory and movement counts
          content:
            application/json:
              schema:
                type: object
                required: [contacts]
                properties:
                  contacts: { type: array, items: { $ref: "#/components/schemas/Contact" } }
    post:
      tags: [Contacts]
      summary: Create a customer, supplier, or dual-role contact
      requestBody:
        required: true
        content:
          application/json:
            schema: { $ref: "#/components/schemas/ContactInput" }
      responses:
        "201":
          description: Contact created
          content:
            application/json:
              schema:
                type: object
                required: [contact]
                properties:
                  contact: { $ref: "#/components/schemas/Contact" }
        "422": { description: Invalid contact or cross-organization inventory assignment }
  /contacts/{id}:
    parameters:
      - { $ref: "#/components/parameters/OrganizationId" }
      - name: id
        in: path
        required: true
        schema: { type: string, format: uuid }
    get:
      tags: [Contacts]
      summary: Get one contact
      responses:
        "200":
          description: Contact details
          content:
            application/json:
              schema:
                type: object
                required: [contact]
                properties:
                  contact: { $ref: "#/components/schemas/Contact" }
        "404": { $ref: "#/components/responses/NotFound" }
    patch:
      tags: [Contacts]
      summary: Update a contact and its inventory assignments
      requestBody:
        required: true
        content:
          application/json:
            schema: { $ref: "#/components/schemas/ContactPatch" }
      responses:
        "200":
          description: Contact updated
          content:
            application/json:
              schema:
                type: object
                required: [contact]
                properties:
                  contact: { $ref: "#/components/schemas/Contact" }
        "404": { $ref: "#/components/responses/NotFound" }
        "422": { description: Invalid contact or cross-organization inventory assignment }
    delete:
      tags: [Contacts]
      summary: Archive a contact
      description: Historical stock-movement references are retained.
      responses:
        "204": { description: Contact archived }
        "404": { $ref: "#/components/responses/NotFound" }
  /contacts/{id}/comments:
    parameters:
      - { $ref: "#/components/parameters/OrganizationId" }
      - name: id
        in: path
        required: true
        schema: { type: string, format: uuid }
    get:
      tags: [Contacts]
      summary: List a contact's comments
      responses:
        "200":
          description: Chronological contact comment thread
          content:
            application/json:
              schema:
                type: object
                required: [comments]
                properties:
                  comments: { type: array, items: { $ref: "#/components/schemas/ContactComment" } }
        "404": { $ref: "#/components/responses/NotFound" }
    post:
      tags: [Contacts]
      summary: Add a comment to a contact
      requestBody:
        required: true
        content:
          application/json:
            schema: { $ref: "#/components/schemas/CommentInput" }
      responses:
        "201":
          description: Comment created
          content:
            application/json:
              schema:
                type: object
                required: [comment]
                properties:
                  comment: { $ref: "#/components/schemas/ContactComment" }
        "404": { $ref: "#/components/responses/NotFound" }
        "422": { description: Invalid comment body }
  /contacts/{id}/comments/{commentId}:
    parameters:
      - { $ref: "#/components/parameters/OrganizationId" }
      - name: id
        in: path
        required: true
        schema: { type: string, format: uuid }
      - name: commentId
        in: path
        required: true
        schema: { type: string, format: uuid }
    patch:
      tags: [Contacts]
      summary: Edit an owned contact comment
      requestBody:
        required: true
        content:
          application/json:
            schema: { $ref: "#/components/schemas/CommentInput" }
      responses:
        "200": { description: Comment updated }
        "403": { description: Comment belongs to another author }
        "404": { $ref: "#/components/responses/NotFound" }
    delete:
      tags: [Contacts]
      summary: Delete an owned contact comment
      responses:
        "204": { description: Comment deleted }
        "403": { description: Comment belongs to another author }
        "404": { $ref: "#/components/responses/NotFound" }
  /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: 500 } }
        - { 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/ResourceReference" }
    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" }
        "409": { description: Primary item still has linked first-class variants }
  /resources/{id}/family:
    parameters:
      - { $ref: "#/components/parameters/OrganizationId" }
      - { $ref: "#/components/parameters/ResourceId" }
    get:
      tags: [Resources]
      summary: Retrieve a first-class inventory variant family
      description: Resolves the primary item and its ordinary resource variants. Every returned member has independent stock, locations, serialization, relationships, and manufacturing history. The request fails rather than exposing a partially authorized family.
      responses:
        "200":
          description: Variant family
          content:
            application/json:
              schema: { $ref: "#/components/schemas/ResourceFamily" }
        "403": { description: One or more family members are outside the caller's inventory access rules }
        "404": { $ref: "#/components/responses/NotFound" }
    post:
      tags: [Resources]
      summary: Create a first-class variant or connect an existing inventory item
      description: With identity fields, creates a zero-stock resource linked to this primary by a protected `variant_of` relationship. With `existingResourceId`, connects an existing standalone item without moving its stock, locations, serial units, relationships, assignments, or history. Existing catalog and BOM differences are retained as sparse overrides. Variant families cannot be nested.
      requestBody:
        required: true
        content:
          application/json:
            schema:
              oneOf:
                - { $ref: "#/components/schemas/ResourceFamilyVariantInput" }
                - { $ref: "#/components/schemas/ResourceFamilyExistingVariantInput" }
      responses:
        "200":
          description: Existing inventory item connected as a first-class variant
          content:
            application/json:
              schema:
                type: object
                required: [variant]
                properties:
                  variant: { $ref: "#/components/schemas/ResourceFamilyMember" }
        "201":
          description: First-class variant created
          content:
            application/json:
              schema:
                type: object
                required: [variant]
                properties:
                  variant: { $ref: "#/components/schemas/ResourceFamilyMember" }
        "403": { description: "Missing primary-item update access, candidate-item update access, or inventory create permission when creating" }
        "404": { $ref: "#/components/responses/NotFound" }
        "409": { description: "Identifier conflict, archived item, option-managed family, existing membership, or nested family attempt" }
        "422": { description: Invalid variant identity }
    delete:
      tags: [Resources]
      summary: Detach a first-class variant from its primary item
      description: Removes only the protected `variant_of` family link. The currently effective inherited bill of materials is materialized onto the same inventory resource first. Stock, locations, serial units, ordinary relationships, assignments, and manufacturing history stay attached to that resource.
      responses:
        "200":
          description: Variant detached and its effective bill of materials materialized
          content:
            application/json:
              schema:
                type: object
                required: [detached]
                properties:
                  detached: { $ref: "#/components/schemas/ResourceVariantDetachResult" }
        "403": { description: Missing update access to the variant item }
        "404": { $ref: "#/components/responses/NotFound" }
        "409": { description: Item is not a linked variant or its effective bill of materials cannot be preserved }
        "422": { description: Invalid resource id }
  /resources/{id}/options:
    parameters:
      - { $ref: "#/components/parameters/OrganizationId" }
      - { $ref: "#/components/parameters/ResourceId" }
    get:
      tags: [Resources, Manufacturing]
      summary: Retrieve option groups and the current resource's selections
      description: The primary item represents the all-default combination. Generated combinations are ordinary first-class resource variants with independent operational data and sparse BOM overrides.
      responses:
        "200":
          description: Option matrix, BOM slots, and current selection
          content:
            application/json:
              schema: { $ref: "#/components/schemas/ResourceOptions" }
        "403": { description: The primary item is outside the caller's inventory access rules }
        "404": { $ref: "#/components/responses/NotFound" }
    put:
      tags: [Resources, Manufacturing]
      summary: Replace option-group definitions before variants are generated
      description: A group may map to one stable primary BOM slot. Its default component must equal the primary BOM component in that slot. Definitions lock after generation.
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              additionalProperties: false
              required: [groups]
              properties:
                groups:
                  type: array
                  maxItems: 4
                  items: { $ref: "#/components/schemas/ResourceOptionGroupInput" }
      responses:
        "200":
          description: Saved option definitions
          content:
            application/json:
              schema: { $ref: "#/components/schemas/ResourceOptions" }
        "409": { description: "Definitions are locked, the item is a child variant, or the family already has manual variants" }
        "422": { description: "Invalid group, value, component, BOM mapping, or matrix size" }
    post:
      tags: [Resources, Manufacturing]
      summary: Generate every non-default option combination
      description: Atomically creates the Cartesian product as protected first-class variants, copies catalog and stock settings, stores selections, and writes only BOM differences. Maximum 100 total combinations.
      responses:
        "201":
          description: Option variants generated
          content:
            application/json:
              schema: { $ref: "#/components/schemas/ResourceOptionGenerationResult" }
        "403": { description: Missing inventory create or primary-item update access }
        "409": { description: "Variants already exist, generation already ran, or the primary is archived" }
        "422": { description: Invalid option matrix or circular effective BOM }

  /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. Variants are ordinary resources and return their own resource ID.
      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" }
                  matchedBy: { type: string, enum: [id, sku, barcode, serialNumber] }
        "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/{workflowId}/runner:
    parameters:
      - &a1
        $ref: "#/components/parameters/OrganizationId"
      - name: workflowId
        in: path
        required: true
        schema:
          type: string
          format: uuid
    get:
      tags: &a3
        - Scan workflows
      summary: Load targets, variants and conditional inputs for an action chain
      responses: &a4
        "200":
          description: "Runner configuration in workflow: id, name, description,
            extraction, codeTypes, inputFields, targetSelectionMode, fixedCode,
            action summaries and targetGroups"
        "404":
          description: Flow unavailable or has no action chain
    post:
      tags: [Scan workflows]
      summary: Prepare a native scan and return the server-extracted identifier
      description: Requires workflows.read. Returns the runner configuration plus identifier. Validates the code type and extraction rules without changing stock or requiring form inputs.
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              additionalProperties: false
              required: [code]
              properties:
                code: { type: string, minLength: 1, maxLength: 2048 }
                codeType:
                  type: [string, "null"]
                  enum: [qr_code, data_matrix, aztec, pdf417, code_128, code_93, code_39, codabar, ean_13, ean_8, upc_a, upc_e, itf, null]
      responses:
        "200": { description: Runner configuration including the extracted identifier }
        "404": { description: Flow unavailable }
        "422": { description: Invalid code type or code does not match extraction rules }
  /stock/action-chains/preview:
    parameters: &a2
      - *a1
    post:
      tags:
        - Scan workflows
      summary: Preview all actions without committing changes
      description: Requires workflows.read. Runs the complete chain and rolls it back,
        returning planHash. Conditions and component selections are evaluated in
        order.
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: "#/components/schemas/ActionChainRunInput"
      responses:
        "200":
          description: Reviewed plan or previously completed execution
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ActionChainRunReport"
        "404":
          description: Flow or target unavailable
        "409":
          description: Stale preview, stock conflict or idempotency mismatch
        "422":
          description: Invalid input or failed action, with actionId in details
  /stock/action-chains/execute:
    parameters: *a2
    post:
      tags:
        - Scan workflows
      summary: Confirm all actions in one transaction
      description: Requires workflows.manage, an Idempotency-Key UUID and the exact
        reviewed inputs with expectedPlanHash. Stale plans return 409. Stock,
        units, manufacturing records, audit and webhook outbox commit together.
        oncePerCode also deduplicates workflow + identifier + selected target set.
      parameters:
        - $ref: "#/components/parameters/IdempotencyKey"
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: "#/components/schemas/ActionChainRunInput"
      responses:
        "200":
          description: Idempotent replay
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ActionChainRunReport"
        "201":
          description: All actions committed
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ActionChainRunReport"
        "404":
          description: Flow or target unavailable
        "409":
          description: Stale preview, stock conflict or idempotency mismatch
        "422":
          description: Invalid input or failed action, with actionId in details
  /public/action-flows/{triggerId}/chain:
    servers:
      - url: /api
    parameters:
      - name: triggerId
        in: path
        required: true
        schema:
          type: string
          format: uuid
    get:
      tags: *a3
      summary: Load targets, variants and conditional inputs for an action chain
      responses: *a4
      security: []
  /public/action-flows/{triggerId}/chain/{mode}:
    servers:
      - url: /api
    parameters:
      - name: triggerId
        in: path
        required: true
        schema:
          type: string
          format: uuid
      - name: mode
        in: path
        required: true
        schema:
          type: string
          enum:
            - preview
            - execute
    post:
      tags:
        - Scan workflows
      summary: Preview or confirm an enabled public action chain
      description: Same-origin, rate-limited public endpoint. The trigger must remain
        enabled. The configured fixed code overrides submitted code. Preview does
        not need an Idempotency-Key; execute requires a UUID key and
        expectedPlanHash. All actions roll back on failure.
      parameters:
        - $ref: "#/components/parameters/IdempotencyKey"
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: "#/components/schemas/ActionChainRunInput"
      responses:
        "200":
          description: Idempotent replay
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ActionChainRunReport"
        "201":
          description: All actions committed
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ActionChainRunReport"
        "404":
          description: Flow or target unavailable
        "409":
          description: Stale preview, stock conflict or idempotency mismatch
        "422":
          description: Invalid input or failed action, with actionId in details
      security: []
  /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/scan-workflows/{workflowId}/public-trigger/rotate:
    parameters:
      - { $ref: "#/components/parameters/OrganizationId" }
      - name: workflowId
        in: path
        required: true
        schema: { type: string, format: uuid }
    post:
      tags: [Scan workflows]
      summary: Replace a workflow's unlisted public action URL
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [revision]
              additionalProperties: false
              properties:
                revision: { type: integer, minimum: 1 }
      responses:
        "200":
          description: Public trigger replaced; the previous URL is immediately invalid
          content:
            application/json:
              schema:
                type: object
                required: [workflow]
                properties:
                  workflow: { $ref: "#/components/schemas/ScanWorkflow" }
        "404": { description: Workflow not found }
        "409": { description: Workflow revision changed }
        "422": { description: Invalid workflow or revision }
  /stock/scan-workflows/extraction-suggestions:
    parameters:
      - { $ref: "#/components/parameters/OrganizationId" }
    post:
      tags: [Scan workflows]
      summary: Generate a validated regex extractor from a sample code with AI
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [sampleCode]
              additionalProperties: false
              properties:
                sampleCode: { type: string, minLength: 1, maxLength: 2048 }
                codeType:
                  oneOf:
                    - { $ref: "#/components/schemas/ScanCodeType" }
                    - { type: "null" }
                selectedResourceIds:
                  type: array
                  maxItems: 24
                  uniqueItems: true
                  items: { type: string, format: uuid }
                desiredValue: { type: string, maxLength: 180 }
                instruction: { type: string, maxLength: 500 }
      responses:
        "200":
          description: Validated extraction suggestion
          content:
            application/json:
              schema: { $ref: "#/components/schemas/ScanExtractionSuggestion" }
        "402": { description: Organization AI budget exceeded }
        "422": { description: Invalid sample or extraction goal }
        "429": { description: AI request limit reached }
  /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 }
                codeType:
                  oneOf:
                    - { $ref: "#/components/schemas/ScanCodeType" }
                    - { type: "null" }
      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: Apply a previously reviewed stock scan to its selected targets
      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 }
  /public/action-flows/{triggerId}:
    servers:
      - url: /api
    parameters:
      - name: triggerId
        in: path
        required: true
        description: Random bearer identifier embedded in the unlisted public URL.
        schema: { type: string, format: uuid }
    get:
      tags: [Scan workflows]
      summary: Load the form definition for an enabled public action URL
      security: []
      responses:
        "200":
          description: Public action form
          content:
            application/json:
              schema:
                type: object
                required: [action]
                properties:
                  action: { $ref: "#/components/schemas/PublicActionFlow" }
        "404": { description: "Public action URL is disabled, revoked, or unknown" }
  /public/action-flows/{triggerId}/execute:
    servers:
      - url: /api
    parameters:
      - name: triggerId
        in: path
        required: true
        schema: { type: string, format: uuid }
    post:
      tags: [Scan workflows]
      summary: Confirm and execute an unlisted public action
      security: []
      parameters:
        - { $ref: "#/components/parameters/IdempotencyKey" }
      requestBody:
        required: true
        content:
          application/json:
            schema: { $ref: "#/components/schemas/PublicActionFlowExecutionInput" }
      responses:
        "200": { description: Replayed an earlier execution with the same Idempotency-Key }
        "201":
          description: Public action applied
          content:
            application/json:
              schema: { $ref: "#/components/schemas/StockScanExecution" }
        "400": { description: Missing or invalid Idempotency-Key }
        "403": { description: Cross-origin request rejected }
        "404": { description: "Public action URL is disabled, revoked, or unknown" }
        "409": { description: "Workflow revision, stock, or idempotency conflict" }
        "422": { description: "Invalid code, quantity, or form input" }
        "429": { description: Public action rate limit reached }
  /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 }
  /internal-requests:
    parameters:
      - { $ref: "#/components/parameters/OrganizationId" }
    get:
      tags: [Internal requests]
      summary: List visible internal requests
      description: Users with request-management access see the workspace queue; other users see only requests they submitted.
      parameters:
        - name: status
          in: query
          schema: { $ref: "#/components/schemas/InternalRequestStatus" }
        - name: mine
          in: query
          schema: { type: boolean, default: false }
        - name: limit
          in: query
          schema: { type: integer, minimum: 1, maximum: 200, default: 100 }
      responses:
        "200":
          description: Requests visible to the current principal and their available actions
          content:
            application/json:
              schema:
                type: object
                required: [requests, capabilities]
                properties:
                  requests:
                    type: array
                    items: { $ref: "#/components/schemas/InternalRequest" }
                  capabilities:
                    type: object
                    required: [canCreate, canManage]
                    properties:
                      canCreate: { type: boolean }
                      canManage: { type: boolean }
        "403": { description: Request read access is required }
    post:
      tags: [Internal requests]
      summary: Submit a multi-item inventory request
      description: Creates a request without changing stock. Policies that require approval leave it submitted; when every item permits automatic approval, availability is checked and the request is approved atomically.
      parameters:
        - { $ref: "#/components/parameters/RequiredIdempotencyKey" }
      requestBody:
        required: true
        content:
          application/json:
            schema: { $ref: "#/components/schemas/InternalRequestCreate" }
      responses:
        "200":
          description: Replayed the original request for this Idempotency-Key
          content:
            application/json:
              schema: { $ref: "#/components/schemas/InternalRequestMutation" }
        "201":
          description: Request submitted
          content:
            application/json:
              schema: { $ref: "#/components/schemas/InternalRequestMutation" }
        "400": { description: Missing or invalid Idempotency-Key }
        "403": { description: Request creation and inventory read access are required }
        "409": { description: Conflicting Idempotency-Key }
        "422": { description: "Invalid request window, items, quantities, or delivery resource" }
  /internal-requests/calendar:
    parameters:
      - { $ref: "#/components/parameters/OrganizationId" }
    get:
      tags: [Internal requests]
      summary: List reservation calendar entries
      description: Returns submitted, approved, and fulfilled request lines that overlap the range. Active direct reservations are included when the caller may view assignments.
      parameters:
        - name: from
          in: query
          required: true
          schema: { type: string, format: date-time }
        - name: to
          in: query
          required: true
          schema: { type: string, format: date-time }
      responses:
        "200":
          description: Calendar entries in the requested range
          content:
            application/json:
              schema: { $ref: "#/components/schemas/ReservationCalendar" }
        "403": { description: Request read access is required }
        "422": { description: Invalid or greater-than-370-day range }
  /internal-requests/{id}:
    parameters:
      - { $ref: "#/components/parameters/OrganizationId" }
      - name: id
        in: path
        required: true
        schema: { type: string, format: uuid }
    get:
      tags: [Internal requests]
      summary: Read one visible internal request
      responses:
        "200":
          description: Request with item lines and lifecycle events
          content:
            application/json:
              schema: { $ref: "#/components/schemas/InternalRequestMutation" }
        "403": { description: Request read access is required }
        "404": { description: Request not found or not visible to this principal }
        "422": { description: Invalid request id }
    patch:
      tags: [Internal requests]
      summary: Transition an internal request
      description: Managers may approve, reject, cancel, or fulfill requests. A requester may cancel their own submitted or approved request. Approval atomically checks overlapping availability; fulfillment issues stock and creates active checkouts.
      requestBody:
        required: true
        content:
          application/json:
            schema: { $ref: "#/components/schemas/InternalRequestAction" }
      responses:
        "200":
          description: Updated request
          content:
            application/json:
              schema: { $ref: "#/components/schemas/InternalRequestMutation" }
        "403": { description: The principal cannot perform this transition }
        "404": { description: Request not found }
        "409": { description: Invalid lifecycle transition or unavailable inventory }
        "422": { description: Invalid request id or action }
  /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 loan or assignment and updates stock. A future reservation blocks its time window without changing current stock; its serialized unit is selected during checkout.
      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" }
  /resources/{id}/lending:
    parameters:
      - { $ref: "#/components/parameters/OrganizationId" }
      - { $ref: "#/components/parameters/ResourceId" }
    get:
      tags: [Assignments]
      summary: Read the resource lending policy
      responses:
        "200":
          description: Resource lending policy, with disabled defaults when none has been stored
          content:
            application/json:
              schema:
                type: object
                required: [lending]
                properties:
                  lending: { $ref: "#/components/schemas/LendingSettings" }
        "404": { $ref: "#/components/responses/NotFound" }
    patch:
      tags: [Assignments]
      summary: Enable or configure loans for an inventory item
      requestBody:
        required: true
        content:
          application/json:
            schema: { $ref: "#/components/schemas/LendingSettings" }
      responses:
        "200": { description: Lending policy updated }
        "404": { $ref: "#/components/responses/NotFound" }
        "422": { description: Invalid duration policy }
  /loans:
    parameters:
      - { $ref: "#/components/parameters/OrganizationId" }
    get:
      tags: [Assignments]
      summary: List loans and direct reservations across the workspace
      parameters:
        - name: limit
          in: query
          schema: { type: integer, minimum: 1, maximum: 500, default: 500 }
      responses:
        "200":
          description: Loans and reservations with resource context and management capability
        "403": { description: Assignment read access is required }
  /assignments/{assignmentId}:
    parameters:
      - { $ref: "#/components/parameters/OrganizationId" }
      - name: assignmentId
        in: path
        required: true
        schema: { type: string, format: uuid }
    patch:
      tags: [Assignments]
      summary: Check out, return, or cancel an active assignment
      description: A checkout action converts a reservation into a stock-affecting loan. Completion restores stock only when the assignment had previously affected stock.
      parameters:
        - { $ref: "#/components/parameters/RequiredIdempotencyKey" }
      requestBody:
        required: true
        content:
          application/json:
            schema:
              oneOf:
                - { $ref: "#/components/schemas/InventoryAssignmentCompletion" }
                - { $ref: "#/components/schemas/InventoryReservationCheckout" }
      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 effective bill of materials and current build capacity
      description: A primary item returns its local BOM. A first-class variant returns the primary BOM with its sparse removals, replacements, and variant-only slots applied.
      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 effective bill of materials
      description: Primary items store the submitted recipe as stable BOM slots. Variants compare the submitted effective recipe with the primary and persist only sparse differences.
      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" }
    delete:
      tags: [Manufacturing]
      summary: Reset a variant's bill of materials to its primary item
      description: Removes manual BOM overrides for a linked first-class variant while preserving configured component selections. Primary and standalone items cannot use this operation.
      responses:
        "200":
          description: Reset effective bill of materials
          content:
            application/json:
              schema: { $ref: "#/components/schemas/BomDetail" }
        "404": { $ref: "#/components/responses/NotFound" }
        "409": { description: Item is not a linked variant }
  /resources/{id}/assembly-configuration:
    parameters:
      - { $ref: "#/components/parameters/OrganizationId" }
      - { $ref: "#/components/parameters/ResourceId" }
    get:
      tags: [Manufacturing]
      summary: Preview an automatic finished-product configuration without creating records
      parameters:
        - name: selection
          in: query
          required: true
          content:
            application/json:
              schema: { $ref: "#/components/schemas/AssemblyOutputConfiguration" }
      responses:
        "200":
          description: Effective recipe; existingResourceId is null for a virtual configuration, otherwise the reusable finished article ID
          content:
            application/json:
              schema:
                allOf:
                  - { $ref: "#/components/schemas/BomDetail" }
                  - type: object
                    properties:
                      existingResourceId: { type: [string, "null"], format: uuid }
        "403": { description: Matching variant or components are inaccessible or archived }
        "409": { description: Multiple finished articles match; select one explicitly }
        "422": { description: Incomplete or invalid configuration }
    post:
      tags: [Manufacturing]
      summary: Prepare only the selected configuration for manual customization
      description: Requires inventory.update and inventory.create. Reuses matching articles and preserves customizations. Creates no stock movements. Repeated requests return the same resource.
      requestBody:
        required: true
        content:
          application/json:
            schema: { $ref: "#/components/schemas/AssemblyOutputConfiguration" }
      responses:
        "200":
          description: Ready to customize
          content:
            application/json:
              schema:
                type: object
                properties:
                  resourceId: { type: string, format: uuid }
        "403": { description: Missing permission or archived resource }
        "409": { description: Ambiguous configuration }
        "422": { description: Invalid configuration }
  /resources/{id}/bom-parents:
    parameters:
      - { $ref: "#/components/parameters/OrganizationId" }
      - { $ref: "#/components/parameters/ResourceId" }
    get:
      tags: [Manufacturing]
      summary: List assemblies whose effective bill of materials uses this item
      description: Includes direct primary-item BOM lines, inherited variant BOM lines, and sparse variant overrides. Parent assemblies that the caller cannot read are omitted.
      responses:
        "200":
          description: Readable parent assemblies using this item
          content:
            application/json:
              schema:
                type: object
                required: [parents]
                properties:
                  parents:
                    type: array
                    items: { $ref: "#/components/schemas/BomParent" }
        "404": { $ref: "#/components/responses/NotFound" }
  /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 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/movements/{movementId}:
    parameters:
      - { $ref: "#/components/parameters/OrganizationId" }
      - { $ref: "#/components/parameters/ResourceId" }
      - name: movementId
        in: path
        required: true
        schema: { type: string, format: uuid }
    patch:
      tags: [Stock]
      summary: Correct a manual bulk stock movement
      description: System-linked movements and inbound movements whose cost layer has already been consumed cannot be changed.
      requestBody:
        required: true
        content:
          application/json:
            schema: { $ref: "#/components/schemas/StockMovementInput" }
      responses:
        "200":
          description: Movement and current balance updated atomically
          content:
            application/json:
              schema:
                type: object
                required: [resource, movement]
                properties:
                  resource:
                    type: object
                    required: [id, quantity]
                    properties:
                      id: { type: string, format: uuid }
                      quantity: { type: integer }
                  movement: { $ref: "#/components/schemas/StockMovement" }
        "404": { description: Movement not found }
        "409": { description: Movement is system-linked or cannot be corrected safely }
        "422": { description: Invalid movement correction }
    delete:
      tags: [Stock]
      summary: Delete a manual bulk stock movement
      description: Corrects the current quantity and subsequent ledger balances atomically. System-linked movements cannot be deleted.
      responses:
        "204": { description: Movement deleted and stock corrected }
        "404": { description: Movement not found }
        "409": { description: Movement is system-linked or cannot be deleted safely }
  /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 } }
    delete:
      tags: [Stock]
      summary: Delete an unlinked serialized unit while retaining movement history
      description: Requires stock.manage. Atomically deletes the unit, records a unit-deleted audit entry with its code and UUID, and reduces available stock and its cost layers by one only when the unit was available. Existing QR links will no longer select this unit. Units linked to orders, assignments, or assemblies (including historical links and assembly outputs) cannot be deleted.
      responses:
        "200":
          description: Unit deleted, stock reconciled, and audit movement recorded
          content:
            application/json:
              schema:
                type: object
                required: [deletedUnitId, quantity, movement]
                properties:
                  deletedUnitId: { type: string, format: uuid }
                  quantity: { type: integer, minimum: 0 }
                  movement: { $ref: "#/components/schemas/StockMovement" }
        "403": { description: Stock management permission required }
        "404": { $ref: "#/components/responses/NotFound" }
        "409": { description: "Unit is linked to another operation, tracking is not serialized, or stock would become negative" }
        "422": { description: Invalid resource or unit ID }
    patch:
      tags: [Stock]
      summary: Change a unit's code, 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 }
        "409": { description: Unit code already exists for this item or unit update conflicts with its current state }
        "422": { description: Invalid unit update }
        "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" }
  /orders:
    parameters:
      - { $ref: "#/components/parameters/OrganizationId" }
    get:
      tags: [Orders]
      summary: List purchase, sales, or loan orders from the shared order model
      parameters:
        - name: type
          in: query
          schema: { type: string, enum: [purchase, sale, loan] }
        - name: status
          in: query
          schema: { $ref: "#/components/schemas/OrderStatus" }
        - name: limit
          in: query
          schema: { type: integer, minimum: 1, maximum: 100, default: 100 }
      responses:
        "200":
          description: Orders and their embedded lines
          content:
            application/json:
              schema:
                type: object
                required: [orders]
                properties:
                  orders: { type: array, items: { $ref: "#/components/schemas/Order" } }
    post:
      tags: [Orders]
      summary: Create a purchase, sales, or multi-item loan order
      parameters:
        - { $ref: "#/components/parameters/RequiredIdempotencyKey" }
      requestBody:
        required: true
        content:
          application/json:
            schema: { $ref: "#/components/schemas/OrderInput" }
      responses:
        "200": { description: Replayed original response }
        "201": { description: Order created }
        "409": { description: Idempotency conflict }
        "422": { description: "Invalid contact, status, or order line" }
  /orders/{orderId}:
    parameters:
      - { $ref: "#/components/parameters/OrganizationId" }
      - name: orderId
        in: path
        required: true
        schema: { type: string, format: uuid }
    get:
      tags: [Orders]
      summary: Retrieve one shared order with all lines
      responses:
        "200":
          description: Order
          content:
            application/json:
              schema:
                type: object
                required: [order]
                properties:
                  order: { $ref: "#/components/schemas/Order" }
        "404": { $ref: "#/components/responses/NotFound" }
    patch:
      tags: [Orders]
      summary: Update common order fields or change lifecycle state
      requestBody:
        required: true
        content:
          application/json:
            schema: { $ref: "#/components/schemas/OrderPatch" }
      responses:
        "200": { description: Order updated }
        "409": { description: Completed or cancelled orders cannot be reopened }
        "422": { description: Invalid type-specific status or contact role }
  /orders/{orderId}/shipments:
    parameters:
      - { $ref: "#/components/parameters/OrganizationId" }
      - name: orderId
        in: path
        required: true
        schema: { type: string, format: uuid }
    get:
      tags: [Orders]
      summary: List outbound parcels for a sales order
      responses:
        "200":
          description: Shipments with lines, serialized units, and immutable status history
          content:
            application/json:
              schema:
                type: object
                required: [shipments]
                properties:
                  shipments:
                    type: array
                    items: { $ref: "#/components/schemas/Shipment" }
    post:
      tags: [Orders]
      summary: Create an outbound parcel from fulfilled sales-order lines
      description: Assigns fulfilled bulk quantities and concrete serialized units to a parcel. This endpoint records Inventory shipping data and does not write to WooCommerce.
      parameters:
        - { $ref: "#/components/parameters/RequiredIdempotencyKey" }
      requestBody:
        required: true
        content:
          application/json:
            schema: { $ref: "#/components/schemas/ShipmentInput" }
      responses:
        "200": { description: Replayed the original shipment response }
        "201": { description: Shipment created }
        "400": { description: Missing or invalid Idempotency-Key }
        "409": { description: "Quantity, serialized-unit, tracking-number, or idempotency conflict" }
        "422": { description: "Invalid shipment, line, or order type" }
  /orders/{orderId}/shipments/{shipmentId}:
    parameters:
      - { $ref: "#/components/parameters/OrganizationId" }
      - name: orderId
        in: path
        required: true
        schema: { type: string, format: uuid }
      - name: shipmentId
        in: path
        required: true
        schema: { type: string, format: uuid }
    patch:
      tags: [Orders]
      summary: Update shipment details or advance its delivery status
      description: Records every change in the shipment event history. Entering shipped or delivered records the effective timestamp automatically unless occurredAt is supplied.
      requestBody:
        required: true
        content:
          application/json:
            schema: { $ref: "#/components/schemas/ShipmentPatch" }
      responses:
        "200":
          description: Shipment updated
          content:
            application/json:
              schema:
                type: object
                required: [shipment]
                properties:
                  shipment: { $ref: "#/components/schemas/Shipment" }
        "404": { $ref: "#/components/responses/NotFound" }
        "409": { description: Invalid lifecycle transition or duplicate tracking number }
        "422": { description: Invalid shipment details or timestamp }
  /orders/{orderId}/lines/{lineId}/movements:
    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: [Orders]
      summary: Issue or return a bulk-tracked sales or loan line
      description: Creates an idempotent stock movement linked to the order line and recalculates the order status atomically. Bulk-tracked inventory is supported by this endpoint.
      parameters:
        - { $ref: "#/components/parameters/RequiredIdempotencyKey" }
      requestBody:
        required: true
        content:
          application/json:
            schema: { $ref: "#/components/schemas/OrderLineMovementInput" }
      responses:
        "200": { description: Replayed original stock movement }
        "201": { description: Movement created and order progress updated }
        "409": { description: "Insufficient stock, excessive quantity, or invalid lifecycle state" }
        "422": { description: Invalid movement for the order type }
  /orders/{orderId}/lines/{lineId}/units:
    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 }
    get:
      tags: [Orders]
      summary: List available and assigned serialized units for an order line
      responses:
        "200":
          description: Serialized units available to the line and its assignment history
          content:
            application/json:
              schema: { $ref: "#/components/schemas/SerializedOrderLineUnits" }
        "404": { $ref: "#/components/responses/NotFound" }
        "422": { description: The line does not use serialized stock tracking }
    post:
      tags: [Orders]
      summary: Reserve, release, issue, or return concrete serialized units
      description: Updates the unit lifecycle, available stock, order progress, cost layers, and immutable stock movement ledger atomically.
      requestBody:
        required: true
        content:
          application/json:
            schema: { $ref: "#/components/schemas/OrderLineUnitActionInput" }
      responses:
        "200":
          description: Unit action applied, including replay-safe no-op actions
          content:
            application/json:
              schema:
                type: object
                required: [changed, movementIds, order, units]
                properties:
                  changed: { type: integer, minimum: 0 }
                  movementIds:
                    type: array
                    items: { type: string, format: uuid }
                  order: { $ref: "#/components/schemas/Order" }
                  units: { $ref: "#/components/schemas/SerializedOrderLineUnits" }
        "409": { description: "Unit state, capacity, assignment, or order lifecycle conflict" }
        "422": { description: Invalid action or non-serialized order line }
  /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 }
                prompt:
                  type: string
                  maxLength: 5000
                  description: Optional replacement for the built-in inventory-analysis prompt. Omit it to use the server default.
      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 }
        "422": { description: Invalid analysis request }
        "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}/research:
    parameters:
      - { $ref: "#/components/parameters/OrganizationId" }
    post:
      tags: [AI]
      summary: Add missing item details using saved content, images, and GPT web search
      description: Preserves existing fields, appends newly researched description details, merges tags and categories, fills tracking identifiers or value only at high confidence, and adds sourced internal notes only when blank.
      parameters:
        - { $ref: "#/components/parameters/ResourceId" }
        - { $ref: "#/components/parameters/IdempotencyKey" }
      requestBody:
        content:
          application/json:
            schema:
              type: object
              additionalProperties: false
      responses:
        "200": { description: "Missing details added, no reliable additions found, or completed response replayed" }
        "202": { description: An operation with this key is still processing }
        "403": { description: Proposed changes fall outside the caller's inventory access rules }
        "409": { description: Idempotency key belongs to another resource or payload }
        "422": { description: Invalid AI research request }
        "429":
          description: AI web-research request limit reached
          headers:
            Retry-After:
              schema: { type: integer, minimum: 1 }
        "502": { description: AI provider or web-search 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
                  description: Optional replacement for the built-in studio or transparent-cover prompt. Omit it to use the matching server default.
                modelId:
                  type: string
                  maxLength: 240
                  description: An ID returned by GET /ai/image-models. Omit it to use the deployment default.
                maximumImageSize:
                  type: integer
                  enum: [1024, 2048, 4096]
                  default: 1024
                  description: Maximum requested longest edge in pixels. Models use the largest supported standard square size at or below this value; transparent covers are capped at 2048 for safe processing.
                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 }
  /resources/{id}/image:
    parameters:
      - { $ref: "#/components/parameters/OrganizationId" }
    post:
      tags: [AI]
      summary: Find a reusable web image or generate a new AI image and append it to an item
      parameters:
        - { $ref: "#/components/parameters/ResourceId" }
        - { $ref: "#/components/parameters/IdempotencyKey" }
      requestBody:
        required: true
        content:
          application/json:
            schema:
              oneOf:
                - type: object
                  additionalProperties: false
                  required: [mode]
                  properties:
                    mode: { type: string, const: search }
                    query:
                      type: string
                      minLength: 1
                      maxLength: 500
                      description: Optional brand, model, or exact-item search hint. Saved item details are always included.
                - type: object
                  additionalProperties: false
                  required: [mode]
                  properties:
                    mode: { type: string, const: generate }
                    prompt:
                      type: string
                      minLength: 1
                      maxLength: 5000
                      description: Optional image direction. Omit it for a catalogue-style prompt based on the saved item.
                    modelId:
                      type: string
                      maxLength: 240
                      description: An ID returned by GET /ai/image-models. Omit it to use the deployment default.
                    maximumImageSize:
                      type: integer
                      enum: [1024, 2048, 4096]
                      default: 1024
      responses:
        "200": { description: "Image imported or generated, appended to the item, 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: AI research or image-generation limit reached }
        "502": { description: "AI provider, web-search, remote-image, or storage 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 }
  /label-workspace:
    parameters:
      - $ref: "#/components/parameters/OrganizationId"
    get:
      tags:
        - Label printing
      summary: Read the personal saved print list
      description: Requires a user browser session with labels.read. Data is scoped to the current user
        and organization.
      security:
        - adminSession: []
      responses:
        "200":
          description: Saved queue and preferences.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/LabelWorkspace"
        "403":
          description: Required permission or browser session missing; printing mutations also reject
            read-only organizations and cross-site requests.
    patch:
      tags:
        - Label printing
      summary: Save print preferences or replace the prepared list
      description: Requires a user browser session with labels.read. Queue updates require the current
        revision; 409 prevents a stale browser from overwriting another window. At most 100 labels,
        one tape width and a 24 MiB JSON body. Preference updates merge only supplied keys.
      security:
        - adminSession: []
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: "#/components/schemas/LabelWorkspaceMutation"
      responses:
        "200":
          description: Queue updates return the workspace; preference updates return preferences only.
          content:
            application/json:
              schema:
                oneOf:
                  - $ref: "#/components/schemas/LabelWorkspace"
                  - type: object
                    required:
                      - preferences
                    properties:
                      preferences:
                        $ref: "#/components/schemas/LabelPreferences"
        "400":
          description: Malformed request.
        "403":
          description: Required permission or browser session missing; printing mutations also reject
            read-only organizations and cross-site requests.
        "409":
          description: Saved queue revision changed; reload before editing.
        "413":
          description: Request exceeds the size limit.
        "422":
          description: Invalid input or raster data.
  /label-printers:
    parameters:
      - $ref: "#/components/parameters/OrganizationId"
    get:
      tags:
        - Label printing
      summary: List enabled shared printers
      description: Requires a browser session with labels.read. online means an agent poll was received
        within 45 seconds.
      security:
        - adminSession: []
      responses:
        "200":
          description: Enabled printers in this organization.
          content:
            application/json:
              schema:
                type: object
                required:
                  - printers
                properties:
                  printers:
                    type: array
                    items:
                      $ref: "#/components/schemas/SharedPrinter"
        "403":
          description: Required permission or browser session missing; printing mutations also reject
            read-only organizations and cross-site requests.
    post:
      tags:
        - Label printing
      summary: Provision a shared Office printer
      description: Requires a browser session with tokens.manage. Currently provisions 24 mm tape only.
        The returned iprt_ token is shown once and is restricted to this printer; store it in the
        agent configuration.
      security:
        - adminSession: []
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required:
                - name
              properties:
                name:
                  type: string
                  minLength: 1
                  maxLength: 120
                tape:
                  type: number
                  const: 24
                  default: 24
              additionalProperties: false
      responses:
        "201":
          description: Printer and one-time agent credential.
          content:
            application/json:
              schema:
                type: object
                required:
                  - printer
                  - token
                properties:
                  printer:
                    type: object
                    required:
                      - id
                      - name
                      - tape
                    properties:
                      id:
                        type: string
                        format: uuid
                      name:
                        type: string
                      tape:
                        type: number
                        const: 24
                  token:
                    type: string
        "400":
          description: Malformed request.
        "403":
          description: Required permission or browser session missing; printing mutations also reject
            read-only organizations and cross-site requests.
        "413":
          description: Request exceeds the size limit.
        "422":
          description: Invalid input or raster data.
    patch:
      tags:
        - Label printing
      summary: Disable a shared printer
      description: Requires a browser session with tokens.manage. Revokes agent access and cancels queued
        jobs. To rotate credentials, provision a replacement printer.
      security:
        - adminSession: []
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required:
                - id
                - enabled
              properties:
                id:
                  type: string
                  format: uuid
                enabled:
                  type: boolean
                  const: false
              additionalProperties: false
      responses:
        "200":
          description: Printer disabled.
          content:
            application/json:
              schema:
                type: object
                required:
                  - disabled
                properties:
                  disabled:
                    type: boolean
                    const: true
        "400":
          description: Malformed request.
        "403":
          description: Required permission or browser session missing; printing mutations also reject
            read-only organizations and cross-site requests.
        "404":
          description: Printer not found in this organization.
        "413":
          description: Request exceeds the size limit.
        "422":
          description: Invalid input or raster data.
  /label-print-jobs:
    parameters:
      - $ref: "#/components/parameters/OrganizationId"
    get:
      tags:
        - Label printing
      summary: List your latest shared print jobs
      description: Requires a browser session with labels.read. Returns at most 20 jobs submitted by the
        current user in this organization.
      security:
        - adminSession: []
      responses:
        "200":
          description: Recent jobs.
          content:
            application/json:
              schema:
                type: object
                required:
                  - jobs
                properties:
                  jobs:
                    type: array
                    items:
                      $ref: "#/components/schemas/SharedPrintJob"
        "403":
          description: Required permission or browser session missing; printing mutations also reject
            read-only organizations and cross-site requests.
    post:
      tags:
        - Label printing
      summary: Queue a prepared PT-P710BT print job
      description: "Requires a browser session with labels.read. The browser renders and validates labels
        before submission. Reuse body.id only with identical data to retrieve an existing job. Limits:
        100 labels, 4 MB RAW data, 10 submissions/minute per user/printer and 20 pending jobs per
        printer. Queued jobs expire after 10 minutes."
      security:
        - adminSession: []
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: "#/components/schemas/SharedPrintInput"
      responses:
        "201":
          description: Queued job or replay of the same submission.
          content:
            application/json:
              schema:
                type: object
                required:
                  - job
                properties:
                  job:
                    $ref: "#/components/schemas/SharedPrintJob"
        "400":
          description: Malformed request.
        "403":
          description: Required permission or browser session missing; printing mutations also reject
            read-only organizations and cross-site requests.
        "404":
          description: Printer unavailable.
        "409":
          description: Idempotency ID reused with different data, or tape width differs from the printer.
        "413":
          description: Request exceeds the size limit.
        "422":
          description: Invalid input or raster data.
        "429":
          description: Submission rate or pending queue limit reached.
    delete:
      tags:
        - Label printing
      summary: Cancel one of your waiting print jobs
      description: Requires a browser session with labels.read. Only a queued job owned by the caller can
        be cancelled.
      security:
        - adminSession: []
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required:
                - id
              properties:
                id:
                  type: string
                  format: uuid
              additionalProperties: false
      responses:
        "200":
          description: Job cancelled.
          content:
            application/json:
              schema:
                type: object
                required:
                  - cancelled
                properties:
                  cancelled:
                    type: boolean
                    const: true
        "400":
          description: Malformed request.
        "403":
          description: Required permission or browser session missing; printing mutations also reject
            read-only organizations and cross-site requests.
        "409":
          description: Job already dispatched or not found.
        "413":
          description: Request exceeds the size limit.
        "422":
          description: Invalid input or raster data.
  /label-print-agent/claim:
    post:
      tags:
        - Label printing
      summary: Claim the next job for a printer
      description: Windows agent endpoint; use the dedicated iprt_ credential, not an inventory API token.
        Reuse claimId to recover the same claim. Stale claims become uncertain after five minutes and
        are never handed out again. History cleanup runs on polling after seven days.
      security:
        - printAgent: []
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required:
                - claimId
              properties:
                claimId:
                  type: string
                  format: uuid
              additionalProperties: false
      responses:
        "200":
          description: Claimed RAW data or null when no job is available.
          content:
            application/json:
              schema:
                type: object
                required:
                  - job
                properties:
                  job:
                    anyOf:
                      - type: object
                        required:
                          - id
                          - data
                          - sha256
                        properties:
                          id:
                            type: string
                            format: uuid
                          data:
                            type: string
                            contentEncoding: base64
                          sha256:
                            type: string
                      - type: "null"
        "400":
          description: Malformed request.
        "401":
          description: Invalid, disabled or read-only printer credential.
        "403":
          description: Required permission or browser session missing; printing mutations also reject
            read-only organizations and cross-site requests.
        "413":
          description: Request exceeds the size limit.
  /label-print-agent/ack:
    post:
      tags:
        - Label printing
      summary: Acknowledge a printer claim
      description: Agent only. Report submitted after Windows accepts the RAW job, failed after a definite
        failure, or uncertain if acceptance is ambiguous. The receipt clears the stored payload. It
        does not prove physical printing.
      security:
        - printAgent: []
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required:
                - id
                - claimId
                - status
              properties:
                id:
                  type: string
                  format: uuid
                claimId:
                  type: string
                  format: uuid
                status:
                  type: string
                  enum:
                    - submitted
                    - failed
                    - uncertain
                spoolJobId:
                  anyOf:
                    - type: integer
                      minimum: 1
                    - type: "null"
                  default: null
              additionalProperties: false
      responses:
        "200":
          description: Receipt recorded.
          content:
            application/json:
              schema:
                type: object
                required:
                  - ok
                properties:
                  ok:
                    type: boolean
                    const: true
        "400":
          description: Malformed request.
        "401":
          description: Invalid printer credential.
        "409":
          description: Matching claim not found.
        "413":
          description: Request exceeds the size limit.
  /labels/qz-certificate:
    parameters:
      - $ref: "#/components/parameters/OrganizationId"
    get:
      tags:
        - Label printing
      summary: Read the optional QZ signing certificate
      description: Requires a browser session with labels.read. Without signing configuration the
        application uses QZ approval prompts.
      security:
        - adminSession: []
      responses:
        "200":
          description: PEM signing certificate.
          content:
            text/plain:
              schema:
                type: string
        "403":
          description: Required permission or browser session missing; printing mutations also reject
            read-only organizations and cross-site requests.
        "500":
          description: Certificate could not be read.
        "503":
          description: QZ signing not configured.
  /labels/qz-sign:
    parameters:
      - $ref: "#/components/parameters/OrganizationId"
    post:
      tags:
        - Label printing
      summary: Sign an allowed QZ print request
      description: Requires a browser session with labels.read. Only recent printer-discovery requests and
        bounded, single-copy raw PT-P710BT jobs are accepted. Cross-site, file, network, serial and
        direct USB commands are rejected. Private keys remain on the server.
      security:
        - adminSession: []
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required:
                - request
              properties:
                request:
                  type: string
                  description: Exact serialized QZ request string.
      responses:
        "200":
          description: Base64 RSA SHA-512 signature.
          content:
            text/plain:
              schema:
                type: string
        "400":
          description: Malformed request.
        "403":
          description: Session permission missing or signing request disallowed.
        "413":
          description: Request exceeds the size limit.
        "500":
          description: Signing failed.
        "503":
          description: QZ signing not configured.
  /stock/forecast:
    parameters:
      - $ref: "#/components/parameters/OrganizationId"
    get:
      tags:
        - Stock
      summary: Read stock planning inputs and history
      description: "Requires stock.read. Returns a consistent snapshot for non-archived items with stock
        settings: up to 90 days of consumption, recent 30-day consumption, daily stock balances and
        outstanding purchase receipts. Consumption includes issues, production consumption and waste;
        loans, transfers and corrections are excluded. This endpoint returns inputs; the web client
        calculates forecast curves and replenishment suggestions. It neither orders nor changes
        stock."
      responses:
        "200":
          description: Planning data.
          content:
            application/json:
              schema:
                type: object
                required:
                  - generatedAt
                  - items
                properties:
                  generatedAt:
                    type: string
                    format: date-time
                  items:
                    type: array
                    items:
                      $ref: "#/components/schemas/StockPlanningItem"
        "403":
          description: stock.read required.
        "500":
          description: Unable to load planning data.
  /ai/count/bulk:
    parameters:
      - $ref: "#/components/parameters/OrganizationId"
      - $ref: "#/components/parameters/RequiredIdempotencyKey"
    post:
      tags:
        - AI
      summary: Estimate bin contents from 3–5 photos (experimental)
      description: Requires ai.count and access to input.itemId. Sends images to the configured
        OpenAI-compatible provider in two analysis calls. Supply input as a JSON string, including
        nullable containerDimensions and partDimensions. Reuse the Idempotency-Key only for the same
        request. An estimate may be null; no stock is booked. Accuracy is not validated.
      requestBody:
        required: true
        content:
          multipart/form-data:
            schema:
              type: object
              required:
                - input
                - images
              properties:
                input:
                  type: string
                  description: JSON-encoded BulkCountInput; see the matching schema.
                  contentMediaType: application/json
                  contentSchema:
                    $ref: "#/components/schemas/BulkCountInput"
                images:
                  type: array
                  minItems: 3
                  maxItems: 5
                  items:
                    type: string
                    format: binary
                  description: Distinct views of the same unchanged bin; at most 8 MiB per image.
      responses:
        "200":
          description: Estimate with assumptions, or no usable estimate.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/BulkCountResult"
        "400":
          description: Missing Idempotency-Key or malformed multipart body.
        "402":
          description: Monthly AI budget exceeded.
        "403":
          description: ai.count and access to the item are required.
        "409":
          description: Operation still processing or idempotency key conflicts.
        "413":
          description: Upload too large.
        "422":
          description: Invalid annotations, input or images; duplicate photos rejected.
        "429":
          description: Counting disabled or rate limit reached.
        "502":
          description: Provider returned no usable result.
        "503":
          description: Required runtime/provider or service unavailable.
  /ai/count/3d:
    parameters:
      - $ref: "#/components/parameters/OrganizationId"
    get:
      tags:
        - AI
      summary: Check local 3D counting availability
      description: Requires ai.count and item access. Availability requires BULK_3D_ENABLED and a
        separately provisioned Python/GPU runtime; the standard Docker image does not include it.
      parameters:
        - name: itemId
          in: query
          required: true
          schema:
            type: string
            format: uuid
      responses:
        "200":
          description: Local runtime availability.
          content:
            application/json:
              schema:
                type: object
                required:
                  - available
                properties:
                  available:
                    type: boolean
        "403":
          description: ai.count and access to the item are required.
        "422":
          description: Invalid annotations, input or images; duplicate photos rejected.
    post:
      tags:
        - AI
      summary: Start an experimental 3D bin estimate
      description: Requires ai.count and item access. Upload 3–4 JPEG/PNG/WebP/AVIF photos, manual marks
        and explicit shape/packing assumptions. The first image is the top view. Runs on the
        configured local GPU; no paid inference API is called. Poll the returned jobId. Reuse the same
        key for identical retries. Deadline is ten minutes; results remain accessible for 24 hours. No
        stock is booked.
      parameters:
        - $ref: "#/components/parameters/RequiredIdempotencyKey"
      requestBody:
        required: true
        content:
          multipart/form-data:
            schema:
              type: object
              required:
                - input
                - images
              properties:
                input:
                  type: string
                  description: JSON-encoded Bulk3dInput; see the matching schema.
                  contentMediaType: application/json
                  contentSchema:
                    $ref: "#/components/schemas/Bulk3dInput"
                images:
                  type: array
                  minItems: 3
                  maxItems: 4
                  items:
                    type: string
                    format: binary
                  description: Distinct views of the same unchanged bin; at most 8 MiB per image.
      responses:
        "202":
          description: Durable job accepted or replayed.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Bulk3dJob"
        "400":
          description: Missing Idempotency-Key or malformed multipart body.
        "403":
          description: ai.count and access to the item are required.
        "409":
          description: Operation still processing or idempotency key conflicts.
        "413":
          description: Upload too large.
        "422":
          description: Invalid annotations, input or images; duplicate photos rejected.
        "429":
          description: Counting disabled or rate limit reached.
        "503":
          description: Required runtime/provider or service unavailable.
  /ai/count/3d/{jobId}:
    parameters:
      - $ref: "#/components/parameters/OrganizationId"
      - name: jobId
        in: path
        required: true
        schema:
          type: string
          format: uuid
    get:
      tags:
        - AI
      summary: Read a 3D estimate or download its GLB
      description: "Only the initiating actor in the same organization with current item access may read
        this job. A completed reconstruction can have estimate: null. Results expire after 24 hours;
        physical cleanup occurs on a subsequent submission."
      parameters:
        - name: asset
          in: query
          schema:
            type: string
            enum:
              - model
          description: Download the reconstructed GLB instead of JSON state.
      responses:
        "200":
          description: JSON job state, or GLB when asset=model.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Bulk3dState"
            model/gltf-binary:
              schema:
                type: string
                format: binary
        "403":
          description: ai.count and access to the item are required.
        "404":
          description: Job not found or not owned by caller.
        "409":
          description: Model not ready.
        "410":
          description: Result expired.
        "503":
          description: Required runtime/provider or service unavailable.
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:
    printAgent:
      type: http
      scheme: bearer
      bearerFormat: iprt_ token
      description: Dedicated printer credential issued once by POST /label-printers. Only valid for agent claim and acknowledgement.
    bearerAuth:
      type: http
      scheme: bearer
      bearerFormat: inv_ token
    adminSession:
      type: apiKey
      in: cookie
      name: authjs.session-token
      description: Authenticated Auth.js browser session cookie; each operation declares its required permission. Not restricted to administrators. HTTPS deployments use the secure-prefixed cookie variant.
    superadminSession:
      type: apiKey
      in: cookie
      name: authjs.session-token
      description: Auth.js browser session cookie for an account whose email is listed in the deployment's `SUPERADMIN_EMAILS` environment variable. 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 }
    ResourceReference:
      name: id
      in: path
      required: true
      description: Resource UUID or one of the resource's organization-scoped slugs.
      schema:
        type: string
        minLength: 1
        maxLength: 80
    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:
    StockPlanningItem:
      type: object
      required:
        - resourceId
        - name
        - subtitle
        - sku
        - quantity
        - unitName
        - minimumStock
        - reorderQuantity
        - leadTimeDays
        - purchaseUnitName
        - purchaseUnitFactor
        - observedDays
        - consumed
        - recentConsumed
        - demandDays
        - receipts
        - history
      properties:
        resourceId:
          type: string
          format: uuid
        name:
          type: string
        subtitle:
          type: string
        sku:
          anyOf:
            - type: string
            - type: "null"
        quantity:
          type: number
        unitName:
          type: string
        minimumStock:
          type: number
        reorderQuantity:
          type: number
        leadTimeDays:
          type: number
        purchaseUnitName:
          anyOf:
            - type: string
            - type: "null"
        purchaseUnitFactor:
          anyOf:
            - type: number
            - type: "null"
        observedDays:
          type: number
        consumed:
          type: number
        recentConsumed:
          type: number
        demandDays:
          type: integer
        receipts:
          type: array
          items:
            type: object
            required:
              - quantity
              - expectedAt
            properties:
              quantity:
                type: number
              expectedAt:
                anyOf:
                  - type: string
                    format: date-time
                  - type: "null"
        history:
          type: array
          items:
            type: object
            required:
              - day
              - quantity
            properties:
              day:
                type: number
              quantity:
                type: number
    SharedPrintJob:
      type: object
      required:
        - id
        - printerId
        - count
        - status
        - createdAt
        - spoolJobId
      properties:
        id:
          type: string
          format: uuid
        printerId:
          type: string
          format: uuid
        count:
          type: integer
          minimum: 1
          maximum: 100
        status:
          type: string
          enum:
            - queued
            - claimed
            - submitted
            - failed
            - uncertain
            - cancelled
            - expired
        createdAt:
          type: string
          format: date-time
        spoolJobId:
          anyOf:
            - type: integer
            - type: "null"
      description: submitted confirms Windows spooler acceptance, not physical output. uncertain jobs are
        never automatically reprinted.
    SharedPrinter:
      type: object
      required:
        - id
        - name
        - tape
        - online
        - enabled
      properties:
        id:
          type: string
          format: uuid
        name:
          type: string
        tape:
          type: number
        online:
          type: boolean
        enabled:
          type: boolean
    LabelWorkspace:
      type: object
      required:
        - revision
        - labels
        - preferences
      properties:
        revision:
          type: integer
          minimum: 0
        labels:
          type: array
          items:
            $ref: "#/components/schemas/SavedLabel"
        preferences:
          $ref: "#/components/schemas/LabelPreferences"
    Bulk3dState:
      oneOf:
        - type: object
          properties:
            status:
              type: string
              enum:
                - queued
                - loading
                - reconstructing
                - analyzing
          required:
            - status
          additionalProperties: false
        - type: object
          properties:
            status:
              type: string
              const: completed
            result:
              $ref: "#/components/schemas/Bulk3dResult"
          required:
            - status
            - result
          additionalProperties: false
        - type: object
          properties:
            status:
              type: string
              const: failed
            code:
              type: string
              enum:
                - analysisFailed
                - timeout
          required:
            - status
            - code
          additionalProperties: false
    Bulk3dJob:
      type: object
      properties:
        jobId:
          type: string
          format: uuid
          pattern: ^([0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[1-8][0-9a-fA-F]{3}-[89abAB][0-9a-fA-F]{3}-[0-9a-fA-F]{12}|00000000-0000-0000-0000-000000000000|ffffffff-ffff-ffff-ffff-ffffffffffff)$
        expiresAt:
          type: string
          format: date-time
          pattern: ^(?:(?:\d\d[2468][048]|\d\d[13579][26]|\d\d0[48]|[02468][048]00|[13579][26]00)-02-29|\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\d|30)|(?:02)-(?:0[1-9]|1\d|2[0-8])))T(?:(?:[01]\d|2[0-3]):[0-5]\d(?::[0-5]\d(?:\.\d+)?)?(?:Z))$
      required:
        - jobId
        - expiresAt
      additionalProperties: false
    Bulk3dResult:
      type: object
      properties:
        method:
          type: string
          const: bulk_3d
        version:
          type: string
          const: mapanything_manual_v1
        model:
          type: string
          const: facebook/map-anything
        isExact:
          type: boolean
          const: false
        accuracyValidated:
          type: boolean
          const: false
        estimate:
          anyOf:
            - type: object
              properties:
                count:
                  type: integer
                  minimum: 1
                  maximum: 1000000
                lowerBound:
                  type: integer
                  minimum: 0
                  maximum: 1000000
                upperBound:
                  type: integer
                  minimum: 1
                  maximum: 1000000
              required:
                - count
                - lowerBound
                - upperBound
              additionalProperties: false
            - type: "null"
        warningCodes:
          maxItems: 5
          type: array
          items:
            type: string
            enum:
              - hiddenBottom
              - partShape
              - manualMarks
              - geometry
              - sampleSpread
        imageCount:
          type: integer
          minimum: 3
          maximum: 4
        sampleCount:
          type: integer
          minimum: 3
          maximum: 20
        inferenceSeconds:
          type: number
          minimum: 0
          maximum: 600
      required:
        - method
        - version
        - model
        - isExact
        - accuracyValidated
        - estimate
        - warningCodes
        - imageCount
        - sampleCount
        - inferenceSeconds
      additionalProperties: false
      description: Experimental estimate from reconstructed geometry and manual assumptions. estimate can
        be null even when a GLB is available. No validated accuracy or automatic stock booking.
    Bulk3dInput:
      type: object
      properties:
        itemId:
          type: string
          format: uuid
          pattern: ^([0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[1-8][0-9a-fA-F]{3}-[89abAB][0-9a-fA-F]{3}-[0-9a-fA-F]{12}|00000000-0000-0000-0000-000000000000|ffffffff-ffff-ffff-ffff-ffffffffffff)$
        language:
          type: string
          enum:
            - de
            - en
        marks:
          type: object
          properties:
            pile:
              minItems: 4
              maxItems: 20
              type: array
              items:
                type: array
                prefixItems:
                  - type: number
                    minimum: 0
                    maximum: 1
                  - type: number
                    minimum: 0
                    maximum: 1
            table:
              minItems: 3
              maxItems: 3
              type: array
              items:
                type: array
                prefixItems:
                  - type: number
                    minimum: 0
                    maximum: 1
                  - type: number
                    minimum: 0
                    maximum: 1
            edges:
              minItems: 3
              maxItems: 20
              type: array
              items:
                type: array
                prefixItems:
                  - type: array
                    prefixItems:
                      - type: number
                        minimum: 0
                        maximum: 1
                      - type: number
                        minimum: 0
                        maximum: 1
                  - type: array
                    prefixItems:
                      - type: number
                        minimum: 0
                        maximum: 1
                      - type: number
                        minimum: 0
                        maximum: 1
          required:
            - pile
            - table
            - edges
          additionalProperties: false
        assumptions:
          type: object
          properties:
            shape:
              type: string
              enum:
                - ellipsoid
                - cuboid
                - triangular_prism
            widthRatio:
              type: number
              minimum: 0.1
              maximum: 2
            heightRatio:
              type: number
              minimum: 0.1
              maximum: 2
            packing:
              type: number
              minimum: 0.1
              maximum: 1
            floorOffset:
              type: number
              minimum: 0
              maximum: 3
          required:
            - shape
            - widthRatio
            - heightRatio
            - packing
            - floorOffset
          additionalProperties: false
      required:
        - itemId
        - language
        - marks
        - assumptions
      additionalProperties: false
      description: Coordinates are normalized [0,1] in the first oriented photo. Use a simple fill
        polygon, three non-collinear table points outside it, and at least three representative
        part-length segments within the fill area. Other geometry checks apply.
    BulkCountResult:
      type: object
      properties:
        estimate:
          anyOf:
            - type: object
              properties:
                count:
                  type: integer
                  minimum: 0
                  maximum: 1000000
                lowerBound:
                  type: integer
                  minimum: 0
                  maximum: 1000000
                upperBound:
                  type: integer
                  minimum: 0
                  maximum: 1000000
              required:
                - count
                - lowerBound
                - upperBound
              additionalProperties: false
            - type: "null"
        visibility:
          type: string
          enum:
            - mostly_visible
            - partial_layers
            - surface_only
            - unclear
        explanation:
          type: string
          minLength: 1
          maxLength: 1500
        assumptions:
          maxItems: 6
          type: array
          items:
            type: string
            minLength: 1
            maxLength: 400
        warnings:
          maxItems: 6
          type: array
          items:
            type: string
            minLength: 1
            maxLength: 400
        captureAdvice:
          maxItems: 4
          type: array
          items:
            type: string
            minLength: 1
            maxLength: 400
        method:
          type: string
          const: bulk_visual
        isExact:
          type: boolean
          const: false
        accuracyValidated:
          type: boolean
          const: false
        model:
          type: string
          minLength: 1
          maxLength: 200
        analysis:
          type: object
          properties:
            version:
              type: string
              const: bulk_visual_v3
            agreement:
              type: string
              enum:
                - overlap
                - divergent
                - insufficient
                - empty
            methods:
              minItems: 2
              maxItems: 2
              type: array
              items:
                type: object
                properties:
                  strategy:
                    type: string
                    enum:
                      - layers
                      - volume
                  estimate:
                    anyOf:
                      - type: object
                        properties:
                          count:
                            type: integer
                            minimum: 0
                            maximum: 1000000
                          lowerBound:
                            type: integer
                            minimum: 0
                            maximum: 1000000
                          upperBound:
                            type: integer
                            minimum: 0
                            maximum: 1000000
                        required:
                          - count
                          - lowerBound
                          - upperBound
                        additionalProperties: false
                      - type: "null"
                  observation:
                    type: object
                    properties:
                      status:
                        type: string
                        enum:
                          - estimated
                          - empty
                          - insufficient
                      depthEvidence:
                        type: string
                        enum:
                          - observed
                          - inferred
                          - unconstrained
                      visibleMinimum:
                        type: integer
                        minimum: 0
                        maximum: 1000000
                      views:
                        minItems: 3
                        maxItems: 5
                        type: array
                        items:
                          type: object
                          properties:
                            image:
                              type: integer
                              minimum: 1
                              maximum: 5
                            evidence:
                              type: string
                              minLength: 1
                              maxLength: 400
                          required:
                            - image
                            - evidence
                          additionalProperties: false
                      factorA:
                        anyOf:
                          - type: object
                            properties:
                              low:
                                type: number
                                exclusiveMinimum: 0
                                maximum: 1000000
                              best:
                                type: number
                                exclusiveMinimum: 0
                                maximum: 1000000
                              high:
                                type: number
                                exclusiveMinimum: 0
                                maximum: 1000000
                            required:
                              - low
                              - best
                              - high
                            additionalProperties: false
                          - type: "null"
                      factorB:
                        anyOf:
                          - type: object
                            properties:
                              low:
                                type: number
                                exclusiveMinimum: 0
                                maximum: 1000000
                              best:
                                type: number
                                exclusiveMinimum: 0
                                maximum: 1000000
                              high:
                                type: number
                                exclusiveMinimum: 0
                                maximum: 1000000
                            required:
                              - low
                              - best
                              - high
                            additionalProperties: false
                          - type: "null"
                      volumeGeometry:
                        anyOf:
                          - type: object
                            properties:
                              lengthInItemLengths:
                                type: object
                                properties:
                                  low:
                                    type: number
                                    exclusiveMinimum: 0
                                    maximum: 1000000
                                  best:
                                    type: number
                                    exclusiveMinimum: 0
                                    maximum: 1000000
                                  high:
                                    type: number
                                    exclusiveMinimum: 0
                                    maximum: 1000000
                                required:
                                  - low
                                  - best
                                  - high
                                additionalProperties: false
                              widthInItemWidths:
                                type: object
                                properties:
                                  low:
                                    type: number
                                    exclusiveMinimum: 0
                                    maximum: 1000000
                                  best:
                                    type: number
                                    exclusiveMinimum: 0
                                    maximum: 1000000
                                  high:
                                    type: number
                                    exclusiveMinimum: 0
                                    maximum: 1000000
                                required:
                                  - low
                                  - best
                                  - high
                                additionalProperties: false
                              depthInItemHeights:
                                type: object
                                properties:
                                  low:
                                    type: number
                                    exclusiveMinimum: 0
                                    maximum: 1000000
                                  best:
                                    type: number
                                    exclusiveMinimum: 0
                                    maximum: 1000000
                                  high:
                                    type: number
                                    exclusiveMinimum: 0
                                    maximum: 1000000
                                required:
                                  - low
                                  - best
                                  - high
                                additionalProperties: false
                              containerVolumeFraction:
                                type: object
                                properties:
                                  low:
                                    type: number
                                    exclusiveMinimum: 0
                                    maximum: 1000000
                                  best:
                                    type: number
                                    exclusiveMinimum: 0
                                    maximum: 1000000
                                  high:
                                    type: number
                                    exclusiveMinimum: 0
                                    maximum: 1000000
                                required:
                                  - low
                                  - best
                                  - high
                                additionalProperties: false
                              itemShapeFraction:
                                type: object
                                properties:
                                  low:
                                    type: number
                                    exclusiveMinimum: 0
                                    maximum: 1000000
                                  best:
                                    type: number
                                    exclusiveMinimum: 0
                                    maximum: 1000000
                                  high:
                                    type: number
                                    exclusiveMinimum: 0
                                    maximum: 1000000
                                required:
                                  - low
                                  - best
                                  - high
                                additionalProperties: false
                              outerEnvelopePacking:
                                type: object
                                properties:
                                  low:
                                    type: number
                                    exclusiveMinimum: 0
                                    maximum: 1000000
                                  best:
                                    type: number
                                    exclusiveMinimum: 0
                                    maximum: 1000000
                                  high:
                                    type: number
                                    exclusiveMinimum: 0
                                    maximum: 1000000
                                required:
                                  - low
                                  - best
                                  - high
                                additionalProperties: false
                            required:
                              - lengthInItemLengths
                              - widthInItemWidths
                              - depthInItemHeights
                              - containerVolumeFraction
                              - itemShapeFraction
                              - outerEnvelopePacking
                            additionalProperties: false
                          - type: "null"
                      explanation:
                        type: string
                        minLength: 1
                        maxLength: 600
                      assumptions:
                        maxItems: 3
                        type: array
                        items:
                          type: string
                          minLength: 1
                          maxLength: 400
                      warnings:
                        maxItems: 2
                        type: array
                        items:
                          type: string
                          minLength: 1
                          maxLength: 400
                      captureAdvice:
                        maxItems: 2
                        type: array
                        items:
                          type: string
                          minLength: 1
                          maxLength: 400
                    required:
                      - status
                      - depthEvidence
                      - visibleMinimum
                      - views
                      - factorA
                      - factorB
                      - volumeGeometry
                      - explanation
                      - assumptions
                      - warnings
                      - captureAdvice
                    additionalProperties: false
                required:
                  - strategy
                  - estimate
                  - observation
                additionalProperties: false
          required:
            - version
            - agreement
            - methods
          additionalProperties: false
      required:
        - estimate
        - visibility
        - explanation
        - assumptions
        - warnings
        - captureAdvice
        - method
        - isExact
        - accuracyValidated
        - model
      additionalProperties: false
      description: Experimental, uncalibrated estimate. estimate can be null when evidence is
        insufficient. The range is not a confidence interval. This response never books stock.
    BulkCountInput:
      type: object
      properties:
        itemId:
          type: string
          format: uuid
          pattern: ^([0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[1-8][0-9a-fA-F]{3}-[89abAB][0-9a-fA-F]{3}-[0-9a-fA-F]{12}|00000000-0000-0000-0000-000000000000|ffffffff-ffff-ffff-ffff-ffffffffffff)$
        itemHint:
          type: string
          minLength: 1
          maxLength: 240
        language:
          type: string
          enum:
            - de
            - en
        containerDimensions:
          anyOf:
            - type: object
              properties:
                lengthMm:
                  type: number
                  exclusiveMinimum: 0
                  maximum: 10000
                widthMm:
                  type: number
                  exclusiveMinimum: 0
                  maximum: 10000
                heightMm:
                  type: number
                  exclusiveMinimum: 0
                  maximum: 10000
              required:
                - lengthMm
                - widthMm
                - heightMm
              additionalProperties: false
            - type: "null"
        partDimensions:
          anyOf:
            - type: object
              properties:
                lengthMm:
                  type: number
                  exclusiveMinimum: 0
                  maximum: 10000
                widthMm:
                  type: number
                  exclusiveMinimum: 0
                  maximum: 10000
                heightMm:
                  type: number
                  exclusiveMinimum: 0
                  maximum: 10000
              required:
                - lengthMm
                - widthMm
                - heightMm
              additionalProperties: false
            - type: "null"
      required:
        - itemId
        - itemHint
        - language
        - containerDimensions
        - partDimensions
      additionalProperties: false
    SharedPrintInput:
      type: object
      properties:
        id:
          type: string
          format: uuid
          pattern: ^([0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[1-8][0-9a-fA-F]{3}-[89abAB][0-9a-fA-F]{3}-[0-9a-fA-F]{12}|00000000-0000-0000-0000-000000000000|ffffffff-ffff-ffff-ffff-ffffffffffff)$
        printerId:
          type: string
          format: uuid
          pattern: ^([0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[1-8][0-9a-fA-F]{3}-[89abAB][0-9a-fA-F]{3}-[0-9a-fA-F]{12}|00000000-0000-0000-0000-000000000000|ffffffff-ffff-ffff-ffff-ffffffffffff)$
        tape:
          type: number
          enum:
            - 3.5
            - 6
            - 9
            - 12
            - 18
            - 24
        labels:
          minItems: 1
          maxItems: 100
          type: array
          items:
            type: string
            minLength: 1
            maxLength: 5333334
            pattern: ^[A-Za-z0-9+/]+={0,2}$
      required:
        - id
        - printerId
        - tape
        - labels
      additionalProperties: false
      description: id is a client-generated UUID used for idempotency. labels contains base64-encoded
        PT-P710BT raster labels, not images or arbitrary printer commands. All labels must use the
        printer tape width. At most 100 labels and 4,000,000 bytes in the combined RAW job.
    LabelWorkspaceMutation:
      oneOf:
        - type: object
          properties:
            action:
              type: string
              const: preferences
            preferences:
              $ref: "#/components/schemas/LabelPreferences"
          required:
            - action
            - preferences
          additionalProperties: false
        - type: object
          properties:
            action:
              type: string
              const: queue
            revision:
              type: integer
              minimum: 0
              maximum: 9007199254740991
            labels:
              maxItems: 100
              type: array
              items:
                $ref: "#/components/schemas/SavedLabel"
          required:
            - action
            - revision
            - labels
          additionalProperties: false
    LabelPreferences:
      type: object
      properties:
        setupId:
          type: string
          maxLength: 100
        printTarget:
          type: string
          enum:
            - browser
            - ptouch
            - shared
        tape:
          type: number
          enum:
            - 3.5
            - 6
            - 9
            - 12
            - 18
            - 24
        printer:
          type: string
          maxLength: 320
        sharedPrinter:
          anyOf:
            - type: string
              format: uuid
              pattern: ^([0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[1-8][0-9a-fA-F]{3}-[89abAB][0-9a-fA-F]{3}-[0-9a-fA-F]{12}|00000000-0000-0000-0000-000000000000|ffffffff-ffff-ffff-ffff-ffffffffffff)$
            - type: string
              const: ""
      additionalProperties: false
    SavedLabel:
      type: object
      properties:
        id:
          type: string
          format: uuid
          pattern: ^([0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[1-8][0-9a-fA-F]{3}-[89abAB][0-9a-fA-F]{3}-[0-9a-fA-F]{12}|00000000-0000-0000-0000-000000000000|ffffffff-ffff-ffff-ffff-ffffffffffff)$
        name:
          type: string
          maxLength: 10000
        template:
          type: string
          maxLength: 10000
        tape:
          type: number
          enum: [3.5, 6, 9, 12, 18, 24]
        widthMm:
          type: number
          exclusiveMinimum: 0
          maximum: 1000
        data:
          type: string
          maxLength: 400000
          pattern: ^[A-Za-z0-9+/]+={0,2}$
        preview:
          type: string
          maxLength: 1500000
          pattern: ^data:image\/png;base64,[A-Za-z0-9+/]+={0,2}$
        resource:
          type: object
          properties:
            id:
              type: string
              format: uuid
              pattern: ^([0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[1-8][0-9a-fA-F]{3}-[89abAB][0-9a-fA-F]{3}-[0-9a-fA-F]{12}|00000000-0000-0000-0000-000000000000|ffffffff-ffff-ffff-ffff-ffffffffffff)$
            name:
              type: string
              maxLength: 10000
            subtitle:
              type: string
              maxLength: 10000
            sku:
              anyOf:
                - type: string
                  maxLength: 10000
                - type: "null"
            barcode:
              anyOf:
                - type: string
                  maxLength: 10000
                - type: "null"
            location:
              anyOf:
                - type: string
                  maxLength: 10000
                - type: "null"
            type:
              type: string
              maxLength: 64
            quantity:
              type: number
            cover:
              anyOf:
                - type: object
                  properties:
                    url:
                      type: string
                      maxLength: 8192
                    altText:
                      type: string
                      maxLength: 10000
                  required:
                    - url
                    - altText
                - type: "null"
            stockUnit:
              type: object
              properties:
                id:
                  type: string
                  format: uuid
                  pattern: ^([0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[1-8][0-9a-fA-F]{3}-[89abAB][0-9a-fA-F]{3}-[0-9a-fA-F]{12}|00000000-0000-0000-0000-000000000000|ffffffff-ffff-ffff-ffff-ffffffffffff)$
                code:
                  type: string
                  maxLength: 10000
                location:
                  anyOf:
                    - type: string
                      maxLength: 10000
                    - type: "null"
              required:
                - id
                - code
                - location
          required:
            - id
            - name
            - sku
            - barcode
            - location
            - type
            - quantity
            - cover
        original:
          type: object
          properties:
            id:
              type: string
              format: uuid
              pattern: ^([0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[1-8][0-9a-fA-F]{3}-[89abAB][0-9a-fA-F]{3}-[0-9a-fA-F]{12}|00000000-0000-0000-0000-000000000000|ffffffff-ffff-ffff-ffff-ffffffffffff)$
            name:
              type: string
              maxLength: 10000
            subtitle:
              type: string
              maxLength: 10000
            sku:
              anyOf:
                - type: string
                  maxLength: 10000
                - type: "null"
            barcode:
              anyOf:
                - type: string
                  maxLength: 10000
                - type: "null"
            location:
              anyOf:
                - type: string
                  maxLength: 10000
                - type: "null"
            type:
              type: string
              maxLength: 64
            quantity:
              type: number
            cover:
              anyOf:
                - type: object
                  properties:
                    url:
                      type: string
                      maxLength: 8192
                    altText:
                      type: string
                      maxLength: 10000
                  required:
                    - url
                    - altText
                - type: "null"
            stockUnit:
              type: object
              properties:
                id:
                  type: string
                  format: uuid
                  pattern: ^([0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[1-8][0-9a-fA-F]{3}-[89abAB][0-9a-fA-F]{3}-[0-9a-fA-F]{12}|00000000-0000-0000-0000-000000000000|ffffffff-ffff-ffff-ffff-ffffffffffff)$
                code:
                  type: string
                  maxLength: 10000
                location:
                  anyOf:
                    - type: string
                      maxLength: 10000
                    - type: "null"
              required:
                - id
                - code
                - location
          required:
            - id
            - name
            - sku
            - barcode
            - location
            - type
            - quantity
            - cover
        setup:
          type: object
          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"
            inverted:
              type: boolean
            id:
              type: string
              maxLength: 100
            revision:
              type: integer
              exclusiveMinimum: 0
              maximum: 9007199254740991
            createdAt:
              type: string
              maxLength: 10000
            updatedAt:
              type: string
              maxLength: 10000
          required:
            - name
            - widthMm
            - heightMm
            - elements
            - id
            - revision
            - createdAt
            - updatedAt
          additionalProperties: false
      required:
        - id
        - name
        - template
        - tape
        - widthMm
        - data
        - preview
        - resource
        - original
        - setup
    OrganizationSummary:
      type: object
      additionalProperties: false
      required: [id, name, slug, isReadOnly, allowNegativeStock]
      properties:
        id: { type: string, format: uuid }
        name: { type: string, minLength: 1, maxLength: 160 }
        slug:
          type: string
          minLength: 1
          maxLength: 80
          pattern: '^[a-z0-9]+(?:-[a-z0-9]+)*$'
        isReadOnly: { type: boolean }
        allowNegativeStock: { type: boolean }
    OrganizationMembership:
      type: object
      additionalProperties: false
      required: [id, name, slug, isReadOnly, allowNegativeStock, role, roleName]
      properties:
        id: { type: string, format: uuid }
        name: { type: string, minLength: 1, maxLength: 160 }
        slug:
          type: string
          minLength: 1
          maxLength: 80
          pattern: '^[a-z0-9]+(?:-[a-z0-9]+)*$'
          description: Short, editable identifier used as the first Webapp URL segment.
        role:
          type: [string, "null"]
          pattern: '^[a-z][a-z0-9_-]{0,63}$'
        roleName: { type: [string, "null"] }
        isReadOnly: { type: boolean }
        allowNegativeStock:
          type: boolean
          description: Whether bulk stock balances may fall below zero in this organization.
        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 }
    WooCommerceConnectionInput:
      type: object
      additionalProperties: false
      required: [storeUrl, consumerKey, consumerSecret]
      properties:
        storeUrl:
          type: string
          format: uri
          minLength: 1
          maxLength: 2048
          description: Public HTTPS WooCommerce store URL, optionally including a WordPress subdirectory. Query strings and fragments are rejected.
          examples: [https://shop.example.com]
        consumerKey:
          type: string
          minLength: 8
          maxLength: 200
          pattern: '^ck_'
          writeOnly: true
        consumerSecret:
          type: string
          minLength: 8
          maxLength: 200
          pattern: '^cs_'
          writeOnly: true
    WooCommerceConnection:
      type: object
      additionalProperties: false
      required: [id, storeUrl, consumerKeyHint, status, syncEnabled, lastCheckedAt, lastSuccessAt, lastError, lastWebhookAt, lastSyncAt, lastSyncError, createdAt, updatedAt]
      properties:
        id: { type: string, format: uuid }
        storeUrl: { type: string, format: uri }
        consumerKeyHint:
          type: string
          description: Redacted Consumer Key. The complete key and secret are never returned.
          examples: [ck_1234…cdef]
        status: { type: string, enum: [connected, error] }
        syncEnabled: { type: boolean }
        lastCheckedAt: { type: [string, "null"], format: date-time }
        lastSuccessAt: { type: [string, "null"], format: date-time }
        lastError: { type: [string, "null"] }
        lastWebhookAt: { type: [string, "null"], format: date-time }
        lastSyncAt: { type: [string, "null"], format: date-time }
        lastSyncError: { type: [string, "null"] }
        createdAt: { type: string, format: date-time }
        updatedAt: { type: string, format: date-time }
    WooCommerceConnectionTest:
      type: object
      additionalProperties: false
      required: [productCount, checkedAt]
      properties:
        productCount:
          type: [integer, "null"]
          minimum: 0
          description: Product count reported by WooCommerce when available.
        checkedAt: { type: string, format: date-time }
    WooCommerceConnectionStatusResponse:
      type: object
      additionalProperties: false
      required: [connection, sync, encryptionConfigured]
      properties:
        connection:
          oneOf:
            - { $ref: "#/components/schemas/WooCommerceConnection" }
            - { type: "null" }
        sync:
          oneOf:
            - { $ref: "#/components/schemas/WooCommerceSyncOverview" }
            - { type: "null" }
        encryptionConfigured: { type: boolean }
    WooCommerceSyncPatch:
      type: object
      additionalProperties: false
      required: [syncEnabled]
      properties:
        syncEnabled: { type: boolean }
    WooCommerceManualSyncRequest:
      oneOf:
        - type: object
          additionalProperties: false
          required: [orderId]
          properties:
            orderId: { type: integer, minimum: 1 }
        - type: object
          additionalProperties: false
          required: [window]
          properties:
            window: { type: string, const: last-7-days }
        - type: object
          additionalProperties: false
          maxProperties: 0
    WooCommerceOrderLineIssue:
      type: object
      additionalProperties: false
      required: [lineItemId, sku, status, error]
      properties:
        lineItemId: { type: integer, minimum: 1 }
        sku: { type: string }
        status: { type: string, enum: [unmapped, error] }
        error: { type: [string, "null"] }
    WooCommerceOrderSync:
      type: object
      additionalProperties: false
      required: [orderId, localOrderId, orderNumber, orderStatus, status, totalLines, syncedLines, lastError, updatedAt, issues]
      properties:
        orderId: { type: integer, minimum: 1 }
        localOrderId: { type: [string, "null"], format: uuid }
        orderNumber: { type: string }
        orderStatus: { type: string }
        status: { type: string, enum: [succeeded, partial, failed] }
        totalLines: { type: integer, minimum: 0 }
        syncedLines: { type: integer, minimum: 0 }
        lastError: { type: [string, "null"] }
        updatedAt: { type: string, format: date-time }
        issues:
          type: array
          items: { $ref: "#/components/schemas/WooCommerceOrderLineIssue" }
    WooCommerceSyncOverview:
      type: object
      additionalProperties: false
      required: [totalOrders, issueOrders, recentOrders]
      properties:
        totalOrders: { type: integer, minimum: 0 }
        issueOrders: { type: integer, minimum: 0 }
        recentOrders:
          type: array
          items: { $ref: "#/components/schemas/WooCommerceOrderSync" }
    WooCommerceConnectionMutationResponse:
      type: object
      additionalProperties: false
      required: [connection, test]
      properties:
        connection: { $ref: "#/components/schemas/WooCommerceConnection" }
        test: { $ref: "#/components/schemas/WooCommerceConnectionTest" }
    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
        - contacts.read
        - contacts.manage
        - 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, attributes, 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"] }
        attributes:
          type: object
          additionalProperties: true
          description: System-managed typed relationship metadata. `variant_of` stores inherited-field overrides here.
        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
          properties:
            type:
              type: string
              const: text
            id:
              type: string
              format: uuid
              pattern: ^([0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[1-8][0-9a-fA-F]{3}-[89abAB][0-9a-fA-F]{3}-[0-9a-fA-F]{12}|00000000-0000-0000-0000-000000000000|ffffffff-ffff-ffff-ffff-ffffffffffff)$
            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
            layer:
              type: integer
              minimum: 0
              maximum: 31
            autoWidth:
              type: boolean
            fontSizeMm:
              type: number
              exclusiveMinimum: 0
              maximum: 100
            minFontSizeMm:
              type: number
              exclusiveMinimum: 0
              maximum: 100
            fontFamily:
              type: string
              enum:
                - sans
                - serif
                - monospace
                - rounded
                - barlow-semi-condensed
                - inter
                - roboto
                - roboto-condensed
                - nunito-sans
            fontWeight:
              type: integer
              minimum: 100
              maximum: 900
              multipleOf: 100
            letterSpacingEm:
              type: number
              minimum: -0.1
              maximum: 1
            lineHeight:
              type: number
              minimum: 0.8
              maximum: 3
            align:
              type: string
              enum:
                - left
                - center
                - right
            verticalAlign:
              type: string
              enum:
                - top
                - middle
                - bottom
            textOverflow:
              type: string
              enum:
                - ellipsis
                - shrink
            content:
              maxItems: 500
              type: array
              items:
                type: object
                properties:
                  text:
                    type: string
                    maxLength: 8000
                  weight:
                    type: integer
                    minimum: 100
                    maximum: 900
                    multipleOf: 100
                  italic:
                    type: boolean
                  underline:
                    type: boolean
                required:
                  - text
                additionalProperties: false
          required:
            - type
            - id
            - x
            - y
            - width
            - height
            - visible
            - content
          additionalProperties: false
        - type: object
          properties:
            type:
              type: string
              const: background
            id:
              type: string
              format: uuid
              pattern: ^([0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[1-8][0-9a-fA-F]{3}-[89abAB][0-9a-fA-F]{3}-[0-9a-fA-F]{12}|00000000-0000-0000-0000-000000000000|ffffffff-ffff-ffff-ffff-ffffffffffff)$
            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
            layer:
              type: integer
              minimum: 0
              maximum: 31
            source:
              type: string
              maxLength: 3000000
              pattern: ^data:image\/(?:png|jpeg|webp|gif|avif|svg\+xml);base64,[a-z0-9+/=\s]+$
            fit:
              type: string
              enum:
                - cover
                - contain
            opacity:
              type: number
              minimum: 0
              maximum: 1
          required:
            - type
            - x
            - y
            - width
            - height
            - visible
          additionalProperties: false
        - type: object
          properties:
            type:
              type: string
              const: qr
            id:
              type: string
              format: uuid
              pattern: ^([0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[1-8][0-9a-fA-F]{3}-[89abAB][0-9a-fA-F]{3}-[0-9a-fA-F]{12}|00000000-0000-0000-0000-000000000000|ffffffff-ffff-ffff-ffff-ffffffffffff)$
            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
            layer:
              type: integer
              minimum: 0
              maximum: 31
            foregroundColor:
              type: string
              pattern: ^#[0-9a-f]{6}$
            backgroundColor:
              type: string
              pattern: ^#[0-9a-f]{6}$
            quietZoneModules:
              type: integer
              minimum: 0
              maximum: 4
          required:
            - type
            - x
            - y
            - width
            - height
            - visible
          additionalProperties: false
        - type: object
          properties:
            type:
              type: string
              const: image
            id:
              type: string
              format: uuid
              pattern: ^([0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[1-8][0-9a-fA-F]{3}-[89abAB][0-9a-fA-F]{3}-[0-9a-fA-F]{12}|00000000-0000-0000-0000-000000000000|ffffffff-ffff-ffff-ffff-ffffffffffff)$
            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
            layer:
              type: integer
              minimum: 0
              maximum: 31
            source:
              type: string
              maxLength: 3000000
              pattern: ^data:image\/(?:png|jpeg|webp|gif|avif|svg\+xml);base64,[a-z0-9+/=\s]+$
            altText:
              type: string
              maxLength: 300
            fit:
              type: string
              enum:
                - cover
                - contain
            photoOptimization:
              type: string
              enum:
                - auto
                - standard
            cropWhiteMargins:
              type: boolean
            cropTolerance:
              type: number
              minimum: 0
              maximum: 80
            cropPadding:
              type: number
              minimum: -10
              maximum: 20
          required:
            - type
            - x
            - y
            - width
            - height
            - visible
          additionalProperties: false
        - type: object
          properties:
            type:
              type: string
              const: name
            id:
              type: string
              format: uuid
              pattern: ^([0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[1-8][0-9a-fA-F]{3}-[89abAB][0-9a-fA-F]{3}-[0-9a-fA-F]{12}|00000000-0000-0000-0000-000000000000|ffffffff-ffff-ffff-ffff-ffffffffffff)$
            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
            layer:
              type: integer
              minimum: 0
              maximum: 31
            autoWidth:
              type: boolean
            fontSizeMm:
              type: number
              exclusiveMinimum: 0
              maximum: 100
            minFontSizeMm:
              type: number
              exclusiveMinimum: 0
              maximum: 100
            fontFamily:
              type: string
              enum:
                - sans
                - serif
                - monospace
                - rounded
                - barlow-semi-condensed
                - inter
                - roboto
                - roboto-condensed
                - nunito-sans
            fontWeight:
              type: integer
              minimum: 100
              maximum: 900
              multipleOf: 100
            letterSpacingEm:
              type: number
              minimum: -0.1
              maximum: 1
            lineHeight:
              type: number
              minimum: 0.8
              maximum: 3
            align:
              type: string
              enum:
                - left
                - center
                - right
            verticalAlign:
              type: string
              enum:
                - top
                - middle
                - bottom
            textOverflow:
              type: string
              enum:
                - ellipsis
                - shrink
            content:
              maxItems: 500
              type: array
              items:
                type: object
                properties:
                  text:
                    type: string
                    maxLength: 8000
                  weight:
                    type: integer
                    minimum: 100
                    maximum: 900
                    multipleOf: 100
                  italic:
                    type: boolean
                  underline:
                    type: boolean
                required:
                  - text
                additionalProperties: false
          required:
            - type
            - x
            - y
            - width
            - height
            - visible
          additionalProperties: false
        - type: object
          properties:
            type:
              type: string
              const: subtitle
            id:
              type: string
              format: uuid
              pattern: ^([0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[1-8][0-9a-fA-F]{3}-[89abAB][0-9a-fA-F]{3}-[0-9a-fA-F]{12}|00000000-0000-0000-0000-000000000000|ffffffff-ffff-ffff-ffff-ffffffffffff)$
            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
            layer:
              type: integer
              minimum: 0
              maximum: 31
            autoWidth:
              type: boolean
            fontSizeMm:
              type: number
              exclusiveMinimum: 0
              maximum: 100
            minFontSizeMm:
              type: number
              exclusiveMinimum: 0
              maximum: 100
            fontFamily:
              type: string
              enum:
                - sans
                - serif
                - monospace
                - rounded
                - barlow-semi-condensed
                - inter
                - roboto
                - roboto-condensed
                - nunito-sans
            fontWeight:
              type: integer
              minimum: 100
              maximum: 900
              multipleOf: 100
            letterSpacingEm:
              type: number
              minimum: -0.1
              maximum: 1
            lineHeight:
              type: number
              minimum: 0.8
              maximum: 3
            align:
              type: string
              enum:
                - left
                - center
                - right
            verticalAlign:
              type: string
              enum:
                - top
                - middle
                - bottom
            textOverflow:
              type: string
              enum:
                - ellipsis
                - shrink
            content:
              maxItems: 500
              type: array
              items:
                type: object
                properties:
                  text:
                    type: string
                    maxLength: 8000
                  weight:
                    type: integer
                    minimum: 100
                    maximum: 900
                    multipleOf: 100
                  italic:
                    type: boolean
                  underline:
                    type: boolean
                required:
                  - text
                additionalProperties: false
          required:
            - type
            - x
            - y
            - width
            - height
            - visible
          additionalProperties: false
        - type: object
          properties:
            type:
              type: string
              const: identifier
            id:
              type: string
              format: uuid
              pattern: ^([0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[1-8][0-9a-fA-F]{3}-[89abAB][0-9a-fA-F]{3}-[0-9a-fA-F]{12}|00000000-0000-0000-0000-000000000000|ffffffff-ffff-ffff-ffff-ffffffffffff)$
            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
            layer:
              type: integer
              minimum: 0
              maximum: 31
            autoWidth:
              type: boolean
            fontSizeMm:
              type: number
              exclusiveMinimum: 0
              maximum: 100
            minFontSizeMm:
              type: number
              exclusiveMinimum: 0
              maximum: 100
            fontFamily:
              type: string
              enum:
                - sans
                - serif
                - monospace
                - rounded
                - barlow-semi-condensed
                - inter
                - roboto
                - roboto-condensed
                - nunito-sans
            fontWeight:
              type: integer
              minimum: 100
              maximum: 900
              multipleOf: 100
            letterSpacingEm:
              type: number
              minimum: -0.1
              maximum: 1
            lineHeight:
              type: number
              minimum: 0.8
              maximum: 3
            align:
              type: string
              enum:
                - left
                - center
                - right
            verticalAlign:
              type: string
              enum:
                - top
                - middle
                - bottom
            textOverflow:
              type: string
              enum:
                - ellipsis
                - shrink
            content:
              maxItems: 500
              type: array
              items:
                type: object
                properties:
                  text:
                    type: string
                    maxLength: 8000
                  weight:
                    type: integer
                    minimum: 100
                    maximum: 900
                    multipleOf: 100
                  italic:
                    type: boolean
                  underline:
                    type: boolean
                required:
                  - text
                additionalProperties: false
          required:
            - type
            - x
            - y
            - width
            - height
            - visible
          additionalProperties: false
        - type: object
          properties:
            type:
              type: string
              const: barcode
            id:
              type: string
              format: uuid
              pattern: ^([0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[1-8][0-9a-fA-F]{3}-[89abAB][0-9a-fA-F]{3}-[0-9a-fA-F]{12}|00000000-0000-0000-0000-000000000000|ffffffff-ffff-ffff-ffff-ffffffffffff)$
            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
            layer:
              type: integer
              minimum: 0
              maximum: 31
          required:
            - type
            - x
            - y
            - width
            - height
            - visible
          additionalProperties: false
        - type: object
          properties:
            type:
              type: string
              const: url
            id:
              type: string
              format: uuid
              pattern: ^([0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[1-8][0-9a-fA-F]{3}-[89abAB][0-9a-fA-F]{3}-[0-9a-fA-F]{12}|00000000-0000-0000-0000-000000000000|ffffffff-ffff-ffff-ffff-ffffffffffff)$
            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
            layer:
              type: integer
              minimum: 0
              maximum: 31
            autoWidth:
              type: boolean
            fontSizeMm:
              type: number
              exclusiveMinimum: 0
              maximum: 100
            minFontSizeMm:
              type: number
              exclusiveMinimum: 0
              maximum: 100
            fontFamily:
              type: string
              enum:
                - sans
                - serif
                - monospace
                - rounded
                - barlow-semi-condensed
                - inter
                - roboto
                - roboto-condensed
                - nunito-sans
            fontWeight:
              type: integer
              minimum: 100
              maximum: 900
              multipleOf: 100
            letterSpacingEm:
              type: number
              minimum: -0.1
              maximum: 1
            lineHeight:
              type: number
              minimum: 0.8
              maximum: 3
            align:
              type: string
              enum:
                - left
                - center
                - right
            verticalAlign:
              type: string
              enum:
                - top
                - middle
                - bottom
            textOverflow:
              type: string
              enum:
                - ellipsis
                - shrink
            content:
              maxItems: 500
              type: array
              items:
                type: object
                properties:
                  text:
                    type: string
                    maxLength: 8000
                  weight:
                    type: integer
                    minimum: 100
                    maximum: 900
                    multipleOf: 100
                  italic:
                    type: boolean
                  underline:
                    type: boolean
                required:
                  - text
                additionalProperties: false
          required:
            - type
            - x
            - y
            - width
            - height
            - visible
          additionalProperties: false
        - type: object
          properties:
            type:
              type: string
              const: location
            id:
              type: string
              format: uuid
              pattern: ^([0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[1-8][0-9a-fA-F]{3}-[89abAB][0-9a-fA-F]{3}-[0-9a-fA-F]{12}|00000000-0000-0000-0000-000000000000|ffffffff-ffff-ffff-ffff-ffffffffffff)$
            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
            layer:
              type: integer
              minimum: 0
              maximum: 31
            autoWidth:
              type: boolean
            fontSizeMm:
              type: number
              exclusiveMinimum: 0
              maximum: 100
            minFontSizeMm:
              type: number
              exclusiveMinimum: 0
              maximum: 100
            fontFamily:
              type: string
              enum:
                - sans
                - serif
                - monospace
                - rounded
                - barlow-semi-condensed
                - inter
                - roboto
                - roboto-condensed
                - nunito-sans
            fontWeight:
              type: integer
              minimum: 100
              maximum: 900
              multipleOf: 100
            letterSpacingEm:
              type: number
              minimum: -0.1
              maximum: 1
            lineHeight:
              type: number
              minimum: 0.8
              maximum: 3
            align:
              type: string
              enum:
                - left
                - center
                - right
            verticalAlign:
              type: string
              enum:
                - top
                - middle
                - bottom
            textOverflow:
              type: string
              enum:
                - ellipsis
                - shrink
            content:
              maxItems: 500
              type: array
              items:
                type: object
                properties:
                  text:
                    type: string
                    maxLength: 8000
                  weight:
                    type: integer
                    minimum: 100
                    maximum: 900
                    multipleOf: 100
                  italic:
                    type: boolean
                  underline:
                    type: boolean
                required:
                  - text
                additionalProperties: false
          required:
            - type
            - x
            - y
            - width
            - height
            - visible
          additionalProperties: false
      description: Normalized percentage box; x + width and y + height must not exceed 100. Text content
        totals at most 8000 characters. Optional IDs distinguish multiple elements of the same type.
    LabelElements:
      maxItems: 32
      type: array
      items:
        $ref: "#/components/schemas/LabelElement"
      description: Up to 32 elements with unique identities (id, or type for legacy elements). Visible
        images must not overlap visible QR codes. At most one text element may use autoWidth to control
        the physical label length.
    LabelSetupInput:
      type: object
      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"
        inverted:
          type: boolean
      required:
        - name
        - widthMm
        - heightMm
        - elements
      additionalProperties: false
    LabelSetupPatch:
      type: object
      properties:
        revision:
          type: integer
          exclusiveMinimum: 0
          maximum: 9007199254740991
        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"
        inverted:
          type: boolean
      required:
        - revision
      additionalProperties: false
      description: Supply the last observed revision and at least one changed property. A stale revision
        returns 409.
    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
        inverted:
          type: boolean
    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 }
        resourceIds:
          type: array
          maxItems: 24
          uniqueItems: true
          description: Configured targets; older clients may omit this to use resourceId as the only target.
          items: { type: string, format: uuid }
        targetSelectionMode: { type: string, enum: [all, radio, checkbox], default: all }
        allowVariantSelection: { type: boolean, default: false }
        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 }
        resourceIds:
          type: array
          maxItems: 24
          uniqueItems: true
          items: { type: string, format: uuid }
        targetSelectionMode: { type: string, enum: [all, radio, checkbox] }
        allowVariantSelection: { type: boolean }
        slotKey:
          type: string
          minLength: 1
          maxLength: 80
          pattern: "^[A-Za-z0-9_-]+$"
          description: Stable logical BOM slot. Omit only for backward-compatible matching or creation.
        quantityPerAssembly: { type: integer, minimum: 1 }
        position: { type: integer, minimum: 0 }
        note: { type: string, maxLength: 20000 }
    BomCover:
      type: object
      required: [url, altText]
      additionalProperties: false
      properties:
        url: { type: string }
        altText: { type: string }
    BomComponent:
      allOf:
        - { $ref: "#/components/schemas/BomComponentInput" }
        - type: object
          required: [id, slotKey, origin, name, availableQuantity, trackingMode, cover, availableUnits, choices]
          properties:
            id: { type: string, format: uuid }
            slotKey: { type: string, minLength: 1, maxLength: 80 }
            origin:
              type: string
              enum: [local, inherited, override, variant]
              description: Whether this effective row is local, unchanged from the primary, replaces a primary slot, or exists only on the variant.
            name: { type: string }
            sku: { type: [string, "null"] }
            availableQuantity: { type: integer, minimum: 0 }
            trackingMode: { type: string, enum: [bulk, serialized] }
            cover:
              oneOf:
                - { $ref: "#/components/schemas/BomCover" }
                - { type: "null" }
            availableUnits:
              type: array
              items:
                type: object
                required: [id, code, location]
                properties:
                  id: { type: string, format: uuid }
                  code: { type: string }
                  location: { type: [string, "null"] }
            choices:
              type: array
              minItems: 1
              description: The BOM component itself followed by readable direct first-class variants that may be consumed for this slot during a build.
              items:
                type: object
                required: [resourceId, name, sku, availableQuantity, trackingMode, cover, availableUnits, isPrimary]
                properties:
                  resourceId: { type: string, format: uuid }
                  name: { type: string }
                  sku: { type: [string, "null"] }
                  availableQuantity: { type: integer }
                  trackingMode: { type: string, enum: [bulk, serialized] }
                  cover:
                    oneOf:
                      - { $ref: "#/components/schemas/BomCover" }
                      - { type: "null" }
                  availableUnits:
                    type: array
                    items:
                      type: object
                      required: [id, code, location]
                      properties:
                        id: { type: string, format: uuid }
                        code: { type: string }
                        location: { type: [string, "null"] }
                  isPrimary: { type: boolean }
    BomParent:
      type: object
      required: [id, slotKey, resourceId, name, type, status, quantityPerAssembly, position, note, origin]
      additionalProperties: false
      properties:
        id:
          type: string
          description: Stored BOM row id or a stable inherited-row identifier.
        slotKey: { type: string, minLength: 1, maxLength: 80 }
        resourceId: { type: string, format: uuid }
        name: { type: string }
        type: { type: string }
        status: { type: string }
        quantityPerAssembly: { type: integer, minimum: 1 }
        position: { type: integer, minimum: 0 }
        note: { type: string }
        origin:
          type: string
          enum: [local, inherited, override, variant]
    BomDetail:
      type: object
      required: [resource, components, buildableQuantity, inheritance]
      properties:
        resource:
          type: object
          required: [id, name, quantity, trackingMode]
          properties:
            id: { type: string, format: uuid }
            name: { type: string }
            quantity: { type: integer }
            trackingMode: { type: string, enum: [bulk, serialized] }
        components: { type: array, items: { $ref: "#/components/schemas/BomComponent" } }
        buildableQuantity: { type: integer, minimum: 0 }
        inheritance:
          oneOf:
            - type: object
              required: [primaryResourceId, primaryName, overrideCount]
              properties:
                primaryResourceId: { type: string, format: uuid }
                primaryName: { type: string }
                overrideCount:
                  type: integer
                  minimum: 0
                  description: Stored removals, replacements, and variant-only slots.
            - type: "null"
    AssemblyOutputConfiguration:
      type: object
      minProperties: 1
      maxProperties: 100
      description: Complete selection of variable BOM slots for a finished product. Each value must be the slot's component or an active member of its component family. Mutually exclusive with outputResourceId and componentResourceSelections. Reuses a matching finished article or creates just this configuration atomically with the build. Existing manual recipe overrides are preserved. No Cartesian product is stored.
      propertyNames:
        pattern: "^[A-Za-z0-9_-]{1,80}$"
      additionalProperties: { type: string, format: uuid }
    AssemblyBuildInput:
      type: object
      required: [quantity]
      additionalProperties: false
      properties:
        outputResourceId:
          type: string
          format: uuid
          description: Finished item to credit. Must be the requested item or a direct variant. Required when the requested item has finished variants or variable component slots unless outputConfiguration is supplied; componentResourceSelections never infer the output. Explicitly use the primary ID for unassigned output. When building a variant directly, its ID is the default output. Missing or conflicting output selections return 422.
        outputConfiguration:
          $ref: "#/components/schemas/AssemblyOutputConfiguration"
        quantity: { type: integer, minimum: 1, maximum: 1000 }
        occurredAt: { type: string, format: date-time }
        location: { type: [string, "null"], maxLength: 240 }
        note: { type: string, maxLength: 20000 }
        componentResourceSelections:
          type: object
          description: Maps stable BOM slot keys to either that slot's component resource or an active member of its first-class component family. The selected resource is consumed and snapshotted in build history. When the output is a finished variant, selections must match its complete effective BOM; conflicting substitutions return 422 before any stock is booked.
          propertyNames:
            pattern: "^[A-Za-z0-9_-]{1,80}$"
          additionalProperties:
            type: string
            format: uuid
        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 }
        materialCosts:
          type: object
          additionalProperties: { type: integer, minimum: 0 }
          description: Actual consumed material costs in cents, grouped by ISO currency.
        unpricedComponentQuantity: { type: integer, minimum: 0 }
        costEstimated: { type: boolean }
        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, costs, unpricedQuantity, costEstimated]
            properties:
              resourceId: { type: [string, "null"], format: uuid }
              name: { type: string }
              sku: { type: [string, "null"] }
              quantityPerAssembly: { type: integer, minimum: 1 }
              quantityConsumed: { type: integer, minimum: 1 }
              costs:
                type: object
                additionalProperties: { type: integer, minimum: 0 }
              unpricedQuantity: { type: integer, minimum: 0 }
              costEstimated: { type: boolean }
              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]
      oneOf:
        - required: [orderedQuantity]
        - required: [purchaseQuantity]
      additionalProperties: false
      properties:
        resourceId: { type: string, format: uuid }
        orderedQuantity: { type: integer, minimum: 1, description: Quantity in base stock units. Mutually exclusive with purchaseQuantity. }
        purchaseQuantity: { type: integer, minimum: 1, description: Quantity in the configured purchase unit. Mutually exclusive with orderedQuantity. }
        expectedAt: { type: [string, "null"], format: date-time }
        note: { type: string, maxLength: 20000 }
        unitPriceCents: { type: [integer, "null"], minimum: 0, maximum: 2000000000 }
        priceCurrency: { type: [string, "null"], minLength: 3, maxLength: 3 }
    PurchaseOrderInput:
      type: object
      required: [lines]
      additionalProperties: false
      properties:
        reference: { type: [string, "null"], maxLength: 160 }
        contactId: { type: [string, "null"], format: uuid }
        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, resourceCurrency, baseUnitName, orderedQuantity, receivedQuantity, openQuantity, purchaseUnitName, purchaseUnitFactor, orderedPurchaseQuantity, receivedPurchaseQuantity, openPurchaseQuantity, trackingMode, totalPriceCents]
          properties:
            id: { type: string, format: uuid }
            resourceName: { type: string }
            resourceSku: { type: [string, "null"] }
            resourceCurrency: { type: string, minLength: 3, maxLength: 3 }
            baseUnitName: { type: string }
            purchaseUnitName: { type: [string, "null"] }
            purchaseUnitFactor: { type: integer, minimum: 1 }
            orderedPurchaseQuantity: { type: [integer, "null"], minimum: 0 }
            receivedPurchaseQuantity: { type: [integer, "null"], minimum: 0 }
            openPurchaseQuantity: { type: [integer, "null"], minimum: 0 }
            receivedQuantity: { type: integer, minimum: 0 }
            openQuantity: { type: integer, minimum: 0 }
            trackingMode: { type: string, enum: [bulk, serialized] }
            totalPriceCents: { type: [integer, "null"], minimum: 0 }
    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 }
    OrderType:
      type: string
      enum: [purchase, sale, loan]
    OrderStatus:
      type: string
      enum: [draft, ordered, partially-received, received, confirmed, partially-fulfilled, fulfilled, reserved, partially-issued, issued, partially-returned, returned, overdue, cancelled]
    OrderLineInput:
      type: object
      additionalProperties: false
      required: [resourceId, quantity]
      properties:
        resourceId: { type: string, format: uuid }
        quantity: { type: integer, minimum: 1, maximum: 2000000000 }
        expectedAt: { type: [string, "null"], format: date-time }
        note: { type: string, maxLength: 20000 }
        unitPriceCents: { type: [integer, "null"], minimum: 0, maximum: 2000000000 }
        priceCurrency: { type: [string, "null"], minLength: 3, maxLength: 3 }
    OrderInput:
      type: object
      additionalProperties: false
      required: [type, contactId, lines]
      properties:
        type: { $ref: "#/components/schemas/OrderType" }
        contactId: { type: string, format: uuid }
        reference: { type: [string, "null"], maxLength: 160 }
        status: { $ref: "#/components/schemas/OrderStatus" }
        orderedAt: { type: string, format: date-time }
        expectedAt: { type: [string, "null"], format: date-time, description: Required return due date for loans. }
        note: { type: string, maxLength: 20000 }
        lines:
          type: array
          minItems: 1
          maxItems: 100
          items: { $ref: "#/components/schemas/OrderLineInput" }
    OrderPatch:
      type: object
      additionalProperties: false
      minProperties: 1
      properties:
        contactId: { type: string, format: uuid }
        reference: { type: [string, "null"], maxLength: 160 }
        status: { $ref: "#/components/schemas/OrderStatus" }
        orderedAt: { type: string, format: date-time }
        expectedAt: { type: [string, "null"], format: date-time }
        note: { type: string, maxLength: 20000 }
    OrderLine:
      allOf:
        - { $ref: "#/components/schemas/OrderLineInput" }
        - type: object
          required: [id, orderId, resourceName, fulfilledQuantity, returnedQuantity, openQuantity, reservedQuantity, openReservationQuantity, openReturnQuantity, trackingMode, unitName, units]
          properties:
            id: { type: string, format: uuid }
            orderId: { type: string, format: uuid }
            resourceName: { type: string }
            resourceSku: { type: [string, "null"] }
            fulfilledQuantity: { type: integer, minimum: 0 }
            returnedQuantity: { type: integer, minimum: 0 }
            openQuantity: { type: integer, minimum: 0 }
            reservedQuantity: { type: integer, minimum: 0 }
            openReservationQuantity: { type: integer, minimum: 0 }
            openReturnQuantity: { type: integer, minimum: 0 }
            totalPriceCents: { type: [integer, "null"], minimum: 0 }
            trackingMode: { type: string, enum: [bulk, serialized] }
            unitName: { type: string }
            units:
              type: array
              items: { $ref: "#/components/schemas/OrderLineUnit" }
    Order:
      type: object
      required: [id, type, contactName, status, orderedAt, lines, shipments, totalQuantity, totalFulfilled, totalReturned, totalReserved]
      properties:
        id: { type: string, format: uuid }
        type: { $ref: "#/components/schemas/OrderType" }
        contactId: { type: [string, "null"], format: uuid }
        contactName: { type: string }
        reference: { type: [string, "null"] }
        status: { $ref: "#/components/schemas/OrderStatus" }
        orderedAt: { type: string, format: date-time }
        expectedAt: { type: [string, "null"], format: date-time }
        note: { type: string }
        lines: { type: array, items: { $ref: "#/components/schemas/OrderLine" } }
        shipments: { type: array, items: { $ref: "#/components/schemas/Shipment" } }
        totalQuantity: { type: integer, minimum: 0 }
        totalFulfilled: { type: integer, minimum: 0 }
        totalReturned: { type: integer, minimum: 0 }
        totalReserved: { type: integer, minimum: 0 }
    ShipmentStatus:
      type: string
      enum: [draft, ready, shipped, in_transit, delivered, exception, returned, cancelled]
    ShipmentLineInput:
      type: object
      additionalProperties: false
      required: [orderLineId, quantity]
      properties:
        orderLineId: { type: string, format: uuid }
        quantity: { type: integer, minimum: 1, maximum: 2000000000 }
        unitIds:
          type: array
          maxItems: 100
          uniqueItems: true
          description: Concrete stock-unit ids; required one-for-one for serialized lines and forbidden for bulk lines.
          items: { type: string, format: uuid }
    ShipmentInput:
      type: object
      additionalProperties: false
      required: [carrierCode, lines]
      properties:
        carrierCode: { type: string, pattern: "^[a-z0-9][a-z0-9_-]{0,39}$" }
        service: { type: [string, "null"], minLength: 1, maxLength: 120 }
        trackingNumber: { type: [string, "null"], minLength: 1, maxLength: 180 }
        trackingUrl: { type: [string, "null"], format: uri, maxLength: 2048, description: HTTPS only. Known carriers receive a default URL when omitted. }
        status: { type: string, enum: [draft, ready], default: draft }
        note: { type: string, maxLength: 20000 }
        lines:
          type: array
          minItems: 1
          maxItems: 100
          items: { $ref: "#/components/schemas/ShipmentLineInput" }
    ShipmentPatch:
      type: object
      additionalProperties: false
      minProperties: 1
      properties:
        carrierCode: { type: string, pattern: "^[a-z0-9][a-z0-9_-]{0,39}$" }
        service: { type: [string, "null"], minLength: 1, maxLength: 120 }
        trackingNumber: { type: [string, "null"], minLength: 1, maxLength: 180 }
        trackingUrl: { type: [string, "null"], format: uri, maxLength: 2048, description: HTTPS only. }
        status: { $ref: "#/components/schemas/ShipmentStatus" }
        occurredAt: { type: string, format: date-time, description: Effective status timestamp; requires status. }
        note: { type: string, maxLength: 20000 }
        eventNote: { type: string, maxLength: 2000 }
    ShipmentUnit:
      type: object
      required: [orderLineUnitId, stockUnitId, code]
      properties:
        orderLineUnitId: { type: string, format: uuid }
        stockUnitId: { type: string, format: uuid }
        code: { type: string, maxLength: 180 }
    ShipmentLine:
      type: object
      required: [id, orderLineId, resourceId, resourceName, quantity, units]
      properties:
        id: { type: string, format: uuid }
        orderLineId: { type: string, format: uuid }
        resourceId: { type: string, format: uuid }
        resourceName: { type: string }
        quantity: { type: integer, minimum: 1 }
        units:
          type: array
          items: { $ref: "#/components/schemas/ShipmentUnit" }
    ShipmentEvent:
      type: object
      required: [id, fromStatus, toStatus, note, actor, occurredAt]
      properties:
        id: { type: string, format: uuid }
        fromStatus: { anyOf: [{ $ref: "#/components/schemas/ShipmentStatus" }, { type: "null" }] }
        toStatus: { $ref: "#/components/schemas/ShipmentStatus" }
        note: { type: string }
        actor: { type: [string, "null"] }
        occurredAt: { type: string, format: date-time }
    Shipment:
      type: object
      required: [id, orderId, carrierCode, service, trackingNumber, trackingUrl, status, shippedAt, deliveredAt, note, createdBy, updatedBy, createdAt, updatedAt, lines, events, totalQuantity]
      properties:
        id: { type: string, format: uuid }
        orderId: { type: string, format: uuid }
        carrierCode: { type: string }
        service: { type: [string, "null"] }
        trackingNumber: { type: [string, "null"] }
        trackingUrl: { type: [string, "null"], format: uri }
        status: { $ref: "#/components/schemas/ShipmentStatus" }
        shippedAt: { type: [string, "null"], format: date-time }
        deliveredAt: { type: [string, "null"], format: date-time }
        note: { type: string }
        createdBy: { type: [string, "null"] }
        updatedBy: { type: [string, "null"] }
        createdAt: { type: string, format: date-time }
        updatedAt: { type: string, format: date-time }
        lines:
          type: array
          items: { $ref: "#/components/schemas/ShipmentLine" }
        events:
          type: array
          items: { $ref: "#/components/schemas/ShipmentEvent" }
        totalQuantity: { type: integer, minimum: 1 }
    OrderLineMovementInput:
      type: object
      additionalProperties: false
      required: [action, quantity]
      properties:
        action: { type: string, enum: [issue, return] }
        quantity: { type: integer, minimum: 1, maximum: 2000000000 }
        occurredAt: { type: string, format: date-time }
        location: { type: [string, "null"], maxLength: 240 }
        note: { type: string, maxLength: 20000 }
    OrderLineUnit:
      type: object
      required: [id, stockUnitId, code, status, stockStatus, reservedAt, fulfilledAt, returnedAt]
      properties:
        id: { type: string, format: uuid }
        stockUnitId: { type: string, format: uuid }
        code: { type: string, maxLength: 180 }
        status: { type: string, enum: [reserved, fulfilled, returned] }
        stockStatus: { type: string, enum: [available, reserved, in-use, maintenance, consumed, lost, retired] }
        reservedAt: { type: string, format: date-time }
        fulfilledAt: { type: [string, "null"], format: date-time }
        returnedAt: { type: [string, "null"], format: date-time }
    SerializedOrderLineUnits:
      type: object
      required: [line, availableUnits, assignments]
      properties:
        line:
          type: object
          required: [id, resourceId, resourceName, quantity, fulfilledQuantity, returnedQuantity]
          properties:
            id: { type: string, format: uuid }
            resourceId: { type: string, format: uuid }
            resourceName: { type: string }
            quantity: { type: integer, minimum: 1 }
            fulfilledQuantity: { type: integer, minimum: 0 }
            returnedQuantity: { type: integer, minimum: 0 }
        availableUnits:
          type: array
          items:
            type: object
            required: [id, code, status, location]
            properties:
              id: { type: string, format: uuid }
              code: { type: string, maxLength: 180 }
              status: { type: string, const: available }
              location: { type: [string, "null"], maxLength: 240 }
        assignments:
          type: array
          items: { $ref: "#/components/schemas/OrderLineUnit" }
    OrderLineUnitActionInput:
      type: object
      additionalProperties: false
      required: [action, unitIds]
      properties:
        action: { type: string, enum: [reserve, release, issue, return] }
        unitIds:
          type: array
          minItems: 1
          maxItems: 100
          uniqueItems: true
          items: { type: string, format: uuid }
        occurredAt: { type: string, format: date-time }
        note: { type: string, maxLength: 20000 }
    PurchaseReceiptInput:
      type: object
      oneOf:
        - required: [quantity]
        - required: [purchaseQuantity]
      additionalProperties: false
      properties:
        quantity: { type: integer, minimum: 1, maximum: 1000, description: Receipt quantity in base stock units. }
        purchaseQuantity: { type: integer, minimum: 1, maximum: 1000000, description: Receipt quantity in the purchase unit snapshotted on the order line. }
        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 }
        - type: object
          required: [mode, pattern, group]
          additionalProperties: false
          properties:
            mode: { type: string, const: regex }
            pattern: { type: string, minLength: 1, maxLength: 500 }
            flags: { type: string, maxLength: 4, default: "" }
            group: { type: string, minLength: 1, maxLength: 64 }
    ScanCodeType:
      type: string
      enum: [qr_code, data_matrix, aztec, pdf417, code_128, code_93, code_39, codabar, ean_13, ean_8, upc_a, upc_e, itf]
    ScanExtractionSuggestion:
      type: object
      required: [suggestion, model]
      properties:
        suggestion:
          type: object
          required: [pattern, flags, group, explanation]
          properties:
            pattern: { type: string }
            flags: { type: string }
            group: { type: string }
            explanation: { type: string }
        model: { type: string }
    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 }
        storage: { type: string, enum: [custom-field, metadata, execution], default: metadata }
    ScanWorkflowExtractedField:
      type: object
      required: [key, label, extraction]
      additionalProperties: false
      properties:
        key: { type: string, minLength: 1, maxLength: 80, pattern: "^[A-Za-z0-9_.-]+$" }
        label: { type: string, minLength: 1, maxLength: 120 }
        extraction: { $ref: "#/components/schemas/ScanWorkflowExtraction" }
        storage: { type: string, enum: [custom-field, metadata, execution], default: custom-field }
    ScanWorkflowOperation:
      oneOf:
        - type: object
          required: [type]
          additionalProperties: false
          properties:
            type: { type: string, const: unit }
        - type: object
          required: [type, delta]
          additionalProperties: false
          properties:
            type: { type: string, const: stock-adjustment }
            delta: { type: integer, minimum: -1000000, maximum: 1000000, not: { const: 0 } }
        - type: object
          required: [type, quantity]
          additionalProperties: false
          properties:
            type: { type: string, const: assembly-build }
            quantity: { type: integer, minimum: 1, maximum: 1000 }
    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 }
    ActionChainValue:
      oneOf:
        - type: object
          properties:
            source:
              type: string
              const: literal
            value:
              anyOf:
                - type: string
                  maxLength: 20000
                - type: number
                - type: boolean
                - type: "null"
          required:
            - source
            - value
          additionalProperties: false
        - type: object
          properties:
            source:
              type: string
              const: scan
            field:
              type: string
              enum:
                - identifier
                - raw
          required:
            - source
            - field
          additionalProperties: false
        - type: object
          properties:
            source:
              type: string
              const: input
            key:
              type: string
              pattern: ^[A-Za-z0-9_.-]{1,80}$
          required:
            - source
            - key
          additionalProperties: false
        - type: object
          properties:
            source:
              type: string
              const: result
            actionId:
              type: string
              pattern: ^[A-Za-z0-9_-]{1,80}$
            path:
              type: string
              pattern: ^(resourceId|unitId|code|found|status|quantity|metadata\.[A-Za-z0-9_.-]{1,80}|customFields\.[A-Za-z0-9_.-]{1,80})$
          required:
            - source
            - actionId
            - path
          additionalProperties: false
    ActionChainConditions:
      type: object
      properties:
        mode:
          type: string
          enum:
            - all
            - any
        rules:
          minItems: 1
          maxItems: 8
          type: array
          items:
            type: object
            properties:
              left:
                $ref: "#/components/schemas/ActionChainValue"
              operator:
                type: string
                enum:
                  - equals
                  - not-equals
                  - exists
                  - missing
                  - gt
                  - gte
                  - lt
                  - lte
              right:
                $ref: "#/components/schemas/ActionChainValue"
            required:
              - left
              - operator
            additionalProperties: false
      required:
        - mode
        - rules
      additionalProperties: false
    ActionChainTarget:
      oneOf:
        - type: object
          properties:
            source:
              type: string
              const: selected
          required:
            - source
          additionalProperties: false
        - type: object
          properties:
            source:
              type: string
              const: resource
            resourceId:
              type: string
              format: uuid
              pattern: ^([0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[1-8][0-9a-fA-F]{3}-[89abAB][0-9a-fA-F]{3}-[0-9a-fA-F]{12}|00000000-0000-0000-0000-000000000000|ffffffff-ffff-ffff-ffff-ffffffffffff)$
          required:
            - source
            - resourceId
          additionalProperties: false
        - type: object
          properties:
            source:
              type: string
              const: result
            actionId:
              type: string
              pattern: ^[A-Za-z0-9_-]{1,80}$
          required:
            - source
            - actionId
          additionalProperties: false
    ActionChainAction:
      oneOf:
        - type: object
          properties:
            id:
              type: string
              pattern: ^[A-Za-z0-9_-]{1,80}$
            label:
              type: string
              minLength: 1
              maxLength: 160
            enabled:
              default: true
              type: boolean
            when:
              default: null
              anyOf:
                - $ref: "#/components/schemas/ActionChainConditions"
                - type: "null"
            target:
              $ref: "#/components/schemas/ActionChainTarget"
            code:
              $ref: "#/components/schemas/ActionChainValue"
              default:
                source: scan
                field: identifier
            type:
              type: string
              const: find-unit
            allowMissing:
              default: false
              type: boolean
          required:
            - id
            - label
            - target
            - type
          additionalProperties: false
        - type: object
          properties:
            id:
              type: string
              pattern: ^[A-Za-z0-9_-]{1,80}$
            label:
              type: string
              minLength: 1
              maxLength: 160
            enabled:
              default: true
              type: boolean
            when:
              default: null
              anyOf:
                - $ref: "#/components/schemas/ActionChainConditions"
                - type: "null"
            target:
              $ref: "#/components/schemas/ActionChainTarget"
            code:
              $ref: "#/components/schemas/ActionChainValue"
              default:
                source: scan
                field: identifier
            properties:
              default: []
              maxItems: 24
              type: array
              items:
                type: object
                properties:
                  key:
                    type: string
                    pattern: ^[A-Za-z0-9_.-]{1,80}$
                  storage:
                    type: string
                    enum:
                      - metadata
                      - custom-field
                  value:
                    $ref: "#/components/schemas/ActionChainValue"
                required:
                  - key
                  - storage
                  - value
                additionalProperties: false
            applyFlowValues:
              default: false
              type: boolean
            type:
              type: string
              const: unit
            mode:
              type: string
              enum:
                - create
                - update
                - upsert
            status:
              default: null
              anyOf:
                - type: string
                  enum:
                    - available
                    - reserved
                    - in-use
                    - maintenance
                    - consumed
                    - lost
                    - retired
                - type: "null"
          required:
            - id
            - label
            - target
            - type
            - mode
          additionalProperties: false
        - type: object
          properties:
            id:
              type: string
              pattern: ^[A-Za-z0-9_-]{1,80}$
            label:
              type: string
              minLength: 1
              maxLength: 160
            enabled:
              default: true
              type: boolean
            when:
              default: null
              anyOf:
                - $ref: "#/components/schemas/ActionChainConditions"
                - type: "null"
            target:
              $ref: "#/components/schemas/ActionChainTarget"
            code:
              $ref: "#/components/schemas/ActionChainValue"
              default:
                source: scan
                field: identifier
            properties:
              default: []
              maxItems: 24
              type: array
              items:
                type: object
                properties:
                  key:
                    type: string
                    pattern: ^[A-Za-z0-9_.-]{1,80}$
                  storage:
                    type: string
                    enum:
                      - metadata
                      - custom-field
                  value:
                    $ref: "#/components/schemas/ActionChainValue"
                required:
                  - key
                  - storage
                  - value
                additionalProperties: false
            applyFlowValues:
              default: false
              type: boolean
            type:
              type: string
              const: assembly-build
            quantity:
              $ref: "#/components/schemas/ActionChainValue"
            components:
              default: []
              maxItems: 40
              type: array
              items:
                type: object
                properties:
                  slotKey:
                    type: string
                    pattern: ^[A-Za-z0-9_-]{1,80}$
                  resource:
                    $ref: "#/components/schemas/ActionChainValue"
                  unitFromAction:
                    type: string
                    pattern: ^[A-Za-z0-9_-]{1,80}$
                  choice:
                    type: object
                    properties:
                      inputKey:
                        type: string
                        pattern: ^[A-Za-z0-9_.-]{1,80}$
                      resources:
                        type: object
                        propertyNames:
                          type: string
                          minLength: 1
                          maxLength: 120
                        additionalProperties:
                          type: string
                          format: uuid
                          pattern: ^([0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[1-8][0-9a-fA-F]{3}-[89abAB][0-9a-fA-F]{3}-[0-9a-fA-F]{12}|00000000-0000-0000-0000-000000000000|ffffffff-ffff-ffff-ffff-ffffffffffff)$
                    required:
                      - inputKey
                      - resources
                    additionalProperties: false
                required:
                  - slotKey
                additionalProperties: false
          required:
            - id
            - label
            - target
            - type
            - quantity
          additionalProperties: false
        - type: object
          properties:
            id:
              type: string
              pattern: ^[A-Za-z0-9_-]{1,80}$
            label:
              type: string
              minLength: 1
              maxLength: 160
            enabled:
              default: true
              type: boolean
            when:
              default: null
              anyOf:
                - $ref: "#/components/schemas/ActionChainConditions"
                - type: "null"
            type:
              type: string
              const: stock-adjustment
            target:
              $ref: "#/components/schemas/ActionChainTarget"
            delta:
              $ref: "#/components/schemas/ActionChainValue"
            factor:
              default: 1
              anyOf:
                - type: number
                  const: 1
                - type: number
                  const: -1
          required:
            - id
            - label
            - type
            - target
            - delta
          additionalProperties: false
        - type: object
          properties:
            id:
              type: string
              pattern: ^[A-Za-z0-9_-]{1,80}$
            label:
              type: string
              minLength: 1
              maxLength: 160
            enabled:
              default: true
              type: boolean
            when:
              default: null
              anyOf:
                - $ref: "#/components/schemas/ActionChainConditions"
                - type: "null"
            target:
              $ref: "#/components/schemas/ActionChainTarget"
            code:
              $ref: "#/components/schemas/ActionChainValue"
              default:
                source: scan
                field: identifier
            type:
              type: string
              const: set-location
            location:
              $ref: "#/components/schemas/ActionChainValue"
            structured:
              default: true
              type: boolean
          required:
            - id
            - label
            - target
            - type
            - location
          additionalProperties: false
        - type: object
          properties:
            id:
              type: string
              pattern: ^[A-Za-z0-9_-]{1,80}$
            label:
              type: string
              minLength: 1
              maxLength: 160
            enabled:
              default: true
              type: boolean
            when:
              default: null
              anyOf:
                - $ref: "#/components/schemas/ActionChainConditions"
                - type: "null"
            type:
              type: string
              const: assert
            check:
              $ref: "#/components/schemas/ActionChainConditions"
            message:
              type: string
              minLength: 1
              maxLength: 500
          required:
            - id
            - label
            - type
            - check
            - message
          additionalProperties: false
        - type: object
          properties:
            id:
              type: string
              pattern: ^[A-Za-z0-9_-]{1,80}$
            label:
              type: string
              minLength: 1
              maxLength: 160
            enabled:
              default: true
              type: boolean
            when:
              default: null
              anyOf:
                - $ref: "#/components/schemas/ActionChainConditions"
                - type: "null"
            type:
              type: string
              const: webhook
            eventName:
              type: string
              minLength: 1
              maxLength: 120
            properties:
              default: []
              maxItems: 24
              type: array
              items:
                type: object
                properties:
                  key:
                    type: string
                    pattern: ^[A-Za-z0-9_-]{1,80}$
                  value:
                    $ref: "#/components/schemas/ActionChainValue"
                required:
                  - key
                  - value
                additionalProperties: false
          required:
            - id
            - label
            - type
            - eventName
          additionalProperties: false
    ActionChainRunInput:
      type: object
      properties:
        workflowId:
          type: string
          format: uuid
          pattern: ^([0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[1-8][0-9a-fA-F]{3}-[89abAB][0-9a-fA-F]{3}-[0-9a-fA-F]{12}|00000000-0000-0000-0000-000000000000|ffffffff-ffff-ffff-ffff-ffffffffffff)$
        code:
          type: string
          minLength: 1
          maxLength: 2048
        codeType:
          type: [string, "null"]
          enum: [qr_code, data_matrix, aztec, pdf417, code_128, code_93, code_39, codabar, ean_13, ean_8, upc_a, upc_e, itf, null]
        selectedResourceIds:
          default: []
          maxItems: 24
          type: array
          items:
            type: string
            format: uuid
            pattern: ^([0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[1-8][0-9a-fA-F]{3}-[89abAB][0-9a-fA-F]{3}-[0-9a-fA-F]{12}|00000000-0000-0000-0000-000000000000|ffffffff-ffff-ffff-ffff-ffffffffffff)$
        inputs:
          default: {}
          type: object
          propertyNames:
            type: string
            maxLength: 80
          additionalProperties:
            anyOf:
              - type: string
                maxLength: 20000
              - type: number
              - type: boolean
              - maxItems: 12
                type: array
                items:
                  type: string
                  maxLength: 2048
        expectedPlanHash:
          type: string
          pattern: ^[a-f0-9]{64}$
      required:
        - workflowId
        - code
      additionalProperties: false
    ActionChainRunReport:
      type: object
      properties:
        workflowId:
          type: string
          format: uuid
          pattern: ^([0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[1-8][0-9a-fA-F]{3}-[89abAB][0-9a-fA-F]{3}-[0-9a-fA-F]{12}|00000000-0000-0000-0000-000000000000|ffffffff-ffff-ffff-ffff-ffffffffffff)$
        revision:
          type: integer
          minimum: -9007199254740991
          maximum: 9007199254740991
        identifier:
          type: string
        planHash:
          type: string
          pattern: ^[a-f0-9]{64}$
        replayed:
          type: boolean
        steps:
          type: array
          items:
            type: object
            properties:
              id:
                type: string
              label:
                type: string
              type:
                type: string
              skipped:
                type: boolean
              target:
                anyOf:
                  - type: string
                  - type: "null"
              eventName:
                type: string
              code:
                type: string
              quantityBefore:
                type: integer
                minimum: -9007199254740991
                maximum: 9007199254740991
              quantityAfter:
                type: integer
                minimum: -9007199254740991
                maximum: 9007199254740991
              statusBefore:
                anyOf:
                  - type: string
                  - type: "null"
              statusAfter:
                anyOf:
                  - type: string
                  - type: "null"
              locationBefore:
                anyOf:
                  - type: string
                  - type: "null"
              locationAfter:
                anyOf:
                  - type: string
                  - type: "null"
              metadata:
                type: object
                propertyNames:
                  type: string
                additionalProperties: {}
              customFields:
                type: object
                propertyNames:
                  type: string
                additionalProperties: {}
              components:
                type: array
                items:
                  type: object
                  properties:
                    name:
                      type: string
                    quantity:
                      type: integer
                      minimum: -9007199254740991
                      maximum: 9007199254740991
                    codes:
                      type: array
                      items:
                        type: string
                  required:
                    - name
                    - quantity
                    - codes
            required:
              - id
              - label
              - type
              - skipped
              - target
      required:
        - workflowId
        - revision
        - identifier
        - planHash
        - steps
    ScanWorkflowInputField:
      type: object
      required: [key, label, required]
      additionalProperties: false
      properties:
        visibleWhen:
          anyOf:
            - $ref: "#/components/schemas/ActionChainConditions"
            - type: "null"
          description: May depend only on scan values and earlier visible inputs. Hidden
            values are ignored, including required fields.
        key: { type: string, minLength: 1, maxLength: 80, pattern: "^[A-Za-z0-9_.-]+$" }
        label: { type: string, minLength: 1, maxLength: 120 }
        required: { type: boolean }
        type:
          type: string
          enum: [text, textarea, number, checkbox, select, radio, media, file]
          default: select
        storage: { type: string, enum: [custom-field, metadata, execution], default: metadata }
        placeholder: { type: string, maxLength: 240, default: "" }
        options:
          type: array
          maxItems: 40
          default: []
          items: { $ref: "#/components/schemas/ScanWorkflowOption" }
    ScanWorkflowInput:
      type: object
      required: [name, resourceId, extraction, identifierPropertyKey]
      properties:
        actions:
          type: array
          maxItems: 24
          default: []
          description: Ordered action list. Empty preserves the legacy operation. Results
            may reference only earlier actions. At least one action must be enabled.
          items:
            $ref: "#/components/schemas/ActionChainAction"
        oncePerCode:
          type: boolean
          default: false
          description: Deduplicate workflow + extracted identifier + selected target set,
            even with a new execution key.
        name: { type: string, minLength: 1, maxLength: 160 }
        description: { type: string, maxLength: 5000, default: "" }
        enabled: { type: boolean, default: true }
        resourceId: { type: string, format: uuid }
        codeTypes:
          type: array
          minItems: 1
          uniqueItems: true
          default: [qr_code, data_matrix, aztec, pdf417, code_128, code_93, code_39, codabar, ean_13, ean_8, upc_a, upc_e, itf]
          items: { $ref: "#/components/schemas/ScanCodeType" }
        publicTriggerEnabled: { type: boolean, default: false }
        publicTriggerCode:
          type: [string, "null"]
          minLength: 1
          maxLength: 2048
          description: Optional fixed code used by the public URL; when null the public form asks for a code.
        quantityInputKey:
          type: [string, "null"]
          minLength: 1
          maxLength: 80
          pattern: "^[A-Za-z0-9_.-]+$"
          description: Required number input whose positive integer value replaces the configured operation magnitude.
        extraction: { $ref: "#/components/schemas/ScanWorkflowExtraction" }
        identifierPropertyKey: { type: string, minLength: 1, maxLength: 80, pattern: "^[A-Za-z0-9_.-]+$" }
        identifierStorage: { type: string, enum: [custom-field, metadata, execution], default: metadata }
        extractedFields:
          type: array
          maxItems: 24
          default: []
          items: { $ref: "#/components/schemas/ScanWorkflowExtractedField" }
        operation:
          allOf:
            - { $ref: "#/components/schemas/ScanWorkflowOperation" }
          default: { type: unit }
        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: 24
          items: { $ref: "#/components/schemas/ScanWorkflowInputField" }
        triggerWebhook: { type: boolean, default: false }
        webhookEventName: { type: string, minLength: 1, maxLength: 120, default: inventory.action.executed }
    ScanWorkflowPatch:
      type: object
      required: [revision]
      minProperties: 2
      additionalProperties: false
      description: Current revision plus at least one writable ScanWorkflowInput property.
      properties:
        actions:
          type: array
          maxItems: 24
          default: []
          description: Ordered action list. Empty preserves the legacy operation. Results
            may reference only earlier actions. At least one action must be enabled.
          items:
            $ref: "#/components/schemas/ActionChainAction"
        oncePerCode:
          type: boolean
          default: false
          description: Deduplicate workflow + extracted identifier + selected target set,
            even with a new execution key.
        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 }
        codeTypes:
          type: array
          minItems: 1
          uniqueItems: true
          items: { $ref: "#/components/schemas/ScanCodeType" }
        publicTriggerEnabled: { type: boolean }
        publicTriggerCode: { type: [string, "null"], minLength: 1, maxLength: 2048 }
        quantityInputKey: { type: [string, "null"], minLength: 1, maxLength: 80, pattern: "^[A-Za-z0-9_.-]+$" }
        extraction: { $ref: "#/components/schemas/ScanWorkflowExtraction" }
        identifierPropertyKey: { type: string, minLength: 1, maxLength: 80, pattern: "^[A-Za-z0-9_.-]+$" }
        identifierStorage: { type: string, enum: [custom-field, metadata, execution] }
        extractedFields:
          type: array
          maxItems: 24
          items: { $ref: "#/components/schemas/ScanWorkflowExtractedField" }
        operation: { $ref: "#/components/schemas/ScanWorkflowOperation" }
        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: 24
          items: { $ref: "#/components/schemas/ScanWorkflowInputField" }
        triggerWebhook: { type: boolean }
        webhookEventName: { type: string, minLength: 1, maxLength: 120 }
    ScanWorkflow:
      allOf:
        - { $ref: "#/components/schemas/ScanWorkflowInput" }
        - type: object
          required: [id, name, description, enabled, resourceId, resourceIds, targetSelectionMode, allowVariantSelection, codeTypes, publicTriggerEnabled, publicTriggerId, publicTriggerCode, quantityInputKey, revision, extraction, identifierPropertyKey, identifierStorage, extractedFields, operation, createMissingUnit, unitStatus, fixedProperties, inputFields, triggerWebhook, webhookEventName, createdBy, updatedBy, createdAt, updatedAt]
          properties:
            id: { type: string, format: uuid }
            publicTriggerId:
              type: string
              format: uuid
              description: Random bearer identifier used in the public action URL.
            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 }
    ScanWorkflowTargetOption:
      type: object
      additionalProperties: false
      required: [id, name, quantity, trackingMode, updatedAt]
      properties:
        id: { type: string, format: uuid }
        name: { type: string }
        quantity: { type: integer }
        trackingMode: { type: string, enum: [bulk, serialized] }
        updatedAt: { type: string, format: date-time }
    ScanWorkflowTargetGroup:
      type: object
      additionalProperties: false
      required: [resourceId, name, options]
      properties:
        resourceId: { type: string, format: uuid }
        name: { type: string }
        options:
          type: array
          minItems: 1
          items: { $ref: "#/components/schemas/ScanWorkflowTargetOption" }
    StockScanExpectedTarget:
      type: object
      additionalProperties: false
      required: [resourceId, resourceUpdatedAt, unitId, unitUpdatedAt]
      properties:
        resourceId: { type: string, format: uuid }
        resourceUpdatedAt: { type: string, format: date-time }
        unitId: { type: [string, "null"], format: uuid }
        unitUpdatedAt: { type: [string, "null"], format: date-time }
    PublicActionFlow:
      type: object
      additionalProperties: false
      required: [triggerId, name, description, resourceName, targetSelectionMode, allowVariantSelection, targetGroups, requiresCode, identifierLabel, quantityInputKey, inputFields, operation]
      properties:
        triggerId: { type: string, format: uuid }
        name: { type: string }
        description: { type: string }
        resourceName: { type: string }
        targetSelectionMode: { type: string, enum: [all, radio, checkbox] }
        allowVariantSelection: { type: boolean }
        targetGroups:
          type: array
          items: { $ref: "#/components/schemas/ScanWorkflowTargetGroup" }
        requiresCode: { type: boolean }
        identifierLabel: { type: string }
        quantityInputKey: { type: [string, "null"] }
        inputFields:
          type: array
          items: { $ref: "#/components/schemas/ScanWorkflowInputField" }
        operation:
          oneOf:
            - type: object
              required: [type]
              additionalProperties: false
              properties:
                type: { type: string, const: unit }
            - type: object
              required: [type, direction, quantity]
              additionalProperties: false
              properties:
                type: { type: string, const: stock-adjustment }
                direction: { type: string, enum: [in, out] }
                quantity: { type: integer, minimum: 1 }
            - type: object
              required: [type, quantity]
              additionalProperties: false
              properties:
                type: { type: string, const: assembly-build }
                quantity: { type: integer, minimum: 1 }
    PublicActionFlowExecutionInput:
      type: object
      additionalProperties: false
      properties:
        code: { type: string, minLength: 1, maxLength: 2048 }
        selectedResourceIds:
          type: array
          maxItems: 24
          uniqueItems: true
          items: { type: string, format: uuid }
        inputs:
          type: object
          default: {}
          maxProperties: 24
          propertyNames: { minLength: 1, maxLength: 80 }
          additionalProperties:
            oneOf:
              - { type: string, maxLength: 20000 }
              - { type: number }
              - { type: boolean }
              - type: array
                maxItems: 12
                items: { type: string, maxLength: 2048 }
    StockScanResolution:
      type: object
      required: [workflow, resource, resources, targetGroups, selectedResourceIds, expectedTargets, identifier, operation, unit, willCreate, fields, fixedProperties, metadataPreview, customFieldsPreview, executionPreview, 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 }
            trackingMode: { type: string, enum: [bulk, serialized] }
        resources:
          type: array
          items: { $ref: "#/components/schemas/ScanWorkflowTargetOption" }
        targetGroups:
          type: array
          items: { $ref: "#/components/schemas/ScanWorkflowTargetGroup" }
        selectedResourceIds:
          type: array
          uniqueItems: true
          items: { type: string, format: uuid }
        expectedTargets:
          type: array
          items: { $ref: "#/components/schemas/StockScanExpectedTarget" }
        identifier: { type: string, minLength: 1, maxLength: 180 }
        operation: { $ref: "#/components/schemas/ScanWorkflowOperation" }
        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:
          oneOf:
            - { type: string, enum: [available, reserved, in-use, maintenance, consumed, lost, retired] }
            - { type: "null" }
        quantityBefore: { type: integer }
        quantityAfter: { type: integer }
        delta: { type: integer }
        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: 24
          items: { $ref: "#/components/schemas/ScanWorkflowInputField" }
        fixedProperties:
          type: array
          maxItems: 24
          items: { $ref: "#/components/schemas/ScanWorkflowFixedProperty" }
        metadataPreview: { type: object, additionalProperties: true }
        customFieldsPreview: { type: object, additionalProperties: true }
        executionPreview: { type: object, additionalProperties: true }
    StockScanExecutionInput:
      type: object
      required: [workflowId, revision, code, expectedResourceUpdatedAt, expectedUnitId, expectedUnitUpdatedAt]
      additionalProperties: false
      properties:
        expectedPlanHash:
          type: string
          pattern: ^[a-f0-9]{64}$
          description: Required for action chains. Prefer the dedicated action-chains endpoints.
        workflowId: { type: string, format: uuid }
        revision: { type: integer, minimum: 1 }
        code: { type: string, minLength: 1, maxLength: 2048 }
        codeType:
          oneOf:
            - { $ref: "#/components/schemas/ScanCodeType" }
            - { type: "null" }
        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" }
        selectedResourceIds:
          type: array
          maxItems: 24
          uniqueItems: true
          items: { type: string, format: uuid }
        expectedTargets:
          type: array
          maxItems: 24
          items: { $ref: "#/components/schemas/StockScanExpectedTarget" }
        inputs:
          type: object
          default: {}
          description: Operator-provided workflow fields. May be omitted when the workflow has no input fields.
          maxProperties: 24
          propertyNames: { minLength: 1, maxLength: 80 }
          additionalProperties:
            oneOf:
              - { type: string, maxLength: 20000 }
              - { type: number }
              - { type: boolean }
              - type: array
                maxItems: 12
                items: { type: string, maxLength: 2048 }
    StockScanExecution:
      type: object
      required: [workflowId, revision, operation, resource, unit, movement, created, metadataBefore, metadataAfter]
      additionalProperties: true
      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 }
        operation: { $ref: "#/components/schemas/ScanWorkflowOperation" }
        unit:
          oneOf:
            - { $ref: "#/components/schemas/StockUnit" }
            - { type: "null" }
        movement:
          oneOf:
            - { $ref: "#/components/schemas/StockMovement" }
            - { type: "null" }
        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 }
    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 }
            trackingMode: { type: [string, "null"], enum: [bulk, serialized, null] }
        trackingMode: { type: string, enum: [bulk, serialized] }
        assignedQuantity: { type: integer }
        unassignedQuantity: { type: integer }
        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 }
            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 }
        intervalDays: { type: integer, minimum: 1, maximum: 3650 }
        nextDueAt: { type: string, format: date-time }
        lastCompletedAt: { type: [string, "null"], format: date-time }
    InternalRequestStatus:
      type: string
      enum: [submitted, approved, rejected, fulfilled, cancelled]
    InternalRequestCreate:
      type: object
      required: [startsAt, dueAt, lines]
      additionalProperties: false
      properties:
        deliveryResourceId: { type: [string, "null"], format: uuid }
        startsAt: { type: string, format: date-time }
        dueAt: { type: string, format: date-time }
        note: { type: string, maxLength: 20000 }
        lines:
          type: array
          minItems: 1
          maxItems: 50
          items:
            type: object
            required: [resourceId, quantity]
            additionalProperties: false
            properties:
              resourceId: { type: string, format: uuid }
              quantity: { type: integer, minimum: 1, maximum: 2000000000 }
              note: { type: string, maxLength: 20000 }
    InternalRequestAction:
      type: object
      required: [action]
      additionalProperties: false
      properties:
        action: { type: string, enum: [approve, reject, cancel, fulfill] }
        note: { type: string, maxLength: 20000 }
    InternalRequestLine:
      type: object
      required: [id, resource, quantity, note, createdAt, updatedAt]
      properties:
        id: { type: string, format: uuid }
        resource:
          type: object
          required: [id, name, sku, status, currentQuantity, trackingMode]
          properties:
            id: { type: string, format: uuid }
            name: { type: string }
            sku: { type: [string, "null"] }
            status: { type: string }
            currentQuantity: { type: integer }
            trackingMode: { type: string, enum: [bulk, serialized] }
        quantity: { type: integer, minimum: 1 }
        note: { type: string }
        createdAt: { type: string, format: date-time }
        updatedAt: { type: string, format: date-time }
    InternalRequestEvent:
      type: object
      required: [id, type, actor, note, occurredAt]
      properties:
        id: { type: string, format: uuid }
        type: { $ref: "#/components/schemas/InternalRequestStatus" }
        actor: { type: string }
        note: { type: string }
        occurredAt: { type: string, format: date-time }
    InternalRequest:
      type: object
      required: [id, reference, status, requester, delivery, startsAt, dueAt, note, decisionNote, decidedBy, decidedAt, fulfilledBy, fulfilledAt, createdBy, createdAt, updatedAt, canCancel, lines, events]
      properties:
        id: { type: string, format: uuid }
        reference: { type: string, example: REQ-A1B2C3D4 }
        status: { $ref: "#/components/schemas/InternalRequestStatus" }
        requester:
          type: object
          required: [userId, name, email]
          properties:
            userId: { type: [string, "null"], format: uuid }
            name: { type: string }
            email: { type: [string, "null"], format: email }
        delivery:
          oneOf:
            - type: object
              required: [resourceId, name]
              properties:
                resourceId: { type: string, format: uuid }
                name: { type: string }
            - { type: "null" }
        startsAt: { type: string, format: date-time }
        dueAt: { type: string, format: date-time }
        note: { type: string }
        decisionNote: { type: string }
        decidedBy: { type: [string, "null"] }
        decidedAt: { type: [string, "null"], format: date-time }
        fulfilledBy: { type: [string, "null"] }
        fulfilledAt: { type: [string, "null"], format: date-time }
        createdBy: { type: string }
        createdAt: { type: string, format: date-time }
        updatedAt: { type: string, format: date-time }
        canCancel: { type: boolean }
        lines:
          type: array
          items: { $ref: "#/components/schemas/InternalRequestLine" }
        events:
          type: array
          items: { $ref: "#/components/schemas/InternalRequestEvent" }
    InternalRequestMutation:
      type: object
      required: [request]
      properties:
        request: { $ref: "#/components/schemas/InternalRequest" }
    ReservationCalendarEntry:
      type: object
      required: [id, source, sourceId, reference, status, title, subtitle, resourceId, quantity, startsAt, dueAt]
      properties:
        id: { type: string }
        source: { type: string, enum: [internal-request, reservation] }
        sourceId: { type: string, format: uuid }
        reference: { type: [string, "null"] }
        status: { type: string, enum: [submitted, approved, fulfilled, reserved] }
        title: { type: string }
        subtitle: { type: string }
        resourceId: { type: string, format: uuid }
        quantity: { type: integer, minimum: 1 }
        startsAt: { type: string, format: date-time }
        dueAt: { type: [string, "null"], format: date-time }
    ReservationCalendar:
      type: object
      required: [entries, range]
      properties:
        entries:
          type: array
          items: { $ref: "#/components/schemas/ReservationCalendarEntry" }
        range:
          type: object
          required: [from, to]
          properties:
            from: { type: string, format: date-time }
            to: { type: string, 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 }
    InventoryReservationCheckout:
      type: object
      required: [action]
      additionalProperties: false
      properties:
        action: { type: string, const: checkout }
        stockUnitId: { type: [string, "null"], format: uuid, description: Required when the reserved item uses serialized tracking. }
        checkedOutAt: { type: string, format: date-time }
        note: { type: string, maxLength: 20000 }
    LendingSettings:
      type: object
      required: [enabled, approvalRequired, defaultDurationDays, maxDurationDays]
      additionalProperties: false
      properties:
        enabled: { type: boolean }
        approvalRequired: { type: boolean }
        defaultDurationDays: { type: integer, minimum: 1, maximum: 3650 }
        maxDurationDays: { type: integer, minimum: 1, maximum: 3650 }
    InventoryAssignment:
      type: object
      required: [id, resourceId, stockUnitId, kind, status, stockApplied, overdue, 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] }
        stockApplied: { type: boolean }
        overdue: { type: boolean, description: Derived from an active checkout whose due date has passed. }
        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 }
        movement: { $ref: "#/components/schemas/StockMovement" }
    InventoryAssignmentList:
      type: object
      required: [resource, trackingMode, lending, availability, recipients, availableUnits, assignments]
      properties:
        resource:
          type: object
          required: [id, name, quantity]
          properties:
            id: { type: string, format: uuid }
            name: { type: string }
            quantity: { type: integer }
        trackingMode: { type: string, enum: [bulk, serialized] }
        lending: { $ref: "#/components/schemas/LendingSettings" }
        availability:
          type: object
          required: [availableQuantity, activeQuantity, reservedQuantity]
          properties:
            availableQuantity: { type: integer }
            activeQuantity: { type: integer, minimum: 0 }
            reservedQuantity: { type: integer, minimum: 0 }
        recipients:
          type: object
          required: [users]
          properties:
            users:
              type: array
              maxItems: 500
              items:
                type: object
                required: [id, name, email]
                properties:
                  id: { type: string, format: uuid }
                  name: { type: string }
                  email: { type: string, format: email }
        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 }
        purchaseUnitName: { type: [string, "null"], minLength: 1, maxLength: 80 }
        purchaseUnitFactor: { type: [integer, "null"], minimum: 1, maximum: 2000000000 }
    ContactFields:
      type: object
      properties:
        name: { type: string, minLength: 1, maxLength: 240 }
        company: { type: [string, "null"], maxLength: 240 }
        roles:
          type: array
          minItems: 1
          uniqueItems: true
          items: { type: string, enum: [customer, supplier] }
        email: { type: [string, "null"], format: email, maxLength: 320 }
        phone: { type: [string, "null"], maxLength: 80 }
        website: { type: [string, "null"], format: uri, maxLength: 2048 }
        customerNumber: { type: [string, "null"], maxLength: 80 }
        supplierNumber: { type: [string, "null"], maxLength: 80 }
        taxId: { type: [string, "null"], maxLength: 80 }
        addressLine1: { type: [string, "null"], maxLength: 240 }
        addressLine2: { type: [string, "null"], maxLength: 240 }
        postalCode: { type: [string, "null"], maxLength: 32 }
        city: { type: [string, "null"], maxLength: 120 }
        state: { type: [string, "null"], maxLength: 120 }
        countryCode: { type: [string, "null"], pattern: "^[A-Z]{2}$" }
        tags:
          type: array
          maxItems: 50
          uniqueItems: true
          items: { type: string, minLength: 1, maxLength: 80 }
        notes: { type: string, maxLength: 20000 }
        resourceIds:
          type: array
          maxItems: 500
          uniqueItems: true
          items: { type: string, format: uuid }
    ContactInput:
      allOf:
        - { $ref: "#/components/schemas/ContactFields" }
        - type: object
          required: [name, roles]
    ContactPatch:
      allOf:
        - { $ref: "#/components/schemas/ContactFields" }
        - type: object
          minProperties: 1
    Contact:
      allOf:
        - { $ref: "#/components/schemas/ContactFields" }
        - type: object
          required: [id, name, roles, tags, notes, archivedAt, createdAt, updatedAt, resources, movementCount, commentCount]
          properties:
            id: { type: string, format: uuid }
            archivedAt: { type: [string, "null"], format: date-time }
            createdAt: { type: string, format: date-time }
            updatedAt: { type: string, format: date-time }
            resources:
              type: array
              items:
                type: object
                required: [id, name, sku, type]
                properties:
                  id: { type: string, format: uuid }
                  name: { type: string }
                  sku: { type: [string, "null"] }
                  type: { type: string }
            movementCount: { type: integer, minimum: 0 }
            commentCount: { type: integer, minimum: 0 }
    CommentInput:
      type: object
      additionalProperties: false
      required: [body]
      properties:
        body: { type: string, minLength: 1, maxLength: 10000 }
    ContactComment:
      type: object
      required: [id, contactId, body, authorName, createdAt, updatedAt, canEdit]
      properties:
        id: { type: string, format: uuid }
        contactId: { type: string, format: uuid }
        body: { type: string, minLength: 1, maxLength: 10000 }
        authorName: { type: string, minLength: 1, maxLength: 160 }
        createdAt: { type: string, format: date-time }
        updatedAt: { type: string, format: date-time }
        canEdit: { type: boolean }
    StockConfig:
      allOf:
        - { $ref: "#/components/schemas/StockConfigInput" }
        - type: object
          required: [trackingMode, minimumStock, reorderQuantity, leadTimeDays, unitName, purchaseUnitName, purchaseUnitFactor]
    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. }
        contactId: { type: [string, "null"], format: uuid, description: Active customer or supplier associated with this movement. }
        occurredAt: { type: string, format: date-time }
        totalPriceCents: { type: [integer, "null"], minimum: -2000000000, maximum: 2000000000, description: Signed transaction value; negative values can represent outbound revenue. Inbound prices must be nonnegative. }
        priceCurrency: { type: [string, "null"], minLength: 3, maxLength: 3 }
    StockMovement:
      type: object
      required: [id, resourceId, contactId, delta, quantity, balanceAfter, fromLocationBalanceAfter, toLocationBalanceAfter, type, note, location, fromLocationResourceId, toLocationResourceId, occurredAt, createdAt, createdBy]
      properties:
        id: { type: string, format: uuid }
        resourceId: { type: string, format: uuid }
        contactId: { type: [string, "null"], 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 }
        totalPriceCents: { type: [integer, "null"], minimum: -2000000000 }
        priceCurrency: { type: [string, "null"] }
        costCents: { type: [integer, "null"], minimum: 0 }
        costCurrency: { type: [string, "null"] }
        costEstimated: { type: boolean }
        balanceAfter: { type: integer }
        fromLocationBalanceAfter: { type: [integer, "null"] }
        toLocationBalanceAfter: { type: [integer, "null"] }
        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 }
        totalPriceCents: { type: [integer, "null"], minimum: 0, maximum: 2000000000 }
        priceCurrency: { type: [string, "null"], minLength: 3, maxLength: 3 }
    StockUnitPatch:
      type: object
      properties:
        code: { type: string, minLength: 1, maxLength: 180, description: Editable identifier unique within this item. Leading and trailing whitespace is removed. The unit UUID and its QR links remain unchanged. }
        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 }
        totalPriceCents: { type: [integer, "null"], minimum: -2000000000, maximum: 2000000000, description: Signed transaction value; negative values can represent outbound revenue. Inbound prices must be nonnegative. }
        priceCurrency: { type: [string, "null"], minLength: 3, maxLength: 3 }
    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 }
            acquisitionCostCents: { type: [integer, "null"], minimum: 0 }
            costCurrency: { type: [string, "null"] }
            acquiredAt: { type: string, format: date-time }
            lastMovedAt: { type: string, format: date-time }
            createdAt: { type: string, format: date-time }
            updatedAt: { type: string, format: date-time }
    StockOverviewCover:
      type: object
      required: [url, altText]
      properties:
        url: { type: string }
        altText: { type: string }
    StockOverviewItem:
      type: object
      required: [resourceId, variantOfResourceId, name, type, cover, quantity, onOrder, projectedQuantity, minimumStock, trackingMode, reorderSuggested, unitName]
      properties:
        resourceId: { type: string, format: uuid }
        variantOfResourceId: { type: [string, "null"], format: uuid }
        name: { type: string }
        type: { type: string }
        cover:
          oneOf:
            - { $ref: "#/components/schemas/StockOverviewCover" }
            - { type: "null" }
        quantity: { type: integer }
        onOrder: { type: integer, minimum: 0 }
        projectedQuantity: { type: integer }
        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, family, config, forecast, procurement, movements, units]
      properties:
        family:
          description: Individual variant balances and their total including unassigned primary stock, read from the same database snapshot.
          $ref: "#/components/schemas/ResourceFamily"
        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, description: "This item’s own stock. For the family total use family.summary.totalQuantity." }
        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 }
            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
        slugs:
          type: array
          maxItems: 20
          uniqueItems: true
          items:
            type: string
            minLength: 1
            maxLength: 80
            pattern: ^[a-z0-9]+(?:-[a-z0-9]+)*$
          description: Ordered organization-scoped URL slugs. The first value is the standard slug; remaining
            values are aliases. Values are normalized to lowercase; `new` and UUID-shaped values are
            reserved.
        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"
        subtitle:
          type: string
          maxLength: 240
          description: Optional subtitle used by label templates. Empty string clears it.
    ResourcePatch:
      type: object
      description: Any subset of the writable resource fields.
      properties:
        name:
          type: string
          minLength: 1
          maxLength: 240
        slugs:
          type: array
          maxItems: 20
          uniqueItems: true
          items:
            type: string
            minLength: 1
            maxLength: 80
            pattern: ^[a-z0-9]+(?:-[a-z0-9]+)*$
          description: Replaces the ordered URL slugs for this resource. The first value becomes the standard
            slug; remaining values are aliases.
        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"
        subtitle:
          type: string
          maxLength: 240
          description: Optional subtitle used by label templates. Empty string clears it.
    ResourceFamilyVariantInput:
      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 }
    ResourceFamilyExistingVariantInput:
      type: object
      additionalProperties: false
      required: [existingResourceId]
      properties:
        existingResourceId:
          type: string
          format: uuid
          description: Existing standalone inventory item to connect as the child variant while preserving its operational records.
    ResourceVariantDetachResult:
      type: object
      required: [resourceId, materializedBomLineCount]
      properties:
        resourceId: { type: string, format: uuid }
        materializedBomLineCount: { type: integer, minimum: 0 }
    ResourceFamilyMember:
      type: object
      required: [id, name, type, status, sku, barcode, quantity, trackingMode, updatedAt, overriddenFields]
      properties:
        id: { type: string, format: uuid }
        name: { type: string }
        type: { $ref: "#/components/schemas/InventoryTypeKey" }
        status: { type: string, enum: [available, in-use, maintenance, archived] }
        sku: { type: [string, "null"] }
        barcode: { type: [string, "null"] }
        quantity: { type: integer }
        trackingMode: { type: string, enum: [bulk, serialized] }
        updatedAt: { type: string, format: date-time }
        overriddenFields:
          type: array
          items:
            type: string
            enum: [description, type, valueCents, currency, priority, tags, categories, customFields, notes]
    ResourceFamily:
      type: object
      required: [role, currentResourceId, primary, variants, optionGroupCount, summary]
      properties:
        role: { type: string, enum: [primary, variant] }
        currentResourceId: { type: string, format: uuid }
        primary: { $ref: "#/components/schemas/ResourceFamilyMember" }
        variants:
          type: array
          items: { $ref: "#/components/schemas/ResourceFamilyMember" }
        optionGroupCount: { type: integer, minimum: 0 }
        summary:
          type: object
          required: [totalQuantity, primaryQuantity, variantQuantity, variantCount, serializedVariantCount]
          properties:
            totalQuantity: { type: integer }
            primaryQuantity: { type: integer }
            variantQuantity: { type: integer }
            variantCount: { type: integer, minimum: 0 }
            serializedVariantCount: { type: integer, minimum: 0 }
    ResourceOptionValueInput:
      type: object
      additionalProperties: false
      required: [label, code, isDefault]
      properties:
        label: { type: string, minLength: 1, maxLength: 120 }
        code: { type: string, pattern: "^[A-Za-z0-9][A-Za-z0-9_-]{0,39}$" }
        componentResourceId: { type: [string, "null"], format: uuid }
        isDefault: { type: boolean }
        position: { type: integer, minimum: 0 }
    ResourceOptionGroupInput:
      type: object
      additionalProperties: false
      required: [key, name, values]
      properties:
        key: { type: string, pattern: "^[a-z][a-z0-9_-]{0,63}$" }
        name: { type: string, minLength: 1, maxLength: 120 }
        bomSlotKey: { type: [string, "null"], maxLength: 80 }
        position: { type: integer, minimum: 0 }
        values:
          type: array
          minItems: 2
          maxItems: 8
          items: { $ref: "#/components/schemas/ResourceOptionValueInput" }
    ResourceOptionValue:
      allOf:
        - { $ref: "#/components/schemas/ResourceOptionValueInput" }
        - type: object
          required: [id, componentName, position]
          properties:
            id: { type: string, format: uuid }
            componentName: { type: [string, "null"] }
    ResourceOptionGroup:
      type: object
      required: [id, key, name, bomSlotKey, position, values]
      properties:
        id: { type: string, format: uuid }
        key: { type: string }
        name: { type: string }
        bomSlotKey: { type: [string, "null"] }
        position: { type: integer, minimum: 0 }
        values:
          type: array
          items: { $ref: "#/components/schemas/ResourceOptionValue" }
    ResourceOptions:
      type: object
      required: [role, currentResourceId, primary, groups, configurations, currentSelection, bomSlots, combinationCount, generatedVariantCount, familyVariantCount, definitionsLocked]
      properties:
        role: { type: string, enum: [primary, variant] }
        currentResourceId: { type: string, format: uuid }
        primary:
          type: object
          required: [id, name, sku]
          properties:
            id: { type: string, format: uuid }
            name: { type: string }
            sku: { type: [string, "null"] }
        groups:
          type: array
          items: { $ref: "#/components/schemas/ResourceOptionGroup" }
        configurations:
          type: array
          description: Materialized configurations whose inventory items the caller may read.
          items:
            type: object
            required: [resourceId, resourceName, resourceSku, isPrimary, signature, selection]
            properties:
              resourceId: { type: string, format: uuid }
              resourceName: { type: string }
              resourceSku: { type: [string, "null"] }
              isPrimary: { type: boolean }
              signature: { type: string }
              selection:
                type: array
                items:
                  type: object
                  required: [groupId, groupName, valueId, valueLabel]
                  properties:
                    groupId: { type: string, format: uuid }
                    groupName: { type: string }
                    valueId: { type: string, format: uuid }
                    valueLabel: { type: string }
        currentSelection:
          type: array
          items:
            type: object
            required: [groupId, groupName, valueId, valueLabel]
            properties:
              groupId: { type: string, format: uuid }
              groupName: { type: string }
              valueId: { type: string, format: uuid }
              valueLabel: { type: string }
        bomSlots:
          type: array
          items:
            type: object
            required: [slotKey, position, componentResourceId, componentName]
            properties:
              slotKey: { type: string }
              position: { type: integer, minimum: 0 }
              componentResourceId: { type: string, format: uuid }
              componentName: { type: string }
        combinationCount: { type: integer, minimum: 0, maximum: 100 }
        generatedVariantCount: { type: integer, minimum: 0 }
        familyVariantCount: { type: integer, minimum: 0 }
        definitionsLocked: { type: boolean }
    ResourceOptionGenerationResult:
      type: object
      required: [primaryResourceId, generatedVariantCount, variants]
      properties:
        primaryResourceId: { type: string, format: uuid }
        generatedVariantCount: { type: integer, minimum: 1, maximum: 99 }
        variants:
          type: array
          items:
            type: object
            required: [id, name, sku]
            properties:
              id: { type: string, format: uuid }
              name: { type: string }
              sku: { type: [string, "null"] }
    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, slugs, 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" }
