openapi: 3.1.0
info:
  title: Canary API
  version: "1.0"
  description: |
    Build integrations around Canary work orders, requests, equipment, inventory, meters,
    custom fields, and scannable identifiers.

    ## Authentication

    All API requests require authentication using an API key. Include your API key in the `Authorization` header:

    ```
    Authorization: Bearer sk_live_your_api_key_here
    ```

    ## Rate Limits

    - **Live keys**: 1,000 requests per minute
    - **Test keys**: 100 requests per minute

    Both key prefixes access the same organization data. The prefix selects the rate-limit bucket.

    Successful authenticated responses include rate limit headers:
    - `X-RateLimit-Limit`: Maximum requests per window
    - `X-RateLimit-Remaining`: Requests remaining in current window
    - `X-RateLimit-Reset`: Unix timestamp when the window resets

    ## Pagination

    Canary-native list endpoints use ID cursors and the standard `data`/`meta` envelope.
    MaintainX-compatible request endpoints use their compatibility response wrappers and
    encoded offset cursors. Follow each operation's response schema.

    Native list responses include:
    - `data`: Array of items
    - `meta.pagination.cursor`: Cursor for the next page (if `has_more` is true)
    - `meta.pagination.has_more`: Whether more items exist

    Use the cursor value in your next request: `?cursor=<cursor_value>`

servers:
  - url: https://api.oncanary.com/v1
    description: Production
  - url: http://localhost:3000/api/v1
    description: Local Development

security:
  - bearerAuth: []

tags:
  - name: Meters
    description: Meters track equipment usage and readings
  - name: Meter Readings
    description: Record and retrieve meter readings
  - name: Work Orders
    description: Maintenance work orders
  - name: Work Requests
    description: Requests that can be reviewed and converted into work orders
  - name: Work Request Portals
    description: Public request portal configuration
  - name: Assets
    description: Equipment and machinery
  - name: Locations
    description: Physical places where assets, parts, and meters are assigned
  - name: Parts
    description: Spare parts and inventory records
  - name: Identifiers
    description: Scannable identifiers for assets, locations, and parts
  - name: Custom Fields
    description: Typed custom-field definitions shared by maintenance resources

