openapi: 3.0.3
info:
  title: LabelInn Cloud Print API
  description: |
    Send print jobs to physical thermal label printers, manage your fleet, design label templates,
    and receive real-time webhooks — all from a single REST API.

    ## Authentication
    All endpoints require a Bearer token. Get your API key from **LabelInn → Settings → API Keys**.

    | Key prefix | Mode | Description |
    |---|---|---|
    | `sk_test_xxx` | Sandbox | No real prints, separate quota |
    | `sk_live_xxx` | Live | Sends jobs to physical printers |

    ## Rate Limits
    Responses include `X-RateLimit-Limit`, `X-RateLimit-Remaining`, and `X-RateLimit-Reset` headers.

    | Plan | Daily Limit |
    |---|---|
    | Trial | 100 |
    | Pro | 2,000 |
    | Enterprise | 50,000 |
    | Test keys | 500 minimum |

  version: "2.0.0"
  contact:
    name: LabelInn Support
    email: support@labelinn.com
    url: https://labelinn.com
  license:
    name: Proprietary
    url: https://labelinn.com/terms

servers:
  - url: https://labelinn.com/v1
    description: Production (Cloud API)
  - url: http://{local_ip}:6631
    description: Edge Server (Local LAN API — runs on the desktop app)
    variables:
      local_ip:
        default: "192.168.1.100"
        description: LAN IP of the machine running LabelInn desktop app

security:
  - BearerAuth: []

tags:
  - name: Print Jobs
    description: Submit, track, cancel, retry, batch, and reprint label jobs
  - name: Fleet
    description: Printer inventory, status, telemetry, identify, media definition
  - name: Printer Groups
    description: Logical groupings of printers for routing and load balancing
  - name: Designs
    description: Label template CRUD, elements, pages, variables, rendering, snapshots, revisions
  - name: Render
    description: Async render of designs or raw HTML to PDF/PNG
  - name: Webhooks
    description: Event subscriptions, delivery history, test pings
  - name: Data Connect
    description: Universal data ingestion — push XML, CSV, JSON, TSV, or NDJSON and print from external sources
  - name: Rules
    description: Conditional routing & transformation rules for incoming print events
  - name: Sites
    description: Multi-location organisation, printer assignment, geo-nearest lookup, per-site analytics
  - name: Analytics
    description: Fleet trends, predictive maintenance, print quality scoring, digital-twin simulation
  - name: Audit
    description: Tamper-evident audit trail (queries, summary, ingest)
  - name: Compliance
    description: Regulated serial-number pools (allocate / void / report) for FDA, GHS, pharma
  - name: Usage
    description: API usage counters, daily breakdown, plan limits
  - name: Relay
    description: WebSocket relay status (real-time edge-to-cloud bridge)
  - name: MCP
    description: Model Context Protocol Streamable-HTTP endpoint — lets AI agents call every API tool
  - name: Edge Server
    description: |
      Local LAN API running on the LabelInn desktop app (port 6631).
      Print directly to USB/network printers without cloud round-trips.
      Supports optional Bearer token auth and offline operation with sync-back.

# ═══════════════════════════════════════════════════════════════
# PATHS
# ═══════════════════════════════════════════════════════════════

