openapi: 3.1.0
info:
  title: AIOTIC API
  description: |
    AIOTIC turns incoming purchase orders (e-mail attachments, PDFs, images) into
    structured, validated order data and hands them to your ERP.

    This document describes the **data-plane API** of one AIOTIC tenant. Every tenant
    has its own base URL and its own API keys.

    Two directions matter for an integration:

    * **Calls you make to AIOTIC** — the paths below (upload orders, read status,
      keep customers / products / mappings in sync, trigger the ERP send).
    * **Calls AIOTIC makes to you** — described under `webhooks`: the *ERP receive
      endpoint* (the order hand-off) and the optional *processing webhook*.

    See the Integrator Guide for flows, examples and the Python SDK.
  version: 1.0.0
  summary: Purchase-order intake, review and ERP hand-off for AIOTIC tenants
  contact:
    name: AIOTIC integration support
    url: https://www.aiotic.ai
  x-logo:
    url: /logo.svg
    altText: AIOTIC
paths:
  /healthcheck:
    get:
      summary: Liveness check
      description: 'Returns `{"status": "ok"}` when the API process is up. No authentication.'
      operationId: getHealth
      responses:
        '200':
          description: Successful Response
          content:
            application/json:
              schema:
                additionalProperties:
                  type: string
                type: object
                title: Response Healthcheck Healthcheck Get
      tags: []
      x-audience: public
      security: []
  /system-status:
    get:
      summary: Platform status
      description: |
        Current processing status of the tenant: `operational`, `degraded` or `unknown`,
        with an optional message and the time it was last updated. No authentication.
        Suitable for a status widget or for pausing uploads while degraded.
      operationId: getSystemStatus
      responses:
        '200':
          description: Current status
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/SystemStatus'
              example:
                status: operational
                message: null
                updated_at: '2026-04-01T06:03:20+00:00'
      tags: []
      x-audience: public
      security: []
  /order/upload:
    post:
      tags:
        - orders
      summary: Upload document(s) as one order
      description: |
        Upload one or more files (PDF, JPG, PNG, TXT, MD) that together form **one**
        purchase order. Files are stored and queued; extraction runs in the background.

        * Provide your own `request_id` (UUID v4) to make the call idempotent and to
          correlate the order in your own systems.
        * Any additional multipart form field is stored as `metadata` on the order and
          echoed back by the status endpoints — use it for your own references.
        * Multiple files are never split into multiple orders on this endpoint. Use
          `/order/raw/upload` for e-mails that may contain several orders.
      operationId: uploadOrder
      requestBody:
        content:
          multipart/form-data:
            schema:
              $ref: '#/components/schemas/OrderUploadBody'
        required: true
      responses:
        '200':
          description: Files accepted and queued
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/OrderUploadResponse'
              example:
                request_id: 550e8400-e29b-41d4-a716-446655440000
                split: false
        '400':
          $ref: '#/components/responses/BadRequest'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '404':
          $ref: '#/components/responses/NotFound'
        '415':
          description: Unsupported media type
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
        '422':
          description: Validation Error
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/HTTPValidationError'
        '503':
          description: Tenant not initialised yet (no configuration deployed)
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
      x-audience: public
  /order/raw/upload:
    post:
      tags:
        - orders
      summary: Upload a raw e-mail (.eml)
      description: |
        Submit a complete e-mail (`.eml`). AIOTIC classifies it; if it is a purchase order the
        attachments and body are extracted. If the tenant has order splitting enabled and the
        e-mail carries several orders, the response has `split: true` and one child order per
        detected order — **poll the children**, not the source `request_id`.

        A non-purchase-order e-mail is rejected with `400` and a structured `detail` object
        (`error: not_a_purchase_order`, the detected `category`, …) and is recorded in the
        rejected-e-mails list so an operator can override the decision.
      operationId: uploadRawEmail
      requestBody:
        content:
          multipart/form-data:
            schema:
              $ref: '#/components/schemas/RawEmailUploadBody'
        required: true
      responses:
        '200':
          description: Accepted (single order or split children)
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/OrderUploadResponse'
              examples:
                single:
                  summary: Single order
                  value:
                    request_id: 550e8400-e29b-41d4-a716-446655440000
                    split: false
                split:
                  summary: E-mail split into two orders
                  value:
                    request_id: 1c1f2a3e-0000-4000-8000-000000000001
                    split: true
                    email_group_id: 9b7e2b1c-0000-4000-8000-00000000abcd
                    orders:
                      - request_id: 2c2f2a3e-0000-4000-8000-000000000002
                        status: QUEUED
                        order_label: PO-4711
                      - request_id: 3c3f2a3e-0000-4000-8000-000000000003
                        status: QUEUED
                        order_label: PO-4712
        '400':
          description: Invalid file, or the e-mail is not a purchase order (structured `detail`)
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/RawUploadRejection'
              example:
                detail:
                  error: not_a_purchase_order
                  message: Email classified as 'quotation', not a purchase order — nothing to process.
                  request_id: 550e8400-e29b-41d4-a716-446655440000
                  category: quotation
                  subject: Offer 10531
                  language: de
                  line_item_count: 25
        '401':
          $ref: '#/components/responses/Unauthorized'
        '404':
          $ref: '#/components/responses/NotFound'
        '422':
          description: A split was predicted but no child order could be prepared
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
        '503':
          description: Classification not configured on this tenant yet
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
      x-audience: public
  /order/raw/classify:
    post:
      tags:
        - orders
      summary: Classify a raw e-mail without processing it
      description: >-
        Returns the category the classifier would assign (`purchase_order`, `invoice`, `quotation`, …).
        Nothing is stored or queued.
      operationId: classifyRawEmail
      requestBody:
        content:
          multipart/form-data:
            schema:
              $ref: '#/components/schemas/RawEmailClassifyBody'
        required: true
      responses:
        '200':
          description: Successful Response
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/EmailClassificationResponse'
        '404':
          $ref: '#/components/responses/NotFound'
        '422':
          description: Validation Error
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/HTTPValidationError'
      x-audience: public
  /order_status/{request_id}:
    get:
      tags:
        - order-status
      summary: Get order status and extracted data
      description: |
        The current lifecycle status of an order plus, once processing finished, the extracted
        purchase order in `result`. Also carries `erp_ref` after a successful ERP send,
        `last_error` / `retry_count` / `next_retry_at` for transient failures, and the
        `email_group_id` / `order_label` when the order came from a split e-mail.
      operationId: getOrderStatus
      parameters:
        - name: request_id
          in: path
          required: true
          schema:
            type: string
            title: Request Id
          description: The order's id (UUID) as returned by the upload endpoints
      responses:
        '200':
          description: Order status
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/OrderStatus'
              example:
                request_id: 550e8400-e29b-41d4-a716-446655440000
                status: PROCESSED
                timestamp: '2026-03-14T10:30:00'
                attachments:
                  PO-4711.pdf:
                    size: 125840
                    mime_type: application/pdf
                metadata:
                  source: api
                  my_reference: TICKET-8812
                result:
                  order_number: PO-4711
                  order_date: '2026-03-14'
                  delivery_date: '2026-03-21'
                  currency: EUR
                  total_price: 123.4
                  supplier:
                    company: Acme Supplies BV
                    contact_person: null
                    email: null
                    address:
                      street: Industrieweg 5
                      postal_code: 1234 AB
                      city: Amsterdam
                      country: NL
                  customer:
                    customer_id: '58931'
                    company: LUMITECH INSTALLATIES
                    contact_person: J. de Boer
                    email: info@lumitech.example
                    phone: null
                    branch: null
                    vat_id: NL001234567B01
                    iban: null
                    bic: null
                    address:
                      street: Ambachtsweg 12
                      postal_code: 7327 AA
                      city: Apeldoorn
                      country: NL
                  shipping_details: null
                  items:
                    - article_number: PROD-001
                      customer_item_number: LT-ART-001
                      description: LED Driver 48V
                      quantity: 10
                      quantity_state: Valid
                      unit: ST
                      price: 12.34
                      currency: EUR
                      line_total: 123.4
                  additional_information: null
                state: null
                erp_ref: null
                email_group_id: null
                order_label: PO-4711
                retry_count: 0
                next_retry_at: null
                last_error: null
        '400':
          $ref: '#/components/responses/BadRequest'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '404':
          $ref: '#/components/responses/NotFound'
        '422':
          description: Validation Error
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/HTTPValidationError'
      x-audience: public
  /order_status/list:
    get:
      tags:
        - order-status
      summary: List orders (paginated, newest first)
      description: |
        Paginated list of orders with their status. There is no server-side filter or
        `updated_since` parameter today — poll the first page(s) and track transitions
        locally (the Python SDK's `OrderWatcher` does exactly this).
      operationId: listOrders
      parameters:
        - name: page
          in: query
          required: false
          schema:
            type: integer
            minimum: 1
            description: Page number
            default: 1
            title: Page
          description: Page number
        - name: size
          in: query
          required: false
          schema:
            type: integer
            maximum: 1000
            minimum: 1
            description: Items per page
            default: 100
            title: Size
          description: Items per page
      responses:
        '200':
          description: Successful Response
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/OrderListResponse'
        '404':
          $ref: '#/components/responses/NotFound'
        '422':
          description: Validation Error
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/HTTPValidationError'
      x-audience: public
  /order/group/{email_group_id}:
    get:
      tags:
        - orders
      summary: Get all orders from one source e-mail
      description: Aggregate view of a split e-mail — the correlation id, the message id and every child order's status.
      operationId: getOrderGroup
      parameters:
        - name: email_group_id
          in: path
          required: true
          schema:
            type: string
            title: Email Group Id
          description: Correlation id shared by all child orders of one split e-mail
      responses:
        '200':
          description: Group with child orders
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/OrderGroup'
        '404':
          $ref: '#/components/responses/NotFound'
        '422':
          description: Validation Error
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/HTTPValidationError'
      x-audience: public
  /order/{request_id}/{filename}:
    get:
      tags:
        - orders
      summary: Download an order file
      description: |
        Returns one file belonging to the order: an original upload (by its file name) or a
        generated artifact such as `latest_result.json` (the extracted purchase order).
        The `Content-Type` follows the file extension.
      operationId: getOrderFile
      parameters:
        - name: request_id
          in: path
          required: true
          schema:
            type: string
            title: Request Id
          description: The order's id (UUID) as returned by the upload endpoints
        - name: filename
          in: path
          required: true
          schema:
            type: string
            title: Filename
          description: File name as listed in the order's `attachments`, or `latest_result.json`
      responses:
        '200':
          description: Successful Response
          content:
            application/json:
              schema: {}
        '404':
          $ref: '#/components/responses/NotFound'
        '422':
          description: Validation Error
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/HTTPValidationError'
      x-audience: public
  /order/{request_id}/{filename}/preview:
    get:
      tags:
        - orders
      summary: Preview an order file
      description: >-
        Same as the download endpoint but with an inline `Content-Disposition`, for showing a PDF or image
        inside your own review UI.
      operationId: getOrderFilePreview
      parameters:
        - name: request_id
          in: path
          required: true
          schema:
            type: string
            title: Request Id
          description: The order's id (UUID) as returned by the upload endpoints
        - name: filename
          in: path
          required: true
          schema:
            type: string
            title: Filename
          description: File name as listed in the order's `attachments`, or `latest_result.json`
      responses:
        '200':
          description: Successful Response
          content:
            application/json:
              schema: {}
        '404':
          $ref: '#/components/responses/NotFound'
        '422':
          description: Validation Error
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/HTTPValidationError'
      x-audience: public
  /order/retry/{request_id}:
    post:
      tags:
        - orders
      summary: Retry a failed order
      description: |
        Re-runs processing for an order in `FAILED` status by creating a **new** order with the
        same files. The original moves to `REPROCESSED`; the response carries the new
        `request_id` — track that one from now on.
      operationId: retryOrder
      parameters:
        - name: request_id
          in: path
          required: true
          schema:
            type: string
            title: Request Id
          description: The order's id (UUID) as returned by the upload endpoints
      requestBody:
        content:
          application/x-www-form-urlencoded:
            schema:
              $ref: '#/components/schemas/OrderRetryBody'
      responses:
        '200':
          description: New order queued
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/OrderUploadResponse'
        '400':
          description: Order is not in `FAILED` status, or has no files to retry
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '404':
          $ref: '#/components/responses/NotFound'
        '422':
          description: Validation Error
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/HTTPValidationError'
      x-audience: public
  /erp/send/{request_id}:
    post:
      tags:
        - erp
      summary: Send an order to the ERP receive endpoint
      description: |
        Hands a reviewed order to your ERP by calling the receive endpoint configured for the
        tenant (see `webhooks` → `erpReceiveOrder`). The call is synchronous: AIOTIC waits for
        your response (timeout configurable, default 30 s) and returns its outcome.

        * Only orders in `PROCESSED`, `MODIFIED` or `ATTENTION` can be sent. Sending an
          `ATTENTION` order is an explicit override of the review flag.
        * The order is atomically locked (`SENDING`) — concurrent sends of the same order
          get `409`. On success the status becomes `SENT` and `erp_ref` is stored.
        * On any failure the status rolls back to what it was, so the send can be retried.
        * Operator corrections made in the AIOTIC app are merged into the payload before it
          leaves AIOTIC.
      operationId: sendOrderToErp
      parameters:
        - name: request_id
          in: path
          required: true
          schema:
            type: string
            title: Request Id
          description: The order's id (UUID) as returned by the upload endpoints
      responses:
        '200':
          description: Your ERP accepted the order
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErpSendResponse'
              example:
                success: true
                request_id: 550e8400-e29b-41d4-a716-446655440000
                data:
                  success: true
                  order_number: SO-2026-00981
        '400':
          $ref: '#/components/responses/BadRequest'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '404':
          $ref: '#/components/responses/NotFound'
        '409':
          description: Order is not in a sendable status, or another send is in progress
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
        '422':
          description: 'Your ERP answered `success: false`; `detail` carries your `error` text'
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
              example:
                detail: 'ERP error: Unknown article number: PROD-999'
        '500':
          description: Your ERP returned a non-JSON body, timed out or was unreachable (status rolled back)
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
        '503':
          description: No ERP endpoint configured for this tenant
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
      x-audience: public
  /rejected/list:
    get:
      tags:
        - rejected
      summary: List rejected e-mails
      description: >-
        E-mails the classifier did not treat as purchase orders, newest first. `status=pending` (default)
        lists the ones still awaiting a decision; `status=overridden` those already reprocessed.
      operationId: listRejectedEmails
      parameters:
        - name: page
          in: query
          required: false
          schema:
            type: integer
            minimum: 1
            description: Page number
            default: 1
            title: Page
          description: Page number
        - name: size
          in: query
          required: false
          schema:
            type: integer
            maximum: 1000
            minimum: 1
            description: Items per page
            default: 100
            title: Size
          description: Items per page
        - name: status
          in: query
          required: false
          schema:
            type: string
            description: Rejection-triage status filter
            default: pending
            title: Status
          description: Rejection-triage status filter
      responses:
        '200':
          description: Successful Response
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/RejectedEmailListResponse'
        '404':
          $ref: '#/components/responses/NotFound'
        '422':
          description: Validation Error
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/HTTPValidationError'
      x-audience: public
  /rejected/{request_id}:
    get:
      tags:
        - rejected
      summary: Get one rejected e-mail
      description: |-
        Get a single rejected (non-purchase-order) classified email by request ID.

        This is the rejected-email detail endpoint, not a generic classified-email
        lookup: a `purchase_order` row is reported as 404 (it does not belong to
        this surface).
      operationId: getRejectedEmail
      parameters:
        - name: request_id
          in: path
          required: true
          schema:
            type: string
            title: Request Id
          description: The order's id (UUID) as returned by the upload endpoints
      responses:
        '200':
          description: Successful Response
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ClassifiedEmail'
        '404':
          $ref: '#/components/responses/NotFound'
        '422':
          description: Validation Error
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/HTTPValidationError'
      x-audience: public
  /rejected/{request_id}/reprocess:
    post:
      tags:
        - rejected
      summary: Reprocess a rejected e-mail as a purchase order
      description: >-
        Overrides the classifier for one e-mail and runs it through normal processing under the **same**
        `request_id`. The original classification is preserved for audit.
      operationId: reprocessRejectedEmail
      parameters:
        - name: request_id
          in: path
          required: true
          schema:
            type: string
            title: Request Id
          description: The order's id (UUID) as returned by the upload endpoints
      responses:
        '200':
          description: Successful Response
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ReprocessResponse'
        '404':
          $ref: '#/components/responses/NotFound'
        '422':
          description: Validation Error
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/HTTPValidationError'
      x-audience: public
  /customer/list:
    get:
      tags:
        - customers
      summary: List customers
      description: List all customers with pagination.
      operationId: listCustomers
      parameters:
        - name: page
          in: query
          required: false
          schema:
            type: integer
            minimum: 1
            description: Page number
            default: 1
            title: Page
          description: Page number
        - name: size
          in: query
          required: false
          schema:
            type: integer
            maximum: 1000
            minimum: 1
            description: Items per page
            default: 100
            title: Size
          description: Items per page
      responses:
        '200':
          description: Successful Response
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/CustomerListResponse'
        '404':
          $ref: '#/components/responses/NotFound'
        '422':
          description: Validation Error
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/HTTPValidationError'
      x-audience: public
  /customer/search/{query}:
    get:
      tags:
        - customers
      summary: Search customers (semantic)
      description: >-
        Fuzzy search over name, address and contact fields, comparable to how AIOTIC matches the sender of an
        order to your customer records. Handy to check how well your master data resolves.
      operationId: searchCustomers
      parameters:
        - name: query
          in: path
          required: true
          schema:
            type: string
            description: Search query text
            title: Query
          description: Search query text
        - name: top_k
          in: query
          required: false
          schema:
            type: integer
            maximum: 1000
            minimum: 1
            description: Top items by similarity score
            default: 10
            title: Top K
          description: Top items by similarity score
      responses:
        '200':
          description: Successful Response
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/CustomerSearchResponse'
        '404':
          $ref: '#/components/responses/NotFound'
        '422':
          description: Validation Error
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/HTTPValidationError'
      x-audience: public
  /customer/{customer_number}:
    get:
      tags:
        - customers
      summary: Get a customer
      description: Get a specific customer by ID.
      operationId: getCustomer
      parameters:
        - name: customer_number
          in: path
          required: true
          schema:
            type: string
            description: Unique customer number
            title: Customer Number
          description: Unique customer number
      responses:
        '200':
          description: Successful Response
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Customer'
        '404':
          $ref: '#/components/responses/NotFound'
        '422':
          description: Validation Error
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/HTTPValidationError'
      x-audience: public
    put:
      tags:
        - customers
      summary: Create or update a customer (upsert)
      description: |
        Idempotent upsert keyed on **your** customer (debtor) number. The number is what AIOTIC
        returns as `customer.customer_id` on every order it identifies for this customer.
        Populate as many fields as you have — name, address, VAT, e-mail and phone all improve
        identification. Response status is `202 Accepted`.
      operationId: upsertCustomer
      parameters:
        - name: customer_number
          in: path
          required: true
          schema:
            type: string
            description: Unique customer number
            title: Customer Number
          description: Unique customer number
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/CustomerUpsert'
      responses:
        '202':
          description: Successful Response
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Customer'
        '404':
          $ref: '#/components/responses/NotFound'
        '422':
          description: Validation Error
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/HTTPValidationError'
      x-audience: public
    delete:
      tags:
        - customers
      summary: Delete a customer
      description: >-
        Remove a customer. Prefer deleting over leaving inactive records — stale records can be matched by
        mistake. (Renaming a record with a "formerly / do not use" marker archives it instead.)
      operationId: deleteCustomer
      parameters:
        - name: customer_number
          in: path
          required: true
          schema:
            type: string
            description: Unique customer number
            title: Customer Number
          description: Unique customer number
      responses:
        '204':
          description: Successful Response
        '404':
          $ref: '#/components/responses/NotFound'
        '422':
          description: Validation Error
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/HTTPValidationError'
      x-audience: public
  /product/list:
    get:
      tags:
        - products
      summary: List products
      description: List all products with pagination.
      operationId: listProducts
      parameters:
        - name: page
          in: query
          required: false
          schema:
            type: integer
            minimum: 1
            description: Page number
            default: 1
            title: Page
          description: Page number
        - name: size
          in: query
          required: false
          schema:
            type: integer
            maximum: 1000
            minimum: 1
            description: Items per page
            default: 100
            title: Size
          description: Items per page
        - name: language_code
          in: query
          required: false
          schema:
            anyOf:
              - type: string
              - type: 'null'
            description: Filter by language code
            title: Language Code
          description: Filter by language code
      responses:
        '200':
          description: Successful Response
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ProductListResponse'
        '404':
          $ref: '#/components/responses/NotFound'
        '422':
          description: Validation Error
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/HTTPValidationError'
      x-audience: public
  /product/{product_number}/{language_code}:
    get:
      tags:
        - products
      summary: Get a product
      description: Get a specific product by its primary key.
      operationId: getProduct
      parameters:
        - name: product_number
          in: path
          required: true
          schema:
            type: string
            description: Product item number
            title: Product Number
          description: Product item number
        - name: language_code
          in: path
          required: true
          schema:
            type: string
            description: Product language code
            title: Language Code
          description: Product language code
      responses:
        '200':
          description: Successful Response
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Product'
        '404':
          $ref: '#/components/responses/NotFound'
        '422':
          description: Validation Error
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/HTTPValidationError'
      x-audience: public
    put:
      tags:
        - products
      summary: Create or update a product (upsert)
      description: |
        Idempotent upsert keyed on your article number **and** a language code (`nl`, `de`, `en`, …).
        Create one record per language if your catalog is multilingual. The product table is
        AIOTIC's list of *valid* article numbers: an order line whose article is not in it is
        flagged for review instead of being sent. Keep it complete.
      operationId: upsertProduct
      parameters:
        - name: product_number
          in: path
          required: true
          schema:
            type: string
            description: Product item number
            title: Product Number
          description: Product item number
        - name: language_code
          in: path
          required: true
          schema:
            type: string
            description: Product language code
            title: Language Code
          description: Product language code
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/ProductUpsert'
      responses:
        '202':
          description: Successful Response
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Product'
        '404':
          $ref: '#/components/responses/NotFound'
        '422':
          description: Validation Error
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/HTTPValidationError'
      x-audience: public
    delete:
      tags:
        - products
      summary: Delete a product
      description: Delete a product.
      operationId: deleteProduct
      parameters:
        - name: product_number
          in: path
          required: true
          schema:
            type: string
            description: Product item number
            title: Product Number
          description: Product item number
        - name: language_code
          in: path
          required: true
          schema:
            type: string
            description: Product language code
            title: Language Code
          description: Product language code
      responses:
        '204':
          description: Successful Response
        '404':
          $ref: '#/components/responses/NotFound'
        '422':
          description: Validation Error
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/HTTPValidationError'
      x-audience: public
  /customer-product/list:
    get:
      tags:
        - customer-products
      summary: List customer item mappings
      description: List customer product mappings with pagination and optional filters.
      operationId: listCustomerProducts
      parameters:
        - name: page
          in: query
          required: false
          schema:
            type: integer
            minimum: 1
            description: Page number
            default: 1
            title: Page
          description: Page number
        - name: size
          in: query
          required: false
          schema:
            type: integer
            maximum: 1000
            minimum: 1
            description: Items per page
            default: 100
            title: Size
          description: Items per page
        - name: customer_number
          in: query
          required: false
          schema:
            anyOf:
              - type: string
              - type: 'null'
            description: Filter by customer number
            title: Customer Number
          description: Filter by customer number
        - name: customer_item_number
          in: query
          required: false
          schema:
            anyOf:
              - type: string
              - type: 'null'
            description: Filter by customer item number
            title: Customer Item Number
          description: Filter by customer item number
        - name: item_number
          in: query
          required: false
          schema:
            anyOf:
              - type: string
              - type: 'null'
            description: Filter by supplier item number
            title: Item Number
          description: Filter by supplier item number
        - name: language_code
          in: query
          required: false
          schema:
            anyOf:
              - type: string
              - type: 'null'
            description: Filter by language code
            title: Language Code
          description: Filter by language code
      responses:
        '200':
          description: Successful Response
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/CustomerProductListResponse'
        '404':
          $ref: '#/components/responses/NotFound'
        '422':
          description: Validation Error
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/HTTPValidationError'
      x-audience: public
  /customer-product/{customer_number}/{customer_item_number}:
    get:
      tags:
        - customer-products
      summary: Get a customer item mapping
      description: Get a specific customer product mapping by its composite key.
      operationId: getCustomerProduct
      parameters:
        - name: customer_number
          in: path
          required: true
          schema:
            type: string
            description: Customer number
            title: Customer Number
          description: Customer number
        - name: customer_item_number
          in: path
          required: true
          schema:
            type: string
            description: Customer's own item number
            title: Customer Item Number
          description: Customer's own item number
      responses:
        '200':
          description: Successful Response
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/CustomerProduct'
        '404':
          $ref: '#/components/responses/NotFound'
        '422':
          description: Validation Error
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/HTTPValidationError'
      x-audience: public
    put:
      tags:
        - customer-products
      summary: Create or update a customer item mapping (upsert)
      description: |
        Tells AIOTIC that when customer `customer_number` orders **their** article
        `customer_item_number`, it means **your** article `item_number` (`language_code`).
        Both the customer and the product must already exist (`404` otherwise).
      operationId: upsertCustomerProduct
      parameters:
        - name: customer_number
          in: path
          required: true
          schema:
            type: string
            description: Customer number
            title: Customer Number
          description: Customer number
        - name: customer_item_number
          in: path
          required: true
          schema:
            type: string
            description: Customer's own item number
            title: Customer Item Number
          description: Customer's own item number
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/CustomerProductUpsert'
      responses:
        '202':
          description: Successful Response
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/CustomerProduct'
        '404':
          $ref: '#/components/responses/NotFound'
        '422':
          description: Validation Error
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/HTTPValidationError'
      x-audience: public
    delete:
      tags:
        - customer-products
      summary: Delete a customer item mapping
      description: Delete a customer product mapping.
      operationId: deleteCustomerProduct
      parameters:
        - name: customer_number
          in: path
          required: true
          schema:
            type: string
            description: Customer number
            title: Customer Number
          description: Customer number
        - name: customer_item_number
          in: path
          required: true
          schema:
            type: string
            description: Customer's own item number
            title: Customer Item Number
          description: Customer's own item number
      responses:
        '204':
          description: Successful Response
        '404':
          $ref: '#/components/responses/NotFound'
        '422':
          description: Validation Error
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/HTTPValidationError'
      x-audience: public
  /email-watcher/fetch-all:
    post:
      tags:
        - email-watcher
      summary: Fetch all unread e-mails now
      description: >-
        Operational trigger — asks the tenant mailbox watcher to fetch and classify all unread mails
        immediately instead of at the next interval.
      operationId: fetchAllEmails
      responses:
        '200':
          description: Successful Response
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/FetchAllEmailsResponse'
        '404':
          $ref: '#/components/responses/NotFound'
      x-audience: public
components:
  securitySchemes:
    ApiKeyAuth:
      type: apiKey
      in: header
      name: X-API-Key
      description: |
        Tenant API key. Two keys exist per tenant:

        * **Integration key** (`API_KEY`) — accepted on every endpoint.
        * **Sync key** (`API_KEY_CUSTOMER`) — accepted **only** on `/customer/*`,
          `/product/*` and `/customer-product/*`. Use it for master-data sync jobs so
          the integration key never leaves your order-processing service.

        Keys are sent as-is in the `X-API-Key` header. Missing or invalid keys return
        `401` with `{"detail": "Invalid or missing API key"}`.
  responses:
    BadRequest:
      description: Invalid request (malformed UUID, unsupported file, …)
      content:
        application/json:
          schema:
            $ref: '#/components/schemas/ErrorResponse'
          example:
            detail: Invalid document ID format
    Unauthorized:
      description: Missing or invalid API key
      content:
        application/json:
          schema:
            $ref: '#/components/schemas/ErrorResponse'
          example:
            detail: Invalid or missing API key
    NotFound:
      description: Resource not found
      content:
        application/json:
          schema:
            $ref: '#/components/schemas/ErrorResponse'
          example:
            detail: Document with ID 550e8400-e29b-41d4-a716-446655440000 not found
  schemas:
    Address:
      type: object
      properties:
        street:
          type: string
          description: Street and house number.
        postal_code:
          type: string
          description: Postal code.
        city:
          type: string
          description: City.
        country:
          type:
            - string
            - 'null'
          description: Country code as printed, usually ISO 3166-1 alpha-2.
      required:
        - street
        - postal_code
        - city
    ClassifiedEmail:
      properties:
        request_id:
          type: string
          format: uuid
          title: Request Id
          description: Unique identifier for the request/email
        email_type:
          type: string
          title: Email Type
          description: The classified type of the email
        sender:
          type: string
          title: Sender
          description: Email address of the sender
        from_name:
          type: string
          title: From Name
          description: Display name of the sender
        subject:
          type: string
          title: Subject
          description: Email subject
        timestamp:
          type: string
          format: date-time
          title: Timestamp
          description: When the email was received and classified
        metadata:
          additionalProperties: true
          type: object
          title: Metadata
          description: Additional metadata about the email
        message_id:
          anyOf:
            - type: string
            - type: 'null'
          title: Message Id
          description: RFC 5322 Message-ID (dedup key)
        classification_reason:
          anyOf:
            - type: string
            - type: 'null'
          title: Classification Reason
          description: Why the classifier chose the type
        rejection_status:
          anyOf:
            - type: string
            - type: 'null'
          title: Rejection Status
          description: 'Rejected-email triage: None (handled) | pending | overridden'
        original_email_type:
          anyOf:
            - type: string
            - type: 'null'
          title: Original Email Type
          description: Classifier's original type, preserved on operator override
      type: object
      required:
        - request_id
        - email_type
        - sender
        - from_name
        - subject
        - timestamp
      title: ClassifiedEmail
      description: An e-mail that was classified as something other than a purchase order.
    Customer:
      properties:
        number:
          type: string
          title: Number
          description: Your customer (debtor) number — the key of this record.
        id:
          type: string
          format: uuid
          title: Id
          description: Internal identifier, generated by AIOTIC.
        name:
          anyOf:
            - type: string
            - type: 'null'
          title: Name
          description: Company name
        postal_code:
          anyOf:
            - type: string
            - type: 'null'
          title: Postal Code
          description: Postal code
        city:
          anyOf:
            - type: string
            - type: 'null'
          title: City
          description: City
        address:
          anyOf:
            - type: string
            - type: 'null'
          title: Address
          description: Address
        contact_person:
          anyOf:
            - type: string
            - type: 'null'
          title: Contact Person
          description: Contact person
        phone_number:
          anyOf:
            - type: string
            - type: 'null'
          title: Phone Number
          description: Phone number
        vat_number:
          anyOf:
            - type: string
            - type: 'null'
          title: Vat Number
          description: BTW (VAT) number
        email:
          anyOf:
            - type: string
            - type: 'null'
          title: Email
          description: Customer email
        coc_number:
          anyOf:
            - type: string
            - type: 'null'
          title: Coc Number
          description: Chamber of Commerce number
        home_page:
          anyOf:
            - type: string
            - type: 'null'
          title: Home Page
          description: Company website URL
        similarity:
          anyOf:
            - type: number
            - type: 'null'
          title: Similarity
          description: Match score, only populated in search results.
        archived_at:
          anyOf:
            - type: string
              format: date-time
            - type: 'null'
          title: Archived At
          description: >-
            Set when the record is archived because its name carries a legacy or closure marker (e.g.
            "formerly", "do not use"). Archived customers are never chosen by identification but still resolve
            when explicitly referenced.
        superseded_by:
          anyOf:
            - type: string
            - type: 'null'
          title: Superseded By
          description: >-
            Customer number this record redirects to, derived from a "see customer N" marker in the name (e.g.
            `*** ZIE 12306 ***`). Orders matched to this record are filed under the successor.
      type: object
      required:
        - number
      title: Customer
      description: A customer (debtor) record as stored in AIOTIC.
    CustomerListResponse:
      properties:
        items:
          items:
            $ref: '#/components/schemas/Customer'
          type: array
          title: Items
          description: List of customer objects
        total:
          type: integer
          title: Total
          description: Total number of customers in the system
        limit:
          type: integer
          title: Limit
          description: Maximum number of customers returned in this response
        offset:
          type: integer
          title: Offset
          description: Number of customers skipped in this response
      type: object
      required:
        - items
        - total
        - limit
        - offset
      title: CustomerListResponse
      description: A page of customers.
    CustomerProduct:
      properties:
        customer_number:
          type: string
          title: Customer Number
          description: Customer identifier referencing the customers table
        customer_item_number:
          type: string
          title: Customer Item Number
          description: Customer's own item number for this product
        item_number:
          type: string
          title: Item Number
          description: Supplier product item number
        language_code:
          type: string
          title: Language Code
          description: Product language code
        created_at:
          type: string
          format: date-time
          title: Created At
          description: When this mapping was created in AIOTIC
      type: object
      required:
        - customer_number
        - customer_item_number
        - item_number
        - language_code
        - created_at
      title: CustomerProduct
      description: A mapping from a customer's own article code to one of your articles.
    CustomerProductListResponse:
      properties:
        items:
          items:
            $ref: '#/components/schemas/CustomerProduct'
          type: array
          title: Items
        total:
          type: integer
          title: Total
        limit:
          type: integer
          title: Limit
        offset:
          type: integer
          title: Offset
      type: object
      required:
        - items
        - total
        - limit
        - offset
      title: CustomerProductListResponse
      description: A page of customer item mappings.
    CustomerProductUpsert:
      properties:
        item_number:
          type: string
          title: Item Number
          description: Supplier product item number
        language_code:
          type: string
          title: Language Code
          description: Product language code
        created_at:
          type: string
          format: date-time
          title: Created At
          description: When this mapping was created in AIOTIC
      type: object
      required:
        - item_number
        - language_code
      title: CustomerProductUpsert
      description: Fields accepted when creating or updating a customer item mapping.
    CustomerSearchResponse:
      properties:
        items:
          items:
            $ref: '#/components/schemas/Customer'
          type: array
          title: Items
          description: List of customer objects with similarity scores
        total:
          type: integer
          title: Total
          description: Total number of customers in the system
        limit:
          type: integer
          title: Limit
          description: Maximum number of customers returned in this response
      type: object
      required:
        - items
        - total
        - limit
      title: CustomerSearchResponse
      description: Customers matching a search, best match first.
    CustomerUpsert:
      properties:
        id:
          anyOf:
            - type: string
              format: uuid
            - type: 'null'
          title: Id
          description: Unique customer identifier (UUID, auto-generated if not provided)
        name:
          anyOf:
            - type: string
            - type: 'null'
          title: Name
          description: Company name
        postal_code:
          anyOf:
            - type: string
            - type: 'null'
          title: Postal Code
          description: Postal code
        city:
          anyOf:
            - type: string
            - type: 'null'
          title: City
          description: City
        address:
          anyOf:
            - type: string
            - type: 'null'
          title: Address
          description: Address
        contact_person:
          anyOf:
            - type: string
            - type: 'null'
          title: Contact Person
          description: Contact person
        phone_number:
          anyOf:
            - type: string
            - type: 'null'
          title: Phone Number
          description: Phone number
        vat_number:
          anyOf:
            - type: string
            - type: 'null'
          title: Vat Number
          description: BTW (VAT) number
        email:
          anyOf:
            - type: string
            - type: 'null'
          title: Email
          description: Customer email
        coc_number:
          anyOf:
            - type: string
            - type: 'null'
          title: Coc Number
          description: Chamber of Commerce number
        home_page:
          anyOf:
            - type: string
            - type: 'null'
          title: Home Page
          description: Company website URL
        similarity:
          anyOf:
            - type: number
            - type: 'null'
          title: Similarity
          description: Similarity score
      type: object
      title: CustomerUpsert
      description: >-
        Fields accepted when creating or updating a customer. All optional; the more you fill, the better
        identification works.
    EmailClassificationResponse:
      properties:
        category:
          type: string
          enum:
            - purchase_order
            - order_confirmation
            - invoice
            - delivery_note
            - quotation
            - receipt
            - credit_note
            - unknown
          title: Category
          description: The category of the email
      type: object
      required:
        - category
      title: EmailClassificationResponse
      description: The category an uploaded e-mail would be classified as.
    ErpAddress:
      type: object
      description: Postal address in the ERP hand-off payload (all keys present, values may be `null`).
      properties:
        street:
          type:
            - string
            - 'null'
          description: Street and house number.
        postal_code:
          type:
            - string
            - 'null'
          description: Postal code as printed or as stored in your master record.
        city:
          type:
            - string
            - 'null'
          description: City.
        country:
          type:
            - string
            - 'null'
          description: Country code as printed, usually ISO 3166-1 alpha-2.
      required:
        - street
        - postal_code
        - city
        - country
    ErpCustomer:
      type: object
      properties:
        customer_id:
          type:
            - string
            - 'null'
          description: >-
            Your customer (debtor) number as synced via `PUT /customer/{number}`. `null` only when an operator
            explicitly sent an unidentified order.
        company:
          type:
            - string
            - 'null'
          description: Customer company name.
        contact_person:
          type:
            - string
            - 'null'
          description: >-
            Person who placed this order, as printed on the document (document-authoritative, not the master
            record).
        email:
          type:
            - string
            - 'null'
          description: Customer e-mail address.
        phone:
          type:
            - string
            - 'null'
          description: Customer phone number.
        iban:
          type:
            - string
            - 'null'
          description: Bank account when printed on the order; usually `null`.
        bic:
          type:
            - string
            - 'null'
          description: Bank identifier when printed on the order; usually `null`.
        vat_id:
          type:
            - string
            - 'null'
          description: VAT registration number.
        address:
          $ref: '#/components/schemas/ErpAddress'
          description: Customer (bill-to) postal address.
      required:
        - customer_id
        - company
        - contact_person
        - email
        - phone
        - iban
        - bic
        - vat_id
        - address
    ErpOrderItem:
      type: object
      description: One order line in the ERP hand-off payload (a fixed subset of `OrderItem`).
      properties:
        article_number:
          type:
            - string
            - 'null'
          description: >-
            Your article number (SKU) after resolution through your catalog and customer item mappings. `null`
            only on an explicit override send of an unresolved line.
        description:
          type:
            - string
            - 'null'
          description: Line description as printed by the customer.
        quantity:
          type:
            - integer
            - 'null'
          description: >-
            Ordered quantity in whole units. `null` only when the quantity was unreadable and an operator
            overrode the review.
        unit:
          type:
            - string
            - 'null'
          description: Unit of measure as printed (`ST`, `PCS`, `Stk`, `m`, `KG`, …) — map it to your ERP's codes.
        price:
          type:
            - number
            - 'null'
          description: Unit price as stated by the customer, or `null`.
        currency:
          type:
            - string
            - 'null'
          description: Line currency, normally equal to the header currency.
        line_total:
          type:
            - number
            - 'null'
          description: Line amount as printed (quantity × price), or `null`.
      required:
        - article_number
        - description
        - quantity
        - unit
        - price
        - currency
        - line_total
    ErpPurchaseOrder:
      type: object
      description: >-
        The purchase order as delivered to your ERP receive endpoint — the reviewed values with operator
        corrections applied. Keys are always present (`null` when unknown).
      properties:
        order_number:
          type: string
          description: >-
            The customer's purchase-order number as printed, with `/` replaced by `-`. Map it to the "your
            reference" / external document number of the sales order.
        order_date:
          type:
            - string
            - 'null'
          description: Order date as an ISO `YYYY-MM-DD` string where the document allowed it; otherwise as printed.
        delivery_date:
          type:
            - string
            - 'null'
          description: >-
            Requested delivery date (`YYYY-MM-DD`) or `null`. When the document stated a window it is already
            collapsed to a single date.
        currency:
          type:
            - string
            - 'null'
          description: Currency code as printed on the document (e.g. `EUR`) or `null`.
        total_price:
          type:
            - number
            - 'null'
          description: >-
            The document's printed total, which may include VAT, or the recalculated net sum when e-mail
            instructions changed lines. Informational — let your ERP compute totals.
        additional_information:
          type:
            - string
            - 'null'
          description: >-
            Free text from the document and e-mail plus notes AIOTIC appends (e.g. the original printed total
            after a recalculation).
        supplier:
          oneOf:
            - $ref: '#/components/schemas/Supplier'
            - type: 'null'
          description: >-
            Your own company as configured for the tenant — identical on every order, never read from the
            document. Safe to ignore.
        customer:
          $ref: '#/components/schemas/ErpCustomer'
          description: >-
            The identified customer (debtor). `customer_id` is your customer number; the address block is
            canonicalised from your master record on a confident match.
        shipping_details:
          $ref: '#/components/schemas/ErpShippingDetails'
          description: >-
            Ship-to recipient and delivery instructions. Filled from the customer block when the document has
            no explicit delivery address.
        items:
          type: array
          items:
            $ref: '#/components/schemas/ErpOrderItem'
          description: >-
            One entry per order line, in document order. Lines without an ordered quantity (assortment
            listings) are already dropped.
      required:
        - order_number
        - order_date
        - delivery_date
        - currency
        - total_price
        - additional_information
        - supplier
        - customer
        - shipping_details
        - items
    ErpReceiveRequest:
      type: object
      description: Body AIOTIC POSTs to your ERP receive endpoint.
      properties:
        request_id:
          type: string
          format: uuid
          description: >-
            AIOTIC's order id (UUID). Stable across retries of the same send — use it as the idempotency key
            and store it on your sales order as external reference.
        purchase_order:
          $ref: '#/components/schemas/ErpPurchaseOrder'
          description: >-
            The reviewed purchase order with operator corrections applied. Every key below is always present;
            unknown values are `null`.
      required:
        - request_id
        - purchase_order
    ErpReceiveResponse:
      type: object
      description: |
        Body your ERP receive endpoint must return. **`success` is authoritative** — AIOTIC ignores the HTTP
        status for the business outcome (a `500` with `success: false` and a `success: true` with `200`
        are both handled correctly; a non-JSON body is treated as a server error).
      properties:
        success:
          type: boolean
        order_number:
          type: string
          description: Your ERP's reference for the created order; stored as `erp_ref` and shown to operators
        error:
          type: string
          description: Human-readable reason when `success` is `false`; shown to operators
      required:
        - success
    ErpRecipient:
      type: object
      description: Ship-to block in the ERP hand-off payload.
      properties:
        company:
          type:
            - string
            - 'null'
          description: Ship-to company name.
        department:
          type:
            - string
            - 'null'
          description: Department or address line 2.
        contact_person:
          type:
            - string
            - 'null'
          description: Ship-to contact for the carrier.
        email:
          type:
            - string
            - 'null'
          description: Ship-to e-mail.
        phone:
          type:
            - string
            - 'null'
          description: Ship-to phone.
        address:
          $ref: '#/components/schemas/ErpAddress'
          description: Delivery address.
      required:
        - company
        - department
        - contact_person
        - email
        - phone
        - address
    ErpSendResponse:
      type: object
      properties:
        success:
          type: boolean
        request_id:
          type: string
          format: uuid
        data:
          description: The JSON body your ERP receive endpoint returned, verbatim.
          $ref: '#/components/schemas/ErpReceiveResponse'
      required:
        - success
        - request_id
        - data
    ErpShippingDetails:
      type: object
      description: Shipping section of the ERP hand-off payload.
      properties:
        recipient:
          $ref: '#/components/schemas/ErpRecipient'
          description: The ship-to block.
        special_instructions:
          type:
            - string
            - 'null'
          description: Free-text delivery instructions ("deliver before noon", dock number).
      required:
        - recipient
        - special_instructions
    ErrorResponse:
      type: object
      description: >-
        Standard error body. `detail` is usually a string; a few endpoints return a structured object
        (documented per endpoint).
      properties:
        detail:
          oneOf:
            - type: string
            - type: object
              additionalProperties: true
      required:
        - detail
    FetchAllEmailsResponse:
      properties:
        status:
          type: string
          title: Status
        emails_queued:
          type: integer
          title: Emails Queued
        emails_total:
          type: integer
          title: Emails Total
        message:
          type: string
          title: Message
      type: object
      required:
        - status
        - emails_queued
        - emails_total
        - message
      title: FetchAllEmailsResponse
      description: Result of a manual mailbox fetch.
    HTTPValidationError:
      properties:
        detail:
          items:
            $ref: '#/components/schemas/ValidationError'
          type: array
          title: Detail
      type: object
      title: HTTPValidationError
      description: Request validation error (malformed parameters).
    OrderCustomer:
      type: object
      description: >-
        The identified customer (debtor). `customer_id` is your customer number and is set only when AIOTIC
        resolved the customer with confidence.
      properties:
        customer_id:
          type:
            - string
            - 'null'
        company:
          type: string
        contact_person:
          type:
            - string
            - 'null'
        email:
          type:
            - string
            - 'null'
        phone:
          type:
            - string
            - 'null'
        branch:
          type:
            - string
            - 'null'
          description: Issuing branch / location named on the document, when any
        vat_id:
          type:
            - string
            - 'null'
        iban:
          type:
            - string
            - 'null'
        bic:
          type:
            - string
            - 'null'
        address:
          $ref: '#/components/schemas/Address'
      required:
        - company
        - address
    OrderGroup:
      type: object
      properties:
        email_group_id:
          type: string
          format: uuid
        message_id:
          type:
            - string
            - 'null'
        order_count:
          type: integer
        orders:
          type: array
          items:
            $ref: '#/components/schemas/OrderStatus'
      required:
        - email_group_id
        - order_count
        - orders
    OrderItem:
      type: object
      description: One order line as stored on the order (`result.items[]`).
      properties:
        article_number:
          type:
            - string
            - 'null'
          description: >-
            Your (supplier-side) article number, resolved through the catalog and customer mappings where
            possible
        customer_item_number:
          type:
            - string
            - 'null'
          description: The customer's own article number as printed, when present
        description:
          type:
            - string
            - 'null'
        quantity:
          type:
            - integer
            - 'null'
          description: Ordered quantity in whole units; `null` when unreadable (see `quantity_state`)
        quantity_state:
          type:
            - string
            - 'null'
          enum:
            - Valid
            - Empty
            - Zero
            - Unrecognised
            - null
          description: >-
            What the quantity column literally showed. `Unrecognised` rows keep an empty quantity and put the
            order in ATTENTION.
        unit:
          type:
            - string
            - 'null'
          description: Unit of measure as printed (e.g. `ST`, `PCS`, `KG`)
        price:
          type:
            - number
            - 'null'
          description: Unit price
        currency:
          type:
            - string
            - 'null'
        line_total:
          type:
            - number
            - 'null'
    OrderListResponse:
      properties:
        items:
          items:
            $ref: '#/components/schemas/OrderStatus'
          type: array
          title: Items
          description: List of order status objects
        total:
          type: integer
          title: Total
          description: Total number of orders in the system
        limit:
          type: integer
          title: Limit
          description: Maximum number of orders returned in this response
        offset:
          type: integer
          title: Offset
          description: Number of orders skipped in this response
      type: object
      required:
        - items
        - total
        - limit
        - offset
      title: OrderListResponse
      description: A page of orders, newest first.
    OrderRef:
      properties:
        request_id:
          type: string
          format: uuid
          title: Request Id
          description: Unique identifier for the child order
        status:
          $ref: '#/components/schemas/OrderStatusValue'
        order_label:
          anyOf:
            - type: string
            - type: 'null'
          title: Order Label
          description: Splitter-assigned label for the order
      type: object
      required:
        - request_id
        - status
      title: OrderRef
      description: Reference to one child order of a split e-mail.
    OrderRetryBody:
      properties:
        hil_prompt:
          anyOf:
            - type: string
            - type: 'null'
          title: Hil Prompt
      type: object
      title: Body_retry_order_processing_order_retry__request_id__post
    OrderStatus:
      properties:
        request_id:
          type: string
          format: uuid
          title: Request Id
          description: Unique identifier for the request
        attachments:
          additionalProperties: true
          type: object
          title: Attachments
          description: File names of the order with size and MIME type.
        metadata:
          additionalProperties: true
          type: object
          title: Metadata
          description: Extra form fields supplied at upload, plus `source`.
        timestamp:
          type: string
          format: date-time
          title: Timestamp
          description: When this status was recorded
        status:
          $ref: '#/components/schemas/OrderStatusValue'
        result:
          description: The extracted purchase order once processing finished; `null` before that and for failed orders.
          oneOf:
            - $ref: '#/components/schemas/PurchaseOrder'
            - type: 'null'
        state:
          anyOf:
            - additionalProperties: true
              type: object
            - type: 'null'
          title: State
          description: Free-form processing details used by the AIOTIC app (validation notes, flags). Informational.
        erp_ref:
          anyOf:
            - type: string
            - type: 'null'
          title: Erp Ref
          description: ERP-assigned order reference returned on successful send
        email_group_id:
          anyOf:
            - type: string
              format: uuid
            - type: 'null'
          title: Email Group Id
          description: Correlates orders from the same source email
        order_label:
          anyOf:
            - type: string
            - type: 'null'
          title: Order Label
          description: Human-readable label, e.g. 'PO-12345' or 'Order 1 of 2'
        retry_count:
          type: integer
          title: Retry Count
          description: Number of retry attempts for transient failures
          default: 0
        next_retry_at:
          anyOf:
            - type: string
              format: date-time
            - type: 'null'
          title: Next Retry At
          description: When to attempt next retry
        last_error:
          anyOf:
            - type: string
            - type: 'null'
          title: Last Error
          description: Last error message from transient failure
      type: object
      required:
        - request_id
        - timestamp
        - status
      title: OrderStatus
      description: The processing status of one order, with the extracted purchase order once available.
    OrderStatusValue:
      type: string
      description: Order lifecycle status.
      enum:
        - QUEUED
        - PROCESSING
        - ATTENTION
        - PROCESSED
        - FAILED
        - RETRY_PENDING
        - MODIFIED
        - REPROCESSED
        - SENDING
        - SENT
        - CANCELED
    OrderUploadBody:
      properties:
        files:
          items:
            type: string
            contentMediaType: application/octet-stream
          type: array
          title: Files
        request_id:
          anyOf:
            - type: string
            - type: 'null'
          title: Request Id
      type: object
      required:
        - files
      title: Body_upload_order_order_upload_post
    OrderUploadResponse:
      properties:
        request_id:
          type: string
          format: uuid
          title: Request Id
          description: Unique identifier for the uploaded document
        split:
          type: boolean
          title: Split
          description: True when the upload was split into multiple orders; poll orders[] rather than request_id
          default: false
        email_group_id:
          anyOf:
            - type: string
              format: uuid
            - type: 'null'
          title: Email Group Id
          description: Correlation id for child orders when the email was split into multiple orders
        orders:
          anyOf:
            - items:
                $ref: '#/components/schemas/OrderRef'
              type: array
            - type: 'null'
          title: Orders
          description: Child order references when the email was split into multiple orders
      type: object
      required:
        - request_id
      title: OrderUploadResponse
      description: Result of an upload. When `split` is true, poll the child `orders[]` instead of `request_id`.
    ProcessingWebhookRequest:
      type: object
      description: Body of the optional processing webhook, sent when extraction completes (before any human review).
      properties:
        request_id:
          type: string
          format: uuid
        purchase_order:
          $ref: '#/components/schemas/PurchaseOrder'
      required:
        - request_id
        - purchase_order
    Product:
      properties:
        item_number:
          type: string
          title: Item Number
          description: Unique identifier for the product
        language_code:
          type: string
          title: Language Code
          description: Language code for the product description
        description:
          anyOf:
            - type: string
            - type: 'null'
          title: Description
          description: Product description
        remark:
          anyOf:
            - type: string
            - type: 'null'
          title: Remark
          description: Additional remarks about the product
        created_at:
          type: string
          format: date-time
          title: Created At
          description: When this product was created in AIOTIC
      type: object
      required:
        - item_number
        - language_code
        - created_at
      title: Product
      description: An article record (one per article number and language).
    ProductListResponse:
      properties:
        items:
          items:
            $ref: '#/components/schemas/Product'
          type: array
          title: Items
        total:
          type: integer
          title: Total
        limit:
          type: integer
          title: Limit
        offset:
          type: integer
          title: Offset
      type: object
      required:
        - items
        - total
        - limit
        - offset
      title: ProductListResponse
      description: A page of products.
    ProductUpsert:
      properties:
        description:
          type: string
          title: Description
          description: Product description
        remark:
          anyOf:
            - type: string
            - type: 'null'
          title: Remark
          description: Additional remarks about the product
        created_at:
          type: string
          format: date-time
          title: Created At
          description: When this product was created in AIOTIC
      type: object
      required:
        - description
      title: ProductUpsert
      description: Fields accepted when creating or updating a product.
    PurchaseOrder:
      type: object
      description: >-
        The extracted purchase order as returned in `OrderStatus.result`. Every field is always present;
        optional fields are `null`, never omitted.
      properties:
        order_number:
          type: string
          description: Customer's PO number. `/` is replaced by `-`.
        order_date:
          type: string
          description: ISO 8601 date (`YYYY-MM-DD`) where the document allowed it
        delivery_date:
          type:
            - string
            - 'null'
          description: Single requested delivery date (resolved from a window per tenant preference)
        delivery_date_from:
          type:
            - string
            - 'null'
          description: Lower bound when the document states a delivery window
        delivery_date_to:
          type:
            - string
            - 'null'
          description: Upper bound when the document states a delivery window
        supplier:
          $ref: '#/components/schemas/Supplier'
        customer:
          oneOf:
            - $ref: '#/components/schemas/OrderCustomer'
            - type: 'null'
        shipping_details:
          oneOf:
            - $ref: '#/components/schemas/ShippingDetails'
            - type: 'null'
        items:
          type: array
          items:
            $ref: '#/components/schemas/OrderItem'
        total_price:
          type:
            - number
            - 'null'
        currency:
          type:
            - string
            - 'null'
        additional_information:
          type:
            - string
            - 'null'
          description: >-
            Free text from the document / e-mail, plus audit notes AIOTIC appends (e.g. original document
            total)
      required:
        - order_number
        - order_date
        - supplier
        - items
    RawEmailClassifyBody:
      properties:
        file:
          type: string
          contentMediaType: application/octet-stream
          title: File
      type: object
      required:
        - file
      title: Body_classify_raw_email_order_raw_classify_post
    RawEmailUploadBody:
      properties:
        file:
          type: string
          contentMediaType: application/octet-stream
          title: File
        request_id:
          anyOf:
            - type: string
            - type: 'null'
          title: Request Id
      type: object
      required:
        - file
      title: Body_upload_raw_email_order_raw_upload_post
    RawUploadRejection:
      type: object
      properties:
        detail:
          oneOf:
            - type: string
            - type: object
              properties:
                error:
                  type: string
                  description: Stable machine-readable code, e.g. `not_a_purchase_order`
                message:
                  type: string
                request_id:
                  type: string
                  format: uuid
                category:
                  type: string
                  description: Classifier category (`quotation`, `invoice`, `unknown`, …)
                subject:
                  type: string
                language:
                  type:
                    - string
                    - 'null'
                line_item_count:
                  type:
                    - integer
                    - 'null'
              required:
                - error
                - message
      required:
        - detail
    RejectedEmailListResponse:
      properties:
        items:
          items:
            $ref: '#/components/schemas/ClassifiedEmail'
          type: array
          title: Items
          description: Rejected emails on this page
        total:
          type: integer
          title: Total
          description: Total rejected emails matching the rejection_status filter
        limit:
          type: integer
          title: Limit
          description: Page size
        offset:
          type: integer
          title: Offset
          description: Pagination offset
      type: object
      required:
        - total
        - limit
        - offset
      title: RejectedEmailListResponse
      description: A page of rejected e-mails.
    ReprocessResponse:
      properties:
        request_id:
          type: string
          format: uuid
          title: Request Id
          description: The request being reprocessed (reuses the original id)
        status:
          type: string
          title: Status
          description: Outcome, e.g. 'reprocessing'
      type: object
      required:
        - request_id
        - status
      title: ReprocessResponse
      description: Result of forcing a rejected e-mail through order processing.
    ShippingDetails:
      type: object
      properties:
        recipient:
          $ref: '#/components/schemas/ShippingRecipient'
          description: The ship-to block.
        special_instructions:
          type:
            - string
            - 'null'
          description: Free-text delivery instructions.
      required:
        - recipient
    ShippingRecipient:
      type: object
      properties:
        company:
          type: string
          description: Ship-to company name.
        contact_person:
          type:
            - string
            - 'null'
          description: Ship-to contact for the carrier.
        department:
          type:
            - string
            - 'null'
          description: Department or address line 2.
        email:
          type:
            - string
            - 'null'
          description: Ship-to e-mail.
        phone:
          type:
            - string
            - 'null'
          description: Ship-to phone.
        address:
          $ref: '#/components/schemas/Address'
          description: Delivery address.
      required:
        - company
        - address
    Supplier:
      type: object
      description: The receiving company — your company. Pinned from tenant configuration, not extracted per document.
      properties:
        company:
          type: string
          description: Your company name as configured for the tenant.
        contact_person:
          type:
            - string
            - 'null'
          description: Contact person configured for the tenant, if any.
        email:
          type:
            - string
            - 'null'
          description: Order intake e-mail address of the tenant.
        address:
          $ref: '#/components/schemas/Address'
          description: Your company address as configured.
      required:
        - company
        - address
    SystemStatus:
      type: object
      properties:
        status:
          type: string
          enum:
            - operational
            - degraded
            - unknown
        message:
          type:
            - string
            - 'null'
        updated_at:
          type:
            - string
            - 'null'
          format: date-time
      required:
        - status
    ValidationError:
      properties:
        loc:
          items:
            anyOf:
              - type: string
              - type: integer
          type: array
          title: Location
        msg:
          type: string
          title: Message
        type:
          type: string
          title: Error Type
        input:
          title: Input
        ctx:
          type: object
          title: Context
      type: object
      required:
        - loc
        - msg
        - type
      title: ValidationError
      description: One request validation problem.
servers:
  - url: https://{tenant}.aiotic.ai
    description: Your tenant's API base URL (provided during onboarding)
    variables:
      tenant:
        default: customer
        description: Tenant host label, e.g. `acme` → `https://acme.aiotic.ai`
  - url: http://localhost:8080
    description: Mock AIOTIC server from the integrator guide (`mock/`)
security:
  - ApiKeyAuth: []
tags:
  - name: health
    x-displayName: Health & status
    description: Unauthenticated liveness and platform status.
  - name: orders
    x-displayName: Orders — intake
    description: Submit documents or raw e-mails for extraction.
  - name: order-status
    x-displayName: Orders — status & files
    description: Poll processing status, read the extracted purchase order and download artifacts.
  - name: erp
    x-displayName: ERP hand-off
    description: Trigger the send of a reviewed order to your ERP receive endpoint.
  - name: rejected
    x-displayName: Rejected e-mails
    description: E-mails the classifier did not treat as purchase orders, and how to override that.
  - name: customers
    x-displayName: Master data — customers
    description: Your debtors. Used to identify who sent an order.
  - name: products
    x-displayName: Master data — products
    description: Your article catalog. Used to validate article numbers.
  - name: customer-products
    x-displayName: Master data — customer item mappings
    description: A customer's own article numbers mapped to your article numbers.
  - name: email-watcher
    x-displayName: Mailbox
    description: Operational trigger for the tenant mailbox watcher.
webhooks:
  erpReceiveOrder:
    post:
      tags:
        - erp
      operationId: erpReceiveOrder
      summary: ERP receive endpoint — implemented by you
      description: |
        AIOTIC calls this URL (configured per tenant as the *ERP API URL*) when an operator — or an
        API caller via `POST /erp/send/{request_id}` — sends a reviewed order to the ERP.

        * Method `POST`, `Content-Type: application/json`, header `X-API-KEY: <key you gave us>`.
        * Respond within the tenant's timeout (default 30 s) with a JSON body carrying `success`.
        * `request_id` is stable across retries: make the handler idempotent (return the same
          `order_number` if you already created the order for this id).
        * Return `success: false` with a clear `error` for business rejections (unknown article,
          blocked customer, …); the text is shown to the operator in AIOTIC.
      parameters:
        - name: X-API-KEY
          in: header
          required: true
          schema:
            type: string
          description: The key you provided during onboarding
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/ErpReceiveRequest'
            example:
              request_id: 550e8400-e29b-41d4-a716-446655440000
              purchase_order:
                order_number: EB2500011645
                order_date: '2026-01-14'
                delivery_date: '2026-02-01'
                currency: EUR
                total_price: 1234.56
                additional_information: Please deliver before noon.
                supplier:
                  company: Acme Supplies BV
                  contact_person: P. Jansen
                  email: orders@acme.example
                  address:
                    street: Industrieweg 5
                    postal_code: 1234 AB
                    city: Amsterdam
                    country: NL
                customer:
                  customer_id: '58931'
                  company: LUMITECH INSTALLATIES
                  contact_person: J. de Boer
                  email: info@lumitech.example
                  phone: +31 55 123 4567
                  vat_id: NL001234567B01
                  iban: null
                  bic: null
                  address:
                    street: Ambachtsweg 12
                    postal_code: 7327 AA
                    city: Apeldoorn
                    country: NL
                shipping_details:
                  recipient:
                    company: LUMITECH INSTALLATIES
                    department: null
                    contact_person: J. de Boer
                    email: null
                    phone: null
                    address:
                      street: Ambachtsweg 12
                      postal_code: 7327 AA
                      city: Apeldoorn
                      country: NL
                  special_instructions: null
                items:
                  - article_number: PROD-001
                    description: LED Driver 48V
                    quantity: 10
                    unit: ST
                    price: 12.34
                    currency: EUR
                    line_total: 123.4
      responses:
        '200':
          description: Outcome (any HTTP status is accepted; the body decides)
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErpReceiveResponse'
              examples:
                accepted:
                  summary: Accepted
                  value:
                    success: true
                    order_number: SO-2026-00981
                rejected:
                  summary: Business rejection
                  value:
                    success: false
                    error: 'Unknown article number: PROD-999'
  processingCompleted:
    post:
      tags:
        - orders
      operationId: processingCompleted
      summary: Processing webhook (optional) — implemented by you
      description: |
        If the tenant has the *processing webhook* enabled, AIOTIC POSTs the extracted order to this
        URL as soon as extraction finishes — **before** any operator review, and regardless of whether
        the order landed in `PROCESSED` or `ATTENTION`.

        Use it as an early signal ("an order arrived, here is the AI's reading"), not as the hand-off:
        the reviewed, corrected version reaches you through the ERP receive endpoint.

        * Header `X-API-KEY: <configured value>`; respond `2xx` to acknowledge.
        * Delivery is best-effort: no retries and no signature today (see the proposal appendix in the guide).
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/ProcessingWebhookRequest'
      responses:
        '200':
          description: Acknowledged