paths:
  # ==================== Custom Fields ====================
  /customfields/{entity}:
    parameters:
      - name: entity
        in: path
        required: true
        description: MaintainX resource family for the custom-field definitions
        schema:
          type: string
          enum: [assets, locations, parts, workOrders]
    get:
      operationId: listCustomFields
      x-required-scope: custom_fields:read
      summary: List custom-field definitions
      tags: [Custom Fields]
      responses:
        "200":
          description: Active custom-field definitions in display order
          content:
            application/json:
              schema:
                type: array
                items:
                  $ref: "#/components/schemas/ListedCustomField"
        "401":
          $ref: "#/components/responses/Unauthorized"
        "403":
          $ref: "#/components/responses/Forbidden"
        "429":
          $ref: "#/components/responses/RateLimited"
    post:
      operationId: createCustomFields
      x-required-scope: custom_fields:write
      summary: Create custom-field definitions
      description: Creates the supplied definitions atomically and appends them in request order
      tags: [Custom Fields]
      parameters:
        - $ref: "#/components/parameters/customFieldSkipWebhook"
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: "#/components/schemas/CreateCustomFieldsRequest"
      responses:
        "200":
          description: Created definitions
          content:
            application/json:
              schema:
                type: array
                items:
                  $ref: "#/components/schemas/CreatedCustomField"
        "400":
          $ref: "#/components/responses/BadRequest"
        "401":
          $ref: "#/components/responses/Unauthorized"
        "403":
          $ref: "#/components/responses/Forbidden"
        "409":
          $ref: "#/components/responses/Conflict"
        "429":
          $ref: "#/components/responses/RateLimited"

  /customfields/{entity}/{id}:
    parameters:
      - name: entity
        in: path
        required: true
        schema:
          type: string
          enum: [assets, locations, parts, workOrders]
      - name: id
        in: path
        required: true
        schema:
          type: integer
          format: int32
          minimum: 1
          maximum: 2147483647
    patch:
      operationId: updateCustomField
      x-required-scope: custom_fields:write
      summary: Update or reorder a custom-field definition
      tags: [Custom Fields]
      parameters:
        - $ref: "#/components/parameters/customFieldSkipWebhook"
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: "#/components/schemas/UpdateCustomFieldRequest"
      responses:
        "204":
          description: Definition updated
        "400":
          $ref: "#/components/responses/BadRequest"
        "401":
          $ref: "#/components/responses/Unauthorized"
        "403":
          $ref: "#/components/responses/Forbidden"
        "404":
          $ref: "#/components/responses/NotFound"
        "409":
          $ref: "#/components/responses/Conflict"
        "429":
          $ref: "#/components/responses/RateLimited"
    delete:
      operationId: archiveCustomField
      x-required-scope: custom_fields:write
      summary: Archive a custom-field definition
      tags: [Custom Fields]
      parameters:
        - $ref: "#/components/parameters/customFieldSkipWebhook"
      responses:
        "204":
          description: Definition archived
        "400":
          $ref: "#/components/responses/BadRequest"
        "401":
          $ref: "#/components/responses/Unauthorized"
        "403":
          $ref: "#/components/responses/Forbidden"
        "404":
          $ref: "#/components/responses/NotFound"
        "429":
          $ref: "#/components/responses/RateLimited"

  # ==================== Meters ====================
  /meters:
    get:
      operationId: listMeters
      x-required-scope: meters:read
      summary: List meters
      description: Retrieve a paginated list of meters
      tags: [Meters]
      parameters:
        - $ref: "#/components/parameters/limit"
        - $ref: "#/components/parameters/cursor"
        - $ref: "#/components/parameters/direction"
        - name: meter_type
          in: query
          description: Filter by meter type
          schema:
            $ref: "#/components/schemas/MeterType"
        - name: category
          in: query
          description: Filter by category
          schema:
            $ref: "#/components/schemas/MeterCategory"
        - name: asset_id
          in: query
          description: Filter by asset ID
          schema:
            type: string
        - name: location_id
          in: query
          description: Filter by location ID
          schema:
            type: string
      responses:
        "200":
          description: List of meters
          content:
            application/json:
              schema:
                allOf:
                  - $ref: "#/components/schemas/ListResponse"
                  - type: object
                    properties:
                      data:
                        type: array
                        items:
                          $ref: "#/components/schemas/Meter"
        "400":
          $ref: "#/components/responses/BadRequest"
        "401":
          $ref: "#/components/responses/Unauthorized"
        "403":
          $ref: "#/components/responses/Forbidden"
        "429":
          $ref: "#/components/responses/RateLimited"

    post:
      operationId: createMeter
      x-required-scope: meters:write
      summary: Create a meter
      description: Create a new meter attached to an asset or location
      tags: [Meters]
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: "#/components/schemas/CreateMeterRequest"
      responses:
        "201":
          description: Meter created
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/MeterResponse"
        "400":
          $ref: "#/components/responses/BadRequest"
        "401":
          $ref: "#/components/responses/Unauthorized"
        "403":
          $ref: "#/components/responses/Forbidden"
        "404":
          $ref: "#/components/responses/NotFound"
        "429":
          $ref: "#/components/responses/RateLimited"

  /meters/{id}:
    get:
      operationId: getMeter
      x-required-scope: meters:read
      summary: Get a meter
      description: Retrieve a single meter by ID
      tags: [Meters]
      parameters:
        - $ref: "#/components/parameters/id"
      responses:
        "200":
          description: Meter details
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/MeterResponse"
        "401":
          $ref: "#/components/responses/Unauthorized"
        "404":
          $ref: "#/components/responses/NotFound"
        "403":
          $ref: "#/components/responses/Forbidden"
        "429":
          $ref: "#/components/responses/RateLimited"

    patch:
      operationId: updateMeter
      x-required-scope: meters:write
      summary: Update a meter
      description: Update an existing meter
      tags: [Meters]
      parameters:
        - $ref: "#/components/parameters/id"
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: "#/components/schemas/UpdateMeterRequest"
      responses:
        "200":
          description: Meter updated
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/MeterResponse"
        "400":
          $ref: "#/components/responses/BadRequest"
        "401":
          $ref: "#/components/responses/Unauthorized"
        "404":
          $ref: "#/components/responses/NotFound"
        "403":
          $ref: "#/components/responses/Forbidden"
        "429":
          $ref: "#/components/responses/RateLimited"

    delete:
      operationId: deleteMeter
      x-required-scope: meters:write
      summary: Archive a meter
      description: Archive an active meter. Existing readings are preserved. A meter used by an automation cannot be archived until that dependency is removed.
      tags: [Meters]
      parameters:
        - $ref: "#/components/parameters/id"
      responses:
        "204":
          description: Meter archived; its existing readings remain stored
        "401":
          $ref: "#/components/responses/Unauthorized"
        "404":
          $ref: "#/components/responses/NotFound"
        "409":
          $ref: "#/components/responses/Conflict"
        "403":
          $ref: "#/components/responses/Forbidden"
        "429":
          $ref: "#/components/responses/RateLimited"

  # ==================== Meter Readings ====================
  /meters/{id}/readings:
    get:
      operationId: listMeterReadings
      x-required-scope: meters:read
      summary: List meter readings
      description: Retrieve paginated readings for a meter
      tags: [Meter Readings]
      parameters:
        - $ref: "#/components/parameters/id"
        - $ref: "#/components/parameters/limit"
        - $ref: "#/components/parameters/cursor"
        - $ref: "#/components/parameters/direction"
      responses:
        "200":
          description: List of readings
          content:
            application/json:
              schema:
                allOf:
                  - $ref: "#/components/schemas/ListResponse"
                  - type: object
                    properties:
                      data:
                        type: array
                        items:
                          $ref: "#/components/schemas/MeterReading"
        "400":
          $ref: "#/components/responses/BadRequest"
        "401":
          $ref: "#/components/responses/Unauthorized"
        "404":
          $ref: "#/components/responses/NotFound"
        "403":
          $ref: "#/components/responses/Forbidden"
        "429":
          $ref: "#/components/responses/RateLimited"

    post:
      operationId: createMeterReading
      x-required-scope: meters:write
      summary: Create a meter reading
      description: Record a new reading for a meter
      tags: [Meter Readings]
      parameters:
        - $ref: "#/components/parameters/id"
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: "#/components/schemas/CreateMeterReadingRequest"
      responses:
        "201":
          description: Reading created
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/MeterReadingResponse"
        "400":
          $ref: "#/components/responses/BadRequest"
        "401":
          $ref: "#/components/responses/Unauthorized"
        "404":
          $ref: "#/components/responses/NotFound"
        "403":
          $ref: "#/components/responses/Forbidden"
        "429":
          $ref: "#/components/responses/RateLimited"

  # ==================== Work Orders ====================
  /work-orders:
    get:
      operationId: listWorkOrders
      x-required-scope: work_orders:read
      summary: List work orders
      description: Retrieve a paginated list of work orders
      tags: [Work Orders]
      parameters:
        - $ref: "#/components/parameters/limit"
        - $ref: "#/components/parameters/cursor"
        - $ref: "#/components/parameters/direction"
        - name: status
          in: query
          description: Filter by status (use 'todo' for open, on-hold, and in-progress work)
          schema:
            type: string
            enum: [open, on_hold, in_progress, done, canceled, skipped, todo]
        - name: type
          in: query
          description: Filter by work-order type
          schema:
            $ref: "#/components/schemas/WorkOrderType"
        - name: priority
          in: query
          description: Filter by priority
          schema:
            $ref: "#/components/schemas/WorkOrderPriority"
        - name: asset_id
          in: query
          description: Filter by asset ID
          schema:
            type: string
        - name: location_id
          in: query
          description: Filter by location ID
          schema:
            type: string
        - name: assigned_to_user_id
          in: query
          description: Filter by assigned user ID
          schema:
            type: string
        - name: assigned_to_team_id
          in: query
          description: Filter by assigned team ID
          schema:
            type: string
      responses:
        "200":
          description: List of work orders
          content:
            application/json:
              schema:
                allOf:
                  - $ref: "#/components/schemas/ListResponse"
                  - type: object
                    properties:
                      data:
                        type: array
                        items:
                          $ref: "#/components/schemas/WorkOrder"
        "400":
          $ref: "#/components/responses/BadRequest"
        "401":
          $ref: "#/components/responses/Unauthorized"
        "403":
          $ref: "#/components/responses/Forbidden"
        "429":
          $ref: "#/components/responses/RateLimited"

    post:
      operationId: createWorkOrder
      x-required-scope: work_orders:write
      summary: Create a work order
      description: Create a new work order
      tags: [Work Orders]
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: "#/components/schemas/CreateWorkOrderRequest"
      responses:
        "201":
          description: Work order created
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/WorkOrderResponse"
        "400":
          $ref: "#/components/responses/BadRequest"
        "401":
          $ref: "#/components/responses/Unauthorized"
        "403":
          $ref: "#/components/responses/Forbidden"
        "404":
          $ref: "#/components/responses/NotFound"
        "409":
          $ref: "#/components/responses/Conflict"
        "429":
          $ref: "#/components/responses/RateLimited"

  /work-orders/{id}:
    get:
      operationId: getWorkOrder
      x-required-scope: work_orders:read
      summary: Get a work order
      description: Retrieve a single work order by ID
      tags: [Work Orders]
      parameters:
        - $ref: "#/components/parameters/id"
      responses:
        "200":
          description: Work order details
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/WorkOrderResponse"
        "401":
          $ref: "#/components/responses/Unauthorized"
        "404":
          $ref: "#/components/responses/NotFound"
        "403":
          $ref: "#/components/responses/Forbidden"
        "429":
          $ref: "#/components/responses/RateLimited"

    patch:
      operationId: updateWorkOrder
      x-required-scope: work_orders:write
      summary: Update a work order
      description: Update an existing work order
      tags: [Work Orders]
      parameters:
        - $ref: "#/components/parameters/id"
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: "#/components/schemas/UpdateWorkOrderRequest"
      responses:
        "200":
          description: Work order updated
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/WorkOrderResponse"
        "400":
          $ref: "#/components/responses/BadRequest"
        "401":
          $ref: "#/components/responses/Unauthorized"
        "404":
          $ref: "#/components/responses/NotFound"
        "403":
          $ref: "#/components/responses/Forbidden"
        "429":
          $ref: "#/components/responses/RateLimited"

    delete:
      operationId: deleteWorkOrder
      x-required-scope: work_orders:write
      summary: Soft-delete a work order
      description: >-
        Soft-delete a work order while retaining maintenance history, costs, procedure results,
        and audit evidence. The work order is removed from normal public reads. Repeated requests
        for the same work order return 204 without changing its original deletion timestamp.
      tags: [Work Orders]
      parameters:
        - $ref: "#/components/parameters/id"
      responses:
        "204":
          description: Work order deleted
        "401":
          $ref: "#/components/responses/Unauthorized"
        "404":
          $ref: "#/components/responses/NotFound"
        "403":
          $ref: "#/components/responses/Forbidden"
        "429":
          $ref: "#/components/responses/RateLimited"

  # ==================== Assets ====================
  /work-orders/{id}/other-costs:
    get:
      operationId: getWorkOrderOtherCosts
      x-required-scope: work_orders:read
      summary: Read retained work-order expense entries
      tags: [Work Orders]
      parameters:
        - $ref: "#/components/parameters/id"
      responses:
        "200":
          description: Direct section object. Historical owner or currency can be null when never recorded. Deleted entries are omitted.
          content:
            application/json:
              schema:
                type: object
                required: [items, currency, canCreate]
                properties:
                  items:
                    type: array
                    items:
                      $ref: "#/components/schemas/WorkOrderOtherCost"
                  currency: { type: string, description: Current organization currency for new entries only. }
                  canCreate: { type: boolean }
        "403":
          $ref: "#/components/responses/Forbidden"
        "429":
          $ref: "#/components/responses/RateLimited"
        "404": { description: Work order not found }
    post:
      operationId: createWorkOrderOtherCost
      x-required-scope: work_orders:write
      summary: Record an expense attributed to an active organization user
      description: The API key remains the actor. user_id attributes the expense without impersonating the user. Requires enabled cost tracking and an active work order. Does not modify employee rates or purchase costs.
      tags: [Work Orders]
      parameters:
        - $ref: "#/components/parameters/id"
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              additionalProperties: false
              required: [user_id, type, amount_minor]
              properties:
                user_id: { type: string, minLength: 1 }
                type: { type: string, enum: [labor, travel, other] }
                amount_minor: { type: integer, minimum: 0, maximum: 2147483647, description: Integer cents using 100 per currency unit. Zero is valid. }
                description: { type: [string, 'null'], maxLength: 5000 }
      responses:
        "201":
          description: Created expense with the current organization currency recorded
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/WorkOrderOtherCost"
        "400": { description: Invalid input }
        "403":
          $ref: "#/components/responses/Forbidden"
        "429":
          $ref: "#/components/responses/RateLimited"
        "404": { description: Work order or eligible owner not found }
  /work-orders/{id}/other-costs/{costId}:
    delete:
      operationId: deleteWorkOrderOtherCost
      x-required-scope: work_orders:write
      summary: Retire an expense while retaining its audit history
      description: Available for authorized cleanup after cost tracking is disabled. Does not physically delete the expense.
      tags: [Work Orders]
      parameters:
        - $ref: "#/components/parameters/id"
        - name: costId
          in: path
          required: true
          schema: { type: string }
      responses:
        "204": { description: Expense retired or already retired }
        "403":
          $ref: "#/components/responses/Forbidden"
        "429":
          $ref: "#/components/responses/RateLimited"
        "404": { description: Work order or expense not found }
  /work-orders/{id}/parts:
    get:
      operationId: getWorkOrderParts
      x-required-scope: work_orders:read
      summary: Read work-order parts and available operations
      tags: [Work Orders]
      parameters:
        - $ref: "#/components/parameters/id"
      responses:
        "200":
          description: Direct section object with effective quantities, costs when authorized, capabilities and aggregate version
          content:
            application/json:
              schema:
                type: object
                required: [workOrderId, partStatus, partAvailability, aggregateVersion, items, competingDemands, capabilities, historyOnly]
                properties:
                  workOrderId: { type: string }
                  partStatus:
                    $ref: "#/components/schemas/WorkOrderPartStatus"
                  partAvailability: { type: string, enum: [available, partial, unavailable] }
                  aggregateVersion: { type: string, pattern: '^[0-9a-f]{64}$' }
                  items:
                    type: array
                    items:
                      type: object
                      required: [id, partId, locationId, quantity, quantityUsed, part, location, inStockQuantity, availableQuantity, readiness, issuedSnapshot, linkable]
                      properties:
                        id: { type: string }
                        partId: { type: string }
                        locationId: { type: string }
                        quantity: { type: integer, minimum: 1 }
                        quantityUsed: { type: [integer, 'null'], minimum: 0 }
                        unitCost:
                          description: Omitted when cost access is denied; null when no cost is available.
                          anyOf:
                            - $ref: "#/components/schemas/PartMoney"
                            - type: 'null'
                        part:
                          type: [object, 'null']
                          required: [id, name, partNumber, imageUrl, imageBlur, archived]
                          properties:
                            id: { type: string }
                            name: { type: string }
                            partNumber: { type: [string, 'null'] }
                            imageUrl: { type: [string, 'null'] }
                            imageBlur: { type: [string, 'null'] }
                            archived: { type: boolean }
                        location:
                          type: [object, 'null']
                          required: [id, name, area, imageUrl, imageBlur]
                          properties:
                            id: { type: string }
                            name: { type: string }
                            area: { type: [string, 'null'] }
                            imageUrl: { type: [string, 'null'] }
                            imageBlur: { type: [string, 'null'] }
                        inStockQuantity: { type: [integer, 'null'] }
                        availableQuantity: { type: [integer, 'null'] }
                        readiness: { type: string, enum: [available, partial, unavailable] }
                        linkable: { type: boolean }
                        issuedSnapshot:
                          type: [object, 'null']
                          required: [quantityUsed, issuedAt, partName, partNumber, locationName, locationArea, snapshotKind]
                          properties:
                            quantityUsed: { type: integer, minimum: 0 }
                            issuedAt: { type: string, format: date-time }
                            partName: { type: string }
                            partNumber: { type: [string, 'null'] }
                            locationName: { type: string }
                            locationArea: { type: [string, 'null'] }
                            snapshotKind: { type: string, enum: [exact, current_at_migration] }
                            unitCost:
                              description: Omitted when cost access is denied.
                              anyOf:
                                - $ref: "#/components/schemas/PartMoney"
                                - type: 'null'
                  competingDemands:
                    type: array
                    items:
                      type: object
                      required: [workOrderId, title, partId, locationId, partStatus, quantity]
                      properties:
                        workOrderId: { type: string }
                        title: { type: string }
                        partId: { type: string }
                        locationId: { type: string }
                        partStatus: { type: string, enum: [reserved, kitted, staged] }
                        quantity: { type: integer }
                  capabilities:
                    type: object
                    required: [canEditParts, canRemoveParts, canTransitionPartStatus, canRelease, canOverrideOvercommit, canReconcile, canViewCost, canOverrideUnitCost, allowedTransitions]
                    properties:
                      canEditParts: { type: boolean }
                      canRemoveParts: { type: boolean }
                      canTransitionPartStatus: { type: boolean }
                      canRelease: { type: boolean }
                      canOverrideOvercommit: { type: boolean }
                      canReconcile: { type: boolean }
                      canViewCost: { type: boolean }
                      canOverrideUnitCost: { type: boolean }
                      allowedTransitions:
                        type: array
                        items:
                          $ref: "#/components/schemas/WorkOrderPartStatus"
                  historyOnly: { type: boolean }
        "401":
          $ref: "#/components/responses/Unauthorized"
        "403":
          $ref: "#/components/responses/Forbidden"
        "404":
          $ref: "#/components/responses/NotFound"
        "429":
          $ref: "#/components/responses/RateLimited"
    post:
      operationId: commandWorkOrderParts
      x-required-scope: work_orders:write
      summary: Replace usage or transition work-order part status
      description: Reuse the identical command and idempotency key after an uncertain response. Refetch the section after a version conflict. Issued quantity edits append stock corrections.
      tags: [Work Orders]
      parameters:
        - $ref: "#/components/parameters/id"
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: "#/components/schemas/WorkOrderPartsCommand"
      responses:
        "200":
          description: Direct mutation result; replayed indicates a previously accepted command
          content:
            application/json:
              schema:
                type: object
                required: [workOrderId, partStatus, aggregateVersion, replayed, affectedPartIds, competingWorkOrderIds]
                properties:
                  workOrderId: { type: string }
                  partStatus:
                    $ref: "#/components/schemas/WorkOrderPartStatus"
                  aggregateVersion: { type: string }
                  replayed: { type: boolean }
                  affectedPartIds: { type: array, items: { type: string } }
                  competingWorkOrderIds: { type: array, items: { type: string } }
        "400":
          $ref: "#/components/responses/BadRequest"
        "401":
          $ref: "#/components/responses/Unauthorized"
        "403":
          $ref: "#/components/responses/Forbidden"
        "404":
          $ref: "#/components/responses/NotFound"
        "409":
          $ref: "#/components/responses/Conflict"
        "429":
          $ref: "#/components/responses/RateLimited"

  /assets:
    get:
      operationId: listAssets
      x-required-scope: assets:read
      summary: List assets
      description: Retrieve a paginated list of assets
      tags: [Assets]
      parameters:
        - $ref: "#/components/parameters/limit"
        - $ref: "#/components/parameters/cursor"
        - $ref: "#/components/parameters/direction"
        - name: asset_type
          in: query
          description: Filter by a current asset type slug, including custom types or unassigned
          schema:
            $ref: "#/components/schemas/AssetCurrentType"
        - name: status
          in: query
          description: Filter by status
          schema:
            $ref: "#/components/schemas/AssetStatus"
        - name: criticality
          in: query
          description: Filter by operational criticality
          schema:
            $ref: "#/components/schemas/AssetCriticality"
        - name: location_id
          in: query
          description: Filter by location ID
          schema:
            type: string
      responses:
        "200":
          description: List of assets
          content:
            application/json:
              schema:
                allOf:
                  - $ref: "#/components/schemas/ListResponse"
                  - type: object
                    properties:
                      data:
                        type: array
                        items:
                          $ref: "#/components/schemas/Asset"
        "400":
          $ref: "#/components/responses/BadRequest"
        "401":
          $ref: "#/components/responses/Unauthorized"
        "403":
          $ref: "#/components/responses/Forbidden"
        "429":
          $ref: "#/components/responses/RateLimited"

    post:
      operationId: createAsset
      x-required-scope: assets:write
      summary: Create an asset
      description: Create a new asset
      tags: [Assets]
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: "#/components/schemas/CreateAssetRequest"
      responses:
        "201":
          description: Asset created
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/AssetResponse"
        "400":
          $ref: "#/components/responses/BadRequest"
        "401":
          $ref: "#/components/responses/Unauthorized"
        "404":
          $ref: "#/components/responses/NotFound"
        "403":
          $ref: "#/components/responses/Forbidden"
        "429":
          $ref: "#/components/responses/RateLimited"

  /assets/{id}:
    get:
      operationId: getAsset
      x-required-scope: assets:read
      summary: Get an asset
      description: Retrieve a single asset by ID
      tags: [Assets]
      parameters:
        - $ref: "#/components/parameters/id"
      responses:
        "200":
          description: Asset details
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/AssetResponse"
        "401":
          $ref: "#/components/responses/Unauthorized"
        "404":
          $ref: "#/components/responses/NotFound"
        "403":
          $ref: "#/components/responses/Forbidden"
        "429":
          $ref: "#/components/responses/RateLimited"

    patch:
      operationId: updateAsset
      x-required-scope: assets:write
      summary: Update an asset
      description: Update an existing asset
      tags: [Assets]
      parameters:
        - $ref: "#/components/parameters/id"
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: "#/components/schemas/UpdateAssetRequest"
      responses:
        "200":
          description: Asset updated
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/AssetResponse"
        "400":
          $ref: "#/components/responses/BadRequest"
        "401":
          $ref: "#/components/responses/Unauthorized"
        "404":
          $ref: "#/components/responses/NotFound"
        "403":
          $ref: "#/components/responses/Forbidden"
        "429":
          $ref: "#/components/responses/RateLimited"

  /assets/{id}/parts/{partId}:
    post:
      operationId: linkPartToAsset
      x-required-scope: parts:write
      summary: Link a part to an asset
      description: Link an existing part to an existing asset
      tags: [Assets, Parts]
      parameters:
        - $ref: "#/components/parameters/id"
        - $ref: "#/components/parameters/partId"
      responses:
        "200":
          description: Part linked to asset
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/AssetPartLinkResponse"
        "401":
          $ref: "#/components/responses/Unauthorized"
        "404":
          $ref: "#/components/responses/NotFound"
        "403":
          $ref: "#/components/responses/Forbidden"
        "429":
          $ref: "#/components/responses/RateLimited"

    delete:
      operationId: unlinkPartFromAsset
      x-required-scope: parts:write
      summary: Unlink a part from an asset
      description: Remove a part link from an asset
      tags: [Assets, Parts]
      parameters:
        - $ref: "#/components/parameters/id"
        - $ref: "#/components/parameters/partId"
      responses:
        "204":
          description: Part unlinked from asset
        "401":
          $ref: "#/components/responses/Unauthorized"
        "404":
          $ref: "#/components/responses/NotFound"
        "403":
          $ref: "#/components/responses/Forbidden"
        "429":
          $ref: "#/components/responses/RateLimited"

  # ==================== Locations ====================
  /locations:
    get:
      operationId: listLocations
      x-required-scope: locations:read
      summary: List locations
      description: Retrieve a paginated list of locations
      tags: [Locations]
      parameters:
        - $ref: "#/components/parameters/limit"
        - $ref: "#/components/parameters/cursor"
        - $ref: "#/components/parameters/direction"
        - name: expand
          in: query
          description: Include custom-field values on list records
          schema:
            type: string
            enum: [extra_fields]
      responses:
        "200":
          description: List of locations
          content:
            application/json:
              schema:
                allOf:
                  - $ref: "#/components/schemas/ListResponse"
                  - type: object
                    properties:
                      data:
                        type: array
                        items:
                          $ref: "#/components/schemas/Location"
        "400":
          $ref: "#/components/responses/BadRequest"
        "401":
          $ref: "#/components/responses/Unauthorized"
        "403":
          $ref: "#/components/responses/Forbidden"
        "429":
          $ref: "#/components/responses/RateLimited"

    post:
      operationId: createLocation
      x-required-scope: locations:write
      summary: Create a location
      description: Create a new location
      tags: [Locations]
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: "#/components/schemas/CreateLocationRequest"
      responses:
        "201":
          description: Location created
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/LocationResponse"
        "409":
          $ref: "#/components/responses/Conflict"
        "400":
          $ref: "#/components/responses/BadRequest"
        "401":
          $ref: "#/components/responses/Unauthorized"
        "403":
          $ref: "#/components/responses/Forbidden"
        "404":
          $ref: "#/components/responses/NotFound"
        "429":
          $ref: "#/components/responses/RateLimited"

  /locations/{id}:
    get:
      operationId: getLocation
      x-required-scope: locations:read
      summary: Get a location
      description: Retrieve a single location by ID
      tags: [Locations]
      parameters:
        - $ref: "#/components/parameters/id"
      responses:
        "200":
          description: Location details
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/LocationDetailResponse"
        "401":
          $ref: "#/components/responses/Unauthorized"
        "404":
          $ref: "#/components/responses/NotFound"
        "403":
          $ref: "#/components/responses/Forbidden"
        "429":
          $ref: "#/components/responses/RateLimited"

    patch:
      operationId: updateLocation
      x-required-scope: locations:write
      summary: Update a location
      description: Update an existing location
      tags: [Locations]
      parameters:
        - $ref: "#/components/parameters/id"
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: "#/components/schemas/UpdateLocationRequest"
      responses:
        "200":
          description: Location updated
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/LocationResponse"
        "409":
          $ref: "#/components/responses/Conflict"
        "400":
          $ref: "#/components/responses/BadRequest"
        "401":
          $ref: "#/components/responses/Unauthorized"
        "404":
          $ref: "#/components/responses/NotFound"
        "403":
          $ref: "#/components/responses/Forbidden"
        "429":
          $ref: "#/components/responses/RateLimited"

  # ==================== Parts ====================
  /parts:
    get:
      operationId: listParts
      x-required-scope: parts:read
      summary: List parts
      description: Retrieve a paginated list of parts
      tags: [Parts]
      parameters:
        - $ref: "#/components/parameters/limit"
        - $ref: "#/components/parameters/cursor"
        - $ref: "#/components/parameters/direction"
      responses:
        "200":
          description: List of parts
          content:
            application/json:
              schema:
                allOf:
                  - $ref: "#/components/schemas/ListResponse"
                  - type: object
                    properties:
                      data:
                        type: array
                        items:
                          $ref: "#/components/schemas/Part"
        "400":
          $ref: "#/components/responses/BadRequest"
        "401":
          $ref: "#/components/responses/Unauthorized"
        "403":
          $ref: "#/components/responses/Forbidden"
        "429":
          $ref: "#/components/responses/RateLimited"

    post:
      operationId: createPart
      x-required-scope: parts:write
      summary: Create a part
      description: Create a new part
      tags: [Parts]
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: "#/components/schemas/CreatePartRequest"
      responses:
        "201":
          description: Part created
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/PartResponse"
        "400":
          $ref: "#/components/responses/BadRequest"
        "401":
          $ref: "#/components/responses/Unauthorized"
        "404":
          $ref: "#/components/responses/NotFound"
        "403":
          $ref: "#/components/responses/Forbidden"
        "429":
          $ref: "#/components/responses/RateLimited"

  /parts/{id}:
    get:
      operationId: getPart
      x-required-scope: parts:read
      summary: Get a part
      description: Retrieve a single part by ID
      tags: [Parts]
      parameters:
        - $ref: "#/components/parameters/id"
      responses:
        "200":
          description: Part details
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/PartResponse"
        "401":
          $ref: "#/components/responses/Unauthorized"
        "404":
          $ref: "#/components/responses/NotFound"
        "403":
          $ref: "#/components/responses/Forbidden"
        "429":
          $ref: "#/components/responses/RateLimited"

    patch:
      operationId: updatePart
      x-required-scope: parts:write
      summary: Update a part
      description: Update an existing part
      tags: [Parts]
      parameters:
        - $ref: "#/components/parameters/id"
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: "#/components/schemas/UpdatePartRequest"
      responses:
        "200":
          description: Part updated
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/PartResponse"
        "400":
          $ref: "#/components/responses/BadRequest"
        "401":
          $ref: "#/components/responses/Unauthorized"
        "404":
          $ref: "#/components/responses/NotFound"
        "403":
          $ref: "#/components/responses/Forbidden"
        "429":
          $ref: "#/components/responses/RateLimited"

  # ==================== Identifiers ====================
  /identifiers:
    get:
      operationId: listIdentifiers
      x-required-scope: identifiers:read
      summary: List identifiers for a record
      description: Retrieve active identifiers assigned to one asset, location, or part
      tags: [Identifiers]
      parameters:
        - $ref: "#/components/parameters/limit"
        - $ref: "#/components/parameters/cursor"
        - $ref: "#/components/parameters/direction"
        - name: target_type
          in: query
          required: true
          schema:
            $ref: "#/components/schemas/IdentifierEntityType"
        - name: target_id
          in: query
          required: true
          schema:
            type: string
      responses:
        "200":
          description: List of identifiers
          content:
            application/json:
              schema:
                allOf:
                  - $ref: "#/components/schemas/ListResponse"
                  - type: object
                    properties:
                      data:
                        type: array
                        items:
                          $ref: "#/components/schemas/Identifier"
        "400":
          $ref: "#/components/responses/BadRequest"
        "401":
          $ref: "#/components/responses/Unauthorized"
        "403":
          $ref: "#/components/responses/Forbidden"
        "429":
          $ref: "#/components/responses/RateLimited"

    post:
      operationId: attachIdentifier
      x-required-scope: identifiers:write
      summary: Attach an identifier
      description: Assign an existing exact barcode or QR payload to a record
      tags: [Identifiers]
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: "#/components/schemas/AttachIdentifierRequest"
      responses:
        "201":
          description: Identifier attached
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/IdentifierResponse"
        "400":
          $ref: "#/components/responses/BadRequest"
        "401":
          $ref: "#/components/responses/Unauthorized"
        "403":
          $ref: "#/components/responses/Forbidden"
        "404":
          $ref: "#/components/responses/NotFound"
        "409":
          $ref: "#/components/responses/Conflict"
        "429":
          $ref: "#/components/responses/RateLimited"

  /identifiers/resolve:
    post:
      operationId: resolveIdentifier
      x-required-scope: identifiers:read
      summary: Resolve a scanned payload
      description: Resolve an exact opaque payload within the API key organization
      tags: [Identifiers]
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: "#/components/schemas/ResolveIdentifierRequest"
      responses:
        "200":
          description: Resolution result
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ResolveIdentifierResponse"
        "400":
          $ref: "#/components/responses/BadRequest"
        "401":
          $ref: "#/components/responses/Unauthorized"
        "403":
          $ref: "#/components/responses/Forbidden"
        "429":
          $ref: "#/components/responses/RateLimited"

  /identifiers/generate:
    post:
      operationId: generateIdentifier
      x-required-scope: identifiers:write
      summary: Generate a Canary label
      description: Generate an opaque Canary QR payload for a record
      tags: [Identifiers]
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: "#/components/schemas/GenerateIdentifierRequest"
      responses:
        "201":
          description: Canary identifier generated
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/IdentifierResponse"
        "400":
          $ref: "#/components/responses/BadRequest"
        "401":
          $ref: "#/components/responses/Unauthorized"
        "403":
          $ref: "#/components/responses/Forbidden"
        "404":
          $ref: "#/components/responses/NotFound"
        "409":
          $ref: "#/components/responses/Conflict"
        "429":
          $ref: "#/components/responses/RateLimited"

  /identifiers/import/preview:
    post:
      operationId: previewIdentifierImport
      x-required-scope: identifiers:write
      summary: Preview an identifier import
      description: Validate a batch of customer-owned identifiers without writing any records
      tags: [Identifiers]
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: "#/components/schemas/IdentifierImportRequest"
      responses:
        "200":
          description: Import validation result
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/IdentifierImportPreviewResponse"
        "400":
          $ref: "#/components/responses/BadRequest"
        "401":
          $ref: "#/components/responses/Unauthorized"
        "403":
          $ref: "#/components/responses/Forbidden"
        "429":
          $ref: "#/components/responses/RateLimited"

  /identifiers/import/apply:
    post:
      operationId: applyIdentifierImport
      x-required-scope: identifiers:write
      summary: Apply an identifier import
      description: Atomically attach a previously validated batch of customer-owned identifiers
      tags: [Identifiers]
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: "#/components/schemas/IdentifierImportRequest"
      responses:
        "200":
          description: Imported identifiers
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/IdentifierImportApplyResponse"
        "400":
          $ref: "#/components/responses/BadRequest"
        "401":
          $ref: "#/components/responses/Unauthorized"
        "403":
          $ref: "#/components/responses/Forbidden"
        "409":
          $ref: "#/components/responses/Conflict"
        "429":
          $ref: "#/components/responses/RateLimited"

  /identifiers/{id}:
    delete:
      operationId: retireIdentifier
      x-required-scope: identifiers:write
      summary: Retire an identifier
      description: Retire an identifier and all of its active scan values
      tags: [Identifiers]
      parameters:
        - $ref: "#/components/parameters/id"
      responses:
        "204":
          description: Identifier retired
        "401":
          $ref: "#/components/responses/Unauthorized"
        "403":
          $ref: "#/components/responses/Forbidden"
        "404":
          $ref: "#/components/responses/NotFound"
        "429":
          $ref: "#/components/responses/RateLimited"

  # ==================== MaintainX-compatible Work Requests ====================
  /workrequests:
    get:
      operationId: listWorkRequests
      x-required-scope: work_requests:read
      summary: List work requests
      tags: [Work Requests]
      parameters:
        - $ref: "#/components/parameters/compatibilityLimit"
        - $ref: "#/components/parameters/cursor"
        - name: title
          in: query
          schema: { type: string }
        - name: assets
          in: query
          schema:
            type: array
            items: { type: string }
        - name: locations
          in: query
          schema:
            type: array
            items: { type: string }
        - name: priorities
          in: query
          schema:
            type: array
            items: { $ref: "#/components/schemas/WorkRequestPriority" }
        - name: statuses
          in: query
          schema:
            type: array
            items: { $ref: "#/components/schemas/WorkRequestStatus" }
        - name: expand
          in: query
          schema:
            type: array
            items:
              type: string
              enum: [asset, location, work_order, extra_fields]
      responses:
        "200":
          description: Work request collection
          content:
            application/json:
              schema: { $ref: "#/components/schemas/WorkRequestListResponse" }
        "400": { $ref: "#/components/responses/BadRequest" }
        "401": { $ref: "#/components/responses/Unauthorized" }
        "403": { $ref: "#/components/responses/Forbidden" }
        "429": { $ref: "#/components/responses/RateLimited" }
    post:
      operationId: createWorkRequest
      x-required-scope: work_requests:write
      summary: Create a work request
      tags: [Work Requests]
      requestBody:
        required: true
        content:
          application/json:
            schema: { $ref: "#/components/schemas/CreateWorkRequest" }
      responses:
        "200":
          description: Work request created
          content:
            application/json:
              schema: { $ref: "#/components/schemas/IdResponse" }
        "400": { $ref: "#/components/responses/BadRequest" }
        "401": { $ref: "#/components/responses/Unauthorized" }
        "403": { $ref: "#/components/responses/Forbidden" }
        "404": { $ref: "#/components/responses/NotFound" }
        "429": { $ref: "#/components/responses/RateLimited" }

  /workrequests/{id}:
    get:
      operationId: getWorkRequest
      x-required-scope: work_requests:read
      summary: Get a work request
      tags: [Work Requests]
      parameters:
        - $ref: "#/components/parameters/id"
        - name: expand
          in: query
          schema:
            type: array
            items:
              type: string
              enum: [asset, location, work_order, extra_fields]
      responses:
        "200":
          description: Work request
          content:
            application/json:
              schema: { $ref: "#/components/schemas/WorkRequestResponse" }
        "400": { $ref: "#/components/responses/BadRequest" }
        "401": { $ref: "#/components/responses/Unauthorized" }
        "403": { $ref: "#/components/responses/Forbidden" }
        "404": { $ref: "#/components/responses/NotFound" }
        "429": { $ref: "#/components/responses/RateLimited" }
    patch:
      operationId: updateWorkRequest
      x-required-scope: work_requests:write
      summary: Update a work request
      tags: [Work Requests]
      parameters:
        - $ref: "#/components/parameters/id"
      requestBody:
        required: true
        content:
          application/json:
            schema: { $ref: "#/components/schemas/UpdateWorkRequest" }
      responses:
        "200":
          description: Updated work request
          content:
            application/json:
              schema: { $ref: "#/components/schemas/WorkRequestResponse" }
        "400": { $ref: "#/components/responses/BadRequest" }
        "401": { $ref: "#/components/responses/Unauthorized" }
        "403": { $ref: "#/components/responses/Forbidden" }
        "404": { $ref: "#/components/responses/NotFound" }
        "429": { $ref: "#/components/responses/RateLimited" }
    delete:
      operationId: deleteWorkRequest
      x-required-scope: work_requests:write
      summary: Delete a work request
      tags: [Work Requests]
      parameters:
        - $ref: "#/components/parameters/id"
      responses:
        "204": { description: Work request deleted }
        "401": { $ref: "#/components/responses/Unauthorized" }
        "403": { $ref: "#/components/responses/Forbidden" }
        "404": { $ref: "#/components/responses/NotFound" }
        "429": { $ref: "#/components/responses/RateLimited" }

  /workrequests/{id}/attachments/{filename}:
    put:
      operationId: uploadWorkRequestAttachment
      x-required-scope: work_requests:write
      summary: Upload a work request attachment
      tags: [Work Requests]
      parameters:
        - $ref: "#/components/parameters/id"
        - $ref: "#/components/parameters/filename"
      requestBody:
        required: true
        content:
          application/octet-stream:
            schema: { type: string, format: binary }
      responses:
        "201":
          description: Attachment created
          content:
            application/json:
              schema: { $ref: "#/components/schemas/WorkRequestUploadResponse" }
        "400": { $ref: "#/components/responses/BadRequest" }
        "401": { $ref: "#/components/responses/Unauthorized" }
        "403": { $ref: "#/components/responses/Forbidden" }
        "404": { $ref: "#/components/responses/NotFound" }
        "409": { $ref: "#/components/responses/Conflict" }
        "413": { $ref: "#/components/responses/ContentTooLarge" }
        "415": { $ref: "#/components/responses/UnsupportedMediaType" }
        "429": { $ref: "#/components/responses/RateLimited" }
    delete:
      operationId: deleteWorkRequestAttachment
      x-required-scope: work_requests:write
      summary: Delete a work request attachment by filename
      tags: [Work Requests]
      parameters:
        - $ref: "#/components/parameters/id"
        - $ref: "#/components/parameters/filename"
      responses:
        "204": { description: Attachment deleted }
        "401": { $ref: "#/components/responses/Unauthorized" }
        "403": { $ref: "#/components/responses/Forbidden" }
        "404": { $ref: "#/components/responses/NotFound" }
        "409": { $ref: "#/components/responses/Conflict" }
        "429": { $ref: "#/components/responses/RateLimited" }

  /workrequests/{id}/thumbnail/{filename}:
    put:
      operationId: uploadWorkRequestThumbnail
      x-required-scope: work_requests:write
      summary: Upload a work request thumbnail
      tags: [Work Requests]
      parameters:
        - $ref: "#/components/parameters/id"
        - $ref: "#/components/parameters/filename"
      requestBody:
        required: true
        content:
          application/octet-stream:
            schema: { type: string, format: binary }
      responses:
        "201":
          description: Thumbnail created
          content:
            application/json:
              schema: { $ref: "#/components/schemas/WorkRequestUploadResponse" }
        "400": { $ref: "#/components/responses/BadRequest" }
        "401": { $ref: "#/components/responses/Unauthorized" }
        "403": { $ref: "#/components/responses/Forbidden" }
        "404": { $ref: "#/components/responses/NotFound" }
        "409": { $ref: "#/components/responses/Conflict" }
        "413": { $ref: "#/components/responses/ContentTooLarge" }
        "415": { $ref: "#/components/responses/UnsupportedMediaType" }
        "429": { $ref: "#/components/responses/RateLimited" }

  /workrequestportals:
    get:
      operationId: listWorkRequestPortals
      x-required-scope: work_request_portals:read
      summary: List work request portals
      tags: [Work Request Portals]
      parameters:
        - $ref: "#/components/parameters/compatibilityLimit"
        - $ref: "#/components/parameters/cursor"
      responses:
        "200":
          description: Work request portal collection
          content:
            application/json:
              schema: { $ref: "#/components/schemas/WorkRequestPortalListResponse" }
        "400": { $ref: "#/components/responses/BadRequest" }
        "401": { $ref: "#/components/responses/Unauthorized" }
        "403": { $ref: "#/components/responses/Forbidden" }
        "429": { $ref: "#/components/responses/RateLimited" }
    post:
      operationId: createWorkRequestPortal
      x-required-scope: work_request_portals:write
      summary: Create a work request portal
      tags: [Work Request Portals]
      requestBody:
        required: true
        content:
          application/json:
            schema: { $ref: "#/components/schemas/CreateWorkRequestPortal" }
      responses:
        "201":
          description: Work request portal created
          content:
            application/json:
              schema: { $ref: "#/components/schemas/IdResponse" }
        "400": { $ref: "#/components/responses/BadRequest" }
        "401": { $ref: "#/components/responses/Unauthorized" }
        "403": { $ref: "#/components/responses/Forbidden" }
        "404": { $ref: "#/components/responses/NotFound" }
        "429": { $ref: "#/components/responses/RateLimited" }

  /workrequestportals/{id}:
    get:
      operationId: getWorkRequestPortal
      x-required-scope: work_request_portals:read
      summary: Get a work request portal
      tags: [Work Request Portals]
      parameters:
        - $ref: "#/components/parameters/id"
      responses:
        "200":
          description: Work request portal
          content:
            application/json:
              schema: { $ref: "#/components/schemas/WorkRequestPortalResponse" }
        "401": { $ref: "#/components/responses/Unauthorized" }
        "403": { $ref: "#/components/responses/Forbidden" }
        "404": { $ref: "#/components/responses/NotFound" }
        "429": { $ref: "#/components/responses/RateLimited" }
    patch:
      operationId: updateWorkRequestPortal
      x-required-scope: work_request_portals:write
      summary: Update a work request portal
      tags: [Work Request Portals]
      parameters:
        - $ref: "#/components/parameters/id"
      requestBody:
        required: true
        content:
          application/json:
            schema: { $ref: "#/components/schemas/UpdateWorkRequestPortal" }
      responses:
        "200":
          description: Updated work request portal
          content:
            application/json:
              schema: { $ref: "#/components/schemas/WorkRequestPortalResponse" }
        "400": { $ref: "#/components/responses/BadRequest" }
        "401": { $ref: "#/components/responses/Unauthorized" }
        "403": { $ref: "#/components/responses/Forbidden" }
        "404": { $ref: "#/components/responses/NotFound" }
        "429": { $ref: "#/components/responses/RateLimited" }
    delete:
      operationId: deleteWorkRequestPortal
      x-required-scope: work_request_portals:write
      summary: Delete a work request portal
      tags: [Work Request Portals]
      parameters:
        - $ref: "#/components/parameters/id"
      responses:
        "204": { description: Work request portal deleted }
        "401": { $ref: "#/components/responses/Unauthorized" }
        "403": { $ref: "#/components/responses/Forbidden" }
        "404": { $ref: "#/components/responses/NotFound" }
        "429": { $ref: "#/components/responses/RateLimited" }

  /workorders:
    post:
      operationId: createMaintainXWorkOrder
      x-required-scope: work_orders:write
      summary: Create a work order or approve a work request
      description: When workRequestId is supplied, creates and links the Work Order atomically and approves the Request. Replays return the existing linked Work Order ID.
      tags: [Work Orders]
      requestBody:
        required: true
        content:
          application/json:
            schema: { $ref: "#/components/schemas/MaintainXCreateWorkOrder" }
      responses:
        "200":
          description: Work order created or existing linked work order returned
          content:
            application/json:
              schema: { $ref: "#/components/schemas/IdResponse" }
        "400": { $ref: "#/components/responses/BadRequest" }
        "401": { $ref: "#/components/responses/Unauthorized" }
        "403": { $ref: "#/components/responses/Forbidden" }
        "404": { $ref: "#/components/responses/NotFound" }
        "409": { $ref: "#/components/responses/Conflict" }
        "429": { $ref: "#/components/responses/RateLimited" }

