openapi: 3.1.0

info:
  title: Zestt API
  version: 2.0.0-draft.1
  summary: Zestt (Zester) API v2 — suppliers, buyers, accounting and BI integrations.
  description: >-
    Source of truth for the Zestt API v2. Draft generated from the product spec
    (docs/spec/api-v2-spec.md, 11/09/2026). Every path carries `x-phase` (1/2/3),
    `x-scopes` (required scopes) and, where relevant, `x-plan` (minimum plan).
    Conventions: JSON only, snake_case, string enums, money as decimal strings with
    2 fraction digits, dates as RFC 3339 with offset, cursor pagination, RFC 9457
    problem+json errors, `Idempotency-Key` on POST.
  termsOfService: https://developers.zester.co.il/terms
  contact:
    name: Zestt Developers
    url: https://developers.zester.co.il
  x-status: draft
  x-spec-source: docs/spec/api-v2-spec.md

servers:
  - url: https://api.zester.co.il/v2
    description: Production (keys prefixed zk_live_)
  - url: https://sandbox-api.zester.co.il/v2
    description: Sandbox (keys prefixed zk_test_; demo buyer + demo supplier, resettable)

security:
  - apiKey: []
  - oauth2: []

tags:
  - name: Auth
    description: Token exchange (optional; direct API keys are also accepted).
  - name: Account
    description: Who am I, what can I do.
  - name: Supplier – Buyers
    description: The buyers linked to the authenticated supplier.
  - name: Supplier – Orders
    description: Orders placed by buyers to the authenticated supplier.
  - name: Supplier – Documents
    description: Delivery notes, invoices and credit notes issued by the supplier.
  - name: Supplier – Catalog
    description: Products, availability and price lists.
  - name: Buyer – Reference
    description: Branches, purchase types, suppliers, catalog, expense categories and accounts.
  - name: Buyer – Purchases
    description: Orders and purchase documents across all suppliers.
  - name: Buyer – Expenses
    description: Expense documents (no products) and card transactions.
  - name: Buyer – Accounting
    description: ZesttBox journal lines and export marking.
  - name: Buyer – Sales
    description: POS Z-reports and item sales.
  - name: Buyer – Inventory
    description: Inventory counts.
  - name: Reports
    description: Computed reports (food cost, purchases, expenses, P&L). For BI use the fact endpoints instead.
  - name: Events
    description: Pull-based change feed. Same catalog as webhooks.
  - name: Webhooks
    description: Signed push notifications.
  - name: Files
    description: Upload attachments; referenced by file_id.
  - name: Exports
    description: Asynchronous bulk export (Enterprise).
  - name: Audit
    description: Request log access (Enterprise).

x-tagGroups:
  - name: Getting started
    tags: [Auth, Account]
  - name: Supplier API
    tags: [Supplier – Buyers, Supplier – Orders, Supplier – Documents, Supplier – Catalog]
  - name: Buyer API
    tags: [Buyer – Reference, Buyer – Purchases, Buyer – Expenses, Buyer – Accounting, Buyer – Sales, Buyer – Inventory, Reports]
  - name: Platform
    tags: [Events, Webhooks, Files, Exports, Audit]