paths:

  # ── Print Jobs ──────────────────────────────────────────────

  /print/jobs:
    post:
      tags: [Print Jobs]
      operationId: createPrintJob
      summary: Submit a print job
      description: |
        Send a label to a printer. Supports ZPL, image (base64 or URL), and template payloads.
        Use the `X-Idempotency-Key` header to prevent duplicate prints (24h TTL).
      parameters:
        - $ref: '#/components/parameters/IdempotencyKey'
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/CreateJobRequest'
            examples:
              zpl:
                summary: ZPL label
                value:
                  printer_id: "prt_abc123"
                  payload_type: "zpl"
                  payload_data: "^XA^FO50,50^ADN,36,20^FDHello World^FS^XZ"
                  copies: 1
              image_url:
                summary: Image from URL
                value:
                  printer_id: "prt_abc123"
                  payload_type: "image"
                  image_url: "https://example.com/labels/shipping.png"
              template:
                summary: Template with variables
                value:
                  printer_id: "prt_abc123"
                  payload_type: "template"
                  design_id: "dsg_shipping"
                  data:
                    order_id: "ORD-9876"
                    customer: "Ali Yilmaz"
                    barcode: "TR123456789"
                  copies: 2
      responses:
        '201':
          description: Job created
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/JobResponse'
        '400':
          $ref: '#/components/responses/BadRequest'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '429':
          $ref: '#/components/responses/RateLimited'

    get:
      tags: [Print Jobs]
      operationId: listPrintJobs
      summary: List print jobs
      parameters:
        - name: status
          in: query
          schema:
            type: string
            enum: [queued, claimed, rendering, printing, completed, failed, cancelled]
        - name: printer_id
          in: query
          schema:
            type: string
        - name: limit
          in: query
          schema:
            type: integer
            minimum: 1
            maximum: 100
            default: 20
        - name: page_token
          in: query
          schema:
            type: string
      responses:
        '200':
          description: List of jobs
          content:
            application/json:
              schema:
                type: object
                properties:
                  success:
                    type: boolean
                  data:
                    type: object
                    properties:
                      jobs:
                        type: array
                        items:
                          $ref: '#/components/schemas/PrintJob'
                      next_page_token:
                        type: string
                        nullable: true
        '401':
          $ref: '#/components/responses/Unauthorized'
        '429':
          $ref: '#/components/responses/RateLimited'

  /print/jobs/{jobId}:
    get:
      tags: [Print Jobs]
      operationId: getPrintJob
      summary: Get a print job
      parameters:
        - $ref: '#/components/parameters/JobId'
      responses:
        '200':
          description: Job details
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/JobResponse'
        '404':
          $ref: '#/components/responses/NotFound'

    delete:
      tags: [Print Jobs]
      operationId: cancelPrintJob
      summary: Cancel a queued job
      description: Only jobs in `queued` status can be cancelled.
      parameters:
        - $ref: '#/components/parameters/JobId'
      responses:
        '200':
          description: Job cancelled
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/JobResponse'
        '400':
          description: Job is not in a cancellable state
          $ref: '#/components/responses/BadRequest'
        '404':
          $ref: '#/components/responses/NotFound'

  /print/jobs/{jobId}/reprint:
    post:
      tags: [Print Jobs]
      operationId: reprintJob
      summary: Reprint a previous job
      description: Creates a new job with the same payload. Optionally override printer, copies, or data.
      parameters:
        - $ref: '#/components/parameters/JobId'
      requestBody:
        content:
          application/json:
            schema:
              type: object
              properties:
                printer_id:
                  type: string
                  description: Override the target printer
                copies:
                  type: integer
                  minimum: 1
                  maximum: 1000
                data:
                  description: Override template variable values
                  oneOf:
                    - type: object
                      additionalProperties:
                        type: string
                    - type: array
                      items:
                        type: object
                        additionalProperties:
                          type: string
      responses:
        '201':
          description: New job created from reprint
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/JobResponse'
        '404':
          $ref: '#/components/responses/NotFound'

  # ── Fleet ───────────────────────────────────────────────────

  /fleet/printers:
    get:
      tags: [Fleet]
      operationId: listPrinters
      summary: List all printers
      parameters:
        - name: status
          in: query
          schema:
            type: string
            enum: [online, offline]
        - name: type
          in: query
          schema:
            type: string
        - name: connection
          in: query
          schema:
            type: string
            enum: [usb, network, bluetooth]
      responses:
        '200':
          description: List of printers
          content:
            application/json:
              schema:
                type: object
                properties:
                  success:
                    type: boolean
                  data:
                    type: object
                    properties:
                      printers:
                        type: array
                        items:
                          $ref: '#/components/schemas/Printer'
        '401':
          $ref: '#/components/responses/Unauthorized'

  /fleet/printers/{printerId}:
    get:
      tags: [Fleet]
      operationId: getPrinter
      summary: Get printer details
      parameters:
        - $ref: '#/components/parameters/PrinterId'
        - name: detailed
          in: query
          description: Include extended telemetry (supply levels, warnings, last job)
          schema:
            type: boolean
            default: false
      responses:
        '200':
          description: Printer details
          content:
            application/json:
              schema:
                type: object
                properties:
                  success:
                    type: boolean
                  data:
                    $ref: '#/components/schemas/PrinterDetailed'
        '404':
          $ref: '#/components/responses/NotFound'

  /fleet/printers/{printerId}/status:
    get:
      tags: [Fleet]
      operationId: getPrinterStatus
      summary: Get live printer status
      description: Lightweight endpoint returning only current status and recent activity.
      parameters:
        - $ref: '#/components/parameters/PrinterId'
      responses:
        '200':
          description: Printer status
          content:
            application/json:
              schema:
                type: object
                properties:
                  success:
                    type: boolean
                  data:
                    $ref: '#/components/schemas/PrinterStatus'
        '404':
          $ref: '#/components/responses/NotFound'

  /fleet/discover:
    post:
      tags: [Fleet]
      operationId: startDiscovery
      summary: Start printer auto-discovery
      description: |
        Triggers a printer auto-discovery scan on a connected device running the LabelInn desktop app.
        The desktop app scans for USB, network (mDNS, port 9100), and Bluetooth printers.

        1. Call this endpoint to create a discovery session (returns 202).
        2. Poll `GET /v1/fleet/discover/{sessionId}` until `status` is `complete`.
        3. Pick printers from results and add them via `POST /v1/fleet/discover/{sessionId}/add`.

        Sessions expire after 5 minutes.
      requestBody:
        content:
          application/json:
            schema:
              type: object
              properties:
                device_id:
                  type: string
                  description: |
                    Target a specific device by its ID. If omitted, any online device in the
                    company may pick up the scan. The device must be running the LabelInn desktop app.
            examples:
              targetDevice:
                summary: Scan on a specific device
                value:
                  device_id: "dev_macbook_office"
              anyDevice:
                summary: Scan on any available device
                value: {}
      responses:
        '202':
          description: Discovery session created
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/DiscoverySessionCreated'
              example:
                data:
                  session_id: "abc123def456"
                  status: "pending"
                  message: "Discovery session created. A device will pick up this scan request. Poll GET /v1/fleet/discover/abc123def456 for results."
        '404':
          description: Target device not found or offline
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
        '401':
          $ref: '#/components/responses/Unauthorized'

  /fleet/discover/{sessionId}:
    parameters:
      - name: sessionId
        in: path
        required: true
        schema:
          type: string
        description: Discovery session ID returned by POST /fleet/discover
    get:
      tags: [Fleet]
      operationId: getDiscoveryStatus
      summary: Poll discovery results
      description: |
        Poll this endpoint after starting a discovery session to check status and retrieve results.

        | Status | Meaning |
        |---|---|
        | `pending` | Waiting for a device to pick up the scan |
        | `scanning` | Device is actively scanning for printers |
        | `complete` | Scan finished — printers listed in response |
        | `expired` | Session exceeded 5 minute TTL |
        | `error` | Scan failed (see error field) |
      responses:
        '200':
          description: Discovery session status and results
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/DiscoverySessionResult'
        '404':
          $ref: '#/components/responses/NotFound'

  /fleet/discover/{sessionId}/add:
    parameters:
      - name: sessionId
        in: path
        required: true
        schema:
          type: string
    post:
      tags: [Fleet]
      operationId: addDiscoveredPrinter
      summary: Add a discovered printer to fleet
      description: |
        Select a printer from the discovery results and register it in your fleet.
        The `address` must match one of the printers returned by the discovery session.
        Generates a deterministic printer ID based on address and MAC.
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/AddDiscoveredPrinterRequest'
            examples:
              withName:
                summary: Add with custom name
                value:
                  address: "192.168.1.50"
                  name: "Warehouse Zebra #3"
              addressOnly:
                summary: Add using discovered name
                value:
                  address: "192.168.1.50"
      responses:
        '201':
          description: Printer registered in fleet
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/AddDiscoveredPrinterResponse'
        '400':
          description: Missing address field
        '404':
          description: Session not found or printer address not in session results

  # ── Designs ─────────────────────────────────────────────────

  /designs:
    get:
      tags: [Designs]
      operationId: listDesigns
      summary: List label designs
      parameters:
        - name: category
          in: query
          schema:
            type: string
        - name: limit
          in: query
          schema:
            type: integer
            default: 50
      responses:
        '200':
          description: List of designs
          content:
            application/json:
              schema:
                type: object
                properties:
                  success:
                    type: boolean
                  data:
                    type: object
                    properties:
                      designs:
                        type: array
                        items:
                          $ref: '#/components/schemas/DesignSummary'

    post:
      tags: [Designs]
      operationId: createDesign
      summary: Create a new design
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/CreateDesignRequest'
      responses:
        '201':
          description: Design created
          content:
            application/json:
              schema:
                type: object
                properties:
                  success:
                    type: boolean
                  data:
                    $ref: '#/components/schemas/Design'

  /designs/{designId}:
    get:
      tags: [Designs]
      operationId: getDesign
      summary: Get a design
      parameters:
        - $ref: '#/components/parameters/DesignId'
      responses:
        '200':
          description: Design details with elements
          content:
            application/json:
              schema:
                type: object
                properties:
                  success:
                    type: boolean
                  data:
                    $ref: '#/components/schemas/Design'
        '404':
          $ref: '#/components/responses/NotFound'

    put:
      tags: [Designs]
      operationId: updateDesign
      summary: Update a design
      parameters:
        - $ref: '#/components/parameters/DesignId'
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/UpdateDesignRequest'
      responses:
        '200':
          description: Design updated
          content:
            application/json:
              schema:
                type: object
                properties:
                  success:
                    type: boolean
                  data:
                    $ref: '#/components/schemas/Design'

    delete:
      tags: [Designs]
      operationId: deleteDesign
      summary: Delete a design
      parameters:
        - $ref: '#/components/parameters/DesignId'
      responses:
        '200':
          description: Design deleted
          content:
            application/json:
              schema:
                type: object
                properties:
                  success:
                    type: boolean

  /designs/{designId}/elements:
    get:
      tags: [Designs]
      operationId: listDesignElements
      summary: List elements of a design
      parameters:
        - $ref: '#/components/parameters/DesignId'
      responses:
        '200':
          description: Element list
          content:
            application/json:
              schema:
                type: object
                properties:
                  success:
                    type: boolean
                  data:
                    type: object
                    properties:
                      elements:
                        type: array
                        items:
                          $ref: '#/components/schemas/DesignElement'

    post:
      tags: [Designs]
      operationId: addDesignElement
      summary: Add an element to a design
      parameters:
        - $ref: '#/components/parameters/DesignId'
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/CreateElementRequest'
      responses:
        '201':
          description: Element added
          content:
            application/json:
              schema:
                type: object
                properties:
                  success:
                    type: boolean
                  data:
                    $ref: '#/components/schemas/DesignElement'

  /designs/{designId}/elements/{elementId}:
    patch:
      tags: [Designs]
      operationId: updateDesignElement
      summary: Update a design element
      parameters:
        - $ref: '#/components/parameters/DesignId'
        - $ref: '#/components/parameters/ElementId'
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/UpdateElementRequest'
      responses:
        '200':
          description: Element updated
          content:
            application/json:
              schema:
                type: object
                properties:
                  success:
                    type: boolean
                  data:
                    $ref: '#/components/schemas/DesignElement'

    delete:
      tags: [Designs]
      operationId: deleteDesignElement
      summary: Remove an element from a design
      parameters:
        - $ref: '#/components/parameters/DesignId'
        - $ref: '#/components/parameters/ElementId'
      responses:
        '200':
          description: Element deleted

  /designs/{designId}/variables:
    get:
      tags: [Designs]
      operationId: listDesignVariables
      summary: List variables of a design
      description: |
        Returns all bindable variables detected in the design's elements.
        Variables are automatically extracted from element data bindings.
      parameters:
        - $ref: '#/components/parameters/DesignId'
      responses:
        '200':
          description: Variable list
          content:
            application/json:
              schema:
                type: object
                properties:
                  success:
                    type: boolean
                  data:
                    type: object
                    properties:
                      variables:
                        type: array
                        items:
                          $ref: '#/components/schemas/DesignVariable'

  /designs/{designId}/clone:
    post:
      tags: [Designs]
      operationId: cloneDesign
      summary: Clone a design
      description: Creates a deep copy of the design including all elements. Name is appended with " (Copy)".
      parameters:
        - $ref: '#/components/parameters/DesignId'
      responses:
        '201':
          description: Cloned design
          content:
            application/json:
              schema:
                type: object
                properties:
                  success:
                    type: boolean
                  data:
                    $ref: '#/components/schemas/Design'

  /designs/{designId}/render:
    post:
      tags: [Designs]
      operationId: renderDesign
      summary: Render a design preview
      description: |
        Queues an async render job. Returns 202 with a render ID.
        Poll the render status until complete to get the preview image URL.
      parameters:
        - $ref: '#/components/parameters/DesignId'
      requestBody:
        content:
          application/json:
            schema:
              type: object
              properties:
                data:
                  type: object
                  additionalProperties:
                    type: string
                  description: Variable values to render with
                format:
                  type: string
                  enum: [png, pdf]
                  default: png
      responses:
        '202':
          description: Render queued
          content:
            application/json:
              schema:
                type: object
                properties:
                  success:
                    type: boolean
                  data:
                    type: object
                    properties:
                      render_id:
                        type: string
                      status:
                        type: string
                        enum: [queued, rendering, completed, failed]

  /designs/{designId}/print:
    post:
      tags: [Designs]
      operationId: printDesign
      summary: Print a design directly
      description: Convenience endpoint — creates a print job from a design with variable data.
      parameters:
        - $ref: '#/components/parameters/DesignId'
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [printer_id]
              properties:
                printer_id:
                  type: string
                data:
                  description: Variable values
                  oneOf:
                    - type: object
                      additionalProperties:
                        type: string
                    - type: array
                      items:
                        type: object
                        additionalProperties:
                          type: string
                copies:
                  type: integer
                  minimum: 1
                  maximum: 1000
                  default: 1
      responses:
        '201':
          description: Print job created
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/JobResponse'

  # ── Webhooks ────────────────────────────────────────────────

  /webhooks:
    post:
      tags: [Webhooks]
      operationId: createWebhook
      summary: Subscribe to events
      description: |
        Register a webhook endpoint to receive real-time notifications.
        Max 20 webhooks per company. URL must be HTTPS (private IPs blocked).
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/CreateWebhookRequest'
      responses:
        '201':
          description: Webhook created
          content:
            application/json:
              schema:
                type: object
                properties:
                  success:
                    type: boolean
                  data:
                    $ref: '#/components/schemas/Webhook'

    get:
      tags: [Webhooks]
      operationId: listWebhooks
      summary: List webhooks
      responses:
        '200':
          description: Webhook list
          content:
            application/json:
              schema:
                type: object
                properties:
                  success:
                    type: boolean
                  data:
                    type: object
                    properties:
                      webhooks:
                        type: array
                        items:
                          $ref: '#/components/schemas/WebhookSummary'

  /webhooks/{webhookId}:
    delete:
      tags: [Webhooks]
      operationId: deleteWebhook
      summary: Delete a webhook
      parameters:
        - $ref: '#/components/parameters/WebhookId'
      responses:
        '200':
          description: Webhook deleted

  /webhooks/{webhookId}/test:
    post:
      tags: [Webhooks]
      operationId: testWebhook
      summary: Send a test ping
      description: |
        Delivers a `test.ping` event to the webhook URL so you can verify your
        endpoint is receiving and validating signatures correctly.
      parameters:
        - $ref: '#/components/parameters/WebhookId'
      responses:
        '200':
          description: Test ping sent
          content:
            application/json:
              schema:
                type: object
                properties:
                  success:
                    type: boolean
                  data:
                    type: object
                    properties:
                      message:
                        type: string
                      delivered:
                        type: boolean
                      status_code:
                        type: integer

  # ── Data Connect ────────────────────────────────────────────

  /connect/ingest:
    post:
      tags: [Data Connect]
      operationId: ingestData
      summary: Push data in any format
      description: |
        Ingest XML, CSV, JSON, TSV, or NDJSON data. Auto-detects format.
        If source_id is omitted, a new connector source is auto-created.
        With sk_test_* keys, data is parsed but not stored.
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [payload]
              properties:
                source_id:
                  type: string
                  description: Existing connector ID (auto-creates if omitted)
                payload:
                  type: string
                  description: Raw data string
                format:
                  type: string
                  enum: [xml, csv, tsv, json, ndjson]
                config:
                  $ref: '#/components/schemas/ParserConfig'
                name:
                  type: string
                  description: Name for auto-created source
      responses:
        '201':
          description: Data ingested
          content:
            application/json:
              schema:
                type: object
                properties:
                  data:
                    $ref: '#/components/schemas/IngestResult'
        '413':
          description: Payload too large (max 5 MB)
        '422':
          description: Parse error or too many records (max 5,000)

  /connect/test-parse:
    post:
      tags: [Data Connect]
      operationId: testParse
      summary: Dry-run parse — returns schema without storing
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [payload]
              properties:
                payload:
                  type: string
                format:
                  type: string
                  enum: [xml, csv, tsv, json, ndjson]
                config:
                  $ref: '#/components/schemas/ParserConfig'
      responses:
        '200':
          description: Parse result
          content:
            application/json:
              schema:
                type: object
                properties:
                  data:
                    $ref: '#/components/schemas/ParseResult'

  /connect/sources:
    post:
      tags: [Data Connect]
      operationId: createSource
      summary: Create a connector source
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/CreateSourceRequest'
      responses:
        '201':
          description: Source created
          content:
            application/json:
              schema:
                type: object
                properties:
                  data:
                    $ref: '#/components/schemas/ConnectorSource'
        '409':
          description: Connector limit reached (max 50)
    get:
      tags: [Data Connect]
      operationId: listSources
      summary: List connector sources
      parameters:
        - name: limit
          in: query
          schema: { type: integer, minimum: 1, maximum: 100, default: 25 }
        - name: active
          in: query
          schema: { type: boolean }
        - name: page_token
          in: query
          schema: { type: string }
      responses:
        '200':
          description: Source list
          content:
            application/json:
              schema:
                type: object
                properties:
                  data:
                    type: array
                    items:
                      $ref: '#/components/schemas/ConnectorSourceSummary'
                  pagination:
                    $ref: '#/components/schemas/Pagination'

  /connect/sources/{sourceId}:
    parameters:
      - name: sourceId
        in: path
        required: true
        schema: { type: string }
    get:
      tags: [Data Connect]
      operationId: getSource
      summary: Get source details
      responses:
        '200':
          description: Source details
          content:
            application/json:
              schema:
                type: object
                properties:
                  data:
                    $ref: '#/components/schemas/ConnectorSource'
    put:
      tags: [Data Connect]
      operationId: updateSource
      summary: Update source config
      requestBody:
        content:
          application/json:
            schema:
              type: object
              properties:
                name: { type: string, maxLength: 100 }
                description: { type: string, maxLength: 500 }
                format: { type: string, enum: [xml, csv, tsv, json, ndjson] }
                config: { $ref: '#/components/schemas/ParserConfig' }
                isActive: { type: boolean }
      responses:
        '200':
          description: Source updated
    delete:
      tags: [Data Connect]
      operationId: deleteSource
      summary: Soft-delete a source
      responses:
        '200':
          description: Source deleted (soft)

  /connect/sources/{sourceId}/records:
    parameters:
      - name: sourceId
        in: path
        required: true
        schema: { type: string }
    get:
      tags: [Data Connect]
      operationId: listRecords
      summary: List ingested records
      parameters:
        - name: limit
          in: query
          schema: { type: integer, minimum: 1, maximum: 100, default: 25 }
        - name: ingest_id
          in: query
          schema: { type: string }
        - name: page_token
          in: query
          schema: { type: string }
      responses:
        '200':
          description: Record list
          content:
            application/json:
              schema:
                type: object
                properties:
                  data:
                    type: array
                    items:
                      $ref: '#/components/schemas/ConnectorRecord'
                  pagination:
                    $ref: '#/components/schemas/Pagination'

  /connect/sources/{sourceId}/schema:
    parameters:
      - name: sourceId
        in: path
        required: true
        schema: { type: string }
    get:
      tags: [Data Connect]
      operationId: getSchema
      summary: Get auto-discovered schema
      responses:
        '200':
          description: Schema
          content:
            application/json:
              schema:
                type: object
                properties:
                  data:
                    $ref: '#/components/schemas/SchemaResult'

  /connect/sources/{sourceId}/mappings:
    parameters:
      - name: sourceId
        in: path
        required: true
        schema: { type: string }
    get:
      tags: [Data Connect]
      operationId: getMappings
      summary: Get field→label mappings
      responses:
        '200':
          description: Mappings with schema
          content:
            application/json:
              schema:
                type: object
                properties:
                  data:
                    $ref: '#/components/schemas/MappingsResult'
    put:
      tags: [Data Connect]
      operationId: updateMappings
      summary: Set field→label mappings
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [mappings]
              properties:
                mappings:
                  type: object
                  additionalProperties: { type: string }
                  description: "Keys = source field paths, values = label variable names"
      responses:
        '200':
          description: Mappings updated

  /connect/sources/{sourceId}/print:
    parameters:
      - name: sourceId
        in: path
        required: true
        schema: { type: string }
    post:
      tags: [Data Connect]
      operationId: printFromSource
      summary: Print labels from connector data
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/ConnectorPrintRequest'
      responses:
        '201':
          description: Print jobs created
          content:
            application/json:
              schema:
                type: object
                properties:
                  data:
                    $ref: '#/components/schemas/ConnectorPrintResult'

  # ── Edge Server (Local LAN API) ────────────────────────────

  /local/v1/health:
    get:
      tags: [Edge Server]
      operationId: edgeHealth
      summary: Health check
      description: |
        Returns Edge Server status, version, mode, and LAN addresses.
        This endpoint is always public (no auth required).
      security: []
      servers:
        - url: http://{local_ip}:6631
          variables:
            local_ip:
              default: "192.168.1.100"
      responses:
        '200':
          description: Server is healthy
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/EdgeHealthResponse'

  /local/v1/print/jobs:
    post:
      tags: [Edge Server]
      operationId: edgeCreateJob
      summary: Submit a local print job
      description: |
        Send a label directly to a USB or network printer on the local LAN.
        Returns immediately with a job ID (HTTP 202). Poll `/local/v1/jobs/{jobId}` for status.

        **Payload types:** `zpl`, `escLabel`, `design`
      servers:
        - url: http://{local_ip}:6631
          variables:
            local_ip:
              default: "192.168.1.100"
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/EdgeCreateJobRequest'
            examples:
              zpl:
                summary: ZPL label
                value:
                  printerId: "usb_zebra_01"
                  payloadType: "zpl"
                  payloadData: "^XA^FO50,50^ADN,36,20^FDHello Edge^FS^XZ"
                  copies: 1
              design:
                summary: Design template with data
                value:
                  printerId: "usb_zebra_01"
                  payloadType: "design"
                  designId: "tmpl_shipping"
                  designDataRows:
                    - name: "Jane Doe"
                      order_id: "ORD-4521"
      responses:
        '202':
          description: Job accepted
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/EdgeJobAccepted'
        '400':
          description: Validation error
        '401':
          description: Auth token required
        '413':
          description: Request body too large (max 8 MB)

  /local/v1/jobs/{jobId}:
    parameters:
      - name: jobId
        in: path
        required: true
        schema: { type: string, format: uuid }
    get:
      tags: [Edge Server]
      operationId: edgeGetJobStatus
      summary: Get job status
      description: Poll this endpoint after submitting a job to check completion status.
      servers:
        - url: http://{local_ip}:6631
          variables:
            local_ip:
              default: "192.168.1.100"
      responses:
        '200':
          description: Job result
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/EdgeJobResult'
        '404':
          description: Job not found (still processing or expired from cache)

  /local/v1/fleet/printers:
    get:
      tags: [Edge Server]
      operationId: edgeListPrinters
      summary: List locally connected printers
      description: Returns USB and network printers discovered on the local machine.
      servers:
        - url: http://{local_ip}:6631
          variables:
            local_ip:
              default: "192.168.1.100"
      responses:
        '200':
          description: Printer list
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/EdgePrinterList'

  /local/v1/fleet/discover:
    post:
      tags: [Edge Server]
      operationId: edgeStartDiscovery
      summary: Start local printer discovery
      description: |
        Triggers a full auto-discovery scan on the local machine: mDNS/Bonjour, port 9100 subnet scanning,
        USB enumeration, and ZPL validation. Returns immediately with a session ID (HTTP 202).
        Poll `GET /local/v1/fleet/discover/{sessionId}` for results.

        Max 3 concurrent discovery sessions allowed.
      servers:
        - url: http://{local_ip}:6631
          variables:
            local_ip:
              default: "192.168.1.100"
      responses:
        '202':
          description: Discovery started
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/EdgeDiscoveryStarted'
              example:
                sessionId: "a3f8c2d1-1234-5678-abcd-ef0123456789"
                status: "scanning"
                message: "Discovery started. Poll GET /local/v1/fleet/discover/a3f8c2d1-1234-5678-abcd-ef0123456789 for results."
        '429':
          description: Too many concurrent discovery sessions (max 3)

  /local/v1/fleet/discover/{sessionId}:
    parameters:
      - name: sessionId
        in: path
        required: true
        schema:
          type: string
          format: uuid
    get:
      tags: [Edge Server]
      operationId: edgeGetDiscoveryStatus
      summary: Poll local discovery results
      description: |
        Check the status and results of a local discovery session.

        | Status | Meaning |
        |---|---|
        | `scanning` | Discovery in progress |
        | `complete` | All discovery methods finished |
      servers:
        - url: http://{local_ip}:6631
          variables:
            local_ip:
              default: "192.168.1.100"
      responses:
        '200':
          description: Discovery status and results
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/EdgeDiscoveryResult'
        '404':
          description: Session not found or expired

  # ═══════════════════════════════════════════════════════════════
  # PRINT JOBS — Additional operations (v2.0.0)
  # ═══════════════════════════════════════════════════════════════

  /print/jobs/{jobId}/retry:
    post:
      tags: [Print Jobs]
      operationId: retryPrintJob
      summary: Retry a failed print job
      description: Re-queues a job in `failed` status with the same payload and printer.
      parameters:
        - $ref: '#/components/parameters/JobId'
      responses:
        '201': { description: Retry job created, content: { application/json: { schema: { $ref: '#/components/schemas/JobResponse' } } } }
        '404': { $ref: '#/components/responses/NotFound' }

  /print/batch:
    post:
      tags: [Print Jobs]
      operationId: batchPrint
      summary: Submit up to 100 print jobs in one call
      description: Submit a batch of independent jobs. Each item follows the same shape as `POST /print/jobs`. Returns per-item results.
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [jobs]
              properties:
                jobs:
                  type: array
                  minItems: 1
                  maxItems: 100
                  items:
                    $ref: '#/components/schemas/CreateJobRequest'
      responses:
        '200':
          description: Per-item results
          content:
            application/json:
              schema:
                type: object
                properties:
                  data:
                    type: object
                    properties:
                      total: { type: integer }
                      succeeded: { type: integer }
                      failed: { type: integer }
                      results:
                        type: array
                        items:
                          type: object
                          properties:
                            index: { type: integer }
                            success: { type: boolean }
                            job_id: { type: string, nullable: true }
                            error: { type: object, nullable: true }

  # ═══════════════════════════════════════════════════════════════
  # FLEET — Printer mutation, identify, media, groups
  # ═══════════════════════════════════════════════════════════════

  /fleet/printers/{printerId}/identify:
    post:
      tags: [Fleet]
      operationId: identifyPrinter
      summary: Flash/beep a printer to identify it physically
      parameters:
        - $ref: '#/components/parameters/PrinterId'
      responses:
        '202': { description: Identify command queued }
        '404': { $ref: '#/components/responses/NotFound' }

  /fleet/printers/{printerId}/media:
    put:
      tags: [Fleet]
      operationId: setPrinterMedia
      summary: Push a media definition to a printer remotely
      parameters:
        - $ref: '#/components/parameters/PrinterId'
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              properties:
                width_mm: { type: number }
                height_mm: { type: number }
                gap_mm: { type: number }
                media_type: { type: string, enum: [gap, blackmark, continuous] }
                darkness: { type: integer, minimum: 0, maximum: 30 }
                speed_ips: { type: number }
      responses:
        '200': { description: Media set }
        '404': { $ref: '#/components/responses/NotFound' }

  /fleet/groups:
    get:
      tags: [Printer Groups]
      operationId: listPrinterGroups
      summary: List printer groups
      responses:
        '200': { description: Groups, content: { application/json: { schema: { type: object, properties: { data: { type: array, items: { $ref: '#/components/schemas/PrinterGroup' } } } } } } }
    post:
      tags: [Printer Groups]
      operationId: createPrinterGroup
      summary: Create a printer group
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [name]
              properties:
                name: { type: string }
                description: { type: string }
                printer_ids: { type: array, items: { type: string } }
                routing_strategy: { type: string, enum: [round_robin, least_loaded, fastest, random], default: round_robin }
      responses:
        '201': { description: Group created, content: { application/json: { schema: { $ref: '#/components/schemas/PrinterGroup' } } } }

  /fleet/groups/{groupId}:
    parameters:
      - name: groupId
        in: path
        required: true
        schema: { type: string }
    get:
      tags: [Printer Groups]
      operationId: getPrinterGroup
      responses:
        '200': { description: Group, content: { application/json: { schema: { $ref: '#/components/schemas/PrinterGroup' } } } }
        '404': { $ref: '#/components/responses/NotFound' }
    put:
      tags: [Printer Groups]
      operationId: updatePrinterGroup
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              properties:
                name: { type: string }
                description: { type: string }
                printer_ids: { type: array, items: { type: string } }
                routing_strategy: { type: string }
      responses:
        '200': { description: Updated }
        '404': { $ref: '#/components/responses/NotFound' }
    delete:
      tags: [Printer Groups]
      operationId: deletePrinterGroup
      responses:
        '204': { description: Deleted }
        '404': { $ref: '#/components/responses/NotFound' }

  # ═══════════════════════════════════════════════════════════════
  # DESIGNS — Pages, snapshots, revisions, publish, page elements
  # ═══════════════════════════════════════════════════════════════

  /designs/{designId}/pages:
    parameters:
      - $ref: '#/components/parameters/DesignId'
    get:
      tags: [Designs]
      operationId: listDesignPages
      summary: List pages of a multi-page design
      responses:
        '200': { description: Pages, content: { application/json: { schema: { type: object, properties: { data: { type: array, items: { type: object } } } } } } }

  /designs/{designId}/pages/{pageId}/elements:
    parameters:
      - $ref: '#/components/parameters/DesignId'
      - name: pageId
        in: path
        required: true
        schema: { type: string }
    get:
      tags: [Designs]
      operationId: listPageElements
      responses:
        '200': { description: Elements on page }
    post:
      tags: [Designs]
      operationId: addPageElement
      requestBody:
        required: true
        content: { application/json: { schema: { $ref: '#/components/schemas/DesignElement' } } }
      responses:
        '201': { description: Element added }

  /designs/{designId}/pages/{pageId}/elements/{elementId}:
    parameters:
      - $ref: '#/components/parameters/DesignId'
      - name: pageId
        in: path
        required: true
        schema: { type: string }
      - name: elementId
        in: path
        required: true
        schema: { type: string }
    put:
      tags: [Designs]
      operationId: updatePageElement
      requestBody:
        required: true
        content: { application/json: { schema: { $ref: '#/components/schemas/DesignElement' } } }
      responses:
        '200': { description: Updated }
    patch:
      tags: [Designs]
      operationId: patchPageElement
      requestBody:
        required: true
        content: { application/json: { schema: { type: object } } }
      responses:
        '200': { description: Patched }
    delete:
      tags: [Designs]
      operationId: deletePageElement
      responses:
        '204': { description: Deleted }

  /designs/{designId}/publish:
    post:
      tags: [Designs]
      operationId: publishDesign
      summary: Publish an immutable snapshot of the current design
      parameters:
        - $ref: '#/components/parameters/DesignId'
      requestBody:
        content:
          application/json:
            schema:
              type: object
              properties:
                note: { type: string }
      responses:
        '201':
          description: Snapshot created
          content:
            application/json:
              schema:
                type: object
                properties:
                  data:
                    type: object
                    properties:
                      snapshot_id: { type: string }
                      revision: { type: integer }
                      published_at: { type: string, format: date-time }

  /designs/{designId}/revisions:
    get:
      tags: [Designs]
      operationId: listDesignRevisions
      summary: Revision history (in-place edits)
      parameters:
        - $ref: '#/components/parameters/DesignId'
      responses:
        '200': { description: Revisions }

  /designs/{designId}/snapshots:
    get:
      tags: [Designs]
      operationId: listDesignSnapshots
      summary: Published immutable snapshots
      parameters:
        - $ref: '#/components/parameters/DesignId'
      responses:
        '200': { description: Snapshots }

  /designs/{designId}/snapshots/{snapshotId}:
    get:
      tags: [Designs]
      operationId: getDesignSnapshot
      parameters:
        - $ref: '#/components/parameters/DesignId'
        - name: snapshotId
          in: path
          required: true
          schema: { type: string }
      responses:
        '200': { description: Snapshot detail }
        '404': { $ref: '#/components/responses/NotFound' }

  /designs/renders/{renderId}:
    get:
      tags: [Designs]
      operationId: getDesignRenderStatus
      summary: Poll an async render job started by /designs/{id}/render
      parameters:
        - name: renderId
          in: path
          required: true
          schema: { type: string }
      responses:
        '200': { description: Render status }

  # ═══════════════════════════════════════════════════════════════
  # WEBHOOKS — Events catalogue, test, deliveries
  # ═══════════════════════════════════════════════════════════════

  /webhooks/events:
    get:
      tags: [Webhooks]
      operationId: listWebhookEvents
      summary: List every event type you can subscribe to (categorised)
      responses:
        '200':
          description: Available events
          content:
            application/json:
              schema:
                type: object
                properties:
                  data:
                    type: object
                    properties:
                      total_events: { type: integer }
                      categories: { type: array, items: { type: object } }
                      all_events: { type: array, items: { type: string } }

  /webhooks/{webhookId}/deliveries:
    get:
      tags: [Webhooks]
      operationId: listWebhookDeliveries
      summary: Recent delivery attempts (success/failure/retry history)
      parameters:
        - name: webhookId
          in: path
          required: true
          schema: { type: string }
        - name: limit
          in: query
          schema: { type: integer, default: 50, maximum: 200 }
      responses:
        '200': { description: Delivery history }

  # ═══════════════════════════════════════════════════════════════
  # RULES — Conditional automation
  # ═══════════════════════════════════════════════════════════════

  /rules:
    get:
      tags: [Rules]
      operationId: listRules
      summary: List automation rules
      responses:
        '200': { description: Rules }
    post:
      tags: [Rules]
      operationId: createRule
      summary: Create a routing/transformation rule
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [name, trigger, conditions, actions]
              properties:
                name: { type: string }
                description: { type: string }
                enabled: { type: boolean, default: true }
                trigger:
                  type: object
                  properties:
                    event: { type: string, example: 'connect.record.created' }
                conditions:
                  type: array
                  items:
                    type: object
                    properties:
                      field: { type: string }
                      op: { type: string, enum: [eq, neq, gt, gte, lt, lte, contains, startsWith, endsWith, in, notIn, exists, notExists, matches] }
                      value: {}
                actions:
                  type: array
                  items:
                    type: object
                    properties:
                      type: { type: string, enum: [print, transform, route, webhook] }
                      params: { type: object }
      responses:
        '201': { description: Rule created }

  /rules/{ruleId}:
    parameters:
      - name: ruleId
        in: path
        required: true
        schema: { type: string }
    get:
      tags: [Rules]
      operationId: getRule
      responses: { '200': { description: Rule } }
    put:
      tags: [Rules]
      operationId: updateRule
      requestBody:
        required: true
        content: { application/json: { schema: { type: object } } }
      responses: { '200': { description: Updated } }
    delete:
      tags: [Rules]
      operationId: deleteRule
      responses: { '204': { description: Deleted } }

  /rules/{ruleId}/test:
    post:
      tags: [Rules]
      operationId: testRule
      summary: Dry-run a rule against a sample payload
      parameters:
        - name: ruleId
          in: path
          required: true
          schema: { type: string }
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              properties:
                payload: { type: object }
      responses:
        '200':
          description: Dry-run trace
          content:
            application/json:
              schema:
                type: object
                properties:
                  matched: { type: boolean }
                  conditions_evaluated: { type: array, items: { type: object } }
                  actions_planned: { type: array, items: { type: object } }

  # ═══════════════════════════════════════════════════════════════
  # SITES — Multi-location
  # ═══════════════════════════════════════════════════════════════

  /sites:
    get:
      tags: [Sites]
      operationId: listSites
      responses: { '200': { description: Sites } }
    post:
      tags: [Sites]
      operationId: createSite
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [name]
              properties:
                name: { type: string }
                address: { type: string }
                city: { type: string }
                country: { type: string }
                lat: { type: number }
                lon: { type: number }
                timezone: { type: string }
      responses: { '201': { description: Site created } }

  /sites/nearest:
    get:
      tags: [Sites]
      operationId: findNearestSite
      summary: Geo-search by lat/lon
      parameters:
        - name: lat
          in: query
          required: true
          schema: { type: number }
        - name: lon
          in: query
          required: true
          schema: { type: number }
        - name: limit
          in: query
          schema: { type: integer, default: 5 }
      responses: { '200': { description: Sites sorted by distance } }

  /sites/{siteId}:
    parameters:
      - name: siteId
        in: path
        required: true
        schema: { type: string }
    get:
      tags: [Sites]
      operationId: getSite
      responses: { '200': { description: Site } }
    put:
      tags: [Sites]
      operationId: updateSite
      requestBody: { required: true, content: { application/json: { schema: { type: object } } } }
      responses: { '200': { description: Updated } }
    delete:
      tags: [Sites]
      operationId: deleteSite
      responses: { '204': { description: Deleted } }

  /sites/{siteId}/printers:
    parameters:
      - name: siteId
        in: path
        required: true
        schema: { type: string }
    post:
      tags: [Sites]
      operationId: assignPrintersToSite
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              properties:
                printer_ids: { type: array, items: { type: string } }
      responses: { '200': { description: Assigned } }

  /sites/{siteId}/printers/{printerId}:
    parameters:
      - name: siteId
        in: path
        required: true
        schema: { type: string }
      - name: printerId
        in: path
        required: true
        schema: { type: string }
    delete:
      tags: [Sites]
      operationId: removePrinterFromSite
      responses: { '204': { description: Removed } }

  /sites/{siteId}/analytics:
    parameters:
      - name: siteId
        in: path
        required: true
        schema: { type: string }
    get:
      tags: [Sites]
      operationId: getSiteAnalytics
      responses: { '200': { description: Per-site KPIs } }

  # ═══════════════════════════════════════════════════════════════
  # ANALYTICS — Trends, predictions, quality, digital twin
  # ═══════════════════════════════════════════════════════════════

  /analytics/fleet/trends:
    get:
      tags: [Analytics]
      operationId: getFleetTrends
      summary: Time-series KPIs across the whole fleet
      parameters:
        - name: period
          in: query
          schema: { type: string, enum: [day, week, month, quarter], default: week }
      responses: { '200': { description: Trend series } }

  /analytics/predictions/{printerId}:
    get:
      tags: [Analytics]
      operationId: getPrinterPredictions
      summary: Predictive failure / supply-exhaustion forecast
      parameters:
        - $ref: '#/components/parameters/PrinterId'
        - name: horizon_days
          in: query
          schema: { type: integer, default: 14 }
      responses: { '200': { description: Predictions } }

  /analytics/quality/{printerId}:
    get:
      tags: [Analytics]
      operationId: getPrintQuality
      summary: Print-quality score & defect rate per printer
      parameters:
        - $ref: '#/components/parameters/PrinterId'
      responses: { '200': { description: Quality metrics } }

  /analytics/digital-twin/simulate:
    post:
      tags: [Analytics]
      operationId: simulateDigitalTwin
      summary: Render a label on a virtual printer (no physical print)
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              properties:
                printer_model: { type: string }
                payload_type: { type: string }
                payload_data: { type: string }
                design_id: { type: string }
                data: { type: object }
      responses: { '200': { description: Simulated bitmap + quality score } }

  /analytics/digital-twin/compare:
    post:
      tags: [Analytics]
      operationId: compareDigitalTwin
      summary: Diff a design preview against an actual scan
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              properties:
                design_id: { type: string }
                actual_image_url: { type: string }
      responses: { '200': { description: Diff metrics } }

  # ═══════════════════════════════════════════════════════════════
  # AUDIT & COMPLIANCE
  # ═══════════════════════════════════════════════════════════════

  /audit/trail:
    get:
      tags: [Audit]
      operationId: queryAuditTrail
      summary: Query the tamper-evident audit log
      parameters:
        - name: actor
          in: query
          schema: { type: string }
        - name: action
          in: query
          schema: { type: string }
        - name: from
          in: query
          schema: { type: string, format: date-time }
        - name: to
          in: query
          schema: { type: string, format: date-time }
        - name: limit
          in: query
          schema: { type: integer, default: 100, maximum: 1000 }
      responses: { '200': { description: Audit entries } }

  /audit/trail/{entryId}:
    get:
      tags: [Audit]
      operationId: getAuditEntry
      parameters:
        - name: entryId
          in: path
          required: true
          schema: { type: string }
      responses:
        '200': { description: Entry with hash chain proof }
        '404': { $ref: '#/components/responses/NotFound' }

  /audit/summary:
    get:
      tags: [Audit]
      operationId: getAuditSummary
      summary: Aggregated counts by actor / action / day
      responses: { '200': { description: Summary } }

  /audit/ingest:
    post:
      tags: [Audit]
      operationId: ingestAuditEvents
      summary: Push external audit events into the chain
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              properties:
                events:
                  type: array
                  items:
                    type: object
                    properties:
                      actor: { type: string }
                      action: { type: string }
                      target: { type: string }
                      metadata: { type: object }
                      timestamp: { type: string, format: date-time }
      responses: { '202': { description: Ingested } }

  /audit/compliance/pools:
    get:
      tags: [Compliance]
      operationId: listCompliancePools
      summary: List regulated serial-number pools (FDA, GHS, pharma)
      responses: { '200': { description: Pools } }
    post:
      tags: [Compliance]
      operationId: createCompliancePool
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [name, format, regulator]
              properties:
                name: { type: string }
                regulator: { type: string, example: 'FDA-UDI' }
                format: { type: string, example: 'PREFIX-{:08d}' }
                start: { type: integer, default: 1 }
                end: { type: integer }
      responses: { '201': { description: Pool created } }

  /audit/compliance/pools/{poolId}:
    parameters:
      - name: poolId
        in: path
        required: true
        schema: { type: string }
    get:
      tags: [Compliance]
      operationId: getCompliancePool
      responses: { '200': { description: Pool detail + utilisation } }

  /audit/compliance/pools/{poolId}/allocate:
    parameters:
      - name: poolId
        in: path
        required: true
        schema: { type: string }
    post:
      tags: [Compliance]
      operationId: allocateSerials
      summary: Reserve N serial numbers from the pool
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [count]
              properties:
                count: { type: integer, minimum: 1, maximum: 10000 }
                printer_id: { type: string }
                job_id: { type: string }
      responses: { '200': { description: Allocated serials } }

  /audit/compliance/pools/{poolId}/void:
    parameters:
      - name: poolId
        in: path
        required: true
        schema: { type: string }
    post:
      tags: [Compliance]
      operationId: voidSerials
      summary: Mark previously allocated serials as voided (with reason)
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [serials, reason]
              properties:
                serials: { type: array, items: { type: string } }
                reason: { type: string }
      responses: { '200': { description: Voided } }

  /audit/compliance/pools/{poolId}/report:
    parameters:
      - name: poolId
        in: path
        required: true
        schema: { type: string }
    get:
      tags: [Compliance]
      operationId: getComplianceReport
      summary: Regulator-ready report (CSV/JSON) of all serials in this pool
      parameters:
        - name: format
          in: query
          schema: { type: string, enum: [json, csv], default: json }
      responses: { '200': { description: Report } }

  # ═══════════════════════════════════════════════════════════════
  # RENDER — Async raster
  # ═══════════════════════════════════════════════════════════════

  /render:
    post:
      tags: [Render]
      operationId: renderDesign
      summary: Render a design with variable data to PNG/PDF
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [design_id]
              properties:
                design_id: { type: string }
                data: { type: object }
                format: { type: string, enum: [png, pdf, jpg], default: png }
                dpi: { type: integer, default: 203, enum: [203, 300, 600] }
                async: { type: boolean, default: true }
      responses:
        '202':
          description: Render queued
          content:
            application/json:
              schema:
                type: object
                properties:
                  data:
                    type: object
                    properties:
                      render_id: { type: string }
                      status_url: { type: string }

  /render/html:
    post:
      tags: [Render]
      operationId: renderHtml
      summary: Render arbitrary HTML to a label-sized PNG/PDF
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [html]
              properties:
                html: { type: string }
                width_mm: { type: number, default: 100 }
                height_mm: { type: number, default: 50 }
                dpi: { type: integer, default: 203 }
                format: { type: string, enum: [png, pdf], default: png }
      responses: { '202': { description: Render queued } }

  /render/{renderId}:
    parameters:
      - name: renderId
        in: path
        required: true
        schema: { type: string }
    get:
      tags: [Render]
      operationId: getRenderJob
      summary: Poll render status / get result URLs
      responses: { '200': { description: Render status + URLs } }

  /render/{renderId}/image:
    parameters:
      - name: renderId
        in: path
        required: true
        schema: { type: string }
    get:
      tags: [Render]
      operationId: getRenderImage
      summary: 302-redirect to the rendered binary
      responses:
        '302': { description: Redirect to image URL }
        '404': { $ref: '#/components/responses/NotFound' }

  # ═══════════════════════════════════════════════════════════════
  # USAGE
  # ═══════════════════════════════════════════════════════════════

  /usage:
    get:
      tags: [Usage]
      operationId: getUsageSummary
      summary: Current period summary (today + monthly + plan limits)
      responses: { '200': { description: Usage summary } }

  /usage/daily:
    get:
      tags: [Usage]
      operationId: getDailyUsage
      summary: Daily call counts (default 30 days, max 90)
      parameters:
        - name: days
          in: query
          schema: { type: integer, minimum: 1, maximum: 90, default: 30 }
      responses: { '200': { description: Daily breakdown } }

  # ═══════════════════════════════════════════════════════════════
  # RELAY — WebSocket bridge status
  # ═══════════════════════════════════════════════════════════════

  /relay/status:
    get:
      tags: [Relay]
      operationId: getRelayStatus
      summary: WebSocket relay health snapshot (connected edges, dashboards, buffered messages)
      responses:
        '200':
          description: Relay status
          content:
            application/json:
              schema:
                type: object
                properties:
                  status: { type: string, enum: [ok] }
                  connectedEdges: { type: integer }
                  connectedDashboards: { type: integer }
                  totalBufferedMessages: { type: integer }

  # ═══════════════════════════════════════════════════════════════
  # CONNECT — Help/root endpoint
  # ═══════════════════════════════════════════════════════════════

  /connect:
    get:
      tags: [Data Connect]
      operationId: getConnectIndex
      summary: Service descriptor + endpoint index
      responses: { '200': { description: Service info } }

  # ═══════════════════════════════════════════════════════════════
  # MCP — Model Context Protocol (Streamable HTTP)
  # ═══════════════════════════════════════════════════════════════

  /mcp:
    get:
      tags: [MCP]
      operationId: getMcpServerInfo
      summary: Server descriptor (name, version, transport, tool count, usage hints)
      security: []
      responses:
        '200':
          description: MCP server info
          content:
            application/json:
              schema:
                type: object
                properties:
                  name: { type: string }
                  version: { type: string }
                  transport: { type: string, example: 'streamable-http' }
                  tools: { type: integer, example: 112 }
                  description: { type: string }
                  docs: { type: string }
    post:
      tags: [MCP]
      operationId: callMcp
      summary: JSON-RPC 2.0 over Streamable HTTP (MCP protocol). Auth via Bearer.
      description: |
        Endpoint speaks the MCP Streamable-HTTP transport. Use this from Claude
        Desktop, Cursor, or any MCP-compatible client. 112 tools cover every
        REST endpoint above plus AI helpers. Rate limit: 120 req/min per key.
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              description: JSON-RPC 2.0 request envelope
              properties:
                jsonrpc: { type: string, enum: ['2.0'] }
                id: {}
                method: { type: string }
                params: { type: object }
      responses:
        '200': { description: JSON-RPC response or SSE stream }
        '401': { $ref: '#/components/responses/Unauthorized' }
        '429': { $ref: '#/components/responses/RateLimited' }