components:
  securitySchemes:
    bearerAuth:
      type: http
      scheme: bearer
      description: Organization API key with an `sk_live_` or `sk_test_` prefix

  parameters:
    id:
      name: id
      in: path
      required: true
      description: Resource ID
      schema:
        type: string

    filename:
      name: filename
      in: path
      required: true
      description: Original attachment filename
      schema:
        type: string

    partId:
      name: partId
      in: path
      required: true
      description: Part ID
      schema:
        type: string

    limit:
      name: limit
      in: query
      description: Maximum number of items to return from a Canary-native list (1-100)
      schema:
        type: integer
        minimum: 1
        maximum: 100
        default: 20

    compatibilityLimit:
      name: limit
      in: query
      description: Maximum number of items to return from a MaintainX-compatible list (1-200)
      schema:
        type: integer
        minimum: 1
        maximum: 200
        default: 100

    cursor:
      name: cursor
      in: query
      description: Pagination cursor from previous response
      schema:
        type: string

    direction:
      name: direction
      in: query
      description: Sort direction
      schema:
        type: string
        enum: [asc, desc]
        default: desc

    customFieldSkipWebhook:
      name: skipWebhook
      in: query
      required: false
      description: Compatibility parameter with no behavioral effect. true also accepts 1 or yes.
      schema:
        type: boolean

  schemas:
    WorkOrderOtherCost:
      type: object
      required: [id, userId, type, description, amountMinor, currency, createdAt, canDelete]
      properties:
        id: { type: string }
        userId: { type: [string, 'null'] }
        type: { type: string, enum: [labor, travel, other] }
        description: { type: [string, 'null'] }
        amountMinor: { type: integer, minimum: 0, description: Fixed cents. Historical quantity times unit cost rounded to cents. }
        currency: { type: [string, 'null'], description: Currency recorded at creation; null for unrecorded historical currency. }
        createdAt: { type: string, format: date-time }
        canDelete: { type: boolean }
    PartMoney:
      type: object
      required: [minor, currency]
      properties:
        minor: { type: integer, description: Amount in the currency's minor units. }
        currency: { type: string, description: Supported ISO 4217 currency code. }
    WorkOrderPartStatus:
      type: string
      enum: [assigned, reserved, kitted, staged, issued]
    WorkOrderPartsCommand:
      oneOf:
        - type: object
          additionalProperties: false
          required: [operation, idempotencyKey, expectedAggregateVersion, targetStatus]
          properties:
            operation: { type: string, const: transition }
            idempotencyKey: { type: string, minLength: 1, maxLength: 512 }
            expectedAggregateVersion: { type: string, pattern: '^[0-9a-f]{64}$' }
            targetStatus:
              $ref: "#/components/schemas/WorkOrderPartStatus"
            confirmOvercommit: { type: boolean, default: false }
        - type: object
          additionalProperties: false
          required: [operation, idempotencyKey, expectedAggregateVersion, items]
          properties:
            operation: { type: string, const: replace }
            idempotencyKey: { type: string, minLength: 1, maxLength: 512 }
            expectedAggregateVersion: { type: string, pattern: '^[0-9a-f]{64}$' }
            items:
              type: array
              maxItems: 1000
              items:
                type: object
                additionalProperties: false
                required: [id, partId, locationId, quantity]
                properties:
                  id: { type: string, pattern: '^[A-Za-z0-9][A-Za-z0-9_-]{0,127}$' }
                  partId: { type: string, pattern: '^[A-Za-z0-9][A-Za-z0-9_-]{0,127}$' }
                  locationId: { type: string, pattern: '^[A-Za-z0-9][A-Za-z0-9_-]{0,127}$' }
                  quantity: { type: integer, minimum: 1, maximum: 2147483647 }
                  unitCostMinor: { type: integer, minimum: 0, maximum: 2147483647 }
    # ==================== Custom Field Schemas ====================
    CustomFieldType:
      type: string
      enum: [oneline, multiline, number, singleSelect, date, datetime, link]

    ExtraFields:
      type: object
      description: Label-keyed custom-field values. Omitted labels remain unchanged on update.
      additionalProperties:
        type: string
        maxLength: 4096

    CreatedCustomField:
      type: object
      required: [id, label, sortOrder, type, options]
      properties:
        id:
          type: integer
          format: int32
          minimum: 1
        label:
          type: string
          minLength: 1
        sortOrder:
          type: integer
          minimum: 0
        type:
          $ref: "#/components/schemas/CustomFieldType"
        options:
          type: [array, "null"]
          items:
            type: string

    ListedCustomField:
      type: object
      required: [id, label, sortOrder, type, options, required, unlisted]
      properties:
        id:
          type: integer
          format: int32
          minimum: 1
        label:
          type: string
          minLength: 1
        sortOrder:
          type: integer
          minimum: 0
        type:
          $ref: "#/components/schemas/CustomFieldType"
        options:
          type: [array, "null"]
          items:
            type: string
        required:
          type: [boolean, "null"]
        unlisted:
          type: [boolean, "null"]

    CustomFieldInput:
      type: object
      required: [label, type]
      properties:
        label:
          type: string
          minLength: 1
        type:
          $ref: "#/components/schemas/CustomFieldType"
        options:
          type: [array, "null"]
          items:
            type: string
        required:
          type: [boolean, "null"]
        unlisted:
          type: [boolean, "null"]
        includeInRecurrence:
          type: [boolean, "null"]
          description: Accepted only for work-order definitions

    CreateCustomFieldsRequest:
      type: object
      required: [fields]
      properties:
        fields:
          type: array
          minItems: 1
          items:
            $ref: "#/components/schemas/CustomFieldInput"

    UpdateCustomFieldRequest:
      type: object
      required: [field]
      properties:
        field:
          type: object
          properties:
            label:
              type: string
              minLength: 1
            type:
              $ref: "#/components/schemas/CustomFieldType"
            options:
              type: [array, "null"]
              items:
                type: string
            sortIndex:
              type: integer
              minimum: 0
              description: Zero-based desired position; out-of-range values are clamped
            required:
              type: [boolean, "null"]
            unlisted:
              type: [boolean, "null"]
            includeInRecurrence:
              type: [boolean, "null"]
              description: Accepted only for work-order definitions

    # ==================== Enums ====================
    MeterType:
      type: string
      enum: [numeric, boolean]

    MeterCategory:
      type: string
      enum: [manual, automated]

    MeterUnit:
      type: string
      enum:
        [
          hours,
          miles,
          kilometers,
          cycles,
          gallons,
          liters,
          psi,
          bar,
          fahrenheit,
          celsius,
          rpm,
          volts,
          amps,
          percent,
          count,
          custom,
        ]

    ReadingSource:
      type: string
      enum: [manual, api, integration]

    WorkOrderStatus:
      type: string
      enum: [open, on_hold, in_progress, done, canceled, skipped]

    WorkOrderType:
      type: string
      enum: [preventive, reactive, other, cycle_count]

    WorkOrderPriority:
      type: string
      enum: [low, medium, high, critical]

    WorkRequestPriority:
      type: string
      enum: [NONE, LOW, MEDIUM, HIGH]

    WorkRequestStatus:
      type: string
      enum: [PENDING, REJECTED, APPROVED, DONE]

    WorkRequestAttachment:
      type: object
      required: [id, mimeType, fileName, url, createdAt]
      properties:
        id: { type: string }
        mimeType: { type: string }
        fileName: { type: string }
        url:
          type: string
          format: uri
          description: Signed private URL valid for 60 minutes
        createdAt: { type: string, format: date-time }
        width: { type: [integer, "null"] }
        height: { type: [integer, "null"] }

    WorkRequestUploadResponse:
      type: object
      required: [publicUrl, filename, fileKey]
      properties:
        publicUrl:
          type: string
          format: uri
          description: Signed private URL valid for 60 minutes
        filename: { type: string }
        fileKey: { type: string }

    WorkRequest:
      type: object
      required: [id, title, attachments, priority, requestStatus, createdAt, updatedAt]
      properties:
        id: { type: string }
        title: { type: string }
        attachments:
          type: array
          items: { $ref: "#/components/schemas/WorkRequestAttachment" }
        thumbnail:
          anyOf:
            - $ref: "#/components/schemas/WorkRequestAttachment"
            - type: "null"
        priority: { $ref: "#/components/schemas/WorkRequestPriority" }
        description: { type: [string, "null"] }
        requestStatus: { $ref: "#/components/schemas/WorkRequestStatus" }
        assetId: { type: [string, "null"] }
        locationId: { type: [string, "null"] }
        workOrderId: { type: [string, "null"] }
        sendEmailNotification: { type: boolean }
        extraFields:
          type: object
          additionalProperties: { type: string }
        approverTeamId: { type: [string, "null"] }
        creatorContactInfo:
          type: [object, "null"]
          properties:
            type: { type: string, enum: [PHONE, EMAIL, OTHER] }
            value: { type: string }
        asset: { type: [object, "null"], additionalProperties: true }
        location: { type: [object, "null"], additionalProperties: true }
        workOrder: { type: [object, "null"], additionalProperties: true }
        createdAt: { type: string, format: date-time }
        updatedAt: { type: string, format: date-time }

    CreateWorkRequest:
      type: object
      required: [title]
      additionalProperties: false
      properties:
        title: { type: string, maxLength: 255 }
        description: { type: [string, "null"], maxLength: 4000 }
        priority: { $ref: "#/components/schemas/WorkRequestPriority" }
        assetId: { type: [string, "null"] }
        locationId: { type: [string, "null"] }
        approverTeamId: { type: [string, "null"] }
        creatorContactInfo: { type: [string, "null"], maxLength: 512 }
        extraFields:
          type: object
          additionalProperties: { type: string }

    UpdateWorkRequest:
      type: object
      additionalProperties: false
      properties:
        title: { type: string, maxLength: 255 }
        description: { type: [string, "null"], maxLength: 4000 }
        priority: { $ref: "#/components/schemas/WorkRequestPriority" }
        assetId: { type: [string, "null"] }
        locationId: { type: [string, "null"] }
        approverTeamId: { type: [string, "null"] }
        creatorContactInfo: { type: [string, "null"], maxLength: 512 }
        extraFields:
          type: object
          additionalProperties: { type: string }

    WorkRequestListResponse:
      type: object
      required: [workRequests, nextCursor, nextPageUrl]
      properties:
        workRequests:
          type: array
          items: { $ref: "#/components/schemas/WorkRequest" }
        nextCursor: { type: [string, "null"] }
        nextPageUrl: { type: [string, "null"] }

    WorkRequestResponse:
      type: object
      required: [workRequest]
      properties:
        workRequest: { $ref: "#/components/schemas/WorkRequest" }

    WorkRequestPortal:
      type: object
      required: [id, title, portalUrl]
      properties:
        id: { type: string }
        title: { type: string }
        portalUrl: { type: string, format: uri }
        welcomeText: { type: [string, "null"] }
        descriptionPlaceholder: { type: [string, "null"] }
        contactInformation: { type: [string, "null"] }
        assetId: { type: [string, "null"] }
        locationId: { type: [string, "null"] }
        emailOnly: { type: boolean }
        sendEmailNotification: { type: boolean }

    CreateWorkRequestPortal:
      type: object
      required: [title]
      additionalProperties: false
      properties:
        title: { type: string, maxLength: 255 }
        welcomeText: { type: [string, "null"], maxLength: 5000 }
        descriptionPlaceholder: { type: [string, "null"], maxLength: 500 }
        contactInformation: { type: [string, "null"], maxLength: 512 }
        assetId: { type: [string, "null"] }
        locationId: { type: [string, "null"] }
        emailOnly: { type: [boolean, "null"] }
        sendEmailNotification: { type: [boolean, "null"] }

    UpdateWorkRequestPortal:
      type: object
      additionalProperties: false
      properties:
        title: { type: string, maxLength: 255 }
        welcomeText: { type: [string, "null"], maxLength: 5000 }
        descriptionPlaceholder: { type: [string, "null"], maxLength: 500 }
        contactInformation: { type: [string, "null"], maxLength: 512 }
        assetId: { type: [string, "null"] }
        locationId: { type: [string, "null"] }
        emailOnly: { type: [boolean, "null"] }
        sendEmailNotification: { type: [boolean, "null"] }

    WorkRequestPortalListResponse:
      type: object
      required: [workRequestPortals, nextCursor, nextPageUrl]
      properties:
        workRequestPortals:
          type: array
          items: { $ref: "#/components/schemas/WorkRequestPortal" }
        nextCursor: { type: [string, "null"] }
        nextPageUrl: { type: [string, "null"] }

    WorkRequestPortalResponse:
      type: object
      required: [workRequestPortal]
      properties:
        workRequestPortal: { $ref: "#/components/schemas/WorkRequestPortal" }

    MaintainXCreateWorkOrder:
      type: object
      required: [title]
      additionalProperties: false
      properties:
        title: { type: string, maxLength: 255 }
        description: { type: [string, "null"], maxLength: 5000 }
        type: { type: [string, "null"], enum: [CYCLE_COUNT, OTHER, PREVENTIVE, REACTIVE, null] }
        priority: { $ref: "#/components/schemas/WorkRequestPriority" }
        assetId: { type: [string, "null"] }
        locationId: { type: [string, "null"] }
        dueDate: { type: [string, "null"], format: date-time }
        startDate: { type: [string, "null"], format: date-time }
        estimatedTime: { type: [integer, "null"], minimum: 0 }
        workRequestId:
          type: [string, "null"]
          description: Approves and links this Work Request. Repeated approval returns the existing Work Order.
        extraFields:
          type: object
          additionalProperties: { type: string }

    IdResponse:
      type: object
      required: [id]
      properties:
        id: { type: string }

    AssetType:
      type: string
      enum: [equipment, vehicle, facility]

    AssetCurrentType:
      type: string
      maxLength: 100
      description: >-
        Compatibility slug derived from current active asset type assignments.
        Returns unassigned when prior assignments exist but none remain active.

    AssetStatus:
      type: string
      enum: [online, offline, not_monitored]

    AssetCriticality:
      type: string
      enum: [critical, important, normal]

    AssetTypeAssignment:
      type: object
      required: [id, name]
      properties:
        id:
          type: string
        name:
          type: string

    IdentifierEntityType:
      type: string
      enum: [asset, location, part]

    IdentifierTarget:
      type: object
      required: [type, id]
      properties:
        type:
          $ref: "#/components/schemas/IdentifierEntityType"
        id:
          type: string

    IdentifierScanValue:
      type: object
      required: [id, payload]
      properties:
        id:
          type: string
        payload:
          type: string
        format_hint:
          type: [string, "null"]

    IdentifierScanValueInput:
      type: object
      required: [payload]
      properties:
        payload:
          type: string
          maxLength: 4096
          description: Additional exact representation emitted for the same physical label; limited to 4096 UTF-8 bytes and permits the GS1 group separator
        format_hint:
          type: [string, "null"]
          maxLength: 64
          description: Carrier hint limited to 64 UTF-8 bytes; it is metadata rather than identity

    Identifier:
      type: object
      required: [id, system, value, source, is_primary, created_at, scan_values]
      properties:
        id:
          type: string
        system:
          type: string
        value:
          type: string
        source:
          type: string
          enum: [canary, customer, import, migration]
        is_primary:
          type: boolean
        created_at:
          type: string
          format: date-time
        scan_values:
          type: array
          items:
            $ref: "#/components/schemas/IdentifierScanValue"

    IdentifierResponse:
      allOf:
        - $ref: "#/components/schemas/SingleResponse"
        - type: object
          properties:
            data:
              $ref: "#/components/schemas/Identifier"

    AttachIdentifierRequest:
      type: object
      required: [target, system, value]
      properties:
        target:
          $ref: "#/components/schemas/IdentifierTarget"
        system:
          type: string
          maxLength: 255
          description: Stable customer namespace, limited to 255 UTF-8 bytes
        value:
          type: string
          maxLength: 4096
          description: Exact opaque identifier value, limited to 4096 UTF-8 bytes
        payload:
          type: string
          maxLength: 4096
          description: Exact scan payload, limited to 4096 UTF-8 bytes; defaults to value
        format_hint:
          type: [string, "null"]
          maxLength: 64
        scan_values:
          type: array
          minItems: 1
          maxItems: 32
          description: Additional exact carrier payloads that resolve this identifier
          items:
            $ref: "#/components/schemas/IdentifierScanValueInput"
        is_primary:
          type: boolean

    IdentifierImportRowRequest:
      type: object
      required: [target, system, value]
      properties:
        target:
          $ref: "#/components/schemas/IdentifierTarget"
        system:
          type: string
          maxLength: 255
          description: Stable namespace without control characters, limited to 255 UTF-8 bytes
        value:
          type: string
          maxLength: 4096
          description: Exact opaque identifier value, limited to 4096 UTF-8 bytes
        payload:
          type: string
          maxLength: 4096
          description: Exact scan payload, limited to 4096 UTF-8 bytes; defaults to value
        format_hint:
          type: [string, "null"]
          maxLength: 64
        is_primary:
          type: boolean

    IdentifierImportRequest:
      type: object
      required: [rows]
      properties:
        rows:
          type: array
          minItems: 1
          maxItems: 1000
          items:
            $ref: "#/components/schemas/IdentifierImportRowRequest"

    IdentifierImportPreviewResponse:
      allOf:
        - $ref: "#/components/schemas/SingleResponse"
        - type: object
          properties:
            data:
              type: object
              required: [valid, rows, errors]
              properties:
                valid:
                  type: boolean
                rows:
                  type: array
                  items:
                    type: object
                    required: [index, status]
                    properties:
                      index:
                        type: integer
                      status:
                        type: string
                        enum: [ready, error]
                errors:
                  type: array
                  items:
                    type: object
                    required: [index, code, message]
                    properties:
                      index:
                        type: integer
                      code:
                        type: string
                        enum:
                          [
                            invalid,
                            target_not_found,
                            identifier_conflict,
                            payload_conflict,
                            primary_conflict,
                            duplicate_identifier,
                            duplicate_payload,
                            duplicate_primary,
                          ]
                      message:
                        type: string

    IdentifierImportApplyResponse:
      allOf:
        - $ref: "#/components/schemas/SingleResponse"
        - type: object
          properties:
            data:
              type: object
              required: [identifiers]
              properties:
                identifiers:
                  type: array
                  items:
                    $ref: "#/components/schemas/Identifier"

    GenerateIdentifierRequest:
      type: object
      required: [target]
      properties:
        target:
          $ref: "#/components/schemas/IdentifierTarget"
        is_primary:
          type: boolean

    ResolveIdentifierRequest:
      type: object
      required: [payload]
      properties:
        payload:
          type: string
          maxLength: 4096
          description: Exact opaque value read by the scanner, limited to 4096 UTF-8 bytes and permitting the GS1 group separator
        alternate_payloads:
          type: array
          minItems: 1
          maxItems: 3
          description: Exact alternate representations reported for the same physical label; each is limited to 4096 UTF-8 bytes and permits the GS1 group separator
          items:
            type: string
            maxLength: 4096
        symbology:
          type: string
          enum:
            [
              aztec,
              ean13,
              ean8,
              qr,
              pdf417,
              upc_e,
              datamatrix,
              code39,
              code93,
              itf14,
              codabar,
              code128,
              upc_a,
            ]

    ResolveIdentifierResponse:
      allOf:
        - $ref: "#/components/schemas/SingleResponse"
        - type: object
          properties:
            data:
              oneOf:
                - type: object
                  required: [status, target]
                  properties:
                    status:
                      type: string
                      const: resolved
                    target:
                      $ref: "#/components/schemas/IdentifierTarget"
                - type: object
                  required: [status]
                  properties:
                    status:
                      type: string
                      const: unclaimed
                - type: object
                  description: Multiple live parts share this customer barcode. Require an explicit candidate choice; never choose the first automatically. Canary-generated labels and asset/location identifiers remain unique.
                  required: [status, candidates, hasMore]
                  properties:
                    status:
                      type: string
                      const: ambiguous
                    hasMore:
                      type: boolean
                      description: More matching parts exist; search the parts catalog to find candidates beyond this bounded result.
                    candidates:
                      type: array
                      maxItems: 20
                      items:
                        type: object
                        required: [identifier, target, name, partNumber]
                        properties:
                          identifier:
                            type: object
                            required: [id, system, value]
                            properties:
                              id:
                                type: string
                              system:
                                type: string
                              value:
                                type: string
                          target:
                            type: object
                            required: [type, id]
                            properties:
                              type:
                                type: string
                                const: part
                              id:
                                type: string
                          name:
                            type: string
                          partNumber:
                            type: [string, "null"]

    # ==================== Response Wrappers ====================
    ListResponse:
      type: object
      required: [data, meta]
      properties:
        data:
          type: array
          items: {}
        meta:
          type: object
          required: [request_id, api_version, pagination]
          properties:
            request_id:
              type: string
            api_version:
              type: string
            pagination:
              type: object
              required: [has_more]
              properties:
                cursor:
                  type: string
                has_more:
                  type: boolean
                total_count:
                  type: integer
                  minimum: 0

    SingleResponse:
      type: object
      required: [data, meta]
      properties:
        data:
          type: object
        meta:
          type: object
          required: [request_id, api_version]
          properties:
            request_id:
              type: string
            api_version:
              type: string

    # ==================== Meter Schemas ====================
    Meter:
      type: object
      required:
        [
          id,
          name,
          description,
          meter_type,
          unit,
          custom_unit,
          category,
          frequency,
          frequency_interval,
          asset_id,
          location_id,
          current_value,
          current_reading_at,
          min_value,
          max_value,
          rollover_value,
          image_url,
          is_active,
          created_at,
          updated_at,
        ]
      properties:
        id:
          type: string
        name:
          type: string
        description:
          type: [string, "null"]
        meter_type:
          $ref: "#/components/schemas/MeterType"
        unit:
          anyOf:
            - $ref: "#/components/schemas/MeterUnit"
            - type: "null"
        custom_unit:
          type: [string, "null"]
        category:
          $ref: "#/components/schemas/MeterCategory"
        frequency:
          type: string
        frequency_interval:
          type: integer
          minimum: 1
          description: >-
            How many `frequency` units apart two readings are expected to be.
            Read-only, like `frequency`: neither can be set through this API.
        asset_id:
          type: [string, "null"]
        location_id:
          type: [string, "null"]
        current_value:
          type: [number, "null"]
        current_reading_at:
          type: [string, "null"]
          format: date-time
        min_value:
          type: [number, "null"]
        max_value:
          type: [number, "null"]
        rollover_value:
          type: [number, "null"]
        image_url:
          type: [string, "null"]
        is_active:
          type: boolean
        created_at:
          type: string
          format: date-time
        updated_at:
          type: string
          format: date-time

    MeterResponse:
      allOf:
        - $ref: "#/components/schemas/SingleResponse"
        - type: object
          properties:
            data:
              $ref: "#/components/schemas/Meter"

    CreateMeterRequest:
      type: object
      required: [name]
      properties:
        name:
          type: string
          maxLength: 255
        description:
          type: string
          maxLength: 1000
        meter_type:
          $ref: "#/components/schemas/MeterType"
          default: numeric
        unit:
          $ref: "#/components/schemas/MeterUnit"
        custom_unit:
          type: string
          maxLength: 50
        category:
          $ref: "#/components/schemas/MeterCategory"
          default: manual
        asset_id:
          type: string
          description: Required if location_id is not provided
        location_id:
          type: string
          description: Required if asset_id is not provided
        min_value:
          type: number
        max_value:
          type: number
        rollover_value:
          type: number

    UpdateMeterRequest:
      type: object
      properties:
        name:
          type: string
          maxLength: 255
        description:
          type: [string, "null"]
          maxLength: 1000
        meter_type:
          $ref: "#/components/schemas/MeterType"
        unit:
          anyOf:
            - $ref: "#/components/schemas/MeterUnit"
            - type: "null"
        custom_unit:
          type: [string, "null"]
          maxLength: 50
        category:
          $ref: "#/components/schemas/MeterCategory"
        asset_id:
          type: [string, "null"]
        location_id:
          type: [string, "null"]
        min_value:
          type: [number, "null"]
        max_value:
          type: [number, "null"]
        rollover_value:
          type: [number, "null"]

    # ==================== Meter Reading Schemas ====================
    MeterReading:
      type: object
      properties:
        id:
          type: string
        meter_id:
          type: string
        value:
          type: number
        reading_at:
          type: string
          format: date-time
        source:
          $ref: "#/components/schemas/ReadingSource"
        notes:
          type: [string, "null"]
        recorded_by_id:
          type: [string, "null"]
        external_id:
          type: [string, "null"]
        created_at:
          type: string
          format: date-time

    MeterReadingResponse:
      allOf:
        - $ref: "#/components/schemas/SingleResponse"
        - type: object
          properties:
            data:
              $ref: "#/components/schemas/MeterReading"

    CreateMeterReadingRequest:
      type: object
      required: [value]
      example:
        value: 1500
        reading_at: "2026-09-02T10:30:00.000Z"
        notes: Imported from runtime counter
      properties:
        value:
          type: number
        reading_at:
          type: string
          format: date-time
          description: Defaults to current time if not provided
        notes:
          type: string
          maxLength: 500

    # ==================== Work Order Schemas ====================
    WorkOrder:
      type: object
      required:
        [
          id,
          title,
          description,
          type,
          status,
          priority,
          requested_by,
          creator_id,
          assigned_to_user_id,
          assigned_to_team_id,
          asset_id,
          location_id,
          due_date,
          scheduled_start,
          estimated_hours,
          recurrence_info,
          previous_id,
          next_id,
          completed_at,
          completer_id,
          completion_summary,
          inventory_reconciliation_required,
          image_url,
          created_at,
          updated_at,
        ]
      properties:
        id:
          type: string
        title:
          type: string
        description:
          type: [string, "null"]
        type:
          anyOf:
            - $ref: "#/components/schemas/WorkOrderType"
            - type: "null"
        status:
          $ref: "#/components/schemas/WorkOrderStatus"
        priority:
          $ref: "#/components/schemas/WorkOrderPriority"
        requested_by:
          type: [string, "null"]
        creator_id:
          type: [string, "null"]
          readOnly: true
          description: Immutable authenticated creator when the work order originated from a human app context
        assigned_to_user_id:
          type: [string, "null"]
        assigned_to_team_id:
          type: [string, "null"]
        asset_id:
          type: [string, "null"]
        location_id:
          type: [string, "null"]
        due_date:
          type: [string, "null"]
          format: date-time
        scheduled_start:
          type: [string, "null"]
          format: date-time
        estimated_hours:
          type: [number, "null"]
          format: double
          minimum: 0
          maximum: 9999.99
        recurrence_info:
          anyOf:
            - $ref: "#/components/schemas/WorkOrderRecurrenceInfo"
            - type: "null"
          readOnly: true
        previous_id:
          type: [string, "null"]
          readOnly: true
        next_id:
          type: [string, "null"]
          readOnly: true
        completed_at:
          type: [string, "null"]
          format: date-time
          readOnly: true
        completer_id:
          type: [string, "null"]
          readOnly: true
        completion_summary:
          type: [string, "null"]
          maxLength: 5000
        inventory_reconciliation_required:
          type: boolean
          readOnly: true
          description: True when completion occurred while Parts was disabled and inventory must be reconciled explicitly
        extraFields:
          $ref: "#/components/schemas/ExtraFields"
        image_url:
          type: [string, "null"]
        created_at:
          type: string
          format: date-time
        updated_at:
          type: string
          format: date-time

    WorkOrderResponse:
      allOf:
        - $ref: "#/components/schemas/SingleResponse"
        - type: object
          properties:
            data:
              $ref: "#/components/schemas/WorkOrder"

    CreateWorkOrderRequest:
      type: object
      required: [title]
      example:
        title: Quarterly compressor inspection
        priority: high
        asset_id: 01JAY7C1NQZ5H2R8M4K6T9V3BP
        scheduled_start: "2026-09-10T06:00:00.000Z"
      properties:
        title:
          type: string
          maxLength: 255
        description:
          type: string
          maxLength: 5000
        type:
          anyOf:
            - $ref: "#/components/schemas/WorkOrderType"
            - type: "null"
        status:
          $ref: "#/components/schemas/WorkOrderStatus"
          default: open
        priority:
          $ref: "#/components/schemas/WorkOrderPriority"
          default: medium
        requested_by:
          type: string
          maxLength: 255
        assigned_to_user_id:
          type: string
          description: Cannot be set if assigned_to_team_id is set
        assigned_to_team_id:
          type: string
          description: Cannot be set if assigned_to_user_id is set
        asset_id:
          type: string
        location_id:
          type: string
        due_date:
          type: string
          format: date-time
        scheduled_start:
          type: [string, "null"]
          format: date-time
        estimated_hours:
          type: [number, "null"]
          format: double
          minimum: 0
          maximum: 9999.99
        completion_summary:
          type: [string, "null"]
          maxLength: 5000
        workRequestId:
          type: [string, "null"]
          description: Approves this pending Work Request and atomically links the created Work Order. A replay returns the already linked Work Order.
        extraFields:
          $ref: "#/components/schemas/ExtraFields"

    UpdateWorkOrderRequest:
      type: object
      properties:
        title:
          type: string
          maxLength: 255
        description:
          type: [string, "null"]
          maxLength: 5000
        type:
          anyOf:
            - $ref: "#/components/schemas/WorkOrderType"
            - type: "null"
        status:
          $ref: "#/components/schemas/WorkOrderStatus"
        priority:
          $ref: "#/components/schemas/WorkOrderPriority"
        requested_by:
          type: [string, "null"]
          maxLength: 255
        assigned_to_user_id:
          type: [string, "null"]
        assigned_to_team_id:
          type: [string, "null"]
        asset_id:
          type: [string, "null"]
        location_id:
          type: [string, "null"]
        due_date:
          type: [string, "null"]
          format: date-time
        scheduled_start:
          type: [string, "null"]
          format: date-time
        estimated_hours:
          type: [number, "null"]
          format: double
          minimum: 0
          maximum: 9999.99
        completion_summary:
          type: [string, "null"]
          maxLength: 5000
        extraFields:
          $ref: "#/components/schemas/ExtraFields"

    WorkOrderRecurrenceInfo:
      type: object
      required: [type]
      readOnly: true
      properties:
        type:
          type: string
          enum: [DAILY, WEEKLY, MONTHLY, YEARLY, PERIODICALLY]
        interval:
          type: integer
          minimum: 0
        days:
          type: array
          items:
            type: string
            enum: [SUNDAY, MONDAY, TUESDAY, WEDNESDAY, THURSDAY, FRIDAY, SATURDAY]
        day:
          type: integer
          minimum: 1
          maximum: 31

    # ==================== Asset Schemas ====================
    Asset:
      type: object
      required:
        [
          id,
          name,
          description,
          asset_type,
          asset_types,
          status,
          criticality,
          serial_number,
          manufacturer,
          model,
          year,
          location_id,
          parent_asset_id,
          sub_asset_count,
          deleted_at,
          image_url,
          created_at,
          updated_at,
        ]
      properties:
        id:
          type: string
        name:
          type: string
        description:
          type: [string, "null"]
        asset_type:
          $ref: "#/components/schemas/AssetCurrentType"
        asset_types:
          type: array
          readOnly: true
          description: Active organization-defined asset type assignments
          items:
            $ref: "#/components/schemas/AssetTypeAssignment"
        status:
          $ref: "#/components/schemas/AssetStatus"
        criticality:
          $ref: "#/components/schemas/AssetCriticality"
        serial_number:
          type: [string, "null"]
        manufacturer:
          type: [string, "null"]
        model:
          type: [string, "null"]
        year:
          type: [integer, "null"]
        location_id:
          type: [string, "null"]
        parent_asset_id:
          type: [string, "null"]
          readOnly: true
        sub_asset_count:
          type: integer
          minimum: 0
          readOnly: true
        deleted_at:
          type: [string, "null"]
          format: date-time
          readOnly: true
        image_url:
          type: [string, "null"]
        extraFields:
          $ref: "#/components/schemas/ExtraFields"
        created_at:
          type: string
          format: date-time
        updated_at:
          type: string
          format: date-time

    AssetResponse:
      allOf:
        - $ref: "#/components/schemas/SingleResponse"
        - type: object
          properties:
            data:
              $ref: "#/components/schemas/Asset"

    CreateAssetRequest:
      type: object
      required: [name, asset_type]
      example:
        name: North compressor
        asset_type: equipment
        status: online
        criticality: critical
        manufacturer: Atlas Copco
        model: GA 30
        year: 2025
      properties:
        name:
          type: string
          maxLength: 255
        asset_type:
          $ref: "#/components/schemas/AssetType"
        status:
          $ref: "#/components/schemas/AssetStatus"
          default: online
        criticality:
          $ref: "#/components/schemas/AssetCriticality"
          default: normal
        description:
          type: string
          maxLength: 1000
        image_url:
          type: string
          maxLength: 2000
        image_blur:
          type: string
        location_id:
          type: string
        serial_number:
          type: string
          maxLength: 255
        manufacturer:
          type: string
          maxLength: 255
        model:
          type: string
          maxLength: 255
        year:
          type: integer
          minimum: 1900
          maximum: 2027
          description: Manufacturing year, through the next calendar year
        extraFields:
          $ref: "#/components/schemas/ExtraFields"

    UpdateAssetRequest:
      type: object
      properties:
        name:
          type: string
          maxLength: 255
        asset_type:
          $ref: "#/components/schemas/AssetType"
        status:
          $ref: "#/components/schemas/AssetStatus"
        criticality:
          $ref: "#/components/schemas/AssetCriticality"
        description:
          type: [string, "null"]
          maxLength: 1000
        image_url:
          type: [string, "null"]
          maxLength: 2000
        image_blur:
          type: [string, "null"]
        location_id:
          type: [string, "null"]
        serial_number:
          type: [string, "null"]
          maxLength: 255
        manufacturer:
          type: [string, "null"]
          maxLength: 255
        model:
          type: [string, "null"]
          maxLength: 255
        year:
          type: [integer, "null"]
          minimum: 1900
          maximum: 2027
          description: Manufacturing year, through the next calendar year
        extraFields:
          $ref: "#/components/schemas/ExtraFields"

    AssetPartLinkResponse:
      allOf:
        - $ref: "#/components/schemas/SingleResponse"
        - type: object
          properties:
            data:
              type: object
              properties:
                asset_id:
                  type: string
                part_id:
                  type: string
                linked:
                  type: boolean

    # ==================== Location Schemas ====================
    Location:
      type: object
      properties:
        id:
          type: string
        name:
          type: string
        description:
          type: [string, "null"]
        image_url:
          type: [string, "null"]
        parentId:
          type: [string, "null"]
          description: Direct parent location ID. Locations support one parent/sub-location level.
        extraFields:
          $ref: "#/components/schemas/ExtraFields"
        created_at:
          type: string
          format: date-time
        updated_at:
          type: string
          format: date-time

    LocationDetail:
      allOf:
        - $ref: "#/components/schemas/Location"
        - type: object
          required: [childrenIds]
          properties:
            childrenIds:
              type: array
              readOnly: true
              description: Direct sub-location IDs.
              items:
                type: string

    LocationResponse:
      allOf:
        - $ref: "#/components/schemas/SingleResponse"
        - type: object
          properties:
            data:
              $ref: "#/components/schemas/Location"

    LocationDetailResponse:
      allOf:
        - $ref: "#/components/schemas/SingleResponse"
        - type: object
          properties:
            data:
              $ref: "#/components/schemas/LocationDetail"

    CreateLocationRequest:
      type: object
      required: [name]
      properties:
        name:
          type: string
          maxLength: 255
        description:
          type: string
          maxLength: 1000
        image_url:
          type: string
          maxLength: 2000
        image_blur:
          type: string
        parentId:
          type: [string, "null"]
        extraFields:
          $ref: "#/components/schemas/ExtraFields"

    UpdateLocationRequest:
      type: object
      properties:
        name:
          type: string
          maxLength: 255
        description:
          type: [string, "null"]
          maxLength: 1000
        image_url:
          type: [string, "null"]
          maxLength: 2000
        image_blur:
          type: [string, "null"]
        parentId:
          type: [string, "null"]
        extraFields:
          $ref: "#/components/schemas/ExtraFields"

    # ==================== Part Schemas ====================
    Part:
      type: object
      properties:
        id:
          type: string
        name:
          type: string
        description:
          type: [string, "null"]
        part_number:
          type: [string, "null"]
        manufacturer:
          type: [string, "null"]
        minimum_stock:
          type: [integer, "null"]
        part_type_id:
          type: [string, "null"]
        image_url:
          type: [string, "null"]
        extraFields:
          $ref: "#/components/schemas/ExtraFields"
        created_at:
          type: string
          format: date-time
        updated_at:
          type: string
          format: date-time

    PartResponse:
      allOf:
        - $ref: "#/components/schemas/SingleResponse"
        - type: object
          properties:
            data:
              $ref: "#/components/schemas/Part"

    CreatePartRequest:
      type: object
      required: [name]
      properties:
        name:
          type: string
          maxLength: 255
        description:
          type: string
          maxLength: 1000
        part_number:
          type: string
          maxLength: 255
        manufacturer:
          type: string
          maxLength: 255
        minimum_stock:
          type: integer
          minimum: 0
        image_url:
          type: string
          maxLength: 2000
        image_blur:
          type: string
        part_type_id:
          type: string
        extraFields:
          $ref: "#/components/schemas/ExtraFields"

    UpdatePartRequest:
      type: object
      properties:
        name:
          type: string
          maxLength: 255
        description:
          type: [string, "null"]
          maxLength: 1000
        part_number:
          type: [string, "null"]
          maxLength: 255
        manufacturer:
          type: [string, "null"]
          maxLength: 255
        minimum_stock:
          type: integer
        extraFields:
          $ref: "#/components/schemas/ExtraFields"
          minimum: 0
        image_url:
          type: [string, "null"]
          maxLength: 2000
        image_blur:
          type: [string, "null"]
        part_type_id:
          type: [string, "null"]

    # ==================== Error Schemas ====================
    ProblemDetails:
      type: object
      description: RFC 7807 Problem Details
      required: [type, title, status, detail, request_id, code]
      example:
        type: https://api.oncanary.com/errors/validation_error
        title: Validation Error
        status: 400
        detail: Request validation failed
        request_id: req_01JAY8FVDM6CH4R3X9Q2T7K5NP
        code: validation_error
      properties:
        type:
          type: string
          format: uri
        title:
          type: string
        status:
          type: integer
        detail:
          type: string
        request_id:
          type: string
        code:
          type: string
        validation_errors:
          type: array
          items:
            type: object
            required: [field, message]
            properties:
              field:
                type: string
              message:
                type: string

  headers:
    RequestId:
      description: Unique request identifier to include with support requests
      schema:
        type: string
        example: req_01JAY8FVDM6CH4R3X9Q2T7K5NP

  responses:
    BadRequest:
      description: Bad Request
      headers:
        X-Request-ID:
          $ref: "#/components/headers/RequestId"
      content:
        application/problem+json:
          schema:
            $ref: "#/components/schemas/ProblemDetails"

    Unauthorized:
      description: Unauthorized - Invalid or missing API key
      headers:
        X-Request-ID:
          $ref: "#/components/headers/RequestId"
      content:
        application/problem+json:
          schema:
            $ref: "#/components/schemas/ProblemDetails"

    Forbidden:
      description: Forbidden - `insufficient_scope` when the API key lacks the scope, or `module_disabled` when the organization module is disabled
      headers:
        X-Request-ID:
          $ref: "#/components/headers/RequestId"
      content:
        application/problem+json:
          schema:
            $ref: "#/components/schemas/ProblemDetails"

    NotFound:
      description: Resource not found
      headers:
        X-Request-ID:
          $ref: "#/components/headers/RequestId"
      content:
        application/problem+json:
          schema:
            $ref: "#/components/schemas/ProblemDetails"

    Conflict:
      description: Resource state conflicts with the requested operation
      headers:
        X-Request-ID:
          $ref: "#/components/headers/RequestId"
      content:
        application/problem+json:
          schema:
            $ref: "#/components/schemas/ProblemDetails"

    ContentTooLarge:
      description: Upload exceeds the 1 MiB public API limit
      headers:
        X-Request-ID:
          $ref: "#/components/headers/RequestId"
      content:
        application/problem+json:
          schema:
            $ref: "#/components/schemas/ProblemDetails"

    UnsupportedMediaType:
      description: Picture content type or image data is unsupported
      headers:
        X-Request-ID:
          $ref: "#/components/headers/RequestId"
      content:
        application/problem+json:
          schema:
            $ref: "#/components/schemas/ProblemDetails"

    RateLimited:
      description: Rate limit exceeded
      headers:
        X-Request-ID:
          $ref: "#/components/headers/RequestId"
        Retry-After:
          description: Seconds until rate limit resets
          schema:
            type: integer
      content:
        application/problem+json:
          schema:
            $ref: "#/components/schemas/ProblemDetails"