paths:

  # ---------------------------------------------------------------------------
  # Auth & account
  # ---------------------------------------------------------------------------

  /oauth/token:
    post:
      tags: [Auth]
      operationId: createAccessToken
      summary: Exchange an API key for a short-lived access token
      description: >-
        OAuth 2.0 client_credentials grant. `client_id` is the ApiClient id, `client_secret`
        is the API key (zk_live_… / zk_test_…). Returns a JWT valid for 60 minutes carrying the
        client's scopes. Optional — the API key itself is accepted directly as a Bearer token.
        Accepts both `application/x-www-form-urlencoded` (RFC 6749) and `application/json`.
      security: []
      x-phase: 1
      requestBody:
        required: true
        content:
          application/x-www-form-urlencoded:
            schema:
              $ref: '#/components/schemas/TokenRequest'
          application/json:
            schema:
              $ref: '#/components/schemas/TokenRequest'
      responses:
        '200':
          description: Access token issued.
          headers:
            X-Request-Id:
              $ref: '#/components/headers/X-Request-Id'
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/TokenResponse'
        '400':
          $ref: '#/components/responses/BadRequest'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/Forbidden'
        '429':
          $ref: '#/components/responses/RateLimited'

  /me:
    get:
      tags: [Account]
      operationId: getMe
      summary: Describe the authenticated client
      description: >-
        Account, account type, plan, effective scopes, branch restriction, rate limits and
        enabled features. The first call every integration should make; support asks for its output.
      x-phase: 1
      x-scopes: []
      responses:
        '200':
          description: Client context.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Me'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '429':
          $ref: '#/components/responses/RateLimited'

  # ---------------------------------------------------------------------------
  # Supplier – Buyers
  # ---------------------------------------------------------------------------

  /supplier/buyers:
    get:
      tags: [Supplier – Buyers]
      operationId: listSupplierBuyers
      summary: List buyers linked to this supplier
      x-phase: 1
      x-scopes: [supplier.buyers:read]
      security:
        - apiKey: []
        - oauth2: [supplier.buyers:read]
      parameters:
        - $ref: '#/components/parameters/cursor'
        - $ref: '#/components/parameters/limit'
        - $ref: '#/components/parameters/updated_since'
      responses:
        '200':
          description: Page of buyers.
          content:
            application/json:
              schema:
                allOf:
                  - $ref: '#/components/schemas/Page'
                  - type: object
                    properties:
                      data:
                        type: array
                        items:
                          $ref: '#/components/schemas/SupplierBuyer'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/Forbidden'
        '429':
          $ref: '#/components/responses/RateLimited'

  /supplier/buyers/{buyer_id}:
    parameters:
      - $ref: '#/components/parameters/buyer_id'
    get:
      tags: [Supplier – Buyers]
      operationId: getSupplierBuyer
      summary: Get one linked buyer
      x-phase: 1
      x-scopes: [supplier.buyers:read]
      security:
        - apiKey: []
        - oauth2: [supplier.buyers:read]
      responses:
        '200':
          description: Buyer.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/SupplierBuyer'
        '404':
          $ref: '#/components/responses/NotFound'
    patch:
      tags: [Supplier – Buyers]
      operationId: updateSupplierBuyer
      summary: Update supplier-side fields of a buyer (customer number, ERP reference)
      x-phase: 1
      x-scopes: [supplier.buyers:write]
      security:
        - apiKey: []
        - oauth2: [supplier.buyers:write]
      parameters:
        - $ref: '#/components/parameters/If-Match'
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              properties:
                customer_number:
                  type: string
                  description: Customer number in the supplier's ERP.
                erp_ref:
                  type: string
                  description: Free reference (e.g. ERP account key).
      responses:
        '200':
          description: Updated buyer.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/SupplierBuyer'
        '404':
          $ref: '#/components/responses/NotFound'
        '412':
          $ref: '#/components/responses/PreconditionFailed'
        '422':
          $ref: '#/components/responses/ValidationError'

  /supplier/buyer-invitations:
    post:
      tags: [Supplier – Buyers]
      operationId: createBuyerInvitation
      summary: Invite a new buyer to Zestt
      description: Replaces v1 "Create New customer". Does not create a user; returns an onboarding link.
      x-phase: 1
      x-scopes: [supplier.buyers:write]
      security:
        - apiKey: []
        - oauth2: [supplier.buyers:write]
      parameters:
        - $ref: '#/components/parameters/Idempotency-Key'
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/BuyerInvitationCreate'
      responses:
        '201':
          description: Invitation created.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/BuyerInvitation'
        '409':
          $ref: '#/components/responses/Conflict'
        '422':
          $ref: '#/components/responses/ValidationError'

  # ---------------------------------------------------------------------------
  # Supplier – Orders
  # ---------------------------------------------------------------------------

  /supplier/orders:
    get:
      tags: [Supplier – Orders]
      operationId: listSupplierOrders
      summary: List orders sent to this supplier
      description: Default returns all statuses. Use `status` to fetch only `pending_approval`, or `updated_since` for incremental polling.
      x-phase: 1
      x-scopes: [supplier.orders:read]
      security:
        - apiKey: []
        - oauth2: [supplier.orders:read]
      parameters:
        - $ref: '#/components/parameters/cursor'
        - $ref: '#/components/parameters/limit'
        - $ref: '#/components/parameters/updated_since'
        - $ref: '#/components/parameters/from'
        - $ref: '#/components/parameters/to'
        - $ref: '#/components/parameters/order_status'
        - name: buyer_id
          in: query
          schema:
            type: string
        - $ref: '#/components/parameters/expand'
      responses:
        '200':
          description: Page of orders.
          content:
            application/json:
              schema:
                allOf:
                  - $ref: '#/components/schemas/Page'
                  - type: object
                    properties:
                      data:
                        type: array
                        items:
                          $ref: '#/components/schemas/Order'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/Forbidden'
        '429':
          $ref: '#/components/responses/RateLimited'

  /supplier/orders/{order_id}:
    parameters:
      - $ref: '#/components/parameters/order_id'
    get:
      tags: [Supplier – Orders]
      operationId: getSupplierOrder
      summary: Get an order
      x-phase: 1
      x-scopes: [supplier.orders:read]
      security:
        - apiKey: []
        - oauth2: [supplier.orders:read]
      parameters:
        - $ref: '#/components/parameters/expand'
      responses:
        '200':
          description: Order.
          headers:
            ETag:
              $ref: '#/components/headers/ETag'
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Order'
        '404':
          $ref: '#/components/responses/NotFound'

  /supplier/orders/{order_id}/confirm:
    post:
      tags: [Supplier – Orders]
      operationId: confirmSupplierOrder
      summary: Confirm an order (fully or with line changes)
      description: Empty body = full confirmation. Send `lines[]` to confirm partially, substitute items or adjust prices (if the buyer allows).
      x-phase: 1
      x-scopes: [supplier.orders:write]
      security:
        - apiKey: []
        - oauth2: [supplier.orders:write]
      parameters:
        - $ref: '#/components/parameters/order_id'
        - $ref: '#/components/parameters/Idempotency-Key'
      requestBody:
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/OrderConfirmRequest'
      responses:
        '200':
          description: Order after confirmation.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Order'
        '404':
          $ref: '#/components/responses/NotFound'
        '409':
          $ref: '#/components/responses/Conflict'
        '422':
          $ref: '#/components/responses/ValidationError'

  /supplier/orders/{order_id}/reject:
    post:
      tags: [Supplier – Orders]
      operationId: rejectSupplierOrder
      summary: Reject an order
      x-phase: 1
      x-scopes: [supplier.orders:write]
      security:
        - apiKey: []
        - oauth2: [supplier.orders:write]
      parameters:
        - $ref: '#/components/parameters/order_id'
        - $ref: '#/components/parameters/Idempotency-Key'
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [reason]
              properties:
                reason:
                  type: string
                  maxLength: 500
      responses:
        '200':
          description: Order after rejection.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Order'
        '404':
          $ref: '#/components/responses/NotFound'
        '409':
          $ref: '#/components/responses/Conflict'

  /supplier/orders/{order_id}/schedule:
    post:
      tags: [Supplier – Orders]
      operationId: scheduleSupplierOrder
      summary: Set or change the delivery date / window
      x-phase: 1
      x-scopes: [supplier.orders:write]
      security:
        - apiKey: []
        - oauth2: [supplier.orders:write]
      parameters:
        - $ref: '#/components/parameters/order_id'
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [delivery_date]
              properties:
                delivery_date:
                  type: string
                  format: date
                delivery_window:
                  $ref: '#/components/schemas/TimeWindow'
      responses:
        '200':
          description: Order after scheduling.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Order'
        '404':
          $ref: '#/components/responses/NotFound'
        '422':
          $ref: '#/components/responses/ValidationError'

  /supplier/orders/{order_id}/acknowledge:
    post:
      tags: [Supplier – Orders]
      operationId: acknowledgeSupplierOrder
      summary: Mark the order as received in the supplier's ERP
      description: Replaces v1 "Export Status". `external_ref` (ERP sales-order number) is shown to the buyer.
      x-phase: 1
      x-scopes: [supplier.orders:write]
      security:
        - apiKey: []
        - oauth2: [supplier.orders:write]
      parameters:
        - $ref: '#/components/parameters/order_id'
      requestBody:
        content:
          application/json:
            schema:
              type: object
              properties:
                external_ref:
                  type: string
                  maxLength: 64
      responses:
        '200':
          description: Order after acknowledgement.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Order'
        '404':
          $ref: '#/components/responses/NotFound'

  # ---------------------------------------------------------------------------
  # Supplier – Documents
  # ---------------------------------------------------------------------------

  /supplier/documents:
    get:
      tags: [Supplier – Documents]
      operationId: listSupplierDocuments
      summary: List documents issued by this supplier, with the buyer's receiving status
      x-phase: 1
      x-scopes: [supplier.documents:read]
      security:
        - apiKey: []
        - oauth2: [supplier.documents:read]
      parameters:
        - $ref: '#/components/parameters/cursor'
        - $ref: '#/components/parameters/limit'
        - $ref: '#/components/parameters/updated_since'
        - $ref: '#/components/parameters/from'
        - $ref: '#/components/parameters/to'
        - $ref: '#/components/parameters/document_type'
        - name: buyer_status
          in: query
          schema:
            $ref: '#/components/schemas/DocumentBuyerStatus'
        - $ref: '#/components/parameters/expand'
      responses:
        '200':
          description: Page of documents.
          content:
            application/json:
              schema:
                allOf:
                  - $ref: '#/components/schemas/Page'
                  - type: object
                    properties:
                      data:
                        type: array
                        items:
                          $ref: '#/components/schemas/Document'
    post:
      tags: [Supplier – Documents]
      operationId: createSupplierDocument
      summary: Create a delivery note, invoice, credit note or consolidated invoice
      description: >-
        Attachments are uploaded first via `POST /files` and referenced by `file_ids[]`.
        `Idempotency-Key` is required — use the document number.
      x-phase: 1
      x-scopes: [supplier.documents:write]
      security:
        - apiKey: []
        - oauth2: [supplier.documents:write]
      parameters:
        - $ref: '#/components/parameters/Idempotency-Key-Required'
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/DocumentCreate'
      responses:
        '201':
          description: Document created.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Document'
        '409':
          $ref: '#/components/responses/Conflict'
        '422':
          $ref: '#/components/responses/ValidationError'

  /supplier/documents/batch:
    post:
      tags: [Supplier – Documents]
      operationId: createSupplierDocumentsBatch
      summary: Create up to 500 documents in one call
      description: Per-item result. Returns 422 only when every item failed.
      x-phase: 1
      x-scopes: [supplier.documents:write]
      security:
        - apiKey: []
        - oauth2: [supplier.documents:write]
      parameters:
        - $ref: '#/components/parameters/Idempotency-Key-Required'
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [items]
              properties:
                items:
                  type: array
                  minItems: 1
                  maxItems: 500
                  items:
                    $ref: '#/components/schemas/DocumentCreate'
      responses:
        '200':
          description: Per-item results (at least one succeeded).
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/BatchResult'
        '422':
          description: Every item failed.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/BatchResult'

  /supplier/documents/{document_id}:
    parameters:
      - $ref: '#/components/parameters/document_id'
    get:
      tags: [Supplier – Documents]
      operationId: getSupplierDocument
      summary: Get a document, including what the buyer actually received
      x-phase: 1
      x-scopes: [supplier.documents:read]
      security:
        - apiKey: []
        - oauth2: [supplier.documents:read]
      parameters:
        - $ref: '#/components/parameters/expand'
      responses:
        '200':
          description: Document.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Document'
        '404':
          $ref: '#/components/responses/NotFound'

  # ---------------------------------------------------------------------------
  # Supplier – Catalog
  # ---------------------------------------------------------------------------

  /supplier/products/{sku}:
    parameters:
      - $ref: '#/components/parameters/sku'
    put:
      tags: [Supplier – Catalog]
      operationId: upsertSupplierProduct
      summary: Create or update a product (upsert by SKU)
      x-phase: 1
      x-scopes: [supplier.catalog:write]
      security:
        - apiKey: []
        - oauth2: [supplier.catalog:write]
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/SupplierProductUpsert'
      responses:
        '200':
          description: Product updated.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/SupplierProduct'
        '201':
          description: Product created.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/SupplierProduct'
        '422':
          $ref: '#/components/responses/ValidationError'
    delete:
      tags: [Supplier – Catalog]
      operationId: deleteSupplierProduct
      summary: Remove a product from the catalog (soft delete)
      x-phase: 1
      x-scopes: [supplier.catalog:write]
      security:
        - apiKey: []
        - oauth2: [supplier.catalog:write]
      responses:
        '204':
          description: Removed.
        '404':
          $ref: '#/components/responses/NotFound'

  /supplier/products/batch:
    post:
      tags: [Supplier – Catalog]
      operationId: upsertSupplierProductsBatch
      summary: Upsert up to 500 products
      x-phase: 1
      x-scopes: [supplier.catalog:write]
      security:
        - apiKey: []
        - oauth2: [supplier.catalog:write]
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [items]
              properties:
                items:
                  type: array
                  minItems: 1
                  maxItems: 500
                  items:
                    allOf:
                      - $ref: '#/components/schemas/SupplierProductUpsert'
                      - type: object
                        required: [sku]
                        properties:
                          sku:
                            type: string
      responses:
        '200':
          description: Per-item results.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/BatchResult'
        '422':
          description: Every item failed.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/BatchResult'

  /supplier/products/{sku}/availability:
    post:
      tags: [Supplier – Catalog]
      operationId: setSupplierProductAvailability
      summary: Set product availability
      x-phase: 1
      x-scopes: [supplier.catalog:write]
      security:
        - apiKey: []
        - oauth2: [supplier.catalog:write]
      parameters:
        - $ref: '#/components/parameters/sku'
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [available]
              properties:
                available:
                  type: boolean
                until:
                  type: string
                  format: date
                  description: When `available=false`, optional date the product is expected back.
                reason:
                  type: string
                  maxLength: 200
      responses:
        '200':
          description: Availability updated.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/SupplierProduct'
        '404':
          $ref: '#/components/responses/NotFound'

  /supplier/price-lists:
    get:
      tags: [Supplier – Catalog]
      operationId: listSupplierPriceLists
      summary: List price lists and the buyers assigned to each
      x-phase: 1
      x-scopes: [supplier.catalog:write]
      security:
        - apiKey: []
        - oauth2: [supplier.catalog:write]
      parameters:
        - $ref: '#/components/parameters/cursor'
        - $ref: '#/components/parameters/limit'
      responses:
        '200':
          description: Page of price lists.
          content:
            application/json:
              schema:
                allOf:
                  - $ref: '#/components/schemas/Page'
                  - type: object
                    properties:
                      data:
                        type: array
                        items:
                          $ref: '#/components/schemas/PriceList'

  /supplier/price-lists/{price_list_id}/prices:
    put:
      tags: [Supplier – Catalog]
      operationId: upsertSupplierPrices
      summary: Upsert prices in a price list (up to 500 per call)
      x-phase: 1
      x-scopes: [supplier.catalog:write]
      security:
        - apiKey: []
        - oauth2: [supplier.catalog:write]
      parameters:
        - $ref: '#/components/parameters/price_list_id'
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [items]
              properties:
                items:
                  type: array
                  minItems: 1
                  maxItems: 500
                  items:
                    $ref: '#/components/schemas/PriceEntry'
      responses:
        '200':
          description: Per-item results.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/BatchResult'
        '404':
          $ref: '#/components/responses/NotFound'

  /supplier/price-lists/{price_list_id}/buyers:
    post:
      tags: [Supplier – Catalog]
      operationId: assignPriceListToBuyer
      summary: Assign a price list to a buyer
      x-phase: 1
      x-scopes: [supplier.catalog:write]
      security:
        - apiKey: []
        - oauth2: [supplier.catalog:write]
      parameters:
        - $ref: '#/components/parameters/price_list_id'
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [buyer_id]
              properties:
                buyer_id:
                  type: string
                valid_from:
                  type: string
                  format: date
      responses:
        '204':
          description: Assigned.
        '404':
          $ref: '#/components/responses/NotFound'

  /supplier/price-lists/{price_list_id}/buyers/{buyer_id}:
    delete:
      tags: [Supplier – Catalog]
      operationId: unassignPriceListFromBuyer
      summary: Unlink a price list from a buyer
      x-phase: 1
      x-scopes: [supplier.catalog:write]
      security:
        - apiKey: []
        - oauth2: [supplier.catalog:write]
      parameters:
        - $ref: '#/components/parameters/price_list_id'
        - $ref: '#/components/parameters/buyer_id'
      responses:
        '204':
          description: Unlinked.
        '404':
          $ref: '#/components/responses/NotFound'

  # ---------------------------------------------------------------------------
  # Buyer – Reference
  # ---------------------------------------------------------------------------

  /branches:
    get:
      tags: [Buyer – Reference]
      operationId: listBranches
      summary: List branches (physical sites) of the account
      x-phase: 2
      x-plan: pro
      x-scopes: [buyer.purchases:read]
      security:
        - apiKey: []
        - oauth2: [buyer.purchases:read]
      parameters:
        - $ref: '#/components/parameters/cursor'
        - $ref: '#/components/parameters/limit'
      responses:
        '200':
          description: Page of branches.
          content:
            application/json:
              schema:
                allOf:
                  - $ref: '#/components/schemas/Page'
                  - type: object
                    properties:
                      data:
                        type: array
                        items:
                          $ref: '#/components/schemas/Branch'
        '403':
          $ref: '#/components/responses/Forbidden'

  /purchase-types:
    get:
      tags: [Buyer – Reference]
      operationId: listPurchaseTypes
      summary: List purchase types (business lines, e.g. FC / MK / EV / OC)
      x-phase: 2
      x-plan: pro
      x-scopes: [buyer.purchases:read]
      security:
        - apiKey: []
        - oauth2: [buyer.purchases:read]
      responses:
        '200':
          description: Purchase types.
          content:
            application/json:
              schema:
                type: object
                properties:
                  data:
                    type: array
                    items:
                      $ref: '#/components/schemas/PurchaseType'

  /suppliers:
    get:
      tags: [Buyer – Reference]
      operationId: listSuppliers
      summary: List suppliers (vendors) of the account
      x-phase: 2
      x-plan: pro
      x-scopes: [buyer.suppliers:read]
      security:
        - apiKey: []
        - oauth2: [buyer.suppliers:read]
      parameters:
        - $ref: '#/components/parameters/cursor'
        - $ref: '#/components/parameters/limit'
        - $ref: '#/components/parameters/updated_since'
        - name: vendor_type
          in: query
          schema:
            $ref: '#/components/schemas/VendorType'
      responses:
        '200':
          description: Page of suppliers.
          content:
            application/json:
              schema:
                allOf:
                  - $ref: '#/components/schemas/Page'
                  - type: object
                    properties:
                      data:
                        type: array
                        items:
                          $ref: '#/components/schemas/Supplier'

  /suppliers/{supplier_id}:
    get:
      tags: [Buyer – Reference]
      operationId: getSupplier
      summary: Get a supplier
      x-phase: 2
      x-plan: pro
      x-scopes: [buyer.suppliers:read]
      security:
        - apiKey: []
        - oauth2: [buyer.suppliers:read]
      parameters:
        - $ref: '#/components/parameters/supplier_id_path'
      responses:
        '200':
          description: Supplier.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Supplier'
        '404':
          $ref: '#/components/responses/NotFound'

  /products:
    get:
      tags: [Buyer – Reference]
      operationId: listBuyerProducts
      summary: List the buyer's catalog
      x-phase: 2
      x-plan: pro
      x-scopes: [buyer.catalog:read]
      security:
        - apiKey: []
        - oauth2: [buyer.catalog:read]
      parameters:
        - $ref: '#/components/parameters/cursor'
        - $ref: '#/components/parameters/limit'
        - $ref: '#/components/parameters/updated_since'
        - $ref: '#/components/parameters/supplier_id'
        - name: product_group
          in: query
          schema:
            type: string
      responses:
        '200':
          description: Page of products.
          content:
            application/json:
              schema:
                allOf:
                  - $ref: '#/components/schemas/Page'
                  - type: object
                    properties:
                      data:
                        type: array
                        items:
                          $ref: '#/components/schemas/BuyerProduct'

  /expense-categories:
    get:
      tags: [Buyer – Reference]
      operationId: listExpenseCategories
      summary: List the expense category tree (2 levels)
      x-phase: 2
      x-plan: pro
      x-scopes: [buyer.expenses:read]
      security:
        - apiKey: []
        - oauth2: [buyer.expenses:read]
      responses:
        '200':
          description: Categories (flat list with parent_id).
          content:
            application/json:
              schema:
                type: object
                properties:
                  data:
                    type: array
                    items:
                      $ref: '#/components/schemas/ExpenseCategory'

  /expense-accounts:
    get:
      tags: [Buyer – Reference]
      operationId: listExpenseAccounts
      summary: List the ZesttBox expense account catalog
      x-phase: 2
      x-plan: pro
      x-scopes: [buyer.accounting:read]
      security:
        - apiKey: []
        - oauth2: [buyer.accounting:read]
      responses:
        '200':
          description: Accounts.
          content:
            application/json:
              schema:
                type: object
                properties:
                  data:
                    type: array
                    items:
                      $ref: '#/components/schemas/ExpenseAccount'

  /account-mappings:
    get:
      tags: [Buyer – Reference]
      operationId: listAccountMappings
      summary: List the category × purchase-type → expense-account matrix
      x-phase: 2
      x-plan: pro
      x-scopes: [buyer.accounting:read]
      security:
        - apiKey: []
        - oauth2: [buyer.accounting:read]
      responses:
        '200':
          description: Mappings. `purchase_type = null` is the fallback row.
          content:
            application/json:
              schema:
                type: object
                properties:
                  data:
                    type: array
                    items:
                      $ref: '#/components/schemas/AccountMapping'

  # ---------------------------------------------------------------------------
  # Buyer – Purchases
  # ---------------------------------------------------------------------------

  /orders:
    get:
      tags: [Buyer – Purchases]
      operationId: listOrders
      summary: List orders across all suppliers
      x-phase: 2
      x-plan: pro
      x-scopes: [buyer.purchases:read]
      security:
        - apiKey: []
        - oauth2: [buyer.purchases:read]
      parameters:
        - $ref: '#/components/parameters/cursor'
        - $ref: '#/components/parameters/limit'
        - $ref: '#/components/parameters/updated_since'
        - $ref: '#/components/parameters/from'
        - $ref: '#/components/parameters/to'
        - $ref: '#/components/parameters/branch_id'
        - $ref: '#/components/parameters/supplier_id'
        - $ref: '#/components/parameters/order_status'
        - $ref: '#/components/parameters/expand'
      responses:
        '200':
          description: Page of orders.
          content:
            application/json:
              schema:
                allOf:
                  - $ref: '#/components/schemas/Page'
                  - type: object
                    properties:
                      data:
                        type: array
                        items:
                          $ref: '#/components/schemas/Order'
        '403':
          $ref: '#/components/responses/Forbidden'
    post:
      tags: [Buyer – Purchases]
      operationId: createOrder
      summary: Create an order from an external system (phase 3)
      x-phase: 3
      x-plan: pro
      x-scopes: [buyer.orders:write]
      security:
        - apiKey: []
        - oauth2: [buyer.orders:write]
      parameters:
        - $ref: '#/components/parameters/Idempotency-Key-Required'
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/OrderCreate'
      responses:
        '201':
          description: Order created (status per buyer approval rules).
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Order'
        '422':
          $ref: '#/components/responses/ValidationError'

  /orders/{order_id}:
    get:
      tags: [Buyer – Purchases]
      operationId: getOrder
      summary: Get an order
      x-phase: 2
      x-plan: pro
      x-scopes: [buyer.purchases:read]
      security:
        - apiKey: []
        - oauth2: [buyer.purchases:read]
      parameters:
        - $ref: '#/components/parameters/order_id'
        - $ref: '#/components/parameters/expand'
      responses:
        '200':
          description: Order.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Order'
        '404':
          $ref: '#/components/responses/NotFound'

  /documents:
    get:
      tags: [Buyer – Purchases]
      operationId: listDocuments
      summary: List purchase documents (delivery notes, invoices, credits) with lines and export status
      x-phase: 2
      x-plan: pro
      x-scopes: [buyer.purchases:read]
      security:
        - apiKey: []
        - oauth2: [buyer.purchases:read]
      parameters:
        - $ref: '#/components/parameters/cursor'
        - $ref: '#/components/parameters/limit'
        - $ref: '#/components/parameters/updated_since'
        - $ref: '#/components/parameters/from'
        - $ref: '#/components/parameters/to'
        - $ref: '#/components/parameters/branch_id'
        - $ref: '#/components/parameters/supplier_id'
        - $ref: '#/components/parameters/document_type'
        - name: source
          in: query
          schema:
            $ref: '#/components/schemas/DocumentSource'
        - name: export_status
          in: query
          schema:
            $ref: '#/components/schemas/ExportStatus'
        - $ref: '#/components/parameters/expand'
      responses:
        '200':
          description: Page of documents.
          content:
            application/json:
              schema:
                allOf:
                  - $ref: '#/components/schemas/Page'
                  - type: object
                    properties:
                      data:
                        type: array
                        items:
                          $ref: '#/components/schemas/Document'

  /documents/{document_id}:
    get:
      tags: [Buyer – Purchases]
      operationId: getDocument
      summary: Get a purchase document
      x-phase: 2
      x-plan: pro
      x-scopes: [buyer.purchases:read]
      security:
        - apiKey: []
        - oauth2: [buyer.purchases:read]
      parameters:
        - $ref: '#/components/parameters/document_id'
        - $ref: '#/components/parameters/expand'
      responses:
        '200':
          description: Document.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Document'
        '404':
          $ref: '#/components/responses/NotFound'

  # ---------------------------------------------------------------------------
  # Buyer – Expenses
  # ---------------------------------------------------------------------------

  /expenses:
    get:
      tags: [Buyer – Expenses]
      operationId: listExpenses
      summary: List expense documents
      x-phase: 2
      x-plan: pro
      x-scopes: [buyer.expenses:read]
      security:
        - apiKey: []
        - oauth2: [buyer.expenses:read]
      parameters:
        - $ref: '#/components/parameters/cursor'
        - $ref: '#/components/parameters/limit'
        - $ref: '#/components/parameters/updated_since'
        - $ref: '#/components/parameters/from'
        - $ref: '#/components/parameters/to'
        - $ref: '#/components/parameters/branch_id'
        - name: category_id
          in: query
          schema:
            type: string
        - name: status
          in: query
          schema:
            $ref: '#/components/schemas/ExpenseStatus'
        - $ref: '#/components/parameters/expand'
      responses:
        '200':
          description: Page of expenses.
          content:
            application/json:
              schema:
                allOf:
                  - $ref: '#/components/schemas/Page'
                  - type: object
                    properties:
                      data:
                        type: array
                        items:
                          $ref: '#/components/schemas/Expense'
    post:
      tags: [Buyer – Expenses]
      operationId: createExpense
      summary: Create an expense document from an external system
      description: Applies the buyer's blocking rules (mandatory attachment, allocation number above 5,000 ILS, missing account mapping).
      x-phase: 3
      x-plan: pro
      x-scopes: [buyer.expenses:write]
      security:
        - apiKey: []
        - oauth2: [buyer.expenses:write]
      parameters:
        - $ref: '#/components/parameters/Idempotency-Key-Required'
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/ExpenseCreate'
      responses:
        '201':
          description: Expense created (status `draft` or `pending_approval`).
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Expense'
        '422':
          $ref: '#/components/responses/ValidationError'

  /expenses/{expense_id}:
    parameters:
      - name: expense_id
        in: path
        required: true
        schema:
          type: string
    get:
      tags: [Buyer – Expenses]
      operationId: getExpense
      summary: Get an expense document
      x-phase: 2
      x-plan: pro
      x-scopes: [buyer.expenses:read]
      security:
        - apiKey: []
        - oauth2: [buyer.expenses:read]
      responses:
        '200':
          description: Expense.
          headers:
            ETag:
              $ref: '#/components/headers/ETag'
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Expense'
        '404':
          $ref: '#/components/responses/NotFound'
    patch:
      tags: [Buyer – Expenses]
      operationId: updateExpense
      summary: Update a draft expense
      description: Only `draft` expenses can be edited. After export, create a counter document instead.
      x-phase: 3
      x-plan: pro
      x-scopes: [buyer.expenses:write]
      security:
        - apiKey: []
        - oauth2: [buyer.expenses:write]
      parameters:
        - $ref: '#/components/parameters/If-Match'
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/ExpenseCreate'
      responses:
        '200':
          description: Expense updated.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Expense'
        '409':
          $ref: '#/components/responses/Conflict'
        '412':
          $ref: '#/components/responses/PreconditionFailed'
        '422':
          $ref: '#/components/responses/ValidationError'

  /card-transactions:
    get:
      tags: [Buyer – Expenses]
      operationId: listCardTransactions
      summary: List imported card transactions and their linked expense
      x-phase: 2
      x-plan: pro
      x-scopes: [buyer.expenses:read]
      security:
        - apiKey: []
        - oauth2: [buyer.expenses:read]
      parameters:
        - $ref: '#/components/parameters/cursor'
        - $ref: '#/components/parameters/limit'
        - $ref: '#/components/parameters/updated_since'
        - $ref: '#/components/parameters/from'
        - $ref: '#/components/parameters/to'
        - name: matched
          in: query
          description: Filter transactions with (`true`) or without (`false`) a linked expense.
          schema:
            type: boolean
      responses:
        '200':
          description: Page of transactions.
          content:
            application/json:
              schema:
                allOf:
                  - $ref: '#/components/schemas/Page'
                  - type: object
                    properties:
                      data:
                        type: array
                        items:
                          $ref: '#/components/schemas/CardTransaction'

  /card-transactions/batch:
    post:
      tags: [Buyer – Expenses]
      operationId: importCardTransactions
      summary: Import card transactions (creates draft expenses)
      x-phase: 3
      x-plan: pro
      x-scopes: [buyer.expenses:write]
      security:
        - apiKey: []
        - oauth2: [buyer.expenses:write]
      parameters:
        - $ref: '#/components/parameters/Idempotency-Key-Required'
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [items]
              properties:
                items:
                  type: array
                  minItems: 1
                  maxItems: 500
                  items:
                    $ref: '#/components/schemas/CardTransactionCreate'
      responses:
        '200':
          description: Per-item results. Duplicates by `external_id` are reported as `skipped`.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/BatchResult'

  # ---------------------------------------------------------------------------
  # Buyer – Inventory
  # ---------------------------------------------------------------------------

  /inventory-counts:
    get:
      tags: [Buyer – Inventory]
      operationId: listInventoryCounts
      summary: List inventory counts
      x-phase: 2
      x-plan: pro
      x-scopes: [buyer.inventory:read]
      security:
        - apiKey: []
        - oauth2: [buyer.inventory:read]
      parameters:
        - $ref: '#/components/parameters/cursor'
        - $ref: '#/components/parameters/limit'
        - $ref: '#/components/parameters/from'
        - $ref: '#/components/parameters/to'
        - $ref: '#/components/parameters/branch_id'
        - $ref: '#/components/parameters/expand'
      responses:
        '200':
          description: Page of counts.
          content:
            application/json:
              schema:
                allOf:
                  - $ref: '#/components/schemas/Page'
                  - type: object
                    properties:
                      data:
                        type: array
                        items:
                          $ref: '#/components/schemas/InventoryCount'

  /inventory-counts/{count_id}:
    get:
      tags: [Buyer – Inventory]
      operationId: getInventoryCount
      summary: Get an inventory count with lines
      x-phase: 2
      x-plan: pro
      x-scopes: [buyer.inventory:read]
      security:
        - apiKey: []
        - oauth2: [buyer.inventory:read]
      parameters:
        - name: count_id
          in: path
          required: true
          schema:
            type: string
      responses:
        '200':
          description: Count.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/InventoryCount'
        '404':
          $ref: '#/components/responses/NotFound'

  # ---------------------------------------------------------------------------
  # Buyer – Sales (POS)
  # ---------------------------------------------------------------------------

  /sales/z-reports:
    get:
      tags: [Buyer – Sales]
      operationId: listZReports
      summary: List POS Z-reports
      x-phase: 2
      x-plan: pro
      x-scopes: [buyer.sales:read]
      security:
        - apiKey: []
        - oauth2: [buyer.sales:read]
      parameters:
        - $ref: '#/components/parameters/cursor'
        - $ref: '#/components/parameters/limit'
        - $ref: '#/components/parameters/from'
        - $ref: '#/components/parameters/to'
        - $ref: '#/components/parameters/branch_id'
      responses:
        '200':
          description: Page of Z-reports.
          content:
            application/json:
              schema:
                allOf:
                  - $ref: '#/components/schemas/Page'
                  - type: object
                    properties:
                      data:
                        type: array
                        items:
                          $ref: '#/components/schemas/ZReport'
    post:
      tags: [Buyer – Sales]
      operationId: createZReports
      summary: Push Z-reports from the POS (replaces v1 POSIncome/ZTotals)
      description: Idempotent per `branch_id` + `z_number`. Re-sending the same Z updates it.
      x-phase: 2
      x-scopes: [buyer.sales:write]
      security:
        - apiKey: []
        - oauth2: [buyer.sales:write]
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [items]
              properties:
                items:
                  type: array
                  minItems: 1
                  maxItems: 500
                  items:
                    $ref: '#/components/schemas/ZReportCreate'
      responses:
        '200':
          description: Per-item results.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/BatchResult'

  /sales/item-sales:
    get:
      tags: [Buyer – Sales]
      operationId: listItemSales
      summary: List item-level sales
      x-phase: 2
      x-plan: pro
      x-scopes: [buyer.sales:read]
      security:
        - apiKey: []
        - oauth2: [buyer.sales:read]
      parameters:
        - $ref: '#/components/parameters/cursor'
        - $ref: '#/components/parameters/limit'
        - $ref: '#/components/parameters/from'
        - $ref: '#/components/parameters/to'
        - $ref: '#/components/parameters/branch_id'
        - name: z_number
          in: query
          schema:
            type: string
      responses:
        '200':
          description: Page of item sales.
          content:
            application/json:
              schema:
                allOf:
                  - $ref: '#/components/schemas/Page'
                  - type: object
                    properties:
                      data:
                        type: array
                        items:
                          $ref: '#/components/schemas/ItemSale'
    post:
      tags: [Buyer – Sales]
      operationId: createItemSales
      summary: Push item-level sales (replaces v1 POSIncome/SalesDetails)
      x-phase: 2
      x-scopes: [buyer.sales:write]
      security:
        - apiKey: []
        - oauth2: [buyer.sales:write]
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [items]
              properties:
                items:
                  type: array
                  minItems: 1
                  maxItems: 5000
                  items:
                    $ref: '#/components/schemas/ItemSaleCreate'
      responses:
        '200':
          description: Per-item results.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/BatchResult'

  # ---------------------------------------------------------------------------
  # Buyer – Accounting
  # ---------------------------------------------------------------------------

  /accounting/journal-lines:
    get:
      tags: [Buyer – Accounting]
      operationId: listJournalLines
      summary: List ZesttBox journal lines (debit / credit / VAT) ready for the ERP
      description: >-
        One row per journal line. Filter `status=approved_for_export` to pull what is ready;
        mark with `POST /accounting/exports`. Generic replacement for per-ERP export files.
      x-phase: 2
      x-plan: pro
      x-scopes: [buyer.accounting:read]
      security:
        - apiKey: []
        - oauth2: [buyer.accounting:read]
      parameters:
        - $ref: '#/components/parameters/cursor'
        - $ref: '#/components/parameters/limit'
        - $ref: '#/components/parameters/updated_since'
        - name: period
          in: query
          description: Accounting period `YYYY-MM`.
          schema:
            type: string
            pattern: '^\d{4}-(0[1-9]|1[0-2])$'
        - name: status
          in: query
          schema:
            $ref: '#/components/schemas/ExportStatus'
        - $ref: '#/components/parameters/branch_id'
        - name: source
          in: query
          schema:
            $ref: '#/components/schemas/DocumentSource'
      responses:
        '200':
          description: Page of journal lines.
          content:
            application/json:
              schema:
                allOf:
                  - $ref: '#/components/schemas/Page'
                  - type: object
                    properties:
                      data:
                        type: array
                        items:
                          $ref: '#/components/schemas/JournalLine'
        '403':
          $ref: '#/components/responses/Forbidden'

  /accounting/exports:
    post:
      tags: [Buyer – Accounting]
      operationId: createAccountingExport
      summary: Mark journal lines as exported
      description: Marks the given lines `exported`, records `exported_by` = this client and returns an `export_batch_id`.
      x-phase: 2
      x-plan: pro
      x-scopes: [buyer.accounting:write]
      security:
        - apiKey: []
        - oauth2: [buyer.accounting:write]
      parameters:
        - $ref: '#/components/parameters/Idempotency-Key-Required'
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/AccountingExportCreate'
      responses:
        '201':
          description: Export batch recorded.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/AccountingExport'
        '409':
          description: One or more lines were already exported.
          content:
            application/problem+json:
              schema:
                $ref: '#/components/schemas/Problem'
        '422':
          $ref: '#/components/responses/ValidationError'

  /accounting/exports/{export_batch_id}:
    get:
      tags: [Buyer – Accounting]
      operationId: getAccountingExport
      summary: Get an export batch
      x-phase: 2
      x-plan: pro
      x-scopes: [buyer.accounting:read]
      security:
        - apiKey: []
        - oauth2: [buyer.accounting:read]
      parameters:
        - name: export_batch_id
          in: path
          required: true
          schema:
            type: string
      responses:
        '200':
          description: Export batch.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/AccountingExport'
        '404':
          $ref: '#/components/responses/NotFound'

  # ---------------------------------------------------------------------------
  # Reports
  # ---------------------------------------------------------------------------

  /reports/food-cost:
    get:
      tags: [Reports]
      operationId: getFoodCostReport
      summary: Food cost by branch and purchase type
      description: Same numbers as the in-app report. Excludes `source != purchase` and `vendor_type != goods`.
      x-phase: 2
      x-plan: pro
      x-scopes: [buyer.reports:read]
      security:
        - apiKey: []
        - oauth2: [buyer.reports:read]
      parameters:
        - $ref: '#/components/parameters/from_required'
        - $ref: '#/components/parameters/to_required'
        - $ref: '#/components/parameters/branch_id'
        - name: purchase_type
          in: query
          schema:
            type: string
      responses:
        '200':
          description: Report rows.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ReportResult'

  /reports/purchases:
    get:
      tags: [Reports]
      operationId: getPurchasesReport
      summary: Purchases grouped by supplier, branch or product group
      x-phase: 2
      x-plan: pro
      x-scopes: [buyer.reports:read]
      security:
        - apiKey: []
        - oauth2: [buyer.reports:read]
      parameters:
        - $ref: '#/components/parameters/from_required'
        - $ref: '#/components/parameters/to_required'
        - name: group_by
          in: query
          required: true
          schema:
            type: array
            items:
              type: string
              enum: [supplier, branch, product_group, purchase_type, month]
          style: form
          explode: false
      responses:
        '200':
          description: Report rows.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ReportResult'

  /reports/expenses:
    get:
      tags: [Reports]
      operationId: getExpensesReport
      summary: Expenses pivot (category × purchase type × branch × month)
      x-phase: 2
      x-plan: pro
      x-scopes: [buyer.reports:read]
      security:
        - apiKey: []
        - oauth2: [buyer.reports:read]
      parameters:
        - $ref: '#/components/parameters/from_required'
        - $ref: '#/components/parameters/to_required'
        - name: group_by
          in: query
          required: true
          schema:
            type: array
            items:
              type: string
              enum: [category, purchase_type, branch, month, merchant]
          style: form
          explode: false
      responses:
        '200':
          description: Report rows.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ReportResult'

  /reports/pnl:
    get:
      tags: [Reports]
      operationId: getPnlReport
      summary: Profit and loss for a period
      x-phase: 2
      x-plan: pro
      x-scopes: [buyer.reports:read]
      security:
        - apiKey: []
        - oauth2: [buyer.reports:read]
      parameters:
        - name: period
          in: query
          required: true
          schema:
            type: string
            pattern: '^\d{4}-(0[1-9]|1[0-2])$'
        - $ref: '#/components/parameters/branch_id'
      responses:
        '200':
          description: Report rows (one per P&L group).
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ReportResult'

  # ---------------------------------------------------------------------------
  # Platform: events, webhooks, files, exports, audit
  # ---------------------------------------------------------------------------

  /events:
    get:
      tags: [Events]
      operationId: listEvents
      summary: Pull the change feed
      description: >-
        Ordered by `occurred_at`. Store `next_cursor` and resume from it. Retention 30 days (Pro) /
        90 days (Enterprise). Same event catalog as webhooks.
      x-phase: 2
      x-plan: pro
      x-scopes: [events:read]
      security:
        - apiKey: []
        - oauth2: [events:read]
      parameters:
        - $ref: '#/components/parameters/cursor'
        - $ref: '#/components/parameters/limit'
        - name: type
          in: query
          description: Comma-separated event types.
          schema:
            type: array
            items:
              type: string
          style: form
          explode: false
        - name: since
          in: query
          description: Start of feed when no cursor is given.
          schema:
            type: string
            format: date-time
      responses:
        '200':
          description: Page of events.
          content:
            application/json:
              schema:
                allOf:
                  - $ref: '#/components/schemas/Page'
                  - type: object
                    properties:
                      data:
                        type: array
                        items:
                          $ref: '#/components/schemas/Event'

  /webhooks:
    get:
      tags: [Webhooks]
      operationId: listWebhooks
      summary: List webhook endpoints
      x-phase: 1
      x-scopes: [webhooks:manage]
      security:
        - apiKey: []
        - oauth2: [webhooks:manage]
      responses:
        '200':
          description: Endpoints.
          content:
            application/json:
              schema:
                type: object
                properties:
                  data:
                    type: array
                    items:
                      $ref: '#/components/schemas/Webhook'
    post:
      tags: [Webhooks]
      operationId: createWebhook
      summary: Create a webhook endpoint
      description: Returns the signing `secret` once. Endpoint limit depends on plan.
      x-phase: 1
      x-scopes: [webhooks:manage]
      security:
        - apiKey: []
        - oauth2: [webhooks:manage]
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/WebhookCreate'
      responses:
        '201':
          description: Endpoint created; `secret` shown only in this response.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/WebhookWithSecret'
        '403':
          $ref: '#/components/responses/Forbidden'
        '422':
          $ref: '#/components/responses/ValidationError'

  /webhooks/{webhook_id}:
    parameters:
      - $ref: '#/components/parameters/webhook_id'
    get:
      tags: [Webhooks]
      operationId: getWebhook
      summary: Get a webhook endpoint
      x-phase: 1
      x-scopes: [webhooks:manage]
      security:
        - apiKey: []
        - oauth2: [webhooks:manage]
      responses:
        '200':
          description: Endpoint.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Webhook'
        '404':
          $ref: '#/components/responses/NotFound'
    patch:
      tags: [Webhooks]
      operationId: updateWebhook
      summary: Update URL, events or status
      x-phase: 1
      x-scopes: [webhooks:manage]
      security:
        - apiKey: []
        - oauth2: [webhooks:manage]
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/WebhookUpdate'
      responses:
        '200':
          description: Endpoint updated.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Webhook'
        '404':
          $ref: '#/components/responses/NotFound'
    delete:
      tags: [Webhooks]
      operationId: deleteWebhook
      summary: Delete a webhook endpoint
      x-phase: 1
      x-scopes: [webhooks:manage]
      security:
        - apiKey: []
        - oauth2: [webhooks:manage]
      responses:
        '204':
          description: Deleted.
        '404':
          $ref: '#/components/responses/NotFound'

  /webhooks/{webhook_id}/test:
    post:
      tags: [Webhooks]
      operationId: testWebhook
      summary: Send a signed `ping` event to the endpoint
      x-phase: 1
      x-scopes: [webhooks:manage]
      security:
        - apiKey: []
        - oauth2: [webhooks:manage]
      parameters:
        - $ref: '#/components/parameters/webhook_id'
      responses:
        '200':
          description: Delivery attempt result.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/WebhookDelivery'
        '404':
          $ref: '#/components/responses/NotFound'

  /webhooks/{webhook_id}/deliveries:
    get:
      tags: [Webhooks]
      operationId: listWebhookDeliveries
      summary: List delivery attempts for an endpoint
      x-phase: 1
      x-scopes: [webhooks:manage]
      security:
        - apiKey: []
        - oauth2: [webhooks:manage]
      parameters:
        - $ref: '#/components/parameters/webhook_id'
        - $ref: '#/components/parameters/cursor'
        - $ref: '#/components/parameters/limit'
        - name: status
          in: query
          schema:
            type: string
            enum: [pending, succeeded, failed]
      responses:
        '200':
          description: Page of deliveries.
          content:
            application/json:
              schema:
                allOf:
                  - $ref: '#/components/schemas/Page'
                  - type: object
                    properties:
                      data:
                        type: array
                        items:
                          $ref: '#/components/schemas/WebhookDelivery'

  /webhooks/{webhook_id}/deliveries/{delivery_id}/redeliver:
    post:
      tags: [Webhooks]
      operationId: redeliverWebhookDelivery
      summary: Re-send a delivery
      x-phase: 1
      x-scopes: [webhooks:manage]
      security:
        - apiKey: []
        - oauth2: [webhooks:manage]
      parameters:
        - $ref: '#/components/parameters/webhook_id'
        - name: delivery_id
          in: path
          required: true
          schema:
            type: string
      responses:
        '202':
          description: Queued.
        '404':
          $ref: '#/components/responses/NotFound'

  /files:
    post:
      tags: [Files]
      operationId: uploadFile
      summary: Upload a file (multipart, up to 25 MB)
      description: Returns a `file_id` to reference from documents and expenses. Files not referenced within 24 hours are deleted.
      x-phase: 1
      x-scopes: [supplier.documents:write, buyer.expenses:write]
      security:
        - apiKey: []
        - oauth2: [supplier.documents:write]
        - oauth2: [buyer.expenses:write]
      requestBody:
        required: true
        content:
          multipart/form-data:
            schema:
              type: object
              required: [file]
              properties:
                file:
                  type: string
                  format: binary
                purpose:
                  type: string
                  enum: [document_attachment, expense_attachment, product_image]
      responses:
        '201':
          description: File stored.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/FileObject'
        '413':
          description: File too large.
          content:
            application/problem+json:
              schema:
                $ref: '#/components/schemas/Problem'
        '422':
          $ref: '#/components/responses/ValidationError'

  /files/{file_id}:
    get:
      tags: [Files]
      operationId: getFile
      summary: Get file metadata and a signed download URL (valid 1 hour)
      x-phase: 1
      x-scopes: []
      parameters:
        - name: file_id
          in: path
          required: true
          schema:
            type: string
      responses:
        '200':
          description: File.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/FileObject'
        '404':
          $ref: '#/components/responses/NotFound'

  /exports:
    post:
      tags: [Exports]
      operationId: createExport
      summary: Start an asynchronous bulk export
      x-phase: 3
      x-plan: enterprise
      x-scopes: [exports:manage]
      security:
        - apiKey: []
        - oauth2: [exports:manage]
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/ExportJobCreate'
      responses:
        '202':
          description: Export queued.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ExportJob'
        '403':
          $ref: '#/components/responses/Forbidden'
        '429':
          $ref: '#/components/responses/RateLimited'

  /exports/{export_id}:
    get:
      tags: [Exports]
      operationId: getExport
      summary: Poll an export job; download links appear when `status=completed`
      x-phase: 3
      x-plan: enterprise
      x-scopes: [exports:manage]
      security:
        - apiKey: []
        - oauth2: [exports:manage]
      parameters:
        - name: export_id
          in: path
          required: true
          schema:
            type: string
      responses:
        '200':
          description: Export job.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ExportJob'
        '404':
          $ref: '#/components/responses/NotFound'

  /audit/requests:
    get:
      tags: [Audit]
      operationId: listAuditRequests
      summary: Read the API request log of this account
      x-phase: 2
      x-plan: enterprise
      x-scopes: []
      parameters:
        - $ref: '#/components/parameters/cursor'
        - $ref: '#/components/parameters/limit'
        - $ref: '#/components/parameters/from'
        - $ref: '#/components/parameters/to'
        - name: client_id
          in: query
          schema:
            type: string
        - name: status
          in: query
          description: HTTP status code.
          schema:
            type: integer
      responses:
        '200':
          description: Page of request log rows.
          content:
            application/json:
              schema:
                allOf:
                  - $ref: '#/components/schemas/Page'
                  - type: object
                    properties:
                      data:
                        type: array
                        items:
                          $ref: '#/components/schemas/AuditRequest'
        '403':
          $ref: '#/components/responses/Forbidden'

