openapi: 3.1.0

info:
  title: shopVOX Public API
  version: "1.0.0"
  license:
    name: Proprietary
    url: https://www.shopvox.com/terms
  description: |
    Public REST API for integrating with shopVOX. This spec is hand-authored from the live
    jbuilder responses and controllers and replaces the legacy API Blueprint hosted on Apiary.

    ## Conventions

    - **Everything is JSON.** Send `Content-Type: application/json`; responses are JSON
      (signature uploads use `multipart/form-data`).
    - **Responses use `camelCase` keys** (`primaryEmail`, `createdAt`). **Write payloads use
      `snake_case`** (`primary_email`, `term_code_id`) as shown per endpoint.
    - **Money:**
      - `*InDollars` fields are US dollars (decimal) - read these directly.
      - `*InCents` fields are **not** cents: shopVOX stores money as a 4-decimal fixed-point
        integer, so `*InCents` = `dollars * 10000`. Divide by 10000, not 100.
    - **IDs are UUID strings.**
    - **Timestamps:** list endpoints emit ISO-8601; some detail endpoints emit localized
      display strings (e.g. `Jul 03, 2026 09:15 AM`). Noted per schema where it differs.

    ## Plans (paid vs free)

    Access to each endpoint depends on the account's subscription. Endpoints are labelled
    with a **Plan** line and a machine-readable `x-plan` extension where the tier is known:

    | `x-plan`     | Label          | Meaning                                                        |
    |--------------|----------------|----------------------------------------------------------------|
    | `all`        | All plans      | Available on every plan, including free. No add-on needed.      |
    | `pro`        | Pro            | Requires the Pro plan.                                          |
    | `paid_api`   | Paid API       | Requires the paid API add-on.                                  |
    | `restricted` | Restricted     | Off by default - shopVOX support must enable it for the account.|

    A request to an endpoint the account is not entitled to fails authorization.

    ## Pagination & filter gating

    Index endpoints return a pagination block (`stats`, except `assets` which uses `meta`, and
    `orders` which returns a bare array with no wrapper). Unless the account has the
    `index_api_enabled` feature flag, an index request must include at least one filter
    parameter or it returns `401`, and results are capped to 5 per page.

    ## Webhooks

    shopVOX can push events to your server. See the **Webhooks** section at the bottom for the
    payload shape, supported events, setup, and retry behavior. Webhooks are configured in the
    app (Account Settings -> Webhook Endpoints), not through this API.

servers:
  - url: https://api.shopvox.com/v1
    description: Production

security:
  - accountId: []
    authToken: []

tags:
  - name: Auth
    description: Obtain an authentication token and verify a connection.
  - name: Companies
    description: Customer/company records and their contacts, addresses, emails and phones.
  - name: Contacts
    description: People belonging to a company.
  - name: Quotes
    description: Quotes (read-only via the public API).
  - name: Sales Orders
    description: Sales orders (work orders) - the confirmed order stage.
  - name: Invoices
    description: Invoices and their signatures (read-only apart from signature upload).
  - name: Payments
    description: Payments and their allocation to invoices/sales orders.
  - name: Jobs
    description: Production jobs.
  - name: Job Steps
    description: Start, stop and complete steps on a production job.
  - name: Sales Leads
    description: CRM leads and pipeline board.
  - name: Notes
    description: Notes attached to any parent record.
  - name: Assets
    description: Files attached to a record.
  - name: Orders
    description: Legacy in-process orders feed (Zapier).
  - name: Webhooks
    description: Events shopVOX pushes to your server.