# ═══════════════════════════════════════════════════════════════
# COMPONENTS
# ═══════════════════════════════════════════════════════════════

components:

  # ── Security ────────────────────────────────────────────────

  securitySchemes:
    BearerAuth:
      type: http
      scheme: bearer
      description: API key (sk_live_xxx or sk_test_xxx)

  # ── Parameters ──────────────────────────────────────────────

  parameters:
    JobId:
      name: jobId
      in: path
      required: true
      schema:
        type: string
    PrinterId:
      name: printerId
      in: path
      required: true
      schema:
        type: string
    DesignId:
      name: designId
      in: path
      required: true
      schema:
        type: string
    ElementId:
      name: elementId
      in: path
      required: true
      schema:
        type: string
    WebhookId:
      name: webhookId
      in: path
      required: true
      schema:
        type: string
    IdempotencyKey:
      name: X-Idempotency-Key
      in: header
      required: false
      description: Unique key to prevent duplicate jobs (24h TTL)
      schema:
        type: string

  # ── Schemas ─────────────────────────────────────────────────

  schemas:

    # Print Jobs
    CreateJobRequest:
      type: object
      required: [printer_id, payload_type]
      properties:
        printer_id:
          type: string
          description: Target printer ID
        payload_type:
          type: string
          enum: [zpl, image, template]
        payload_data:
          type: string
          description: ZPL string or base64 image data (max 500 KB)
        image_url:
          type: string
          format: uri
          description: HTTPS URL to image (for payload_type "image"). Max 500 KB download.
        design_id:
          type: string
          description: Template ID (for payload_type "template")
        data:
          description: Variable values for template
          oneOf:
            - type: object
              additionalProperties:
                type: string
            - type: array
              items:
                type: object
                additionalProperties:
                  type: string
        job_name:
          type: string
          description: Human-readable job name
        copies:
          type: integer
          minimum: 1
          maximum: 1000
          default: 1
        media_width_mm:
          type: number
          description: Label width in millimeters
        media_height_mm:
          type: number
          description: Label height in millimeters

    PrintJob:
      type: object
      properties:
        id:
          type: string
        status:
          type: string
          enum: [queued, claimed, rendering, printing, completed, failed, cancelled]
        printer_id:
          type: string
        payload_type:
          type: string
        job_name:
          type: string
        copies:
          type: integer
        is_test_mode:
          type: boolean
        created_at:
          type: string
          format: date-time
        updated_at:
          type: string
          format: date-time
        completed_at:
          type: string
          format: date-time
          nullable: true
        error_message:
          type: string
          nullable: true

    JobResponse:
      type: object
      properties:
        success:
          type: boolean
        data:
          $ref: '#/components/schemas/PrintJob'

    # Fleet
    Printer:
      type: object
      properties:
        id:
          type: string
        name:
          type: string
        model:
          type: string
        type:
          type: string
        connection:
          type: string
          enum: [usb, network, bluetooth]
        status:
          type: string
          enum: [online, offline]
        ip_address:
          type: string
        serial_number:
          type: string
        last_seen:
          type: string
          format: date-time

    PrinterDetailed:
      allOf:
        - $ref: '#/components/schemas/Printer'
        - type: object
          properties:
            firmware_version:
              type: string
            mac_address:
              type: string
            supply_levels:
              type: object
              properties:
                ribbon:
                  type: string
                labels:
                  type: string
            warnings:
              type: array
              items:
                type: string
            last_media_definition:
              type: object
              properties:
                width_mm:
                  type: number
                height_mm:
                  type: number
                type:
                  type: string
            last_print_job:
              type: object
              properties:
                id:
                  type: string
                name:
                  type: string
                completed_at:
                  type: string
                  format: date-time

    PrinterStatus:
      type: object
      properties:
        id:
          type: string
        status:
          type: string
          enum: [online, offline]
        last_seen:
          type: string
          format: date-time
        paper_status:
          type: string
        ribbon_status:
          type: string
        total_jobs_today:
          type: integer
        failed_jobs_today:
          type: integer

    # Designs
    DesignSummary:
      type: object
      properties:
        id:
          type: string
        name:
          type: string
        category:
          type: string
        width_mm:
          type: number
        height_mm:
          type: number
        element_count:
          type: integer
        created_at:
          type: string
          format: date-time
        updated_at:
          type: string
          format: date-time

    Design:
      allOf:
        - $ref: '#/components/schemas/DesignSummary'
        - type: object
          properties:
            description:
              type: string
            elements:
              type: array
              items:
                $ref: '#/components/schemas/DesignElement'
            variables:
              type: array
              items:
                $ref: '#/components/schemas/DesignVariable'

    CreateDesignRequest:
      type: object
      required: [name]
      properties:
        name:
          type: string
          maxLength: 256
        description:
          type: string
          maxLength: 1024
        category:
          type: string
          maxLength: 64
        width_mm:
          type: number
        height_mm:
          type: number

    UpdateDesignRequest:
      type: object
      properties:
        name:
          type: string
          maxLength: 256
        description:
          type: string
          maxLength: 1024
        category:
          type: string
          maxLength: 64
        width_mm:
          type: number
        height_mm:
          type: number

    PrinterGroup:
      type: object
      properties:
        group_id:
          type: string
        name:
          type: string
        description:
          type: string
        printer_ids:
          type: array
          items:
            type: string
        routing_strategy:
          type: string
          enum: [round_robin, least_loaded, fastest, random]
        created_at:
          type: string
          format: date-time

    DesignElement:
      type: object
      properties:
        id:
          type: string
        type:
          type: string
          enum: [text, barcode, shape, image, svg, line, itemsList, group, pdf]
        x:
          type: number
        y:
          type: number
        width:
          type: number
        height:
          type: number
        rotation:
          type: number
        text:
          type: string
          description: For text elements
        font_size:
          type: number
        font_family:
          type: string
        font_weight:
          type: string
        font_color:
          type: string
          description: Hex color (e.g. "#000000")
        barcode_type:
          type: string
          description: For barcode elements (code128, qrcode, ean13, etc.)
        data:
          type: string
          description: Barcode data or binding expression
        background_color:
          type: string
        border_color:
          type: string
        border_width:
          type: number
        data_source:
          type: object
          description: Variable binding configuration
          properties:
            field:
              type: string
            type:
              type: string

    CreateElementRequest:
      type: object
      required: [type, x, y]
      properties:
        type:
          type: string
          enum: [text, barcode, shape, image, svg, line, itemsList, group, pdf]
        x:
          type: number
        y:
          type: number
        width:
          type: number
        height:
          type: number
        text:
          type: string
        font_size:
          type: number
        barcode_type:
          type: string
        data:
          type: string

    UpdateElementRequest:
      type: object
      properties:
        x:
          type: number
        y:
          type: number
        width:
          type: number
        height:
          type: number
        text:
          type: string
        font_size:
          type: number
        data:
          type: string

    DesignVariable:
      type: object
      properties:
        name:
          type: string
        type:
          type: string
        element_id:
          type: string
        default_value:
          type: string

    # Webhooks
    CreateWebhookRequest:
      type: object
      required: [url, events]
      properties:
        url:
          type: string
          format: uri
          description: HTTPS endpoint to receive events (private IPs blocked)
        events:
          type: array
          items:
            type: string
            enum:
              - print.job.completed
              - print.job.failed
              - print.job.cancelled
              - printer.status.changed
              - printer.supply.low
          minItems: 1
        description:
          type: string

    Webhook:
      type: object
      properties:
        id:
          type: string
        url:
          type: string
        events:
          type: array
          items:
            type: string
        description:
          type: string
        signing_secret:
          type: string
          description: "HMAC-SHA256 signing secret (format: whsec_xxx). Only returned on creation."
        is_active:
          type: boolean
        created_at:
          type: string
          format: date-time

    WebhookSummary:
      type: object
      properties:
        id:
          type: string
        url:
          type: string
        events:
          type: array
          items:
            type: string
        is_active:
          type: boolean
        consecutive_failures:
          type: integer
        created_at:
          type: string
          format: date-time

    # Data Connect
    ParserConfig:
      type: object
      properties:
        formatHint:
          type: string
          enum: [xml, csv, tsv, json, ndjson]
        delimiter:
          type: string
          description: Custom delimiter (CSV/TSV)
        hasHeader:
          type: boolean
          default: true
        repeatPath:
          type: string
          description: Dot-path to repeating XML element
        includeAttributes:
          type: boolean
          default: false
        recordsPath:
          type: string
          description: Dot-path to array in JSON wrapper

    SchemaField:
      type: object
      properties:
        path:
          type: string
        type:
          type: string
        sample:
          type: string

    IngestResult:
      type: object
      properties:
        source_id:
          type: string
        ingest_id:
          type: string
        format:
          type: string
        record_count:
          type: integer
        schema:
          type: array
          items:
            $ref: '#/components/schemas/SchemaField'
        records_preview:
          type: array
          items:
            type: object

    ParseResult:
      type: object
      properties:
        format:
          type: string
        record_count:
          type: integer
        schema:
          type: array
          items:
            $ref: '#/components/schemas/SchemaField'
        records_preview:
          type: array
          items:
            type: object

    CreateSourceRequest:
      type: object
      required: [name]
      properties:
        name:
          type: string
          maxLength: 100
        description:
          type: string
          maxLength: 500
        format:
          type: string
          enum: [xml, csv, tsv, json, ndjson]
        config:
          $ref: '#/components/schemas/ParserConfig'

    ConnectorSource:
      type: object
      properties:
        id:
          type: string
        name:
          type: string
        description:
          type: string
        format:
          type: string
        config:
          type: object
        schema_field_count:
          type: integer
        record_count:
          type: integer
        is_active:
          type: boolean
        webhook_url:
          type: string
        webhook_headers:
          type: object
        last_ingest_at:
          type: string
          format: date-time
        created_at:
          type: string
          format: date-time

    ConnectorSourceSummary:
      type: object
      properties:
        id:
          type: string
        name:
          type: string
        format:
          type: string
        schema_field_count:
          type: integer
        record_count:
          type: integer
        is_active:
          type: boolean
        last_ingest_at:
          type: string
          format: date-time

    ConnectorRecord:
      type: object
      properties:
        id:
          type: string
        data:
          type: object
        ingest_id:
          type: string
        created_at:
          type: string
          format: date-time

    SchemaResult:
      type: object
      properties:
        source_id:
          type: string
        source_name:
          type: string
        schema:
          type: array
          items:
            $ref: '#/components/schemas/SchemaField'
        record_count:
          type: integer
        last_ingest_at:
          type: string
          format: date-time

    MappingsResult:
      type: object
      properties:
        source_id:
          type: string
        mappings:
          type: object
          additionalProperties:
            type: string
        schema:
          type: array
          items:
            $ref: '#/components/schemas/SchemaField'

    ConnectorPrintRequest:
      type: object
      required: [printer_id, design_id]
      properties:
        printer_id:
          type: string
        design_id:
          type: string
        record_ids:
          type: array
          items:
            type: string
          maxItems: 50
        ingest_id:
          type: string
        copies:
          type: integer
          minimum: 1
          maximum: 100
          default: 1
        max_records:
          type: integer
          default: 100
        field_mappings:
          type: object
          additionalProperties:
            type: string
        job_name:
          type: string

    ConnectorPrintResult:
      type: object
      properties:
        jobs_created:
          type: integer
        copies_per_job:
          type: integer
        total_labels:
          type: integer
        job_ids:
          type: array
          items:
            type: string
        source_id:
          type: string

    Pagination:
      type: object
      properties:
        has_more:
          type: boolean
        next_page_token:
          type: string

    # Errors
    Error:
      type: object
      properties:
        success:
          type: boolean
          example: false
        error:
          type: object
          properties:
            code:
              type: string
            message:
              type: string

    # ── Edge Server Schemas ─────────────────────────────────────

    EdgeHealthResponse:
      type: object
      properties:
        status:
          type: string
          example: ok
        server:
          type: string
          example: LabelInn Edge Server
        version:
          type: string
          example: "1.0.0"
        mode:
          type: string
          enum: [local, hybrid, cloudRelay]
          description: Edge Server operating mode
        port:
          type: integer
          example: 6631
        localAddresses:
          type: array
          items: { type: string }
          example: ["192.168.1.100", "10.0.0.5"]
        uptime:
          type: string
          example: running

    EdgeCreateJobRequest:
      type: object
      required: [printerId, payloadType]
      properties:
        printerId:
          type: string
          description: ID of the target printer (from /local/v1/fleet/printers)
        payloadType:
          type: string
          enum: [zpl, escLabel, design]
          description: Type of print payload
        payloadData:
          type: string
          description: Raw ZPL string or base64-encoded ESC/POS bytes
        designId:
          type: string
          description: Cloud design template ID (for payloadType "design")
        designDataRows:
          type: array
          items:
            type: object
            additionalProperties: true
          description: Variable data rows to merge with the design template
        jobName:
          type: string
          default: Edge Print Job
        copies:
          type: integer
          minimum: 1
          default: 1
        totalLabels:
          type: integer
          minimum: 1
          default: 1
        mediaWidthMm:
          type: number
          description: Label width in millimeters
        mediaHeightMm:
          type: number
          description: Label height in millimeters

    EdgeJobAccepted:
      type: object
      properties:
        jobId:
          type: string
          format: uuid
        status:
          type: string
          example: accepted
        message:
          type: string
          example: "Job submitted. Poll GET /local/v1/jobs/{jobId} for status."

    EdgeJobResult:
      type: object
      properties:
        jobId:
          type: string
          format: uuid
        success:
          type: boolean
        printedLabels:
          type: integer
        completedAt:
          type: string
          format: date-time
        errorMessage:
          type: string
          description: Present only when success is false

    EdgePrinterList:
      type: object
      properties:
        printers:
          type: array
          items:
            type: object
            properties:
              id:
                type: string
              name:
                type: string
              connectionType:
                type: string
                enum: [usb, tcp, bluetooth]
              model:
                type: string
        count:
          type: integer

    # ── Discovery Schemas (Cloud API) ───────────────────────────

    DiscoveredPrinter:
      type: object
      properties:
        name:
          type: string
          description: Printer name (from mDNS, SNMP, or driver)
        address:
          type: string
          description: IP address or USB path
        type:
          type: string
          description: Printer type identifier
        brand:
          type: string
          description: Manufacturer (e.g. ZEBRA, BROTHER, GENERIC)
        model:
          type: string
          nullable: true
          description: Model name if detected
        connection_type:
          type: string
          enum: [tcp, usb, bluetooth]
        mac_address:
          type: string
          nullable: true
        is_connected:
          type: boolean
        is_thermal:
          type: boolean

    DiscoverySessionCreated:
      type: object
      properties:
        data:
          type: object
          properties:
            session_id:
              type: string
            status:
              type: string
              enum: [pending, scanning]
            message:
              type: string

    DiscoverySessionResult:
      type: object
      properties:
        data:
          type: object
          properties:
            session_id:
              type: string
            status:
              type: string
              enum: [pending, scanning, complete, expired, error]
            device_id:
              type: string
              nullable: true
            printers:
              type: array
              items:
                $ref: '#/components/schemas/DiscoveredPrinter'
            count:
              type: integer
            created_at:
              type: string
              format: date-time

    AddDiscoveredPrinterRequest:
      type: object
      required: [address]
      properties:
        address:
          type: string
          description: Address of the printer from discovery results
        name:
          type: string
          description: Custom name for the printer (optional, uses discovered name if omitted)

    AddDiscoveredPrinterResponse:
      type: object
      properties:
        data:
          type: object
          properties:
            id:
              type: string
              description: Deterministic printer ID (prt_xxx)
            name:
              type: string
            address:
              type: string
            type:
              type: string
            brand:
              type: string
            model:
              type: string
              nullable: true
            connection_type:
              type: string
            status:
              type: string
              enum: [registered, updated]
              description: Whether the printer was newly registered or an existing entry was updated

    # ── Discovery Schemas (Edge Server) ─────────────────────────

    EdgeDiscoveryStarted:
      type: object
      properties:
        sessionId:
          type: string
          format: uuid
        status:
          type: string
          example: scanning
        message:
          type: string

    EdgeDiscoveryResult:
      type: object
      properties:
        sessionId:
          type: string
          format: uuid
        status:
          type: string
          enum: [scanning, complete]
        printers:
          type: array
          items:
            type: object
            properties:
              name:
                type: string
              address:
                type: string
              type:
                type: string
              brand:
                type: string
              model:
                type: string
                nullable: true
              connectionType:
                type: string
                enum: [tcp, usb, bluetooth]
              macAddress:
                type: string
                nullable: true
              isConnected:
                type: boolean
              isThermal:
                type: boolean
              port:
                type: integer
        count:
          type: integer

  # ── Responses ───────────────────────────────────────────────

  responses:
    BadRequest:
      description: Invalid request
      content:
        application/json:
          schema:
            $ref: '#/components/schemas/Error'
    Unauthorized:
      description: Invalid or missing API key
      content:
        application/json:
          schema:
            $ref: '#/components/schemas/Error'
    NotFound:
      description: Resource not found
      content:
        application/json:
          schema:
            $ref: '#/components/schemas/Error'
    RateLimited:
      description: Rate limit exceeded
      headers:
        X-RateLimit-Limit:
          schema:
            type: integer
        X-RateLimit-Remaining:
          schema:
            type: integer
        X-RateLimit-Reset:
          schema:
            type: integer
            description: Seconds until rate limit resets
      content:
        application/json:
          schema:
            $ref: '#/components/schemas/Error'