# =============================================================================
# Webhooks (OpenAPI 3.1 top-level). All deliveries share the Event envelope and
# the Zestt-Signature header.
# =============================================================================

webhooks:
  order.created:
    post:
      summary: A buyer issued a new order to the supplier (status pending_approval)
      requestBody:
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/Event'
      responses:
        '2XX':
          description: Acknowledged. Any other status is retried with exponential backoff (8 attempts, ~24h).
  order.updated:
    post:
      summary: Order changed (lines, dates, status) — including cancelled_after_approval
      requestBody:
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/Event'
      responses:
        '2XX':
          description: Acknowledged.
  document.received:
    post:
      summary: The buyer received a supplier document; `data.differences[]` lists gaps
      requestBody:
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/Event'
      responses:
        '2XX':
          description: Acknowledged.
  journal_lines.approved_for_export:
    post:
      summary: Journal lines were approved for export (buyer side)
      requestBody:
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/Event'
      responses:
        '2XX':
          description: Acknowledged.

# =============================================================================
# Components
# =============================================================================

components:

  securitySchemes:
    apiKey:
      type: http
      scheme: bearer
      bearerFormat: zk_live_… / zk_test_…
      description: >-
        API key of an ApiClient, sent directly as a Bearer token. Issued in the app under
        Settings → Developers, shown once, stored hashed. Subject to the client's scopes,
        IP allowlist and the account's plan.
    oauth2:
      type: oauth2
      description: Optional exchange of the API key for a 60-minute JWT (client_credentials).
      flows:
        clientCredentials:
          tokenUrl: https://api.zester.co.il/v2/oauth/token
          scopes:
            supplier.orders:read: Read orders sent to the supplier
            supplier.orders:write: Confirm, reject, schedule, acknowledge orders
            supplier.documents:read: Read supplier documents and buyer receiving status
            supplier.documents:write: Create delivery notes, invoices, credit notes
            supplier.catalog:write: Manage products, availability, price lists
            supplier.buyers:read: Read linked buyers
            supplier.buyers:write: Update customer numbers, invite buyers
            buyer.purchases:read: Read orders and purchase documents
            buyer.suppliers:read: Read supplier (vendor) records
            buyer.catalog:read: Read the buyer catalog
            buyer.inventory:read: Read inventory counts
            buyer.expenses:read: Read expense documents and card transactions
            buyer.expenses:write: Create expenses, import card transactions
            buyer.accounting:read: Read journal lines, accounts, mappings
            buyer.accounting:write: Mark journal lines as exported
            buyer.sales:read: Read POS sales
            buyer.sales:write: Push POS sales
            buyer.reports:read: Read computed reports
            buyer.orders:write: Create orders from external systems
            events:read: Read the change feed
            webhooks:manage: Manage webhook endpoints
            exports:manage: Create and read bulk exports

  # ---------------------------------------------------------------------------
  parameters:
    cursor:
      name: cursor
      in: query
      description: Opaque cursor from a previous response (`next_cursor`).
      schema:
        type: string
    limit:
      name: limit
      in: query
      schema:
        type: integer
        minimum: 1
        maximum: 200
        default: 50
    updated_since:
      name: updated_since
      in: query
      description: Return items whose `updated_at` is strictly after this instant. Use for incremental sync.
      schema:
        type: string
        format: date-time
    from:
      name: from
      in: query
      description: Inclusive start date (document / order date).
      schema:
        type: string
        format: date
    to:
      name: to
      in: query
      description: Inclusive end date.
      schema:
        type: string
        format: date
    from_required:
      name: from
      in: query
      required: true
      schema:
        type: string
        format: date
    to_required:
      name: to
      in: query
      required: true
      schema:
        type: string
        format: date
    order_status:
      name: status
      in: query
      description: Comma-separated list of order statuses.
      schema:
        type: array
        items:
          $ref: '#/components/schemas/OrderStatus'
      style: form
      explode: false
    document_type:
      name: type
      in: query
      description: Comma-separated list of document types.
      schema:
        type: array
        items:
          $ref: '#/components/schemas/DocumentType'
      style: form
      explode: false
    expand:
      name: expand
      in: query
      description: 'Comma-separated related objects to embed, e.g. `lines,supplier,files,history`. Default responses are lean.'
      schema:
        type: array
        items:
          type: string
      style: form
      explode: false
    branch_id:
      name: branch_id
      in: query
      schema:
        type: string
    supplier_id:
      name: supplier_id
      in: query
      schema:
        type: string
    supplier_id_path:
      name: supplier_id
      in: path
      required: true
      schema:
        type: string
    buyer_id:
      name: buyer_id
      in: path
      required: true
      schema:
        type: string
    order_id:
      name: order_id
      in: path
      required: true
      schema:
        type: string
    document_id:
      name: document_id
      in: path
      required: true
      schema:
        type: string
    sku:
      name: sku
      in: path
      required: true
      description: Supplier SKU (catalog number). URL-encode if needed.
      schema:
        type: string
        maxLength: 64
    price_list_id:
      name: price_list_id
      in: path
      required: true
      schema:
        type: string
    webhook_id:
      name: webhook_id
      in: path
      required: true
      schema:
        type: string
    Idempotency-Key:
      name: Idempotency-Key
      in: header
      description: 'Optional. Same key + same body within 24h returns the original response; same key + different body → 409.'
      schema:
        type: string
        maxLength: 128
    Idempotency-Key-Required:
      name: Idempotency-Key
      in: header
      required: true
      description: 'Required. Use a stable business key (e.g. document number). Same key + same body within 24h returns the original response; same key + different body → 409.'
      schema:
        type: string
        maxLength: 128
    If-Match:
      name: If-Match
      in: header
      description: ETag from a previous GET. Mismatch → 412.
      schema:
        type: string

  # ---------------------------------------------------------------------------
  headers:
    X-Request-Id:
      description: Correlation id. Quote it in support requests.
      schema:
        type: string
    ETag:
      description: Entity tag for optimistic concurrency (`If-Match`).
      schema:
        type: string
    RateLimit-Limit:
      schema:
        type: integer
    RateLimit-Remaining:
      schema:
        type: integer
    RateLimit-Reset:
      description: Seconds until the window resets.
      schema:
        type: integer
    Retry-After:
      schema:
        type: integer

  # ---------------------------------------------------------------------------
  responses:
    BadRequest:
      description: Malformed request.
      content:
        application/problem+json:
          schema:
            $ref: '#/components/schemas/Problem'
    Unauthorized:
      description: 'Missing, invalid, expired or revoked credential (`code`: unauthorized, key_expired, key_revoked).'
      headers:
        X-Request-Id:
          $ref: '#/components/headers/X-Request-Id'
      content:
        application/problem+json:
          schema:
            $ref: '#/components/schemas/Problem'
          examples:
            revoked:
              value:
                type: https://developers.zester.co.il/errors/key_revoked
                title: API key revoked
                status: 401
                code: key_revoked
                detail: This key was revoked on 2026-09-01. Create a new key in Settings → Developers.
                request_id: req_01J9KQ7X4M
    Forbidden:
      description: 'Authenticated but not allowed (`code`: scope_missing, plan_required, ip_not_allowed, client_suspended).'
      content:
        application/problem+json:
          schema:
            $ref: '#/components/schemas/Problem'
          examples:
            plan_required:
              value:
                type: https://developers.zester.co.il/errors/plan_required
                title: Plan upgrade required
                status: 403
                code: plan_required
                detail: The buyer API requires the Pro plan or above.
                request_id: req_01J9KQ7X4M
                upgrade_url: https://app.zester.co.il/billing
    NotFound:
      description: Resource does not exist or belongs to another account.
      content:
        application/problem+json:
          schema:
            $ref: '#/components/schemas/Problem'
    Conflict:
      description: 'State conflict (`code`: conflict, idempotency_conflict).'
      content:
        application/problem+json:
          schema:
            $ref: '#/components/schemas/Problem'
    PreconditionFailed:
      description: ETag mismatch.
      content:
        application/problem+json:
          schema:
            $ref: '#/components/schemas/Problem'
    ValidationError:
      description: Body or query failed validation; see `errors[]`.
      content:
        application/problem+json:
          schema:
            $ref: '#/components/schemas/Problem'
          examples:
            validation:
              value:
                type: https://developers.zester.co.il/errors/validation_error
                title: Validation failed
                status: 422
                code: validation_error
                detail: 2 fields are invalid
                request_id: req_01J9KQ7X4M
                errors:
                  - field: lines[1].quantity
                    code: must_be_positive
                    message: Quantity must be greater than 0
                  - field: delivery_date
                    code: in_the_past
    RateLimited:
      description: Too many requests.
      headers:
        Retry-After:
          $ref: '#/components/headers/Retry-After'
        RateLimit-Limit:
          $ref: '#/components/headers/RateLimit-Limit'
        RateLimit-Remaining:
          $ref: '#/components/headers/RateLimit-Remaining'
        RateLimit-Reset:
          $ref: '#/components/headers/RateLimit-Reset'
      content:
        application/problem+json:
          schema:
            $ref: '#/components/schemas/Problem'

  # ---------------------------------------------------------------------------
  schemas:

    # ---- primitives ----------------------------------------------------------

    Money:
      type: string
      description: Decimal amount with exactly 2 fraction digits, as a string. Currency is on the parent object.
      pattern: '^-?\d+\.\d{2}$'
      examples: ['138.00', '-15.25']

    Quantity:
      type: string
      description: Decimal quantity with up to 3 fraction digits, as a string.
      pattern: '^-?\d+(\.\d{1,3})?$'
      examples: ['2', '2.500']

    Currency:
      type: string
      description: ISO 4217. Phase 1 supports ILS only.
      enum: [ILS]

    TimeWindow:
      type: object
      properties:
        from:
          type: string
          pattern: '^([01]\d|2[0-3]):[0-5]\d$'
          examples: ['06:00']
        to:
          type: string
          pattern: '^([01]\d|2[0-3]):[0-5]\d$'
          examples: ['09:00']

    Page:
      type: object
      required: [data, has_more]
      properties:
        data:
          type: array
          items: {}
        next_cursor:
          type: [string, 'null']
        has_more:
          type: boolean

    Problem:
      type: object
      description: RFC 9457 problem details with a stable `code`.
      required: [type, title, status, code]
      properties:
        type:
          type: string
          format: uri
        title:
          type: string
        status:
          type: integer
        code:
          type: string
          description: Stable machine-readable code.
          enum:
            - unauthorized
            - key_expired
            - key_revoked
            - client_suspended
            - ip_not_allowed
            - scope_missing
            - plan_required
            - not_found
            - validation_error
            - idempotency_conflict
            - precondition_failed
            - conflict
            - rate_limited
            - file_too_large
            - internal_error
        detail:
          type: string
        instance:
          type: string
        request_id:
          type: string
        errors:
          type: array
          items:
            $ref: '#/components/schemas/ValidationErrorItem'
        upgrade_url:
          type: string
          format: uri

    ValidationErrorItem:
      type: object
      required: [field, code]
      properties:
        field:
          type: string
          description: JSON path of the offending field.
        code:
          type: string
        message:
          type: string

    BatchResult:
      type: object
      required: [summary, items]
      properties:
        summary:
          type: object
          properties:
            total: {type: integer}
            succeeded: {type: integer}
            failed: {type: integer}
            skipped: {type: integer}
        items:
          type: array
          items:
            $ref: '#/components/schemas/BatchItemResult'

    BatchItemResult:
      type: object
      required: [index, status]
      properties:
        index:
          type: integer
          description: Position in the request `items[]`.
        key:
          type: string
          description: Business key of the item (document number, SKU, external_id…).
        status:
          type: string
          enum: [created, updated, skipped, failed]
        id:
          type: string
          description: Id of the created / updated resource.
        errors:
          type: array
          items:
            $ref: '#/components/schemas/ValidationErrorItem'

    # ---- auth & account -------------------------------------------------------

    TokenRequest:
      type: object
      required: [grant_type, client_id, client_secret]
      properties:
        grant_type:
          type: string
          enum: [client_credentials]
        client_id:
          type: string
        client_secret:
          type: string
          description: The API key (zk_live_… / zk_test_…).
        scope:
          type: string
          description: Optional space-separated subset of the client's scopes.

    TokenResponse:
      type: object
      required: [access_token, token_type, expires_in]
      properties:
        access_token:
          type: string
        token_type:
          type: string
          enum: [Bearer]
        expires_in:
          type: integer
          description: Seconds (3600).
        scope:
          type: string

    Me:
      type: object
      required: [account, client, scopes, plan, features, rate_limit]
      properties:
        account:
          type: object
          properties:
            id: {type: string}
            name: {type: string}
            type:
              type: string
              enum: [buyer, supplier, partner]
            parent_id:
              type: [string, 'null']
        client:
          type: object
          properties:
            id: {type: string}
            name: {type: string}
            environment:
              type: string
              enum: [live, sandbox]
            key_prefix: {type: string}
            ip_allowlist:
              type: array
              items: {type: string}
            branch_ids:
              type: [array, 'null']
              items: {type: string}
              description: Null = all branches of the account.
        scopes:
          type: array
          items: {type: string}
        plan:
          type: string
          enum: [basic, pro, enterprise, supplier]
        features:
          type: array
          items:
            type: string
            enum: [buyer_api, webhooks, events, bulk_export, ip_allowlist, mtls, audit_api]
        rate_limit:
          type: object
          properties:
            per_minute: {type: integer}
            per_day:
              type: [integer, 'null']
        api_version:
          type: string
          examples: ['2']

    # ---- shared business objects ----------------------------------------------

    OrderStatus:
      type: string
      enum:
        - draft
        - pending_approval
        - approved
        - partially_approved
        - rejected
        - cancelled
        - cancelled_after_approval
        - waiting_to_send

    DocumentType:
      type: string
      enum: [delivery_note, invoice, credit_note, consolidated_invoice]

    DocumentBuyerStatus:
      type: string
      description: What happened to the document on the buyer side.
      enum: [pending, received, received_with_differences, disputed, approved_for_export, exported]

    DocumentSource:
      type: string
      enum: [purchase, expense, card, migration]

    ExportStatus:
      type: string
      enum: [not_ready, approved_for_export, exported]

    VendorType:
      type: string
      description: 'goods = counts in food cost; service / card / internal never do.'
      enum: [goods, service, card, internal]

    PartyRef:
      type: object
      required: [id, name]
      properties:
        id: {type: string}
        name: {type: string}

    BranchRef:
      type: object
      required: [id, name]
      properties:
        id: {type: string}
        name: {type: string}
        tax_id:
          type: [string, 'null']

    FileRef:
      type: object
      required: [id, name]
      properties:
        id: {type: string}
        name: {type: string}
        content_type: {type: string}
        size_bytes: {type: integer}
        download_url:
          type: string
          format: uri
          description: Signed URL, valid ~1 hour. Present when `expand=files`.

    Totals:
      type: object
      required: [before_vat, vat, with_vat]
      properties:
        before_vat:
          $ref: '#/components/schemas/Money'
        vat:
          $ref: '#/components/schemas/Money'
        with_vat:
          $ref: '#/components/schemas/Money'
        discount:
          $ref: '#/components/schemas/Money'

    HistoryEntry:
      type: object
      properties:
        at:
          type: string
          format: date-time
        action:
          type: string
        actor:
          type: object
          properties:
            type:
              type: string
              enum: [user, api_client, system]
            id: {type: string}
            name: {type: string}
        changes:
          type: object
          additionalProperties: true

    # ---- orders ---------------------------------------------------------------

    OrderLine:
      type: object
      required: [id, sku, name, quantity, unit_price, total]
      properties:
        id: {type: string}
        sku: {type: string}
        name: {type: string}
        quantity:
          $ref: '#/components/schemas/Quantity'
        unit:
          type: string
          description: Unit of `quantity` (unit, kg, l, carton…).
        package_quantity:
          $ref: '#/components/schemas/Quantity'
        unit_price:
          $ref: '#/components/schemas/Money'
        discount_pct:
          type: [string, 'null']
          pattern: '^\d+(\.\d{1,2})?$'
        vat_exempt:
          type: boolean
        total:
          $ref: '#/components/schemas/Money'
        quantity_confirmed:
          oneOf:
            - $ref: '#/components/schemas/Quantity'
            - type: 'null'
        substitute_sku:
          type: [string, 'null']
        product_group:
          type: [string, 'null']
          description: Buyer catalog group (buyer view only).
        expense_account:
          type: [string, 'null']
          description: Resolved expense account (buyer view only).

    Order:
      type: object
      required: [id, number, status, buyer, supplier, delivery_date, currency, totals, created_at, updated_at]
      properties:
        id: {type: string}
        number:
          type: string
          description: Human-readable order number.
        status:
          $ref: '#/components/schemas/OrderStatus'
        buyer:
          allOf:
            - $ref: '#/components/schemas/PartyRef'
            - type: object
              properties:
                customer_number:
                  type: [string, 'null']
                  description: The buyer's customer number at this supplier.
        supplier:
          $ref: '#/components/schemas/PartyRef'
        branch:
          $ref: '#/components/schemas/BranchRef'
        purchase_type:
          type: [string, 'null']
          description: Business line (FC / MK / EV / OC…) when the buyer uses purchase types.
        sent_at:
          type: string
          format: date-time
        delivery_date:
          type: string
          format: date
        delivery_window:
          $ref: '#/components/schemas/TimeWindow'
        currency:
          $ref: '#/components/schemas/Currency'
        totals:
          $ref: '#/components/schemas/Totals'
        lines:
          type: array
          description: Present by default; omit with `expand=-lines`.
          items:
            $ref: '#/components/schemas/OrderLine'
        notes:
          type: [string, 'null']
        external_ref:
          type: [string, 'null']
          description: Set by the supplier via `acknowledge` (ERP sales-order number).
        acknowledged_at:
          type: [string, 'null']
          format: date-time
        related_document_ids:
          type: array
          items: {type: string}
        files:
          type: array
          items:
            $ref: '#/components/schemas/FileRef'
        history:
          type: array
          description: Present with `expand=history`.
          items:
            $ref: '#/components/schemas/HistoryEntry'
        created_at:
          type: string
          format: date-time
        updated_at:
          type: string
          format: date-time
        deleted_at:
          type: [string, 'null']
          format: date-time

    OrderConfirmRequest:
      type: object
      description: Empty object = full confirmation.
      properties:
        delivery_date:
          type: string
          format: date
        delivery_window:
          $ref: '#/components/schemas/TimeWindow'
        note:
          type: string
          maxLength: 500
        lines:
          type: array
          description: Only lines that differ from the order need to be sent.
          items:
            type: object
            required: [id]
            properties:
              id: {type: string}
              quantity_confirmed:
                $ref: '#/components/schemas/Quantity'
              unit_price:
                $ref: '#/components/schemas/Money'
              substitute_sku:
                type: string
              reason:
                type: string
                maxLength: 200

    OrderCreate:
      type: object
      required: [supplier_id, branch_id, delivery_date, lines]
      properties:
        supplier_id: {type: string}
        branch_id: {type: string}
        purchase_type: {type: string}
        delivery_date:
          type: string
          format: date
        notes:
          type: string
        external_ref:
          type: string
        lines:
          type: array
          minItems: 1
          items:
            type: object
            required: [sku, quantity]
            properties:
              sku: {type: string}
              quantity:
                $ref: '#/components/schemas/Quantity'
              unit_price:
                $ref: '#/components/schemas/Money'

    # ---- documents ------------------------------------------------------------

    DocumentLine:
      type: object
      required: [id, sku, name, quantity, unit_price, total]
      properties:
        id: {type: string}
        sku: {type: string}
        name: {type: string}
        quantity:
          $ref: '#/components/schemas/Quantity'
        unit: {type: string}
        unit_price:
          $ref: '#/components/schemas/Money'
        discount_pct:
          type: [string, 'null']
        vat_exempt:
          type: boolean
        total:
          $ref: '#/components/schemas/Money'
        order_id:
          type: [string, 'null']
        order_line_id:
          type: [string, 'null']
        quantity_received:
          oneOf:
            - $ref: '#/components/schemas/Quantity'
            - type: 'null'
          description: Buyer-side received quantity.
        purchase_type:
          type: [string, 'null']
        expense_account:
          type: [string, 'null']

    DocumentLineCreate:
      type: object
      required: [sku, quantity, unit_price]
      properties:
        sku: {type: string}
        name: {type: string}
        quantity:
          $ref: '#/components/schemas/Quantity'
        unit: {type: string}
        unit_price:
          $ref: '#/components/schemas/Money'
        discount_pct:
          type: string
          pattern: '^\d+(\.\d{1,2})?$'
        vat_exempt:
          type: boolean
          default: false
        order_id: {type: string}
        order_line_id: {type: string}

    Difference:
      type: object
      description: Gap between what the supplier issued and what the buyer received.
      properties:
        line_id: {type: string}
        sku: {type: string}
        field:
          type: string
          enum: [quantity, unit_price, missing_line, extra_line]
        issued: {type: string}
        received: {type: string}
        note: {type: string}

    Document:
      type: object
      required: [id, type, number, date, supplier, buyer, currency, totals, buyer_status, created_at, updated_at]
      properties:
        id: {type: string}
        type:
          $ref: '#/components/schemas/DocumentType'
        number:
          type: string
          description: Document number as printed by the issuer.
        date:
          type: string
          format: date
        accounting_period:
          type: string
          description: YYYY-MM. Defaults to the month of `date`.
        due_date:
          type: [string, 'null']
          format: date
        supplier:
          $ref: '#/components/schemas/PartyRef'
        buyer:
          $ref: '#/components/schemas/PartyRef'
        branch:
          $ref: '#/components/schemas/BranchRef'
        purchase_type:
          type: [string, 'null']
        source:
          $ref: '#/components/schemas/DocumentSource'
        order_ids:
          type: array
          items: {type: string}
        currency:
          $ref: '#/components/schemas/Currency'
        totals:
          $ref: '#/components/schemas/Totals'
        allocation_number:
          type: [string, 'null']
          description: Israeli invoice allocation number (mandatory above 5,000 ILS).
        lines:
          type: array
          items:
            $ref: '#/components/schemas/DocumentLine'
        buyer_status:
          $ref: '#/components/schemas/DocumentBuyerStatus'
        differences:
          type: array
          items:
            $ref: '#/components/schemas/Difference'
        payment_status:
          type: string
          enum: [unpaid, partially_paid, paid]
        paid_at:
          type: [string, 'null']
          format: date-time
        export_status:
          $ref: '#/components/schemas/ExportStatus'
        exported_at:
          type: [string, 'null']
          format: date-time
        files:
          type: array
          items:
            $ref: '#/components/schemas/FileRef'
        external_ref:
          type: [string, 'null']
        history:
          type: array
          items:
            $ref: '#/components/schemas/HistoryEntry'
        created_at:
          type: string
          format: date-time
        updated_at:
          type: string
          format: date-time
        deleted_at:
          type: [string, 'null']
          format: date-time

    DocumentCreate:
      type: object
      required: [type, number, date, buyer_id, lines, totals]
      properties:
        type:
          $ref: '#/components/schemas/DocumentType'
        number:
          type: string
          maxLength: 64
        date:
          type: string
          format: date
        due_date:
          type: string
          format: date
        buyer_id: {type: string}
        branch_id:
          type: string
          description: Required when the buyer has more than one branch and `order_ids` is empty.
        order_ids:
          type: array
          items: {type: string}
        currency:
          $ref: '#/components/schemas/Currency'
        totals:
          $ref: '#/components/schemas/Totals'
        allocation_number:
          type: string
          maxLength: 32
        lines:
          type: array
          minItems: 1
          items:
            $ref: '#/components/schemas/DocumentLineCreate'
        file_ids:
          type: array
          items: {type: string}
        external_ref:
          type: string
          maxLength: 64
        notes:
          type: string
          maxLength: 1000

    # ---- supplier: buyers & catalog -------------------------------------------

    SupplierBuyer:
      type: object
      required: [id, name, status, updated_at]
      properties:
        id: {type: string}
        name: {type: string}
        tax_id:
          type: [string, 'null']
        status:
          type: string
          enum: [active, inactive]
        customer_number:
          type: [string, 'null']
        erp_ref:
          type: [string, 'null']
        branches:
          type: array
          items:
            allOf:
              - $ref: '#/components/schemas/BranchRef'
              - type: object
                properties:
                  address: {type: string}
                  delivery_days:
                    type: array
                    items:
                      type: string
                      enum: [sun, mon, tue, wed, thu, fri, sat]
        min_order:
          oneOf:
            - $ref: '#/components/schemas/Money'
            - type: 'null'
        payment_terms:
          type: [string, 'null']
        price_list_id:
          type: [string, 'null']
        created_at:
          type: string
          format: date-time
        updated_at:
          type: string
          format: date-time

    BuyerInvitationCreate:
      type: object
      required: [business_name, contact_email]
      properties:
        business_name: {type: string}
        tax_id: {type: string}
        contact_name: {type: string}
        contact_email:
          type: string
          format: email
        contact_phone: {type: string}
        customer_number: {type: string}
        price_list_id: {type: string}

    BuyerInvitation:
      type: object
      properties:
        id: {type: string}
        status:
          type: string
          enum: [sent, accepted, expired]
        onboarding_url:
          type: string
          format: uri
        expires_at:
          type: string
          format: date-time

    SupplierProductUpsert:
      type: object
      required: [name, unit]
      properties:
        name:
          type: string
          maxLength: 200
        unit:
          type: string
          description: Selling unit (unit, kg, l, carton…).
        package_quantity:
          $ref: '#/components/schemas/Quantity'
        category:
          type: string
        barcode:
          type: string
        vat_exempt:
          type: boolean
          default: false
        available:
          type: boolean
          default: true
        image_file_id:
          type: string
        external_ref:
          type: string

    SupplierProduct:
      allOf:
        - $ref: '#/components/schemas/SupplierProductUpsert'
        - type: object
          required: [sku, updated_at]
          properties:
            sku: {type: string}
            available_until:
              type: [string, 'null']
              format: date
            created_at:
              type: string
              format: date-time
            updated_at:
              type: string
              format: date-time
            deleted_at:
              type: [string, 'null']
              format: date-time

    PriceList:
      type: object
      required: [id, name]
      properties:
        id: {type: string}
        name: {type: string}
        currency:
          $ref: '#/components/schemas/Currency'
        buyer_ids:
          type: array
          items: {type: string}
        updated_at:
          type: string
          format: date-time

    PriceEntry:
      type: object
      required: [sku, price]
      properties:
        sku: {type: string}
        price:
          $ref: '#/components/schemas/Money'
        min_quantity:
          $ref: '#/components/schemas/Quantity'
        valid_from:
          type: string
          format: date

    # ---- buyer: reference -----------------------------------------------------

    Branch:
      allOf:
        - $ref: '#/components/schemas/BranchRef'
        - type: object
          properties:
            code: {type: string}
            region: {type: string}
            address: {type: string}
            active: {type: boolean}
            updated_at:
              type: string
              format: date-time

    PurchaseType:
      type: object
      required: [code, name]
      properties:
        code:
          type: string
          examples: ['FC']
        name: {type: string}
        is_default: {type: boolean}

    Supplier:
      type: object
      required: [id, name, vendor_type, counts_in_food_cost, updated_at]
      properties:
        id: {type: string}
        name: {type: string}
        tax_id:
          type: [string, 'null']
        vendor_type:
          $ref: '#/components/schemas/VendorType'
        counts_in_food_cost: {type: boolean}
        vat_status:
          type: string
          enum: [standard, exempt, zero]
        payment_terms:
          type: [string, 'null']
        payment_method:
          type: [string, 'null']
        default_category_id:
          type: [string, 'null']
        default_purchase_type:
          type: [string, 'null']
        credit_account:
          type: [string, 'null']
        merged_into_vendor_id:
          type: [string, 'null']
        active: {type: boolean}
        created_at:
          type: string
          format: date-time
        updated_at:
          type: string
          format: date-time

    BuyerProduct:
      type: object
      required: [id, sku, name, supplier_id, updated_at]
      properties:
        id: {type: string}
        sku: {type: string}
        name: {type: string}
        supplier_id: {type: string}
        product_group:
          type: [string, 'null']
        category_id:
          type: [string, 'null']
          description: Expense category mapped from `product_group`.
        unit: {type: string}
        package_quantity:
          $ref: '#/components/schemas/Quantity'
        current_price:
          oneOf:
            - $ref: '#/components/schemas/Money'
            - type: 'null'
        vat_exempt: {type: boolean}
        inventory_item: {type: boolean}
        active: {type: boolean}
        updated_at:
          type: string
          format: date-time

    ExpenseCategory:
      type: object
      required: [id, name, pl_group, is_active]
      properties:
        id: {type: string}
        parent_id:
          type: [string, 'null']
        name: {type: string}
        pl_group:
          type: string
          enum: [sales_marketing, salaries, general_admin, fixed, delivery_fees, other, finance, taxes]
        default_vat_treatment:
          type: string
          enum: [full, two_thirds, quarter, none]
        is_recurring_fixed: {type: boolean}
        is_active: {type: boolean}

    ExpenseAccount:
      type: object
      required: [code, name]
      properties:
        code:
          type: string
          examples: ['511110200']
        name: {type: string}
        active: {type: boolean}

    AccountMapping:
      type: object
      required: [category_id, expense_account]
      properties:
        category_id: {type: string}
        purchase_type:
          type: [string, 'null']
          description: Null = fallback for any purchase type.
        expense_account: {type: string}

    # ---- buyer: expenses ------------------------------------------------------

    ExpenseStatus:
      type: string
      enum: [draft, pending_approval, approved, exported, cancelled]

    ExpenseLine:
      type: object
      required: [id, category_id, amount_before_vat, vat_amount]
      properties:
        id: {type: string}
        category_id: {type: string}
        purchase_type:
          type: [string, 'null']
        branch_id:
          type: [string, 'null']
        amount_before_vat:
          $ref: '#/components/schemas/Money'
        vat_amount:
          $ref: '#/components/schemas/Money'
        description:
          type: [string, 'null']
        expense_account:
          type: [string, 'null']
          description: Resolved from the account matrix; null = missing mapping (blocks approval).

    ExpenseLineCreate:
      type: object
      required: [category_id, amount_before_vat]
      properties:
        category_id: {type: string}
        purchase_type: {type: string}
        branch_id: {type: string}
        amount_before_vat:
          $ref: '#/components/schemas/Money'
        vat_amount:
          $ref: '#/components/schemas/Money'
        description:
          type: string
          maxLength: 200

    Expense:
      type: object
      required: [id, vendor_id, doc_type, doc_date, branch_id, vat_mode, currency, totals, status, source, lines, created_at, updated_at]
      properties:
        id: {type: string}
        vendor_id: {type: string}
        vendor:
          $ref: '#/components/schemas/PartyRef'
        merchant_name:
          type: [string, 'null']
          description: Free text; required when the vendor is a card.
        doc_type:
          type: string
          enum: [tax_invoice, invoice_receipt, receipt, payment_request, charge_notice, card_txn, credit_note, none]
        doc_number:
          type: [string, 'null']
        allocation_number:
          type: [string, 'null']
        doc_date:
          type: string
          format: date
        due_date:
          type: [string, 'null']
          format: date
        accounting_period:
          type: string
          description: YYYY-MM.
        branch_id: {type: string}
        purchase_type:
          type: [string, 'null']
        vat_mode:
          type: string
          enum: [with_vat, exempt, no_tax_doc]
        currency:
          $ref: '#/components/schemas/Currency'
        totals:
          allOf:
            - $ref: '#/components/schemas/Totals'
            - type: object
              properties:
                deductible_vat:
                  $ref: '#/components/schemas/Money'
        payment_method:
          type: [string, 'null']
        payment_status:
          type: string
          enum: [unpaid, partially_paid, paid]
        paid_at:
          type: [string, 'null']
          format: date-time
        status:
          $ref: '#/components/schemas/ExpenseStatus'
        source:
          type: string
          enum: [manual, recurring, ocr, email, card_import, migration, api]
        lines:
          type: array
          items:
            $ref: '#/components/schemas/ExpenseLine'
        files:
          type: array
          items:
            $ref: '#/components/schemas/FileRef'
        notes:
          type: [string, 'null']
        approved_by:
          type: [string, 'null']
        approved_at:
          type: [string, 'null']
          format: date-time
        exported_at:
          type: [string, 'null']
          format: date-time
        external_ref:
          type: [string, 'null']
        created_at:
          type: string
          format: date-time
        updated_at:
          type: string
          format: date-time
        deleted_at:
          type: [string, 'null']
          format: date-time

    ExpenseCreate:
      type: object
      required: [vendor_id, doc_type, doc_date, branch_id, vat_mode, totals, lines]
      properties:
        vendor_id: {type: string}
        merchant_name:
          type: string
          maxLength: 120
        doc_type:
          type: string
          enum: [tax_invoice, invoice_receipt, receipt, payment_request, charge_notice, card_txn, credit_note, none]
        doc_number:
          type: string
          maxLength: 64
        allocation_number:
          type: string
          maxLength: 32
        doc_date:
          type: string
          format: date
        due_date:
          type: string
          format: date
        accounting_period:
          type: string
          pattern: '^\d{4}-(0[1-9]|1[0-2])$'
        branch_id: {type: string}
        purchase_type: {type: string}
        vat_mode:
          type: string
          enum: [with_vat, exempt, no_tax_doc]
        totals:
          allOf:
            - $ref: '#/components/schemas/Totals'
            - type: object
              properties:
                deductible_vat:
                  $ref: '#/components/schemas/Money'
        payment_method: {type: string}
        lines:
          type: array
          minItems: 1
          items:
            $ref: '#/components/schemas/ExpenseLineCreate'
        file_ids:
          type: array
          items: {type: string}
        notes:
          type: string
          maxLength: 1000
        external_ref:
          type: string
          maxLength: 64
        submit:
          type: boolean
          default: false
          description: When true, moves the expense out of `draft` (to `pending_approval` or `approved` per the buyer's rules).

    CardTransaction:
      type: object
      required: [id, external_id, txn_date, merchant, amount, currency]
      properties:
        id: {type: string}
        external_id:
          type: string
          description: Id from the card provider feed. Unique per account.
        card_last4: {type: string}
        txn_date:
          type: string
          format: date
        merchant: {type: string}
        amount:
          $ref: '#/components/schemas/Money'
        currency:
          type: string
        expense_id:
          type: [string, 'null']
        suggested_category_id:
          type: [string, 'null']
        imported_at:
          type: string
          format: date-time
        updated_at:
          type: string
          format: date-time

    CardTransactionCreate:
      type: object
      required: [external_id, txn_date, merchant, amount, currency]
      properties:
        external_id: {type: string}
        card_last4: {type: string}
        txn_date:
          type: string
          format: date
        merchant: {type: string}
        amount:
          $ref: '#/components/schemas/Money'
        currency:
          type: string
          default: ILS
        vendor_id:
          type: string
          description: Card vendor (type=card). Defaults to the account's default card vendor.

    # ---- buyer: inventory & sales ---------------------------------------------

    InventoryCount:
      type: object
      required: [id, branch_id, counted_at, status, updated_at]
      properties:
        id: {type: string}
        branch_id: {type: string}
        counted_at:
          type: string
          format: date-time
        status:
          type: string
          enum: [open, completed]
        total_value:
          $ref: '#/components/schemas/Money'
        lines:
          type: array
          description: Present with `expand=lines` or on GET by id.
          items:
            type: object
            properties:
              product_id: {type: string}
              sku: {type: string}
              name: {type: string}
              quantity:
                $ref: '#/components/schemas/Quantity'
              unit: {type: string}
              unit_cost:
                $ref: '#/components/schemas/Money'
              value:
                $ref: '#/components/schemas/Money'
        updated_at:
          type: string
          format: date-time

    ZReportCreate:
      type: object
      required: [branch_id, z_number, closed_at, total_before_vat]
      properties:
        branch_id: {type: string}
        z_number: {type: string}
        closed_at:
          type: string
          format: date-time
        description: {type: string}
        total_before_vat:
          $ref: '#/components/schemas/Money'
        orders_count: {type: integer}
        dine_in_total:
          $ref: '#/components/schemas/Money'
        dine_in_count: {type: integer}
        take_away_total:
          $ref: '#/components/schemas/Money'
        take_away_count: {type: integer}
        tips:
          $ref: '#/components/schemas/Money'
        cash_total:
          $ref: '#/components/schemas/Money'
        card_total:
          $ref: '#/components/schemas/Money'
        cheque_total:
          $ref: '#/components/schemas/Money'
        discounts_total:
          $ref: '#/components/schemas/Money'

    ZReport:
      allOf:
        - $ref: '#/components/schemas/ZReportCreate'
        - type: object
          required: [id, updated_at]
          properties:
            id: {type: string}
            created_at:
              type: string
              format: date-time
            updated_at:
              type: string
              format: date-time

    ItemSaleCreate:
      type: object
      required: [branch_id, z_number, value_date, item_code, item_name, quantity, unit_price, total]
      properties:
        branch_id: {type: string}
        z_number: {type: string}
        value_date:
          type: string
          format: date-time
        item_code: {type: string}
        item_name: {type: string}
        quantity:
          $ref: '#/components/schemas/Quantity'
        unit_price:
          $ref: '#/components/schemas/Money'
        total:
          $ref: '#/components/schemas/Money'
        line_number: {type: integer}
        main_category: {type: string}
        sub_category: {type: string}

    ItemSale:
      allOf:
        - $ref: '#/components/schemas/ItemSaleCreate'
        - type: object
          required: [id]
          properties:
            id: {type: string}
            updated_at:
              type: string
              format: date-time

    # ---- buyer: accounting ----------------------------------------------------

    JournalLine:
      type: object
      required: [id, document_id, source, side, account, amount, accounting_period, status, updated_at]
      properties:
        id: {type: string}
        document_id: {type: string}
        document_type:
          type: string
          description: DocumentType or expense doc_type.
        document_number: {type: string}
        source:
          $ref: '#/components/schemas/DocumentSource'
        side:
          type: string
          enum: [debit, credit, vat]
        account:
          type: string
          description: Expense account (debit), vendor credit account (credit) or input-VAT account (vat).
        counter_account:
          type: [string, 'null']
        amount:
          $ref: '#/components/schemas/Money'
        currency:
          $ref: '#/components/schemas/Currency'
        description: {type: string}
        vendor_id: {type: string}
        vendor_tax_id:
          type: [string, 'null']
        branch_id: {type: string}
        branch_tax_id:
          type: [string, 'null']
        purchase_type:
          type: [string, 'null']
        category_id:
          type: [string, 'null']
        reference:
          type: string
          description: Document number.
        allocation_number:
          type: [string, 'null']
        doc_date:
          type: string
          format: date
        accounting_period:
          type: string
          description: YYYY-MM (value date).
        movement_type_code:
          type: [string, 'null']
        batch_number:
          type: [string, 'null']
        status:
          $ref: '#/components/schemas/ExportStatus'
        export_batch_id:
          type: [string, 'null']
        exported_at:
          type: [string, 'null']
          format: date-time
        updated_at:
          type: string
          format: date-time

    AccountingExportCreate:
      type: object
      required: [journal_line_ids]
      properties:
        journal_line_ids:
          type: array
          minItems: 1
          maxItems: 5000
          items: {type: string}
        target:
          type: string
          description: Free label of the destination system (e.g. "meshek", "hashavshevet").
        external_ref:
          type: string

    AccountingExport:
      type: object
      required: [id, line_count, exported_at, exported_by]
      properties:
        id:
          type: string
          description: export_batch_id.
        line_count: {type: integer}
        target:
          type: [string, 'null']
        external_ref:
          type: [string, 'null']
        exported_at:
          type: string
          format: date-time
        exported_by:
          type: object
          properties:
            client_id: {type: string}
            client_name: {type: string}

    # ---- reports --------------------------------------------------------------

    ReportResult:
      type: object
      required: [from, to, generated_at, rows]
      properties:
        from:
          type: string
          format: date
        to:
          type: string
          format: date
        generated_at:
          type: string
          format: date-time
        currency:
          $ref: '#/components/schemas/Currency'
        dimensions:
          type: array
          items: {type: string}
        rows:
          type: array
          items:
            type: object
            description: Keys are the requested dimensions plus metric names (e.g. `purchases`, `sales`, `food_cost_pct`).
            additionalProperties: true
        totals:
          type: object
          additionalProperties: true

    # ---- platform -------------------------------------------------------------

    Event:
      type: object
      required: [id, type, api_version, occurred_at, account_id, data]
      properties:
        id:
          type: string
          examples: ['evt_01J9KQ7X4M9C']
        type:
          type: string
          enum:
            - ping
            - order.created
            - order.updated
            - order.cancelled
            - order.status_changed
            - document.created
            - document.updated
            - document.received
            - document.disputed
            - document.approved_for_export
            - expense.created
            - expense.approved
            - journal_lines.approved_for_export
            - inventory_count.completed
            - supplier.updated
            - card_transaction.imported
            - buyer.linked
            - price_list.assigned
            - api_client.suspended
            - webhook.disabled
        api_version:
          type: string
          enum: ['2']
        occurred_at:
          type: string
          format: date-time
        account_id: {type: string}
        data:
          type: object
          description: The resource as it was at the time of the event. Always includes `object` and `id`.
          required: [object, id]
          properties:
            object:
              type: string
              enum: [order, document, expense, journal_lines, inventory_count, supplier, card_transaction, buyer, price_list, api_client, webhook, ping]
            id: {type: string}
          additionalProperties: true

    WebhookCreate:
      type: object
      required: [url, events]
      properties:
        url:
          type: string
          format: uri
          pattern: '^https://'
        events:
          type: array
          minItems: 1
          items: {type: string}
        description:
          type: string
          maxLength: 200

    WebhookUpdate:
      type: object
      properties:
        url:
          type: string
          format: uri
          pattern: '^https://'
        events:
          type: array
          items: {type: string}
        status:
          type: string
          enum: [active, disabled]
        description:
          type: string

    Webhook:
      type: object
      required: [id, url, events, status, created_at]
      properties:
        id: {type: string}
        url:
          type: string
          format: uri
        events:
          type: array
          items: {type: string}
        status:
          type: string
          enum: [active, disabled, failing]
        description:
          type: [string, 'null']
        last_delivery_at:
          type: [string, 'null']
          format: date-time
        consecutive_failures: {type: integer}
        created_at:
          type: string
          format: date-time

    WebhookWithSecret:
      allOf:
        - $ref: '#/components/schemas/Webhook'
        - type: object
          required: [secret]
          properties:
            secret:
              type: string
              description: HMAC-SHA256 signing secret. Shown once.

    WebhookDelivery:
      type: object
      required: [id, event_id, event_type, attempt, status]
      properties:
        id: {type: string}
        event_id: {type: string}
        event_type: {type: string}
        attempt: {type: integer}
        status:
          type: string
          enum: [pending, succeeded, failed]
        response_status:
          type: [integer, 'null']
        response_time_ms:
          type: [integer, 'null']
        response_snippet:
          type: [string, 'null']
          maxLength: 500
        error:
          type: [string, 'null']
        attempted_at:
          type: string
          format: date-time
        next_retry_at:
          type: [string, 'null']
          format: date-time

    FileObject:
      allOf:
        - $ref: '#/components/schemas/FileRef'
        - type: object
          properties:
            purpose: {type: string}
            sha256: {type: string}
            created_at:
              type: string
              format: date-time
            expires_at:
              type: [string, 'null']
              format: date-time
              description: When unreferenced files are purged.

    ExportJobCreate:
      type: object
      required: [resources]
      properties:
        resources:
          type: array
          minItems: 1
          items:
            type: string
            enum: [orders, documents, expenses, journal_lines, suppliers, products, inventory_counts, z_reports, item_sales, card_transactions]
        from:
          type: string
          format: date
        to:
          type: string
          format: date
        format:
          type: string
          enum: [ndjson, csv, parquet]
          default: ndjson
        branch_ids:
          type: array
          items: {type: string}

    ExportJob:
      type: object
      required: [id, status, created_at]
      properties:
        id: {type: string}
        status:
          type: string
          enum: [queued, running, completed, failed, expired]
        request:
          $ref: '#/components/schemas/ExportJobCreate'
        files:
          type: array
          items:
            type: object
            properties:
              resource: {type: string}
              download_url:
                type: string
                format: uri
              size_bytes: {type: integer}
              row_count: {type: integer}
              expires_at:
                type: string
                format: date-time
        error:
          type: [string, 'null']
        created_at:
          type: string
          format: date-time
        completed_at:
          type: [string, 'null']
          format: date-time

    AuditRequest:
      type: object
      required: [request_id, timestamp, client_id, method, path, status]
      properties:
        request_id: {type: string}
        timestamp:
          type: string
          format: date-time
        environment:
          type: string
          enum: [live, sandbox]
        client_id: {type: string}
        client_name: {type: string}
        key_id: {type: string}
        method: {type: string}
        path: {type: string}
        query:
          type: [string, 'null']
          description: Sensitive parameters redacted.
        status: {type: integer}
        error_code:
          type: [string, 'null']
        latency_ms: {type: integer}
        ip: {type: string}
        user_agent:
          type: [string, 'null']
        request_bytes: {type: integer}
        response_bytes: {type: integer}
        scope_used:
          type: [string, 'null']