paths:
  #########################################################################
  # Auth
  #########################################################################
  /login:
    post:
      tags: [Auth]
      summary: Log in
      description: |
        **Plan: All plans.**

        Exchange user credentials for an auth token. The token is tied to one user and one
        account. Treat it as expirable: on a later `401`, log in again and retry once. Send
        `account_id` + `authToken` on every other request.
      operationId: login
      x-plan: all
      security: []
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [user, password]
              properties:
                user: { type: string, example: jane@example.com }
                password: { type: string, format: password }
      responses:
        "200":
          description: Authenticated.
          content:
            application/json:
              schema: { $ref: "#/components/schemas/LoginResponse" }
        "401":
          description: Invalid e-mail or password.
          content:
            application/json:
              schema: { type: string, example: "E-mail or password is invalid." }

  /connections:
    get:
      tags: [Auth]
      summary: Verify connection
      description: |
        **Plan: All plans.**

        Cheap authenticated call to confirm credentials work. Used by Zapier.
      operationId: getConnection
      x-plan: all
      responses:
        "200":
          description: OK
          content:
            application/json:
              schema:
                type: object
                properties:
                  status: { type: string, example: ok }
                  name: { type: string, description: Authenticated user's name. }
                  account: { type: string, description: Account business name. }
        "401": { $ref: "#/components/responses/Unauthorized" }

  #########################################################################
  # Companies
  #########################################################################
  /companies:
    get:
      tags: [Companies]
      summary: List companies
      description: |
        **Plan: Pro.** At least one filter parameter is required.
      operationId: listCompanies
      x-plan: pro
      parameters:
        - { name: name, in: query, required: false, schema: { type: string }, description: Company name. }
        - { name: active, in: query, required: false, schema: { type: boolean }, description: Active flag. }
        - $ref: "#/components/parameters/Page"
        - $ref: "#/components/parameters/PerPage"
      responses:
        "200":
          description: OK
          content:
            application/json:
              schema:
                type: object
                properties:
                  stats: { $ref: "#/components/schemas/PaginationStats" }
                  companies:
                    type: array
                    items: { $ref: "#/components/schemas/CompanyListItem" }
        "401": { $ref: "#/components/responses/Unauthorized" }
    post:
      tags: [Companies]
      summary: Create a company
      description: "**Plan: Pro.**"
      operationId: createCompany
      x-plan: pro
      requestBody:
        required: true
        content:
          application/json:
            schema: { $ref: "#/components/schemas/CompanyWrite" }
      responses:
        "200":
          description: Created.
          content:
            application/json:
              schema: { $ref: "#/components/schemas/Company" }
        "422": { $ref: "#/components/responses/ValidationError" }

  /companies/{id}:
    parameters:
      - $ref: "#/components/parameters/IdPath"
    get:
      tags: [Companies]
      summary: Get a company
      description: "**Plan: Paid API.**"
      operationId: getCompany
      x-plan: paid_api
      responses:
        "200":
          description: OK
          content:
            application/json:
              schema: { $ref: "#/components/schemas/Company" }
        "401": { $ref: "#/components/responses/Unauthorized" }
    patch:
      tags: [Companies]
      summary: Update a company
      description: "**Plan: Paid API.**"
      operationId: updateCompany
      x-plan: paid_api
      requestBody:
        required: true
        content:
          application/json:
            schema: { $ref: "#/components/schemas/CompanyWrite" }
      responses:
        "200":
          description: Updated.
          content:
            application/json:
              schema: { $ref: "#/components/schemas/Company" }
        "422": { $ref: "#/components/responses/ValidationError" }
    delete:
      tags: [Companies]
      summary: Deactivate a company
      description: |
        **Plan: Paid API.**

        Soft-deletes (deactivates) the company and returns its final state.
      operationId: deleteCompany
      x-plan: paid_api
      responses:
        "200":
          description: Deactivated.
          content:
            application/json:
              schema: { $ref: "#/components/schemas/Company" }
        "401": { $ref: "#/components/responses/Unauthorized" }

  #########################################################################
  # Contacts
  #########################################################################
  /contacts:
    get:
      tags: [Contacts]
      summary: List contacts
      description: |
        **Plan: Pro.** At least one filter parameter is required.
      operationId: listContacts
      x-plan: pro
      parameters:
        - { name: company_id, in: query, required: false, schema: { type: string, format: uuid }, description: Company id. }
        - { name: active, in: query, required: false, schema: { type: boolean }, description: Active flag. }
        - { name: name, in: query, required: false, schema: { type: string }, description: Contact name. }
        - { name: search, in: query, required: false, schema: { type: string }, description: Search in name or email. }
        - { name: with_email, in: query, required: false, schema: { type: string }, description: "If present, only contacts that have an email." }
        - { name: cportal, in: query, required: false, schema: { type: boolean }, description: Customer-portal-enabled contacts. }
        - $ref: "#/components/parameters/Page"
        - $ref: "#/components/parameters/PerPage"
      responses:
        "200":
          description: OK
          content:
            application/json:
              schema:
                type: object
                properties:
                  stats: { $ref: "#/components/schemas/PaginationStats" }
                  contacts:
                    type: array
                    items: { $ref: "#/components/schemas/ContactListItem" }
        "401": { $ref: "#/components/responses/Unauthorized" }
    post:
      tags: [Contacts]
      summary: Create a contact
      description: |
        **Plan: Pro.**

        Requires a company. Pass `company_id`, or `company_name` to find-or-create the company
        by name.
      operationId: createContact
      x-plan: pro
      requestBody:
        required: true
        content:
          application/json:
            schema: { $ref: "#/components/schemas/ContactWrite" }
      responses:
        "200":
          description: Created.
          content:
            application/json:
              schema: { $ref: "#/components/schemas/Contact" }
        "422": { $ref: "#/components/responses/ValidationError" }

  /contacts/{id}:
    parameters:
      - $ref: "#/components/parameters/IdPath"
    get:
      tags: [Contacts]
      summary: Get a contact
      description: "**Plan: Paid API.**"
      operationId: getContact
      x-plan: paid_api
      responses:
        "200":
          description: OK
          content:
            application/json:
              schema: { $ref: "#/components/schemas/Contact" }
        "401": { $ref: "#/components/responses/Unauthorized" }
    patch:
      tags: [Contacts]
      summary: Update a contact
      description: "**Plan: Paid API.**"
      operationId: updateContact
      x-plan: paid_api
      requestBody:
        required: true
        content:
          application/json:
            schema: { $ref: "#/components/schemas/ContactWrite" }
      responses:
        "200":
          description: Updated.
          content:
            application/json:
              schema: { $ref: "#/components/schemas/Contact" }
        "422": { $ref: "#/components/responses/ValidationError" }
    delete:
      tags: [Contacts]
      summary: Deactivate a contact
      description: "**Plan: Paid API.**"
      operationId: deleteContact
      x-plan: paid_api
      responses:
        "200":
          description: Deactivated.
          content:
            application/json:
              schema: { $ref: "#/components/schemas/Contact" }
        "401": { $ref: "#/components/responses/Unauthorized" }

  #########################################################################
  # Quotes
  #########################################################################
  /quotes:
    get:
      tags: [Quotes]
      summary: List quotes
      description: "**Plan: Paid API.**"
      operationId: listQuotes
      x-plan: paid_api
      parameters:
        - $ref: "#/components/parameters/Page"
        - $ref: "#/components/parameters/PerPage"
      responses:
        "200":
          description: OK
          content:
            application/json:
              schema:
                type: object
                properties:
                  stats: { $ref: "#/components/schemas/QuoteListStats" }
                  quotes:
                    type: array
                    items: { $ref: "#/components/schemas/QuoteListItem" }
        "401": { $ref: "#/components/responses/Unauthorized" }

  /quotes/{id}:
    parameters:
      - $ref: "#/components/parameters/IdPath"
    get:
      tags: [Quotes]
      summary: Get a quote
      description: "**Plan: Paid API.**"
      operationId: getQuote
      x-plan: paid_api
      responses:
        "200":
          description: OK
          content:
            application/json:
              schema: { $ref: "#/components/schemas/Quote" }
        "401": { $ref: "#/components/responses/Unauthorized" }

  #########################################################################
  # Sales Orders
  #########################################################################
  /sales_orders:
    get:
      tags: [Sales Orders]
      summary: List sales orders
      description: "**Plan: Paid API.**"
      operationId: listSalesOrders
      x-plan: paid_api
      parameters:
        - $ref: "#/components/parameters/Page"
        - $ref: "#/components/parameters/PerPage"
        - $ref: "#/components/parameters/TxnDateFrom"
        - $ref: "#/components/parameters/TxnDateTo"
      responses:
        "200":
          description: OK
          content:
            application/json:
              schema:
                type: object
                properties:
                  stats: { $ref: "#/components/schemas/PaginationStats" }
                  salesOrders:
                    type: array
                    items: { $ref: "#/components/schemas/SalesOrderListItem" }
        "401": { $ref: "#/components/responses/Unauthorized" }
        "422": { $ref: "#/components/responses/ValidationError" }
    post:
      tags: [Sales Orders]
      summary: Create a sales order
      description: "**Plan: Pro.**"
      operationId: createSalesOrder
      x-plan: pro
      requestBody:
        required: true
        content:
          application/json:
            schema: { $ref: "#/components/schemas/SalesOrderWrite" }
      responses:
        "200":
          description: Created.
          content:
            application/json:
              schema: { $ref: "#/components/schemas/SalesOrder" }
        "422": { $ref: "#/components/responses/ValidationError" }

  /sales_orders/{id}:
    parameters:
      - $ref: "#/components/parameters/IdPath"
    get:
      tags: [Sales Orders]
      summary: Get a sales order
      description: |
        **Plan: Paid API.**

        May also be looked up by `txn_number` query param instead of the id.
      operationId: getSalesOrder
      x-plan: paid_api
      parameters:
        - { name: txn_number, in: query, required: false, schema: { type: string } }
      responses:
        "200":
          description: OK
          content:
            application/json:
              schema: { $ref: "#/components/schemas/SalesOrder" }
        "401": { $ref: "#/components/responses/Unauthorized" }
        "404": { description: Not found. }
    patch:
      tags: [Sales Orders]
      summary: Update a sales order
      description: |
        **Plan: Paid API.**

        Same payload as create, minus `line_items` (header fields only).
      operationId: updateSalesOrder
      x-plan: paid_api
      requestBody:
        required: true
        content:
          application/json:
            schema: { $ref: "#/components/schemas/SalesOrderWrite" }
      responses:
        "200":
          description: Updated.
          content:
            application/json:
              schema: { $ref: "#/components/schemas/SalesOrder" }
        "422": { $ref: "#/components/responses/ValidationError" }

  /sales_orders/{id}/signature:
    parameters:
      - $ref: "#/components/parameters/IdPath"
    post:
      tags: [Sales Orders]
      summary: Attach a signature
      description: "**Plan: Paid API.**"
      operationId: signSalesOrder
      x-plan: paid_api
      requestBody:
        required: true
        content:
          multipart/form-data:
            schema: { $ref: "#/components/schemas/SignatureUpload" }
      responses:
        "200":
          description: Signed.
          content:
            application/json:
              schema: { $ref: "#/components/schemas/SalesOrder" }
        "422": { $ref: "#/components/responses/ValidationError" }

  #########################################################################
  # Invoices
  #########################################################################
  /invoices:
    get:
      tags: [Invoices]
      summary: List invoices
      description: "**Plan: Paid API.**"
      operationId: listInvoices
      x-plan: paid_api
      parameters:
        - $ref: "#/components/parameters/Page"
        - $ref: "#/components/parameters/PerPage"
      responses:
        "200":
          description: OK
          content:
            application/json:
              schema:
                type: object
                properties:
                  stats: { $ref: "#/components/schemas/PaginationStats" }
                  invoices:
                    type: array
                    items: { $ref: "#/components/schemas/InvoiceListItem" }
        "401": { $ref: "#/components/responses/Unauthorized" }

  /invoices/{id}:
    parameters:
      - $ref: "#/components/parameters/IdPath"
    get:
      tags: [Invoices]
      summary: Get an invoice
      description: |
        **Plan: Paid API.**

        May also be looked up by `txn_number` query param instead of the id.
      operationId: getInvoice
      x-plan: paid_api
      parameters:
        - { name: txn_number, in: query, required: false, schema: { type: string } }
      responses:
        "200":
          description: OK
          content:
            application/json:
              schema: { $ref: "#/components/schemas/Invoice" }
        "401": { $ref: "#/components/responses/Unauthorized" }
        "404": { description: Not found. }

  /invoices/{id}/signature:
    parameters:
      - $ref: "#/components/parameters/IdPath"
    post:
      tags: [Invoices]
      summary: Attach a signature
      description: "**Plan: Paid API.**"
      operationId: signInvoice
      x-plan: paid_api
      requestBody:
        required: true
        content:
          multipart/form-data:
            schema: { $ref: "#/components/schemas/SignatureUpload" }
      responses:
        "200":
          description: Signed.
          content:
            application/json:
              schema: { $ref: "#/components/schemas/Invoice" }
        "422": { $ref: "#/components/responses/ValidationError" }

  #########################################################################
  # Payments
  #########################################################################
  /payments:
    get:
      tags: [Payments]
      summary: List payments
      description: "**Plan: Paid API.**"
      operationId: listPayments
      x-plan: paid_api
      parameters:
        - $ref: "#/components/parameters/Page"
        - $ref: "#/components/parameters/PerPage"
      responses:
        "200":
          description: OK
          content:
            application/json:
              schema:
                type: object
                properties:
                  stats: { $ref: "#/components/schemas/PaginationStats" }
                  payments:
                    type: array
                    items: { $ref: "#/components/schemas/PaymentListItem" }
        "401": { $ref: "#/components/responses/Unauthorized" }
    post:
      tags: [Payments]
      summary: Record a payment
      description: "**Plan: Pro.**"
      operationId: createPayment
      x-plan: pro
      requestBody:
        required: true
        content:
          application/json:
            schema: { $ref: "#/components/schemas/PaymentWrite" }
      responses:
        "200":
          description: Created.
          content:
            application/json:
              schema: { $ref: "#/components/schemas/Payment" }
        "422": { $ref: "#/components/responses/ValidationError" }

  #########################################################################
  # Jobs
  #########################################################################
  /jobs:
    get:
      tags: [Jobs]
      summary: List jobs
      description: |
        **Plan: Restricted.** Usage of this endpoint is off by default and must be enabled for
        the account - contact shopVOX support. As with every index endpoint, `txn_type` /
        `txn_number` are required unless the account also has the `index_api_enabled` flag
        (see Pagination & filter gating).
      operationId: listJobs
      x-plan: restricted
      parameters:
        - name: txn_type
          in: query
          required: false
          schema: { type: string, enum: [invoice, sales_order, quote] }
          description: Filter by the transaction the job belongs to.
        - name: txn_number
          in: query
          required: false
          schema: { type: integer }
          description: Transaction number, e.g. 1001.
        - $ref: "#/components/parameters/Page"
        - $ref: "#/components/parameters/PerPage"
      responses:
        "200":
          description: OK
          content:
            application/json:
              schema:
                type: object
                properties:
                  stats: { $ref: "#/components/schemas/PaginationStats" }
                  jobs:
                    type: array
                    items: { $ref: "#/components/schemas/Job" }
        "401": { $ref: "#/components/responses/Unauthorized" }
    post:
      tags: [Jobs]
      summary: Create a job
      description: "**Plan: Pro.**"
      operationId: createJob
      x-plan: pro
      requestBody:
        required: true
        content:
          application/json:
            schema: { $ref: "#/components/schemas/JobWrite" }
      responses:
        "200":
          description: Created.
          content:
            application/json:
              schema: { $ref: "#/components/schemas/Job" }
        "422": { $ref: "#/components/responses/ValidationError" }

  /jobs/{id}:
    parameters:
      - $ref: "#/components/parameters/IdPath"
    get:
      tags: [Jobs]
      summary: Get a job
      description: |
        **Plan: Paid API.**

        Returns the job plus a flattened `steps` summary array.
      operationId: getJob
      x-plan: paid_api
      responses:
        "200":
          description: OK
          content:
            application/json:
              schema:
                allOf:
                  - $ref: "#/components/schemas/Job"
                  - type: object
                    properties:
                      steps:
                        type: array
                        items:
                          type: object
                          properties:
                            id: { type: string, format: uuid }
                            name: { type: string }
                            state: { type: string }
                            assignedToName: { type: string }
                            machineName: { type: string }
                            estimatedTime: { type: [number, "null"] }
        "401": { $ref: "#/components/responses/Unauthorized" }

  #########################################################################
  # Job Steps
  #########################################################################
  /jobs/{job_id}/steps/{id}/start:
    parameters:
      - $ref: "#/components/parameters/JobIdPath"
      - $ref: "#/components/parameters/IdPath"
    post:
      tags: [Job Steps]
      summary: Start a job step
      description: "**Plan: Paid API.**"
      operationId: startJobStep
      x-plan: paid_api
      responses:
        "200":
          description: Started.
          content:
            application/json:
              schema: { $ref: "#/components/schemas/JobStep" }
        "422": { description: Transition not allowed from the current state. }

  /jobs/{job_id}/steps/{id}/stop:
    parameters:
      - $ref: "#/components/parameters/JobIdPath"
      - $ref: "#/components/parameters/IdPath"
    post:
      tags: [Job Steps]
      summary: Stop (pause) a job step
      description: "**Plan: Paid API.**"
      operationId: stopJobStep
      x-plan: paid_api
      responses:
        "200":
          description: Stopped.
          content:
            application/json:
              schema: { $ref: "#/components/schemas/JobStep" }
        "422": { description: Transition not allowed from the current state. }

  /jobs/{job_id}/steps/{id}/complete:
    parameters:
      - $ref: "#/components/parameters/JobIdPath"
      - $ref: "#/components/parameters/IdPath"
    post:
      tags: [Job Steps]
      summary: Complete a job step
      description: "**Plan: Paid API.**"
      operationId: completeJobStep
      x-plan: paid_api
      responses:
        "200":
          description: Completed.
          content:
            application/json:
              schema: { $ref: "#/components/schemas/JobStep" }
        "422": { description: Transition not allowed from the current state. }

  #########################################################################
  # Sales Leads
  #########################################################################
  /sales_leads:
    get:
      tags: [Sales Leads]
      summary: List sales leads
      description: "**Plan: Paid API.**"
      operationId: listSalesLeads
      x-plan: paid_api
      parameters:
        - $ref: "#/components/parameters/Page"
        - $ref: "#/components/parameters/PerPage"
      responses:
        "200":
          description: OK
          content:
            application/json:
              schema:
                type: object
                properties:
                  stats: { $ref: "#/components/schemas/PaginationStats" }
                  salesLeads:
                    type: array
                    items: { $ref: "#/components/schemas/SalesLead" }
        "401": { $ref: "#/components/responses/Unauthorized" }
    post:
      tags: [Sales Leads]
      summary: Create a sales lead
      description: |
        **Plan: Pro.**

        Flat (non-wrapped) `snake_case` payload.
      operationId: createSalesLead
      x-plan: pro
      requestBody:
        required: true
        content:
          application/json:
            schema: { $ref: "#/components/schemas/SalesLeadWrite" }
      responses:
        "200":
          description: Created.
          content:
            application/json:
              schema: { $ref: "#/components/schemas/SalesLead" }
        "422": { $ref: "#/components/responses/ValidationError" }

  /sales_leads/{id}:
    parameters:
      - $ref: "#/components/parameters/IdPath"
    get:
      tags: [Sales Leads]
      summary: Get a sales lead
      description: "**Plan: Paid API.**"
      operationId: getSalesLead
      x-plan: paid_api
      responses:
        "200":
          description: OK
          content:
            application/json:
              schema: { $ref: "#/components/schemas/SalesLead" }
        "401": { $ref: "#/components/responses/Unauthorized" }
    patch:
      tags: [Sales Leads]
      summary: Update a sales lead
      description: "**Plan: Paid API.**"
      operationId: updateSalesLead
      x-plan: paid_api
      requestBody:
        required: true
        content:
          application/json:
            schema: { $ref: "#/components/schemas/SalesLeadWrite" }
      responses:
        "200":
          description: Updated.
          content:
            application/json:
              schema: { $ref: "#/components/schemas/SalesLead" }
        "422": { $ref: "#/components/responses/ValidationError" }
    delete:
      tags: [Sales Leads]
      summary: Deactivate a sales lead
      description: "**Plan: Paid API.**"
      operationId: deleteSalesLead
      x-plan: paid_api
      responses:
        "204": { description: Deactivated. }
        "401": { $ref: "#/components/responses/Unauthorized" }

  /sales_leads_stages:
    get:
      tags: [Sales Leads]
      summary: Pipeline board (stages with leads)
      description: |
        **Plan: Paid API.**

        Kanban-style view of a pipeline: its stages, the leads in each stage, plus the list
        of available pipelines. Pass `pipeline_id` to select a pipeline; otherwise the first
        active pipeline is used.
      operationId: getSalesLeadStages
      x-plan: paid_api
      parameters:
        - { name: pipeline_id, in: query, required: false, schema: { type: string, format: uuid } }
      responses:
        "200":
          description: OK
          content:
            application/json:
              schema: { $ref: "#/components/schemas/SalesLeadStages" }
        "401": { $ref: "#/components/responses/Unauthorized" }

  #########################################################################
  # Notes
  #########################################################################
  /notes:
    get:
      tags: [Notes]
      summary: List notes
      operationId: listNotes
      parameters:
        - $ref: "#/components/parameters/Page"
        - $ref: "#/components/parameters/PerPage"
      responses:
        "200":
          description: OK
          content:
            application/json:
              schema:
                type: object
                properties:
                  stats: { $ref: "#/components/schemas/PaginationStats" }
                  notes:
                    type: array
                    items: { $ref: "#/components/schemas/Note" }
        "401": { $ref: "#/components/responses/Unauthorized" }
    post:
      tags: [Notes]
      summary: Create a note
      description: |
        Attach a note to a parent record. Provide **exactly one** parent reference param
        alongside the note - e.g. `company_id`, `contact_id`, `job_id`, `invoice_id`,
        `work_order_id` (sales order), `quote_id`, `sales_lead_id`. The parent model is
        derived from the `*_id` key name.
      operationId: createNote
      parameters:
        - name: company_id
          in: query
          required: false
          schema: { type: string, format: uuid }
          description: One example parent reference; any `<model>_id` works.
      requestBody:
        required: true
        content:
          application/json:
            schema: { $ref: "#/components/schemas/NoteWrite" }
      responses:
        "200":
          description: Created.
          content:
            application/json:
              schema: { $ref: "#/components/schemas/Note" }
        "422": { $ref: "#/components/responses/ValidationError" }

  /notes/{id}:
    parameters:
      - $ref: "#/components/parameters/IdPath"
    patch:
      tags: [Notes]
      summary: Update a note
      operationId: updateNote
      requestBody:
        required: true
        content:
          application/json:
            schema: { $ref: "#/components/schemas/NoteWrite" }
      responses:
        "200":
          description: Updated.
          content:
            application/json:
              schema: { $ref: "#/components/schemas/Note" }
        "422": { $ref: "#/components/responses/ValidationError" }
    delete:
      tags: [Notes]
      summary: Delete a note
      operationId: deleteNote
      responses:
        "204": { description: Deleted. }
        "401": { $ref: "#/components/responses/Unauthorized" }

  #########################################################################
  # Assets
  #########################################################################
  /{assetable_type}/{assetable_id}/assets:
    parameters:
      - name: assetable_type
        in: path
        required: true
        schema: { type: string }
        description: Parent resource type, e.g. `companies`, `jobs`, `quotes`, `sales_orders`.
      - name: assetable_id
        in: path
        required: true
        schema: { type: string, format: uuid }
    get:
      tags: [Assets]
      summary: List assets for a record
      description: |
        **Plan: Paid API.**

        Files attached to the parent record. The pagination block is `meta`, not `stats`.
        In the legacy Apiary reference these per-parent asset endpoints returned only assets in
        the `Print Ready` category.
      operationId: listAssets
      x-plan: paid_api
      parameters:
        - $ref: "#/components/parameters/Page"
        - $ref: "#/components/parameters/PerPage"
      responses:
        "200":
          description: OK
          content:
            application/json:
              schema:
                type: object
                properties:
                  meta: { $ref: "#/components/schemas/PaginationStats" }
                  assets:
                    type: array
                    items: { $ref: "#/components/schemas/Asset" }
        "401": { $ref: "#/components/responses/Unauthorized" }

  #########################################################################
  # Orders (legacy - Zapier trigger)
  #########################################################################
  /orders:
    get:
      tags: [Orders]
      summary: List in-process orders
      description: |
        **Plan: All plans.**

        Legacy endpoint kept for Zapier's auth check / new-order trigger. Returns a bare JSON
        array (no pagination wrapper) of orders currently in process, newest first.
      operationId: listOrders
      x-plan: all
      responses:
        "200":
          description: OK
          content:
            application/json:
              schema:
                type: array
                items: { $ref: "#/components/schemas/Order" }
        "401": { $ref: "#/components/responses/Unauthorized" }

#########################################################################
# Webhooks (incoming to your server)
#########################################################################
webhooks:
  event:
    post:
      tags: [Webhooks]
      summary: shopVOX event delivered to your endpoint
      description: |
        shopVOX POSTs this payload to each active webhook endpoint you configure in the app
        (Account Settings -> Webhook Endpoints). Verify authenticity with `webhook_token`
        against the Webhook key shown in Account Settings.

        **Plan:** Webhooks require the **Paid API** add-on - delivery is skipped for accounts
        without it.

        **Supported events** (note the delete action is `destroyed`, not `deleted`, and sales
        orders are emitted as `work_order:*`):

        - `company:created`, `company:updated`, `company:destroyed`
        - `invoice:created`, `invoice:updated`, `invoice:destroyed`
        - `job:created`, `job:updated`, `job:destroyed`
        - `job_step:changed`
        - `quote:created`, `quote:updated`, `quote:destroyed`
        - `work_order:created`, `work_order:updated`, `work_order:destroyed`

        **Your response controls retries**

        - `200` (or any other success) - acknowledged, done.
        - `408`, `500`, `502`, `503`, `504` - shopVOX retries (up to 5 times).
        - `410` - shopVOX removes (deactivates) the endpoint.
        - `400`, `401`, `403`, `404`, `422` - treated as temporary; the endpoint is **not**
          deactivated and the payload is **not** retried.
        - Any other 4xx/5xx code - shopVOX deactivates the endpoint; re-enable it after fixing.
      operationId: receiveWebhook
      requestBody:
        required: true
        content:
          application/json:
            schema: { $ref: "#/components/schemas/WebhookPayload" }
      responses:
        "200": { description: Acknowledged. }

components:
  securitySchemes:
    accountId:
      type: apiKey
      in: query
      name: account_id
      description: Account UUID returned by `/login` (as `accountID`).
    authToken:
      type: apiKey
      in: query
      name: authToken
      description: |
        Auth token from `/login`. May alternatively be sent as
        `Authorization: Token token="<authToken>"` with `account_id` still as a query param.

  parameters:
    IdPath:
      name: id
      in: path
      required: true
      schema: { type: string, format: uuid }
    JobIdPath:
      name: job_id
      in: path
      required: true
      schema: { type: string, format: uuid }
    Page:
      name: page
      in: query
      required: false
      schema: { type: integer, minimum: 1, default: 1 }
    PerPage:
      name: per_page
      in: query
      required: false
      schema: { type: integer, default: 10 }
      description: Capped to 5 when the account lacks the `index_api_enabled` flag.
    TxnDateFrom:
      name: txn_date_from
      in: query
      required: false
      schema: { type: string, format: date, pattern: '^\d{4}-\d{2}-\d{2}$' }
      description: >
        Only sales orders with a `txnDate` on or after this date. Exactly `YYYY-MM-DD`;
        anything else -- including a wider year such as `9999999999-01-01`, which
        parses as a date but exceeds what the column can hold -- is rejected with
        `422`, never ignored.
    TxnDateTo:
      name: txn_date_to
      in: query
      required: false
      schema: { type: string, format: date, pattern: '^\d{4}-\d{2}-\d{2}$' }
      description: >
        Only sales orders with a `txnDate` on or before this date. Exactly `YYYY-MM-DD`;
        anything else -- including a wider year such as `9999999999-01-01`, which
        parses as a date but exceeds what the column can hold -- is rejected with
        `422`, never ignored.

  responses:
    Unauthorized:
      description: Missing/expired credentials, an index request without a required filter, or the plan does not include this endpoint.
      content:
        application/json:
          schema: { $ref: "#/components/schemas/Error" }
    ValidationError:
      description: Unprocessable entity - field errors keyed by attribute.
      content:
        application/json:
          schema:
            type: object
            additionalProperties:
              type: array
              items: { type: string }
            example: { name: ["can't be blank"] }

  schemas:
    #####################################################################
    # Shared
    #####################################################################
    LoginResponse:
      type: object
      properties:
        authToken: { type: string, example: "86D05F9EF7B74D8BA9C9D78897216850" }
        accountID: { type: string, example: "95242D38183340B29A86829AEF5C2400" }

    PaginationStats:
      type: object
      description: |
        Pagination metadata attached as `stats` (or `meta` on assets) to index responses.
        The **quotes** list uses a different block - see `QuoteListStats`.
      properties:
        totalCount: { type: integer }
        page: { type: integer }
        perPage: { type: integer }
        totalPages: { type: integer }
        prevPage: { type: [integer, "null"] }
        nextPage: { type: [integer, "null"] }
        outOfRange: { type: boolean }

    QuoteListStats:
      type: object
      description: |
        Pagination block for `GET /quotes` only. Note the current page is `currentPage`
        (not `page`), and there are three extra keys not present on other index endpoints.
      properties:
        totalCount: { type: integer }
        currentPage: { type: integer }
        perPage: { type: integer }
        totalPages: { type: integer }
        prevPage: { type: [integer, "null"] }
        nextPage: { type: [integer, "null"] }
        nextPageResultsCount: { type: [integer, "null"] }
        hasNextPage: { type: boolean }
        hasPrevPage: { type: boolean }
        outOfRange: { type: boolean }

    ContactRef:
      type: object
      description: Contact reference returned by some transactions (name, phone, id - no initials).
      properties:
        id: { type: string, format: uuid }
        name: { type: [string, "null"] }
        phoneWithExt: { type: [string, "null"] }

    Error:
      type: object
      properties:
        error: { type: string, example: "Un Authorized" }

    NamedRef:
      type: object
      description: Lightweight reference to a related record.
      properties:
        id: { type: string, format: uuid }
        name: { type: string }

    Party:
      type: object
      description: A person reference (sales rep, PM, designer) with initials.
      properties:
        id: { type: string, format: uuid }
        name: { type: [string, "null"] }
        initials: { type: [string, "null"] }

    Address:
      type: object
      properties:
        id: { type: string }
        addressType: { type: [string, "null"] }
        name: { type: [string, "null"] }
        nameStreet: { type: [string, "null"] }
        nameStreetCity: { type: [string, "null"] }
        street: { type: [string, "null"] }
        street1: { type: [string, "null"] }
        suburb: { type: [string, "null"] }
        city: { type: [string, "null"] }
        zip: { type: [string, "null"] }
        state: { type: [string, "null"] }
        country: { type: [string, "null"] }
        attentionTo: { type: [string, "null"] }
        coordinates:
          type: object
          properties:
            latitude: { type: [number, "null"] }
            longitude: { type: [number, "null"] }

    Email:
      type: object
      properties:
        id: { type: string }
        email: { type: string }
        emailType: { type: [string, "null"] }

    Phone:
      type: object
      properties:
        id: { type: string }
        phone: { type: string }
        ext: { type: [string, "null"] }
        phoneType: { type: [string, "null"] }
        phoneWithExt: { type: [string, "null"] }

    SignatureUpload:
      type: object
      required: [name, file]
      properties:
        name: { type: string, description: Signer name. }
        location: { type: string }
        file: { type: string, format: binary, description: Signature image. }

    Signature:
      type: object
      properties:
        name: { type: string }
        location: { type: [string, "null"] }
        signatureUrl: { type: [string, "null"] }
        createdAt: { type: string, format: date-time }

    WebhookPayload:
      type: object
      required: [event_object, event_action, timestamp, webhook_token, event]
      properties:
        event_object:
          type: string
          description: "Object name, e.g. job, company, invoice."
          example: job
        event_action:
          type: string
          description: "Action, e.g. created, updated, destroyed."
          example: created
        timestamp:
          type: integer
          description: Unix timestamp (seconds) when the payload was sent.
          example: 1619685799
        webhook_token:
          type: string
          description: Token to verify the payload came from shopVOX.
        event:
          type: object
          description: |
            The affected object. Some events add keys - e.g. `job_step:changed` also sends
            `job_id`, `changed_from`, `changed_to`; updates may include a `changes` map.
          additionalProperties: true
          properties:
            id: { type: string, format: uuid, description: Id of the affected object. }
            name: { type: string, description: Name/title of the affected object. }
            job_id: { type: string, format: uuid, description: "`job_step:changed` only." }
            changed_from: { type: string, description: "`job_step:changed` only." }
            changed_to: { type: string, description: "`job_step:changed` only." }

    #####################################################################
    # Companies
    #####################################################################
    CompanyListItem:
      type: object
      description: Slim projection returned by the list endpoint.
      properties:
        id: { type: string, format: uuid }
        name: { type: string }
        createdAt: { type: string, format: date-time }
        updatedAt: { type: string, format: date-time }

    Company:
      type: object
      description: |
        Full company as returned by show/create/update. `createdAt`/`updatedAt` here are
        localized display strings, unlike the ISO timestamps on the list endpoint.
      properties:
        id: { type: string, format: uuid }
        accountId: { type: string, format: uuid }
        name: { type: string }
        legalName: { type: [string, "null"] }
        status: { type: [string, "null"] }
        active: { type: boolean }
        posted: { type: boolean }
        taxable: { type: boolean }
        legacyType: { type: [string, "null"], description: "e.g. Customer, Vendor." }
        primaryEmail: { type: [string, "null"] }
        primaryPhone: { type: [string, "null"] }
        primaryPhoneExt: { type: [string, "null"] }
        phoneWithExt: { type: [string, "null"] }
        website: { type: [string, "null"] }
        facebook: { type: [string, "null"] }
        twitter: { type: [string, "null"] }
        creditLimitInDollars: { type: [number, "null"], description: US dollars. }
        discount: { type: [number, "null"] }
        markup: { type: [number, "null"] }
        currency: { type: [string, "null"] }
        currencySymbol: { type: [string, "null"] }
        rate: { type: [number, "null"] }
        accountNumber: { type: [string, "null"] }
        licenceNumber: { type: [string, "null"] }
        tollFreeNumber: { type: [string, "null"] }
        taxExemptCode: { type: [string, "null"] }
        taxExemptExpirationDate: { type: [string, "null"], format: date }
        backgroundInfo: { type: [string, "null"] }
        otherInfo: { type: [string, "null"] }
        specialNotes: { type: [string, "null"] }
        localFilePath: { type: [string, "null"] }
        doNotCall: { type: boolean }
        stopSupply: { type: boolean }
        enableCustomerPortal: { type: boolean }
        enableJobBoardView: { type: boolean }
        enableCustomerPortalOrdering: { type: boolean }
        enableWebStore: { type: boolean }
        notesCount: { type: integer }
        assetsCount: { type: integer }
        tasksCount: { type: integer }
        allowDelete: { type: boolean }
        termCodeId: { type: [string, "null"], format: uuid }
        termCodeName: { type: [string, "null"] }
        salesTaxId: { type: [string, "null"], format: uuid }
        leadSourceId: { type: [string, "null"], format: uuid }
        leadSourceName: { type: [string, "null"] }
        categoryName: { type: [string, "null"] }
        pricingLevelId: { type: [string, "null"], format: uuid }
        pricingLevelName: { type: [string, "null"] }
        primaryContactId: { type: [string, "null"], format: uuid }
        apContactId: { type: [string, "null"], format: uuid }
        createdById: { type: [string, "null"], format: uuid }
        updatedById: { type: [string, "null"], format: uuid }
        createdAt: { type: string }
        updatedAt: { type: string }
        primaryContact: { $ref: "#/components/schemas/NamedRef" }
        apContact: { $ref: "#/components/schemas/NamedRef" }
        leadSource: { $ref: "#/components/schemas/NamedRef" }
        marketingArea: { $ref: "#/components/schemas/NamedRef" }
        group: { $ref: "#/components/schemas/NamedRef" }
        category: { $ref: "#/components/schemas/NamedRef" }
        pricingLevel: { $ref: "#/components/schemas/NamedRef" }
        createdBy: { $ref: "#/components/schemas/NamedRef" }
        updatedBy: { $ref: "#/components/schemas/NamedRef" }
        tax:
          type: object
          properties:
            id: { type: string, format: uuid }
            name: { type: string }
            rate: { type: number }
            taxRate: { type: number }
            rateInPercent: { type: string }
        salesReps:
          type: array
          items: { $ref: "#/components/schemas/Party" }
        companySalesReps:
          type: array
          items:
            type: object
            properties:
              companySalesRepId: { type: string, format: uuid }
              companyId: { type: string, format: uuid }
              id: { type: string, format: uuid, description: User id. }
              accountId: { type: string, format: uuid }
              name: { type: [string, "null"] }
        contacts:
          type: array
          items:
            type: object
            properties:
              id: { type: string, format: uuid }
              name: { type: string }
              title: { type: [string, "null"] }
              email: { type: [string, "null"] }
              phone: { type: [string, "null"] }
              ext: { type: [string, "null"] }
              phoneWithExt: { type: [string, "null"] }
        addresses:
          type: array
          items: { $ref: "#/components/schemas/Address" }
        emails:
          type: array
          items: { $ref: "#/components/schemas/Email" }
        phones:
          type: array
          items: { $ref: "#/components/schemas/Phone" }
        companyDivisionsTitle: { type: [string, "null"] }
        companyDivisions:
          type: array
          items: { $ref: "#/components/schemas/NamedRef" }

    CompanyWrite:
      type: object
      description: |
        Create/update payload wrapped under `company`, using `snake_case`. Nested collections
        use Rails `*_attributes` arrays; pass `_destroy: true` to remove a nested address, email
        or phone (nested contacts do not accept `_destroy`). The most common writable fields are
        listed below; additional company attributes are also accepted.
      required: [company]
      properties:
        company:
          type: object
          required: [name]
          properties:
            name: { type: string }
            legal_name: { type: string }
            posted: { type: boolean }
            taxable: { type: boolean }
            status: { type: string }
            legacy_type: { type: string }
            primary_email: { type: string }
            primary_phone: { type: string }
            primary_phone_ext: { type: string }
            website: { type: string }
            discount: { type: number }
            credit_limit_in_dollars: { type: number }
            term_code_id: { type: string, format: uuid }
            category_id: { type: string, format: uuid }
            lead_source_id: { type: string, format: uuid }
            pricing_level_id: { type: string, format: uuid }
            primary_contact_id: { type: string, format: uuid }
            account_number: { type: string }
            tax_exempt_code: { type: string }
            tax_exempt_expiration_date: { type: string, format: date }
            sales_tax_id: { type: string, format: uuid }
            sales_tax_name: { type: string }
            enable_customer_portal: { type: boolean }
            enable_job_board_view: { type: boolean }
            enable_customer_portal_ordering: { type: boolean }
            enable_web_store: { type: boolean }
            term_code_name: { type: string }
            category_name: { type: string }
            lead_source_name: { type: string }
            pricing_level_name: { type: string }
            marketing_area_id: { type: string, format: uuid }
            marketing_area_name: { type: string }
            primary_contact_name: { type: string }
            ap_contact_id: { type: string, format: uuid }
            group_id: { type: string, format: uuid }
            group_name: { type: string }
            division_name: { type: string }
            do_not_call: { type: boolean }
            stop_supply: { type: boolean }
            facebook: { type: string }
            twitter: { type: string }
            other_info: { type: string }
            special_notes: { type: string }
            licence_number: { type: string }
            toll_free_number: { type: string }
            background_info: { type: string }
            local_file_path: { type: string }
            contacts_attributes:
              type: array
              items: { $ref: "#/components/schemas/NestedContactWrite" }
            addresses_attributes:
              type: array
              items: { $ref: "#/components/schemas/NestedAddressWrite" }
            emails_attributes:
              type: array
              items: { $ref: "#/components/schemas/NestedEmailWrite" }
            phones_attributes:
              type: array
              items: { $ref: "#/components/schemas/NestedPhoneWrite" }
            company_sales_reps_attributes:
              type: array
              description: Assign sales reps to the company.
              items:
                type: object
                properties:
                  id: { type: string, format: uuid }
                  user_id: { type: string, format: uuid }
                  company_id: { type: string, format: uuid }

    NestedContactWrite:
      type: object
      description: |
        Nested contact on a company. Note: `_destroy` is **not** accepted here (unlike nested
        addresses/emails/phones) - it is stripped, so nested contacts cannot be removed this way.
      properties:
        id: { type: string, format: uuid }
        name: { type: string }
        title: { type: string }
        email: { type: string }
        phone: { type: string }
        ext: { type: string }
        company_id: { type: string, format: uuid }
    NestedAddressWrite:
      type: object
      properties:
        id: { type: string }
        address_type: { type: string }
        name: { type: string }
        street: { type: string }
        street1: { type: string }
        suburb: { type: string }
        city: { type: string }
        state: { type: string }
        zip: { type: string }
        country: { type: string }
        attention_to: { type: string }
        zip_from_foreign_country: { type: string, description: "Contacts only." }
        _destroy: { type: boolean }
    NestedEmailWrite:
      type: object
      properties:
        id: { type: string }
        email: { type: string }
        email_type: { type: string }
        _destroy: { type: boolean }
    NestedPhoneWrite:
      type: object
      properties:
        id: { type: string }
        phone: { type: string }
        phone_type: { type: string }
        ext: { type: string }
        _destroy: { type: boolean }

    #####################################################################
    # Contacts
    #####################################################################
    ContactListItem:
      type: object
      properties:
        id: { type: string, format: uuid }
        name: { type: string }
        email: { type: [string, "null"] }
        phone: { type: [string, "null"] }
        phoneWithExt: { type: [string, "null"] }
        title: { type: [string, "null"] }
        department: { type: [string, "null"] }
        company: { $ref: "#/components/schemas/NamedRef" }
        emails:
          type: array
          items: { $ref: "#/components/schemas/Email" }
        addresses:
          type: array
          items: { $ref: "#/components/schemas/Address" }

    Contact:
      type: object
      description: Full contact. `createdAt`/`updatedAt` are localized display strings.
      properties:
        id: { type: string, format: uuid }
        name: { type: string }
        email: { type: [string, "null"] }
        phone: { type: [string, "null"] }
        phoneWithExt: { type: [string, "null"] }
        ext: { type: [string, "null"] }
        title: { type: [string, "null"] }
        department: { type: [string, "null"] }
        beeperNumber: { type: [string, "null"] }
        website: { type: [string, "null"] }
        blog: { type: [string, "null"] }
        facebook: { type: [string, "null"] }
        twitter: { type: [string, "null"] }
        linkedin: { type: [string, "null"] }
        active: { type: boolean }
        companyId: { type: string, format: uuid }
        enableCustomerPortal: { type: boolean }
        login: { type: [string, "null"] }
        notesCount: { type: integer }
        tasksCount: { type: integer }
        backgroundInfo: { type: [string, "null"] }
        allowDelete: { type: boolean }
        company:
          type: object
          properties:
            id: { type: string, format: uuid }
            name: { type: string }
            enableCustomerPortal: { type: boolean }
        addresses:
          type: array
          items: { $ref: "#/components/schemas/Address" }
        emails:
          type: array
          items: { $ref: "#/components/schemas/Email" }
        phones:
          type: array
          items: { $ref: "#/components/schemas/Phone" }
        createdById: { type: [string, "null"], format: uuid }
        updatedById: { type: [string, "null"], format: uuid }
        createdBy: { $ref: "#/components/schemas/NamedRef" }
        updatedBy: { $ref: "#/components/schemas/NamedRef" }
        createdAt: { type: string }
        updatedAt: { type: string }

    ContactWrite:
      type: object
      required: [contact]
      properties:
        contact:
          type: object
          required: [name]
          properties:
            name: { type: string }
            email: { type: string }
            phone: { type: string }
            ext: { type: string }
            title: { type: string }
            department: { type: string }
            company_id: { type: string, format: uuid }
            company_name: { type: string, description: "Create only - find-or-create the company by name if company_id is absent. Ignored on update." }
            website: { type: string }
            linkedin: { type: string }
            twitter: { type: string }
            facebook: { type: string }
            blog: { type: string }
            beeper_number: { type: string }
            background_info: { type: string }
            unsubscribe: { type: boolean }
            picture_url: { type: string }
            picture_filename: { type: string }
            picture_mimetype: { type: string }
            picture_size: { type: integer }
            picture_key: { type: string }
            enable_customer_portal: { type: boolean }
            login: { type: string }
            password: { type: string, format: password }
            emails_attributes:
              type: array
              items: { $ref: "#/components/schemas/NestedEmailWrite" }
            phones_attributes:
              type: array
              items: { $ref: "#/components/schemas/NestedPhoneWrite" }
            addresses_attributes:
              type: array
              items: { $ref: "#/components/schemas/NestedAddressWrite" }

    #####################################################################
    # Quotes
    #####################################################################
    QuoteListItem:
      type: object
      properties:
        id: { type: string, format: uuid }
        title: { type: string }
        txnNumber: { type: string }
        txnDate: { type: [string, "null"], format: date }
        expiryDate: { type: [string, "null"], format: date }
        shippingDate: { type: [string, "null"], format: date }
        createdAt: { type: string, format: date-time }
        updatedAt: { type: string, format: date-time }
        workflowState: { type: [string, "null"] }
        totalPriceWithTaxInDollars: { type: [number, "null"], description: US dollars. }
        nextContactDate: { type: [string, "null"], format: date }
        closingPotential: { type: [string, "null"] }
        potentialClosingDate: { type: [string, "null"], format: date }
        lastNote: { type: [string, "null"] }
        age: { type: [integer, "null"] }
        lastEmailedDate: { type: [string, "null"] }
        quoteFor: { type: [string, "null"] }
        customerNote: { type: [string, "null"] }
        site: { type: [string, "null"] }
        quickQuote: { type: [boolean, "null"] }
        dueDate: { type: [string, "null"], format: date }
        inHandDate: { type: [string, "null"], format: date }
        createdBy: { $ref: "#/components/schemas/NamedRef" }
        projectManager: { $ref: "#/components/schemas/NamedRef" }
        designer: { $ref: "#/components/schemas/Party" }
        primaryContact: { $ref: "#/components/schemas/NamedRef" }
        primarySalesRep: { $ref: "#/components/schemas/NamedRef" }
        company: { $ref: "#/components/schemas/NamedRef" }
        project: { $ref: "#/components/schemas/NamedRef" }

    Quote:
      type: object
      description: Single quote with addresses and line items.
      properties:
        id: { type: string, format: uuid }
        active: { type: boolean }
        title: { type: string }
        description: { type: [string, "null"] }
        txnDate: { type: [string, "null"], format: date }
        txnNumber: { type: string }
        totalPriceInDollars: { type: [number, "null"], description: US dollars. }
        totalTaxInDollars: { type: [number, "null"], description: US dollars. }
        totalPriceWithTaxInDollars: { type: [number, "null"], description: US dollars. }
        workflowState: { type: [string, "null"] }
        expiryDate: { type: [string, "null"], format: date }
        nextContactDate: { type: [string, "null"], format: date }
        potentialClosingDate: { type: [string, "null"], format: date }
        closingPotential: { type: [string, "null"] }
        customerPoNumber: { type: [string, "null"] }
        customerPoDate: { type: [string, "null"], format: date }
        autoExpire: { type: [boolean, "null"] }
        downpaymentPercent: { type: [number, "null"] }
        shippingTracking: { type: [string, "null"] }
        shippingDate: { type: [string, "null"], format: date }
        createdAt: { type: string, format: date-time }
        updatedAt: { type: string, format: date-time }
        lastNote: { type: [string, "null"] }
        age: { type: [integer, "null"] }
        lastEmailedDate: { type: [string, "null"] }
        quoteFor: { type: [string, "null"] }
        customerNote: { type: [string, "null"] }
        site: { type: [string, "null"] }
        quickQuote: { type: [boolean, "null"] }
        dueDate: { type: [string, "null"], format: date }
        inHandDate: { type: [string, "null"], format: date }
        leadSourceId: { type: [string, "null"], format: uuid }
        divisionId: { type: [string, "null"], format: uuid }
        divisionName: { type: [string, "null"] }
        billingAddress: { $ref: "#/components/schemas/QuoteAddress" }
        shippingAddress: { $ref: "#/components/schemas/QuoteAddress" }
        installingAddress: { $ref: "#/components/schemas/QuoteAddress" }
        createdBy: { $ref: "#/components/schemas/Party" }
        pm: { $ref: "#/components/schemas/Party" }
        projectManager: { $ref: "#/components/schemas/Party" }
        designer: { $ref: "#/components/schemas/Party" }
        primaryContact:
          type: object
          properties:
            id: { type: string, format: uuid }
            name: { type: string }
            primaryEmail: { type: [string, "null"] }
            phoneWithExt: { type: [string, "null"] }
        invoiceContact: { $ref: "#/components/schemas/ContactRef" }
        primarySalesRep: { $ref: "#/components/schemas/Party" }
        company:
          type: object
          properties:
            id: { type: string, format: uuid }
            name: { type: string }
            phoneWithExt: { type: [string, "null"] }
        project:
          type: object
          description: Related project - id plus transaction number (not name).
          properties:
            id: { type: string, format: uuid }
            txnNumber: { type: string }
        estimator: { $ref: "#/components/schemas/Party" }
        leadSource: { $ref: "#/components/schemas/NamedRef" }
        salesOrders:
          type: array
          items:
            type: object
            properties:
              id: { type: string, format: uuid }
              txnNumber: { type: string }
              active: { type: boolean }
        lineItems:
          type: array
          items: { $ref: "#/components/schemas/QuoteLineItem" }

    QuoteAddress:
      type: object
      properties:
        id: { type: string }
        name: { type: [string, "null"] }
        street: { type: [string, "null"] }
        city: { type: [string, "null"] }
        state: { type: [string, "null"] }
        zip: { type: [string, "null"] }
        countryName: { type: [string, "null"] }

    QuoteLineItem:
      type: object
      properties:
        id: { type: string, format: uuid }
        position: { type: integer }
        name: { type: [string, "null"] }
        description: { type: [string, "null"] }
        fullDescription: { type: [string, "null"] }
        quantity: { type: number }
        uom: { type: [string, "null"] }
        volumeDiscountPercent: { type: number }
        volumeDiscountInDollars: { type: number }
        rangeDiscountPercent: { type: number }
        rangeDiscountInDollars: { type: number }
        costInDollars: { type: number, description: US dollars. }
        suggestedPriceInDollars: { type: number, description: US dollars. }
        priceInDollars: { type: number, description: US dollars. }
        totalPriceInDollars: { type: number, description: US dollars. }
        totalCostInDollars: { type: number, description: US dollars. }
        totalTaxInDollars: { type: number, description: US dollars. }
        workflowState: { type: [string, "null"] }
        saleType: { type: [string, "null"] }
        taxable: { type: boolean }
        taxName: { type: [string, "null"] }
        taxRate: { type: number }
        priceOverride: { type: [boolean, "null"] }
        incomeCoaAccountId: { type: [string, "null"], format: uuid }
        incomeAccountName: { type: [string, "null"] }
        incomeAccountNumber: { type: [string, "null"] }
        cogCoaAccountId: { type: [string, "null"], format: uuid }
        cogAccountName: { type: [string, "null"] }
        cogAccountNumber: { type: [string, "null"] }
        parentLineItemId: { type: [string, "null"], format: uuid }
        job:
          type: object
          properties:
            id: { type: string, format: uuid }
            workflowState: { type: [string, "null"] }

    #####################################################################
    # Sales Orders
    #####################################################################
    SalesOrderListItem:
      type: object
      properties:
        id: { type: string, format: uuid }
        name: { type: string }
        title: { type: string }
        txnNumber: { type: string }
        txnDate: { type: [string, "null"], format: date }
        dueDate: { type: [string, "null"], format: date }
        shippingDate: { type: [string, "null"], format: date }
        createdAt: { type: string, format: date-time }
        updatedAt: { type: string, format: date-time }
        invoiced: { type: [number, "null"], description: Invoiced ratio (0..1). }
        lastInvoicedOn: { type: [string, "null"], format: date }
        shippingTracking: { type: [string, "null"] }
        shippingAddressName: { type: [string, "null"] }
        shippingAddressStreet: { type: [string, "null"] }
        shippingAddressStreet1: { type: [string, "null"] }
        shippingAddressCity: { type: [string, "null"] }
        shippingAddressState: { type: [string, "null"] }
        shippingAddressZip: { type: [string, "null"] }
        shippingAddressCountry: { type: [string, "null"] }
        billingAddressName: { type: [string, "null"] }
        billingAddressStreet: { type: [string, "null"] }
        billingAddressStreet1: { type: [string, "null"] }
        billingAddressCity: { type: [string, "null"] }
        billingAddressState: { type: [string, "null"] }
        billingAddressZip: { type: [string, "null"] }
        billingAddressCountry: { type: [string, "null"] }
        workflowState: { type: [string, "null"] }
        totalPriceWithTaxInDollars: { type: [number, "null"], description: US dollars. }
        totalPaymentsInDollars: { type: [number, "null"], description: US dollars. }
        balanceInDollars: { type: [number, "null"], description: US dollars. }
        setupChargesInDollars: { type: [number, "null"], description: US dollars. }
        setupChargesTaxInDollars: { type: [number, "null"], description: US dollars. }
        setupChargesTaxable: { type: [boolean, "null"] }
        miscChargesInDollars: { type: [number, "null"], description: US dollars. }
        miscChargesTaxInDollars: { type: [number, "null"], description: US dollars. }
        miscChargesTaxable: { type: [boolean, "null"] }
        miscChargesLabel: { type: [string, "null"] }
        shippingChargesInDollars: { type: [number, "null"], description: US dollars. }
        shippingChargesTaxInDollars: { type: [number, "null"], description: US dollars. }
        shippingChargesTaxable: { type: [boolean, "null"] }
        financeChargesInDollars: { type: [number, "null"], description: US dollars. }
        financeChargesTaxInDollars: { type: [number, "null"], description: US dollars. }
        financeChargesTaxable: { type: [boolean, "null"] }
        lineItemsCount: { type: integer }
        lineItemsJobsCount: { type: integer }
        customerPoNumber: { type: [string, "null"] }
        customerPoDate: { type: [string, "null"], format: date }
        lastNote: { type: [string, "null"] }
        lastEmailedDate: { type: [string, "null"] }
        pm: { $ref: "#/components/schemas/Party" }
        projectManager: { $ref: "#/components/schemas/Party" }
        designer: { $ref: "#/components/schemas/Party" }
        primaryContact: { $ref: "#/components/schemas/ContactRef" }
        primarySalesRep: { $ref: "#/components/schemas/Party" }
        company: { $ref: "#/components/schemas/NamedRef" }
        project: { $ref: "#/components/schemas/NamedRef" }

    SalesOrder:
      type: object
      description: |
        Full sales order (internally a WorkOrder). Includes charges, line items, addresses,
        related transactions and payments. Only the most-used fields are listed; the response
        may include additional nested detail.
      properties:
        className: { type: string }
        active: { type: boolean }
        id: { type: string, format: uuid }
        title: { type: string }
        description: { type: [string, "null"] }
        txnDate: { type: [string, "null"], format: date }
        txnNumber: { type: string }
        totalPriceInDollars: { type: [number, "null"], description: US dollars. }
        totalTaxInDollars: { type: [number, "null"], description: US dollars. }
        totalPriceWithTaxInDollars: { type: [number, "null"], description: US dollars. }
        totalPaymentsInDollars: { type: [number, "null"], description: US dollars. }
        balanceInDollars: { type: [number, "null"], description: US dollars. }
        workflowState: { type: [string, "null"] }
        dueDate: { type: [string, "null"], format: date }
        inHandDate: { type: [string, "null"], format: date }
        shippingDate: { type: [string, "null"], format: date }
        customerPoNumber: { type: [string, "null"] }
        customerPoDate: { type: [string, "null"], format: date }
        downpaymentPercent: { type: [number, "null"] }
        invoiced: { type: [boolean, "null"] }
        fullyInvoiced: { type: [boolean, "null"] }
        setupChargesInDollars: { type: [number, "null"], description: US dollars. }
        setupChargesTaxable: { type: boolean }
        miscChargesInDollars: { type: [number, "null"], description: US dollars. }
        miscChargesTaxable: { type: boolean }
        miscChargesLabel: { type: [string, "null"] }
        shippingChargesInDollars: { type: [number, "null"], description: US dollars. }
        shippingChargesTaxable: { type: boolean }
        financeChargesInDollars: { type: [number, "null"], description: US dollars. }
        financeChargesTaxable: { type: boolean }
        companyId: { type: [string, "null"], format: uuid }
        primarySalesRepId: { type: [string, "null"], format: uuid }
        primaryContactId: { type: [string, "null"], format: uuid }
        billingAddressId: { type: [string, "null"], format: uuid }
        shippingAddressId: { type: [string, "null"], format: uuid }
        termCodeId: { type: [string, "null"], format: uuid }
        salesTaxId: { type: [string, "null"], format: uuid }
        shippingMethodId: { type: [string, "null"], format: uuid }
        productionManagerId: { type: [string, "null"], format: uuid }
        projectManagerId: { type: [string, "null"], format: uuid }
        designerId: { type: [string, "null"], format: uuid }
        paymentMethodId: { type: [string, "null"], format: uuid }
        createdAt: { type: string, format: date-time }
        updatedAt: { type: string, format: date-time }
        company: { $ref: "#/components/schemas/NamedRef" }
        primaryContact: { $ref: "#/components/schemas/NamedRef" }
        primarySalesRep: { $ref: "#/components/schemas/Party" }
        productionManager: { $ref: "#/components/schemas/Party" }
        projectManager: { $ref: "#/components/schemas/Party" }
        designer: { $ref: "#/components/schemas/Party" }
        project: { $ref: "#/components/schemas/NamedRef" }
        createdBy: { $ref: "#/components/schemas/NamedRef" }
        updatedBy: { $ref: "#/components/schemas/NamedRef" }
        billingAddress: { $ref: "#/components/schemas/Address" }
        shippingAddress: { $ref: "#/components/schemas/Address" }
        shippingMethod: { $ref: "#/components/schemas/NamedRef" }
        paymentMethod: { $ref: "#/components/schemas/NamedRef" }
        tax:
          type: object
          properties:
            id: { type: string, format: uuid }
            name: { type: [string, "null"] }
            rate: { type: [number, "null"] }
            rateInPercent: { type: [string, "null"] }
        termCode:
          type: object
          properties:
            id: { type: string, format: uuid }
            name: { type: [string, "null"] }
            type: { type: [string, "null"] }
            days: { type: [integer, "null"] }
        lineItems:
          type: array
          items: { $ref: "#/components/schemas/SalesOrderLineItem" }
        relatedTransactions:
          type: array
          items:
            type: object
            properties:
              txnId: { type: string, format: uuid }
              txnNumber: { type: string }
              txnDate: { type: [string, "null"], format: date }
              txnName: { type: [string, "null"] }
              txnType: { type: [string, "null"] }
              txnTotal: { type: [number, "null"] }
        orderPayments:
          type: array
          items:
            type: object
            properties:
              orderPaymentId: { type: string, format: uuid }
              orderPaymentTxnNumber: { type: string }
              orderPaymentPaidOn: { type: [string, "null"], format: date }
              orderPaymentAmount: { type: [number, "null"] }
        signatures:
          type: array
          items: { $ref: "#/components/schemas/Signature" }

    SalesOrderLineItem:
      type: object
      properties:
        id: { type: string, format: uuid }
        workOrderId: { type: string, format: uuid }
        companyId: { type: [string, "null"], format: uuid }
        position: { type: integer }
        name: { type: [string, "null"] }
        description: { type: [string, "null"] }
        fullDescription: { type: [string, "null"] }
        quantity: { type: number }
        uom: { type: [string, "null"] }
        volumeDiscountPercent: { type: [number, "null"] }
        volumeDiscountInDollars: { type: [number, "null"] }
        rangeDiscountPercent: { type: [number, "null"] }
        rangeDiscountInDollars: { type: [number, "null"] }
        costInDollars: { type: [number, "null"], description: US dollars. }
        suggestedPriceInDollars: { type: [number, "null"], description: US dollars. }
        priceInDollars: { type: [number, "null"], description: US dollars. }
        totalCostInDollars: { type: [number, "null"], description: US dollars. }
        totalPriceInDollars: { type: [number, "null"], description: US dollars. }
        totalTaxInDollars: { type: [number, "null"], description: US dollars. }
        workflowState: { type: [string, "null"] }
        productId: { type: [string, "null"], format: uuid }
        productType: { type: [string, "null"] }
        productName: { type: [string, "null"] }
        saleType: { type: [string, "null"] }
        jobId: { type: [string, "null"], format: uuid }
        taxable: { type: boolean }
        taxName: { type: [string, "null"] }
        taxRate: { type: [number, "null"] }
        priceOverride: { type: [boolean, "null"] }
        parentLineItemId: { type: [string, "null"], format: uuid }
        customFields:
          type: array
          items:
            type: object
            properties:
              name: { type: string }
              type: { type: [string, "null"] }
              value: { type: [string, "null"] }
        job:
          type: object
          properties:
            id: { type: string, format: uuid }
            name: { type: [string, "null"] }
            workflowState: { type: [string, "null"] }

    SalesOrderWrite:
      type: object
      description: |
        Create/update payload wrapped under `sales_order` (aliased to `work_order` server-side),
        `snake_case`. Update ignores `line_items`. Only the core fields are listed.
      required: [sales_order]
      properties:
        sales_order:
          type: object
          properties:
            title: { type: string }
            description: { type: string }
            txn_date: { type: string, format: date }
            company_id: { type: string, format: uuid }
            company_name: { type: string }
            primary_sales_rep_id: { type: string, format: uuid }
            primary_contact_id: { type: string, format: uuid }
            due_date: { type: string, format: date }
            in_hand_date: { type: string, format: date }
            shipping_date: { type: string, format: date }
            customer_po_number: { type: string }
            customer_po_date: { type: string, format: date }
            term_code_id: { type: string, format: uuid }
            sales_tax_id: { type: string, format: uuid }
            payment_method_id: { type: string, format: uuid }
            shipping_method_id: { type: string, format: uuid }
            downpayment_percent: { type: number }
            setup_charges_in_dollars: { type: number }
            misc_charges_in_dollars: { type: number }
            shipping_charges_in_dollars: { type: number }
            finance_charges_in_dollars: { type: number }
            billing_address:
              type: object
              properties:
                name: { type: string }
                street: { type: string }
                street1: { type: string }
                city: { type: string }
                state: { type: string }
                zip: { type: string }
                country: { type: string }
            shipping_address:
              type: object
              properties:
                name: { type: string }
                street: { type: string }
                street1: { type: string }
                city: { type: string }
                state: { type: string }
                zip: { type: string }
                country: { type: string }
            line_items:
              type: array
              description: Create only (ignored on update).
              items:
                type: object
                properties:
                  name: { type: string }
                  description: { type: string }
                  position: { type: integer }
                  quantity: { type: number }
                  uom: { type: string }
                  price_in_dollars: { type: number }
                  total_price_in_dollars: { type: number }
                  tax_name: { type: string }
                  tax_rate: { type: number }
                  taxable: { type: boolean }
                  sale_type: { type: string }
                  product_type: { type: string }
                  product_name: { type: string }
                  custom_fields:
                    type: array
                    items:
                      type: object
                      properties:
                        name: { type: string }
                        value: { type: string }

    #####################################################################
    # Invoices
    #####################################################################
    InvoiceListItem:
      type: object
      properties:
        id: { type: string, format: uuid }
        name: { type: string }
        title: { type: string }
        txnNumber: { type: string }
        txnDate: { type: [string, "null"], format: date }
        dueDate: { type: [string, "null"], format: date }
        shippingDate: { type: [string, "null"], format: date }
        shippingTracking: { type: [string, "null"] }
        shippingAddressName: { type: [string, "null"] }
        shippingAddressStreet: { type: [string, "null"] }
        shippingAddressStreet1: { type: [string, "null"] }
        shippingAddressCity: { type: [string, "null"] }
        shippingAddressState: { type: [string, "null"] }
        shippingAddressZip: { type: [string, "null"] }
        shippingAddressCountry: { type: [string, "null"] }
        billingAddressName: { type: [string, "null"] }
        billingAddressStreet: { type: [string, "null"] }
        billingAddressStreet1: { type: [string, "null"] }
        billingAddressCity: { type: [string, "null"] }
        billingAddressState: { type: [string, "null"] }
        billingAddressZip: { type: [string, "null"] }
        billingAddressCountry: { type: [string, "null"] }
        createdAt: { type: string, format: date-time }
        updatedAt: { type: string, format: date-time }
        workflowState: { type: [string, "null"] }
        totalPriceWithTaxInDollars: { type: [number, "null"], description: US dollars. }
        balanceInDollars: { type: [number, "null"], description: US dollars. }
        totalPaymentsInDollars: { type: [number, "null"], description: US dollars. }
        customerPoNumber: { type: [string, "null"] }
        customerPoDate: { type: [string, "null"], format: date }
        changed: { type: string, enum: ["Yes", "No"] }
        accountingSyncDate: { type: string, enum: ["Yes", "No"] }
        lastNote: { type: [string, "null"] }
        jobsCount: { type: integer }
        pm: { $ref: "#/components/schemas/Party" }
        projectManager: { $ref: "#/components/schemas/Party" }
        designer: { $ref: "#/components/schemas/Party" }
        primaryContact: { $ref: "#/components/schemas/ContactRef" }
        primarySalesRep: { $ref: "#/components/schemas/Party" }
        company: { $ref: "#/components/schemas/NamedRef" }
        lastEmail:
          type: object
          properties:
            id: { type: string, format: uuid }
            createdAt: { type: string, format: date-time }

    Invoice:
      type: object
      description: Single invoice with line items and signatures.
      properties:
        id: { type: string, format: uuid }
        name: { type: string }
        title: { type: string }
        txnNumber: { type: string }
        txnDate: { type: [string, "null"], format: date }
        dueDate: { type: [string, "null"], format: date }
        shippingDate: { type: [string, "null"], format: date }
        createdAt: { type: string, format: date-time }
        updatedAt: { type: string, format: date-time }
        workflowState: { type: [string, "null"] }
        totalPriceWithTaxInDollars: { type: [number, "null"], description: US dollars. }
        balanceInDollars: { type: [number, "null"], description: US dollars. }
        totalPaymentsInDollars: { type: [number, "null"], description: US dollars. }
        customerPoNumber: { type: [string, "null"] }
        customerPoDate: { type: [string, "null"], format: date }
        changed: { type: string, enum: ["Yes", "No"] }
        accountingSyncDate: { type: string, enum: ["Yes", "No"] }
        lastNote: { type: [string, "null"] }
        pm: { $ref: "#/components/schemas/Party" }
        projectManager: { $ref: "#/components/schemas/Party" }
        designer: { $ref: "#/components/schemas/Party" }
        primaryContact: { $ref: "#/components/schemas/ContactRef" }
        primarySalesRep: { $ref: "#/components/schemas/Party" }
        company: { $ref: "#/components/schemas/NamedRef" }
        divisionId: { type: [string, "null"], format: uuid }
        division:
          type: object
          description: Full division record (all columns), present only when the invoice has a division.
          additionalProperties: true
        lineItems:
          type: array
          items: { $ref: "#/components/schemas/InvoiceLineItem" }
        signatures:
          type: array
          items: { $ref: "#/components/schemas/Signature" }

    InvoiceLineItem:
      type: object
      properties:
        id: { type: string, format: uuid }
        position: { type: integer }
        name: { type: [string, "null"] }
        description: { type: [string, "null"] }
        fullDescription: { type: [string, "null"] }
        quantity: { type: [number, "null"] }
        volumeDiscountPercent: { type: [number, "null"] }
        volumeDiscountInDollars: { type: [number, "null"] }
        rangeDiscountPercent: { type: [number, "null"] }
        rangeDiscountInDollars: { type: [number, "null"] }
        costInDollars: { type: [number, "null"], description: US dollars. }
        suggestedPriceInDollars: { type: [number, "null"], description: US dollars. }
        priceInDollars: { type: [number, "null"], description: US dollars. }
        totalCostInDollars: { type: [number, "null"], description: US dollars. }
        totalPriceInDollars: { type: [number, "null"], description: US dollars. }
        totalTaxInDollars: { type: [number, "null"], description: US dollars. }
        workflowState: { type: [string, "null"] }
        productId: { type: [string, "null"], format: uuid }
        productType: { type: [string, "null"] }
        productName: { type: [string, "null"] }
        saleType: { type: [string, "null"] }
        taxable: { type: boolean }
        taxName: { type: [string, "null"] }
        taxRate: { type: [number, "null"] }
        priceOverride: { type: [boolean, "null"] }
        incomeCoaAccountId: { type: [string, "null"], format: uuid }
        incomeAccountName: { type: [string, "null"] }
        cogCoaAccountId: { type: [string, "null"], format: uuid }
        cogAccountName: { type: [string, "null"] }
        cogAccountNumber: { type: [string, "null"] }
        parentLineItemId: { type: [string, "null"], format: uuid }

    #####################################################################
    # Payments
    #####################################################################
    PaymentListItem:
      type: object
      properties:
        id: { type: string, format: uuid }
        txnNumber: { type: string }
        paymentMethodId: { type: [string, "null"], format: uuid }
        amountInCents: { type: [integer, "null"], description: "dollars * 10000." }
        balanceInCents: { type: [integer, "null"], description: "dollars * 10000." }
        appliedPaymentsInCents: { type: [integer, "null"], description: "dollars * 10000." }
        paidOn: { type: [string, "null"], format: date }
        posted: { type: boolean }
        company: { $ref: "#/components/schemas/NamedRef" }

    Payment:
      type: object
      properties:
        id: { type: string, format: uuid }
        checkNumber: { type: [string, "null"] }
        ccName: { type: [string, "null"] }
        ccType: { type: [string, "null"] }
        ccAuthCode: { type: [string, "null"] }
        ccNumber: { type: [string, "null"], description: Masked. }
        amountInDollars: { type: [number, "null"], description: US dollars. }
        accountingSyncState: { type: [string, "null"] }
        balanceInCents: { type: [integer, "null"], description: "dollars * 10000." }
        appliedPaymentsInCents: { type: [integer, "null"], description: "dollars * 10000." }
        amountInCents: { type: [integer, "null"], description: "dollars * 10000." }
        posted: { type: boolean }
        info: { type: [string, "null"] }
        company:
          type: object
          properties:
            id: { type: string, format: uuid }
            name: { type: string }
            phoneWithExt: { type: [string, "null"] }
            specialNotes: { type: [string, "null"] }
        paymentMethod: { $ref: "#/components/schemas/NamedRef" }
        invoices:
          type: array
          items:
            type: object
            properties:
              id: { type: string, format: uuid }
              txnNumber: { type: string }
              amountInDollars: { type: [number, "null"] }
              paymentId: { type: string, format: uuid }
              totalPaymentsInCents: { type: [integer, "null"] }
              balanceInCents: { type: [integer, "null"] }
        workOrders:
          type: array
          items:
            type: object
            properties:
              id: { type: string, format: uuid }
              amountInDollars: { type: [number, "null"] }
              paymentId: { type: string, format: uuid }
              totalPaymentsInCents: { type: [integer, "null"] }
              balanceInCents: { type: [integer, "null"] }
        createdById: { type: [string, "null"], format: uuid }
        createdBy: { $ref: "#/components/schemas/NamedRef" }
        updatedById: { type: [string, "null"], format: uuid }
        updatedBy: { $ref: "#/components/schemas/NamedRef" }

    PaymentWrite:
      type: object
      required: [payment]
      properties:
        payment:
          type: object
          properties:
            payment_method_id: { type: string, format: uuid }
            amount_in_dollars: { type: number }
            paid_on: { type: string, format: date }
            company_id: { type: string, format: uuid }
            check_number: { type: string }
            posted: { type: boolean }
            info: { type: string }
            note: { type: string }
            cc_name: { type: string }
            cc_type: { type: string }
            cc_auth_code: { type: string }
            cc_number: { type: string }
            amount_in_cents: { type: integer, description: "4-decimal fixed point (dollars * 10000)." }
            balance_in_cents: { type: integer, description: "4-decimal fixed point (dollars * 10000)." }
            applied_payments_in_cents: { type: integer, description: "4-decimal fixed point (dollars * 10000)." }
            accounting_sync_state: { type: string }
            void_reason: { type: string }
            void_date: { type: string, format: date }
            credit_card_payment:
              type: object
              description: Card details when paying by credit card.
              properties:
                card_type: { type: string }
                first_name: { type: string }
                last_name: { type: string }
                card_number: { type: string }
                expiry_date: { type: string }
                street: { type: string }
                street1: { type: string }
                city: { type: string }
                state: { type: string }
                country: { type: string }
                zip: { type: string }
                cvs: { type: string }
                verification_token: { type: string }
                shopvox_pay_payment_token: { type: string }
            ach_payment:
              type: object
              description: Bank details when paying by ACH.
              properties:
                account_holder_name: { type: string }
                verification_token: { type: string }
            invoice_payments:
              type: array
              description: Allocate the payment to invoices.
              items:
                type: object
                properties:
                  invoice_id: { type: string, format: uuid }
                  amount_in_dollars: { type: number }
                  selected: { type: boolean, default: true }
            order_payments:
              type: array
              description: Allocate the payment to sales orders.
              items:
                type: object
                properties:
                  work_order_id: { type: string, format: uuid }
                  amount_in_dollars: { type: number }
                  selected: { type: boolean, default: true }

    #####################################################################
    # Jobs
    #####################################################################
    Job:
      type: object
      description: |
        A production job. `steps`/`lineItems` are present when applicable.

        The **list** endpoint (`GET /jobs`) returns a slightly different projection: it adds a
        nested `steps[]` array and an `installDate` field, and its `lineItems[]` omit `uom`.
        get-a-job and create return the shape documented here.
      properties:
        id: { type: string, format: uuid }
        active: { type: boolean }
        name: { type: string }
        description: { type: [string, "null"] }
        workflowState: { type: [string, "null"] }
        companyId: { type: [string, "null"], format: uuid }
        contactId: { type: [string, "null"], format: uuid }
        assignedToId: { type: [string, "null"], format: uuid }
        productionManagerId: { type: [string, "null"], format: uuid }
        salesRepId: { type: [string, "null"], format: uuid }
        jobTemplateId: { type: [string, "null"], format: uuid }
        orderId: { type: [string, "null"], format: uuid }
        orderLineItemId: { type: [string, "null"], format: uuid }
        orderNumber: { type: [string, "null"] }
        lineNumber: { type: [integer, "null"] }
        projectNumber: { type: [string, "null"] }
        priority: { type: [string, "null"] }
        quantity: { type: [number, "null"] }
        requireProofing: { type: [boolean, "null"] }
        hasOrder: { type: boolean }
        daysLeft: { type: [integer, "null"] }
        dueDate: { type: [string, "null"], format: date }
        dueTime: { type: [string, "null"] }
        dueDateFrom: { type: [string, "null"] }
        dueDateTo: { type: [string, "null"] }
        dueDateTimeFrom: { type: [string, "null"] }
        dueDateTimeTo: { type: [string, "null"] }
        currentStageId: { type: [string, "null"], format: uuid }
        currentStageName: { type: [string, "null"] }
        currentStepId: { type: [string, "null"], format: uuid }
        currentStepName: { type: [string, "null"] }
        bomDetails: { type: [string, "null"] }
        designDetails: { type: [string, "null"] }
        productionDetails: { type: [string, "null"] }
        shippingDetails: { type: [string, "null"] }
        installDetails: { type: [string, "null"] }
        lastEvent: { type: [string, "null"] }
        lastEventAt: { type: [string, "null"], format: date-time }
        currentProofUrl: { type: [string, "null"] }
        currentProofThumbnailUrl: { type: [string, "null"] }
        createdAt: { type: string, format: date-time }
        updatedAt: { type: string, format: date-time }
        divisionId: { type: [string, "null"], format: uuid }
        division: { $ref: "#/components/schemas/NamedRef" }
        company: { $ref: "#/components/schemas/NamedRef" }
        contact: { $ref: "#/components/schemas/NamedRef" }
        salesRep: { $ref: "#/components/schemas/Party" }
        productionManager: { $ref: "#/components/schemas/Party" }
        assignedTo: { $ref: "#/components/schemas/Party" }
        projectManager: { $ref: "#/components/schemas/Party" }
        designer: { $ref: "#/components/schemas/Party" }
        lineItems:
          type: array
          items:
            type: object
            properties:
              id: { type: string, format: uuid }
              lineNumber: { type: [integer, "null"] }
              position: { type: integer }
              name: { type: [string, "null"] }
              description: { type: [string, "null"] }
              quantity: { type: [number, "null"] }
              workflowState: { type: [string, "null"] }
              uom: { type: [string, "null"] }

    JobStep:
      type: object
      description: |
        Returned by start/stop/complete. Includes the parent `job` (full Job object) and,
        when present, `nextStep`.
      properties:
        id: { type: string, format: uuid }
        jobId: { type: string, format: uuid }
        name: { type: string }
        workflowState: { type: [string, "null"] }
        jobStageName: { type: [string, "null"] }
        isCompleted: { type: boolean }
        isStarted: { type: boolean }
        isIdling: { type: boolean }
        isStopped: { type: boolean }
        isPending: { type: boolean }
        isActive: { type: boolean }
        scheduledAt: { type: [string, "null"], format: date-time }
        estimatedTime: { type: [number, "null"] }
        actualTime: { type: [number, "null"] }
        manualTime: { type: [number, "null"] }
        assignedType: { type: [string, "null"] }
        assignedTo:
          type: array
          items:
            type: object
            properties:
              id: { type: string, format: uuid }
              assignedToId: { type: string, format: uuid }
              assignedToType: { type: string }
              name: { type: [string, "null"] }
              initials: { type: [string, "null"] }
        machineId: { type: [string, "null"], format: uuid }
        machine: { $ref: "#/components/schemas/NamedRef" }
        nextStep:
          type: object
          description: |
            The next step in the job, when present. A fuller step-summary object (not just
            id/name) - includes scheduling and assignment fields. `assignedTo[]` items here
            carry `pictureUrl` instead of `initials`.
          additionalProperties: true
          properties:
            id: { type: string, format: uuid }
            jobId: { type: string, format: uuid }
            name: { type: [string, "null"] }
            workflowState: { type: [string, "null"] }
            jobStageName: { type: [string, "null"] }
            isCompleted: { type: boolean }
            isStarted: { type: boolean }
            isActive: { type: boolean }
            scheduledFrom: { type: [string, "null"], format: date-time }
            scheduledTo: { type: [string, "null"], format: date-time }
            estimatedTime: { type: [number, "null"] }
            machineId: { type: [string, "null"], format: uuid }
            machine: { $ref: "#/components/schemas/NamedRef" }
        job: { $ref: "#/components/schemas/Job" }

    JobWrite:
      type: object
      required: [job]
      properties:
        job:
          type: object
          required: [name]
          properties:
            name: { type: string }
            description: { type: string }
            company_id: { type: string, format: uuid }
            company_name: { type: string }
            contact_id: { type: string, format: uuid }
            contact_name: { type: string }
            po_number: { type: string }
            quantity: { type: number }
            due_date: { type: string, format: date }
            due_date_from: { type: string, format: date }
            auto_schedule: { type: boolean }
            workflow: { type: string }
            division_id: { type: string, format: uuid }
            production_manager: { type: string }
            project_manager: { type: string }
            sales_rep: { type: string }
            designer: { type: string }
            shipping_method: { type: string }
            design_details: { type: string }
            production_details: { type: string }
            shipping_details: { type: string }
            install_details: { type: string }
            billing_address_name: { type: string }
            billing_address_street: { type: string }
            billing_address_street1: { type: string }
            billing_address_city: { type: string }
            billing_address_state: { type: string }
            billing_address_zip: { type: string }
            billing_address_country: { type: string }
            shipping_address_name: { type: string }
            shipping_address_street: { type: string }
            shipping_address_street1: { type: string }
            shipping_address_city: { type: string }
            shipping_address_state: { type: string }
            shipping_address_zip: { type: string }
            shipping_address_country: { type: string }
            installing_address_name: { type: string }
            installing_address_street: { type: string }
            installing_address_street1: { type: string }
            installing_address_city: { type: string }
            installing_address_state: { type: string }
            installing_address_zip: { type: string }
            installing_address_country: { type: string }

    #####################################################################
    # Sales Leads
    #####################################################################
    SalesLead:
      type: object
      properties:
        id: { type: string, format: uuid }
        title: { type: string }
        details: { type: [string, "null"] }
        workflowState: { type: [string, "null"] }
        nextContactDate: { type: [string, "null"], format: date }
        expectedCloseDate: { type: [string, "null"], format: date }
        qualified: { type: [boolean, "null"] }
        dealValueInDollars: { type: [number, "null"], description: US dollars. }
        createdAt: { type: string, format: date-time }
        createdById: { type: [string, "null"], format: uuid }
        updatedById: { type: [string, "null"], format: uuid }
        companyId: { type: [string, "null"], format: uuid }
        company: { $ref: "#/components/schemas/NamedRef" }
        leadSourceId: { type: [string, "null"], format: uuid }
        leadSource: { $ref: "#/components/schemas/NamedRef" }
        salesRepId: { type: [string, "null"], format: uuid }
        salesRep: { $ref: "#/components/schemas/Party" }
        pipelineStageId: { type: [string, "null"], format: uuid }
        pipelineStage: { $ref: "#/components/schemas/NamedRef" }
        primaryContactId: { type: [string, "null"], format: uuid }
        primaryContact:
          type: object
          properties:
            id: { type: string, format: uuid }
            name: { type: string }
            email: { type: [string, "null"] }
        pipelineId: { type: [string, "null"], format: uuid }
        pipeline:
          type: object
          properties:
            id: { type: string, format: uuid }
            name: { type: string }
            stages:
              type: array
              items:
                type: object
                properties:
                  id: { type: string, format: uuid }
                  name: { type: string }
                  position: { type: integer }
        assignedToId: { type: [string, "null"], format: uuid }
        assignedTo: { $ref: "#/components/schemas/Party" }
        divisionId: { type: [string, "null"], format: uuid }
        division:
          type: object
          description: Full division record (all columns), present only when the lead has a division.
          additionalProperties: true

    SalesLeadWrite:
      type: object
      description: Flat (non-wrapped) `snake_case` payload.
      required: [title]
      properties:
        title: { type: string }
        details: { type: string }
        qualified: { type: boolean }
        company_id: { type: string, format: uuid }
        company_name: { type: string }
        primary_contact_id: { type: string, format: uuid }
        primary_contact_name: { type: string }
        email: { type: string }
        phone_number: { type: string }
        sales_rep_id: { type: string, format: uuid }
        sales_rep_name: { type: string }
        deal_value_in_dollars: { type: number }
        workflow_state: { type: string }
        next_contact_date: { type: string, format: date }
        expected_close_date: { type: string, format: date }
        lead_source_id: { type: string, format: uuid }
        lead_source_name: { type: string }
        pipeline_id: { type: string, format: uuid }
        pipeline_name: { type: string }
        pipeline_stage_id: { type: string, format: uuid }
        pipeline_stage_name: { type: string }
        division_id: { type: string, format: uuid }
        lead_source_real_name: { type: string }
        asset:
          type: array
          description: Asset references to attach to the lead.
          items: { type: string }

    SalesLeadStages:
      type: object
      properties:
        pipeline:
          type: object
          properties:
            id: { type: string, format: uuid }
            name: { type: string }
            stages:
              type: array
              items:
                type: object
                properties:
                  id: { type: string, format: uuid }
                  name: { type: string }
                  position: { type: integer }
                  salesLeads:
                    type: array
                    items:
                      type: object
                      properties:
                        id: { type: string, format: uuid }
                        title: { type: string }
                        companyId: { type: [string, "null"], format: uuid }
                        companyName: { type: [string, "null"] }
                        details: { type: [string, "null"] }
                        nextContactDate: { type: [string, "null"], format: date }
                        pipelineStageId: { type: [string, "null"], format: uuid }
                        pipelineStagePosition: { type: [integer, "null"] }
                        salesRep: { $ref: "#/components/schemas/Party" }
        availablePipelines:
          type: array
          items: { $ref: "#/components/schemas/NamedRef" }

    #####################################################################
    # Notes
    #####################################################################
    Note:
      type: object
      properties:
        id: { type: string, format: uuid }
        content: { type: string }
        createdAt: { type: string, format: date-time }
        emailThreadId: { type: [string, "null"] }
        emailThreadSubject: { type: [string, "null"] }
        categoryName: { type: [string, "null"] }
        categoryId: { type: [string, "null"], format: uuid }
        createdBy: { $ref: "#/components/schemas/NamedRef" }

    NoteWrite:
      type: object
      required: [note]
      properties:
        note:
          type: object
          required: [content]
          properties:
            content: { type: string }
            category_id: { type: string, format: uuid }
            email_thread_id: { type: string }
            email_thread_subject: { type: string }

    #####################################################################
    # Assets
    #####################################################################
    Asset:
      type: object
      properties:
        id: { type: string, format: uuid }
        fileUrl: { type: [string, "null"] }
        fileName: { type: [string, "null"] }
        name: { type: [string, "null"] }
        createdAt: { type: string, format: date-time }
        note: { type: [string, "null"] }
        internalUse: { type: [boolean, "null"] }
        fileMimetype: { type: [string, "null"] }
        assetCategoryId: { type: [string, "null"], format: uuid }
        defaultAsset: { type: [boolean, "null"] }
        fileWidth: { type: integer }
        fileHeight: { type: integer }
        assetCategory: { $ref: "#/components/schemas/NamedRef" }

    #####################################################################
    # Orders (legacy)
    #####################################################################
    Order:
      type: object
      properties:
        id: { type: string, format: uuid }
        accountId: { type: string, format: uuid }
        orderNumber: { type: string }
        dueDate: { type: [string, "null"], format: date }
        subtotal: { type: [number, "null"] }
        tax: { type: [number, "null"] }
        total: { type: [number, "null"] }
        taxName: { type: [string, "null"] }
        taxRate: { type: [number, "null"] }
        workflowState: { type: [string, "null"] }
        createdAt: { type: string, format: date-time }
        updatedAt: { type: string, format: date-time }
        canBeCompleted: { type: boolean }
        canBeIgnored: { type: boolean }
        completed: { type: boolean }
        ignored: { type: boolean }
        companyName: { type: [string, "null"] }
        createdById: { type: [string, "null"], format: uuid }
        updatedById: { type: [string, "null"], format: uuid }
        legacy:
          type: object
          properties:
            id: { type: string, format: uuid }
        lineItems:
          type: array
          items:
            type: object
            properties:
              id: { type: string, format: uuid }
              lineNumber: { type: [integer, "null"] }
              position: { type: integer }
              name: { type: [string, "null"] }
              description: { type: [string, "null"] }
              quantity: { type: [number, "null"] }
              price: { type: [number, "null"] }
              totalPrice: { type: [number, "null"] }
              workflowState: { type: [string, "null"] }
              jobStale: { type: [boolean, "null"] }
              jobId: { type: [string, "null"], format: uuid }
              productId: { type: [string, "null"], format: uuid }
              job:
                type: object
                properties:
                  id: { type: string, format: uuid }
                  workflowState: { type: [string, "null"] }
