openapi: 3.1.0
info:
  title: CustomDesigner Integration API
  version: 1.0.0
  description: |
    Company-scoped CRM API. Authenticate with `Authorization: Bearer <token>` using a token
    generated from **Settings → Developer** (64 hex characters).

servers:
  - url: /api/v1
    description: Relative to your site origin

security:
  - bearerAuth: []

paths:
  /clients:
    get:
      summary: List clients
      x-codeSamples:
        - lang: bash
          label: cURL
          source: |
            # Token from Settings → Developer. In /docs/api/explorer, the site base is filled from your browser.
            curl -sS -G "YOUR_APP_ORIGIN/api/v1/clients" \
              -H "Authorization: Bearer YOUR_TOKEN" \
              --data-urlencode "page=1" \
              --data-urlencode "per_page=25" \
              --data-urlencode "q=acme"
      parameters:
        - name: page
          in: query
          schema: { type: integer, default: 1 }
        - name: per_page
          in: query
          schema: { type: integer, default: 25, maximum: 100 }
        - name: q
          in: query
          description: Search company name, name, email, phone
          schema: { type: string }
      responses:
        "200":
          description: Paginated clients
          content:
            application/json:
              schema:
                type: object
                properties:
                  data:
                    type: array
                    items: { type: object }
                  pagination:
                    type: object
                    properties:
                      page: { type: integer }
                      per_page: { type: integer }
                      total: { type: integer }
    post:
      summary: Create client
      x-codeSamples:
        - lang: bash
          label: cURL
          source: |
            curl -sS -X POST "YOUR_APP_ORIGIN/api/v1/clients" \
              -H "Authorization: Bearer YOUR_TOKEN" \
              -H "Content-Type: application/json" \
              -d '{"company_name":"Acme Co","first_name":"Jane","email":"jane@acme.test"}'
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [company_name]
              properties:
                company_name: { type: string }
                first_name: { type: string, nullable: true }
                last_name: { type: string, nullable: true }
                email: { type: string, nullable: true }
                website: { type: string, nullable: true }
                phone: { type: string, nullable: true }
                quickbooks_enabled: { type: boolean }
                tax_exempt: { type: boolean }
                tax_exempt_reason: { type: string, nullable: true }
      responses:
        "200":
          description: Created client (same shape as POST /api/clients/create)
  /clients/{id}:
    get:
      summary: Get one client
      description: |
        Look up a client by numeric ID in the path, by email in the path (URL-encoded), or by
        `email` query parameter (use any placeholder for `{id}` when using the query param, e.g. `_`).
      x-codeSamples:
        - lang: bash
          label: cURL (by ID)
          source: |
            curl -sS "YOUR_APP_ORIGIN/api/v1/clients/CLIENT_ID" \
              -H "Authorization: Bearer YOUR_TOKEN"
        - lang: bash
          label: cURL (by email in path)
          source: |
            curl -sS "YOUR_APP_ORIGIN/api/v1/clients/jane%40acme.test" \
              -H "Authorization: Bearer YOUR_TOKEN"
        - lang: bash
          label: cURL (by email query)
          source: |
            curl -sS -G "YOUR_APP_ORIGIN/api/v1/clients/_" \
              -H "Authorization: Bearer YOUR_TOKEN" \
              --data-urlencode "email=jane@acme.test"
      parameters:
        - name: id
          in: path
          required: true
          description: Client ID, or client email (URL-encoded) when not using the email query param
          schema: { type: string }
        - name: email
          in: query
          description: Client email (case-insensitive exact match). Alternative to putting email in `{id}`.
          schema: { type: string, format: email }
      responses:
        "200":
          content:
            application/json:
              schema:
                type: object
                properties:
                  client: { type: object }
    patch:
      summary: Update client (partial)
      x-codeSamples:
        - lang: bash
          label: cURL
          source: |
            curl -sS -X PATCH "YOUR_APP_ORIGIN/api/v1/clients/CLIENT_ID" \
              -H "Authorization: Bearer YOUR_TOKEN" \
              -H "Content-Type: application/json" \
              -d '{"phone":"555-0100"}'
      parameters:
        - name: id
          in: path
          required: true
          schema: { type: string }
      requestBody:
        content:
          application/json:
            schema:
              type: object
              description: |
                Standard client fields, or `deleted: true` to soft-delete (hides from list).
      responses:
        "200":
          description: Same as PATCH /api/clients/update
  /docs/search:
    get:
      summary: Search in-app product docs
      description: |
        Full-text search over the same markdown as `/docs` (`content/docs/`).
        Product documentation only — not tenant CRM data. Still requires staff v1 auth.
        Returns title, slug, snippet, and a citeable shop URL (`/docs/...`).
      x-codeSamples:
        - lang: bash
          label: cURL
          source: |
            curl -sS -G "YOUR_APP_ORIGIN/api/v1/docs/search" \
              -H "Authorization: Bearer YOUR_TOKEN" \
              --data-urlencode "q=what is My Catalog"
      parameters:
        - name: q
          in: query
          required: true
          description: Staff question or keywords about the Custom Designer interface
          schema: { type: string }
      responses:
        "200":
          content:
            application/json:
              schema:
                type: object
                properties:
                  query: { type: string }
                  source: { type: string, enum: [local, http] }
                  results:
                    type: array
                    items:
                      type: object
                      properties:
                        title: { type: string }
                        slug: { type: string }
                        snippet: { type: string }
                        url: { type: string }
                        path: { type: string }
                        excerpt: { type: string }
                        headings:
                          type: array
                          items: { type: string }
  /orders:
    get:
      summary: List orders (projects)
      x-codeSamples:
        - lang: bash
          label: cURL
          source: |
            curl -sS -G "YOUR_APP_ORIGIN/api/v1/orders" \
              -H "Authorization: Bearer YOUR_TOKEN" \
              --data-urlencode "page=1" \
              --data-urlencode "client_id=CLIENT_UUID" \
              --data-urlencode "from=2025-01-01" \
              --data-urlencode "to=2025-12-31"
      parameters:
        - name: page
          in: query
          schema: { type: integer }
        - name: per_page
          in: query
          schema: { type: integer }
        - name: client_id
          in: query
          schema: { type: string }
        - name: status
          in: query
          description: order_status_id
          schema: { type: string }
        - name: from
          in: query
          description: Filter created_date >= (date string)
          schema: { type: string }
        - name: to
          in: query
          description: Filter created_date <= (date string)
          schema: { type: string }
      responses:
        "200":
          content:
            application/json:
              schema:
                type: object
                properties:
                  data: { type: array, items: { type: object } }
                  pagination:
                    type: object
    post:
      summary: Create order
      x-codeSamples:
        - lang: bash
          label: cURL
          source: |
            curl -sS -X POST "YOUR_APP_ORIGIN/api/v1/orders" \
              -H "Authorization: Bearer YOUR_TOKEN" \
              -H "Content-Type: application/json" \
              -d '{"title":"Spring reorder","client_id":12345,"description":"Rush"}'
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [title, client_id]
              properties:
                title: { type: string }
                client_id: { type: number }
                description: { type: string }
                quickbooks_enabled: { type: boolean }
      responses:
        "200":
          description: createOrder result (project, invoice, …)
  /orders/schedule:
    get:
      summary: List orders on the production calendar
      description: |
        Shop-wide orders whose **due date** (`deadline`) and/or **start date** fall in `[from, to]`
        (inclusive civil dates). Same source as Schedule → Next 20 days. Unlike `GET /orders`,
        this does **not** filter on `created_date`.
      x-codeSamples:
        - lang: bash
          label: cURL
          source: |
            curl -sS -G "YOUR_APP_ORIGIN/api/v1/orders/schedule" \
              -H "Authorization: Bearer YOUR_TOKEN" \
              --data-urlencode "from=2026-08-23" \
              --data-urlencode "to=2026-08-29" \
              --data-urlencode "date_source=deadline"
      parameters:
        - name: from
          in: query
          required: true
          description: Inclusive start (YYYY-MM-DD)
          schema: { type: string, format: date }
        - name: to
          in: query
          required: true
          description: Inclusive end (YYYY-MM-DD)
          schema: { type: string, format: date }
        - name: date_source
          in: query
          description: Which marker to use. Default `deadline` (due date).
          schema: { type: string, enum: [deadline, start, both], default: deadline }
        - name: client_id
          in: query
          schema: { type: integer }
      responses:
        "200":
          content:
            application/json:
              schema:
                type: object
                properties:
                  from: { type: string }
                  to: { type: string }
                  date_source: { type: string, enum: [deadline, start, both] }
                  truncated: { type: boolean }
                  data:
                    type: array
                    items:
                      type: object
                      properties:
                        id: { type: integer }
                        title: { type: string }
                        status: { type: string, nullable: true }
                        client_name: { type: string }
                        start_date: { type: string, nullable: true }
                        deadline: { type: string, nullable: true }
                        marker: { type: string, enum: [deadline, start] }
                        marker_date: { type: string }
  /orders/{id}:
    get:
      summary: Get full order (read action)
      description: |
        Returns **project**, **invoice**, **invoice_items**, **payments**, **client**, **tax**, **order_status**, and **artworks**
        (print method, placements, colors, preview URL, status).
        **order_status** includes title, color, workflow_key, and related workflow fields for the project's `order_status_id`.
      x-codeSamples:
        - lang: bash
          label: cURL
          source: |
            curl -sS "YOUR_APP_ORIGIN/api/v1/orders/ORDER_ID" \
              -H "Authorization: Bearer YOUR_TOKEN"
      parameters:
        - name: id
          in: path
          required: true
          schema: { type: string }
    patch:
      summary: Update order
      description: |
        Partial update: include only fields you want to change. Behavior matches `POST /api/order` with `action: "update"`.
        Project fields update the **projects** row; invoice fields update the order’s **default invoice** when one exists.
      parameters:
        - name: id
          in: path
          required: true
          description: Project (order) id
          schema: { type: string }
      x-codeSamples:
        - lang: bash
          label: cURL
          source: |
            curl -sS -X PATCH "YOUR_APP_ORIGIN/api/v1/orders/ORDER_ID" \
              -H "Authorization: Bearer YOUR_TOKEN" \
              -H "Content-Type: application/json" \
              -d '{"title":"Updated title","status_id":2,"deadline":"2026-06-15","tax_exempt":false}'
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/OrderUpdate'
            examples:
              minimal:
                summary: Title only
                value:
                  title: Summer restock
              status:
                summary: Workflow status
                value:
                  status_id: 3
              scheduleAndClient:
                summary: Dates + client
                value:
                  start_date: '2026-05-01'
                  deadline: '2026-06-30'
                  client_id: 42
              invoiceTotals:
                summary: Tax and discounts (default invoice)
                value:
                  tax_id: 1
                  tax_exempt: false
                  discount_amount: 10
                  discount_amount_type: percentage
                  discount_type: before_tax
              clientVisibleNote:
                summary: Note on invoice (customer-facing)
                value:
                  client_notes: Please ship consolidated.
      responses:
        "200":
          description: Updated **project** and **invoice** (same shape as internal order update)
    delete:
      summary: Delete order (soft-delete project + related invoice; QuickBooks if enabled)
      x-codeSamples:
        - lang: bash
          label: cURL
          source: |
            curl -sS -X DELETE "YOUR_APP_ORIGIN/api/v1/orders/ORDER_ID" \
              -H "Authorization: Bearer YOUR_TOKEN"
      parameters:
        - name: id
          in: path
          required: true
          schema: { type: string }
      responses:
        "200":
          description: Same as /api/order action delete

  /products/search:
    get:
      summary: Search products (all sources, like invoice line search)
      description: |
        Runs the same parallel lookups as the **invoice “Search products…”** combobox:
        **`recommended_products`** — company **My Catalog** (Settings → My Catalog / `recommended_products`); empty `q` returns a sample of suggested styles;
        **`local_items`** — company **`items`** catalog (`textSearch` on title when `q` is non-empty; empty `q` returns a small recent sample);
        **`ss_styles`** — S&S **`/styles?search=`** (same as `/api/ss/search`);
        **`sanmar`** — style token extracted from `q` (same rules as the invoice UI) then full catalog rows;
        **`printful_products`** — Printful catalog search when configured;
        **`invoice_lines`** — **`invoice_items`** rows matching title or description (company-scoped).

        Use **`include`** to limit sources: comma list of `local`, `ss`, `sanmar`, `printful`, `lines`, `recommended`. Supplier or DB failures for one source do not fail the whole response (empty arrays).
      parameters:
        - name: q
          in: query
          required: false
          description: Search text; omit or blank for My Catalog plus a small sample of local catalog
          schema: { type: string }
        - name: include
          in: query
          description: "Subset, e.g. `local,ss` or `sanmar,lines`"
          schema: { type: string }
      responses:
        "200":
          description: |
            `{ query, recommended_products, local_items, ss_styles, sanmar: { style, products, count, summary }, printful_products, invoice_lines }`

  /products/{source}:
    get:
      summary: Product info (SanMar, S&S, or stored invoice line)
      description: |
        - **`sanmar`** — Catalog variants for a style (optional `color`, `size`). Uses the company SanMar integration (same backend as `/api/sanmar/products`).
        - **`ss`** — S&S Activewear JSON, grouped by color (same behavior as `/api/ss/products`). Pass **`sku`** or **`styleId`** (alias `styleid`).
        - **`invoice_item`** — Full **`invoice_items`** row by numeric **`id`** (must belong to your company; not deleted).
      parameters:
        - name: source
          in: path
          required: true
          schema:
            type: string
            enum: [sanmar, ss, invoice_item]
        - name: style
          in: query
          description: Required for **sanmar**. For **ss**, use as **styleId** when `sku` is omitted.
          schema: { type: string }
        - name: color
          in: query
          description: Optional SanMar filter
          schema: { type: string }
        - name: size
          in: query
          description: Optional SanMar filter
          schema: { type: string }
        - name: sku
          in: query
          description: S&S — fetch one SKU (omit if using styleId)
          schema: { type: string }
        - name: styleId
          in: query
          description: S&S style id (query alias **styleid** also accepted)
          schema: { type: string }
        - name: id
          in: query
          description: Required for **invoice_item** — `invoice_items.id`
          schema: { type: integer }
      responses:
        "200":
          description: JSON body; includes `source` and supplier-specific `products` / `query`, or `{ source, item }` for invoice_item
        "404":
          description: invoice_item not found
        "409":
          description: Supplier integration missing or disabled

  /quotes/catalog:
    get:
      summary: My Catalog product list for quoting
      description: |
        Simple product list from **Settings → My Catalog**. Use this as the garment
        catalog for AI agents and custom quote UIs, then call `POST /quotes/calculate`.

        `wholesale` is filled for **local** items. S&amp;S / SanMar piece price is resolved
        during calculate (or pass `wholesale` yourself).
      parameters:
        - name: q
          in: query
          required: false
          description: Optional filter on title, brand, style, or category
          schema: { type: string }
      responses:
        "200":
          description: `{ query, items: [{ id, source, style, brand, title, category, fit, image_url, wholesale }] }`
      x-codeSamples:
        - lang: bash
          label: cURL
          source: |
            curl -sS -G "YOUR_APP_ORIGIN/api/v1/quotes/catalog" \
              -H "Authorization: Bearer YOUR_TOKEN" \
              --data-urlencode "q=tee"

  /quotes/calculate:
    post:
      summary: Calculate a Quick Quote
      description: |
        Runs the same engine as **Quick Quote** (markup tiers, press run, finishing, names/numbers)
        using the company's saved settings.

        Identify the blank with **`recommended_product_id`** or **`style`** from `/quotes/catalog`,
        a **`preset`** (`budget` / `standard` / `premium` / `fleece`), or an explicit **`wholesale`**.

        Personalization uses shop rates from **Settings → Quick Quote**. Send `personalization.type`
        as `none`, `numbers`, `name`, or `name_and_number`. Optional roster:
        `[{ "name": "GARCIA", "number": "23" }]`.
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              properties:
                quantity: { type: integer, default: 24 }
                wholesale: { type: number, description: Override blank cost $/unit }
                preset: { type: string, description: "budget | standard | premium | fleece" }
                recommended_product_id: { type: integer }
                style: { type: string }
                color: { type: string }
                front_colors: { type: integer, default: 1 }
                back_colors: { type: integer, default: 0 }
                garment_color: { type: string, enum: [light, dark] }
                fabric: { type: string, enum: [cotton, polyester] }
                fleece: { type: boolean }
                include_press_run: { type: boolean }
                include_prepress: { type: boolean }
                finishing:
                  type: object
                  properties:
                    folding: { type: boolean }
                    polybagging: { type: boolean }
                    neck_labels: { type: boolean }
                personalization:
                  type: object
                  properties:
                    type: { type: string, enum: [none, numbers, name, name_and_number] }
                    multi_color: { type: boolean }
                    quantity: { type: integer }
                    digits_per_shirt: { type: integer }
                    one_digit_count: { type: integer }
                    two_digit_count: { type: integer }
                    roster:
                      type: array
                      items:
                        type: object
                        properties:
                          name: { type: string }
                          number: { type: string }
      responses:
        "200":
          description: `{ product, preferences, quote: { grand_total, lines, personalization, economics } }`
      x-codeSamples:
        - lang: bash
          label: cURL
          source: |
            curl -sS -X POST "YOUR_APP_ORIGIN/api/v1/quotes/calculate" \
              -H "Authorization: Bearer YOUR_TOKEN" \
              -H "Content-Type: application/json" \
              -d '{"quantity":24,"preset":"standard","personalization":{"type":"name_and_number"}}'

  /gmail/threads/{legacyThreadId}:
    get:
      summary: Gmail thread context (orders / client sync)
      description: |
        Returns cached Gmail thread ↔ CRM context from **`gmail_thread_contexts`**, keyed by
        **`legacy_thread_id`** (Gmail thread id from the extension or prior integration).

        Response includes **`orders_data`**, **`client_data`**, **`clients_data`**, **`participant_emails`**, and
        **`participants_data`** as stored JSON. Access is limited to your company: the row must reference your
        clients or projects, or embed your **`company_id`** in the cached JSON.
      x-codeSamples:
        - lang: bash
          label: cURL
          source: |
            curl -sS "YOUR_APP_ORIGIN/api/v1/gmail/threads/LEGACY_THREAD_ID" \
              -H "Authorization: Bearer YOUR_TOKEN"
      parameters:
        - name: legacyThreadId
          in: path
          required: true
          description: Gmail legacy thread id (same value stored in `gmail_thread_contexts.legacy_thread_id`)
          schema: { type: string }
      responses:
        "200":
          description: Thread context JSON
          content:
            application/json:
              schema:
                type: object
                properties:
                  thread_id: { type: string }
                  legacy_thread_id: { type: string, nullable: true }
                  sender_email: { type: string, nullable: true }
                  client_id: { type: string, nullable: true }
                  client_data: { type: object, nullable: true }
                  clients_data: { type: object, nullable: true }
                  orders_data: { type: object, nullable: true }
                  selected_order_id: { type: string, nullable: true }
                  participant_emails: { nullable: true }
                  participants_data: { type: object, nullable: true }
                  fetched_at: { type: string, format: date-time }
                  updated_at: { type: string, format: date-time }
        "404":
          description: Not found or not accessible for your company

  /gmail/email-threads/{legacyThreadId}:
    put:
      summary: Upsert email thread (full messages from CLI)
      description: |
        Creates or updates **`email_threads`** for your company and **`account_email`**.
        Conflict target: **`(company_id, account_email, legacy_thread_id)`**. Send the full **`messages`**
        array on each sync. See **`docs/api/GMAIL_INTEGRATION.md`** for payload examples.
      x-codeSamples:
        - lang: bash
          label: cURL
          source: |
            curl -sS -X PUT "YOUR_APP_ORIGIN/api/v1/gmail/email-threads/LEGACY_THREAD_ID" \
              -H "Authorization: Bearer YOUR_TOKEN" \
              -H "Content-Type: application/json" \
              -d '{"account_email":"shop@example.com","subject":"Re: Quote","messages":[]}'
      parameters:
        - name: legacyThreadId
          in: path
          required: true
          schema: { type: string }
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [account_email, messages]
              properties:
                account_email: { type: string, format: email }
                messages: { type: array, items: { type: object } }
                participants: { type: array, items: { type: object } }
                labels: { type: array, items: { type: string } }
                subject: { type: string, nullable: true }
                snippet: { type: string, nullable: true }
                gmail_thread_id: { type: string, nullable: true }
                first_message_at: { type: string, format: date-time, nullable: true }
                last_message_at: { type: string, format: date-time, nullable: true }
                gmail_history_id: { type: string, nullable: true }
                last_synced_at: { type: string, format: date-time, nullable: true }
      responses:
        "200":
          description: Upserted thread
          content:
            application/json:
              schema:
                type: object
                properties:
                  email_thread: { type: object }
    get:
      summary: Get email thread by mailbox + legacy id
      x-codeSamples:
        - lang: bash
          label: cURL
          source: |
            curl -sS -G "YOUR_APP_ORIGIN/api/v1/gmail/email-threads/LEGACY_THREAD_ID" \
              -H "Authorization: Bearer YOUR_TOKEN" \
              --data-urlencode "account_email=shop@example.com"
      parameters:
        - name: legacyThreadId
          in: path
          required: true
          schema: { type: string }
        - name: account_email
          in: query
          required: true
          schema: { type: string, format: email }
      responses:
        "200":
          description: Stored email thread
        "404":
          description: Not found

  /orders/{id}/files/presign:
    post:
      summary: Presign R2 upload for a Gmail attachment
      description: |
        Returns a short-lived PUT URL for Cloudflare R2. Object key:
        `orders/{companyId}/{orderId}/email/{legacyThreadId}/{safeBasename}`.
        See **`docs/api/GMAIL_ATTACHMENT_SYNC.md`**.
      x-codeSamples:
        - lang: bash
          label: cURL
          source: |
            curl -sS -X POST "YOUR_APP_ORIGIN/api/v1/orders/1383/files/presign" \
              -H "Authorization: Bearer YOUR_TOKEN" \
              -H "Content-Type: application/json" \
              -d '{"fileName":"Tiger.jpg","contentType":"image/jpeg","size":245760,"legacyThreadId":"19f38fde180ceb36"}'
      parameters:
        - name: id
          in: path
          required: true
          schema: { type: string }
          description: Order / project id
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [fileName, size, legacyThreadId]
              properties:
                fileName: { type: string }
                contentType: { type: string }
                size: { type: number, description: Bytes; max 150MB }
                legacyThreadId: { type: string }
      responses:
        "200":
          description: Presigned upload
          content:
            application/json:
              schema:
                type: object
                properties:
                  uploadUrl: { type: string }
                  key: { type: string }
                  contentType: { type: string }
                  expiresIn: { type: integer }
        "404":
          description: Order not found
        "503":
          description: File storage not configured

  /orders/{id}/files:
    get:
      summary: List order R2 files (optional email thread filter)
      description: |
        Use for idempotency before uploading. Pass `legacyThreadId` to list only
        `…/email/{legacyThreadId}/`. Each file includes `source`: `email` or `manual`.
      x-codeSamples:
        - lang: bash
          label: cURL
          source: |
            curl -sS -G "YOUR_APP_ORIGIN/api/v1/orders/1383/files" \
              -H "Authorization: Bearer YOUR_TOKEN" \
              --data-urlencode "legacyThreadId=19f38fde180ceb36"
      parameters:
        - name: id
          in: path
          required: true
          schema: { type: string }
        - name: legacyThreadId
          in: query
          required: false
          schema: { type: string }
      responses:
        "200":
          description: File list
          content:
            application/json:
              schema:
                type: object
                properties:
                  configured: { type: boolean }
                  files:
                    type: array
                    items:
                      type: object
                      properties:
                        path: { type: string }
                        objectName: { type: string }
                        length: { type: number }
                        lastChanged: { type: string, format: date-time }
                        source: { type: string, enum: [email, manual] }
        "404":
          description: Order not found
        "503":
          description: File storage not configured

components:
  schemas:
    OrderUpdate:
      type: object
      description: |
        All properties are optional; send a non-empty object. **`status_id`** is an **order_statuses.id** for your company.
        **`discount_amount_type`**: use `percentage` or `fixed_amount` (aliases `fixed` / `amount` are accepted server-side).
        **`discount_type`**: `before_tax` (default) or `after_tax`.
      properties:
        title:
          type: string
          description: Project / order title
        description:
          type: string
          nullable: true
          description: Project description (also synced to QuickBooks private note when applicable)
        start_date:
          type: string
          description: Project start date (pass-through to DB; ISO date string recommended)
        deadline:
          type: string
          description: "Project deadline; also copied to invoice due_date when an invoice exists"
        client_id:
          type: integer
          description: Client id; updates project and default invoice
        status_id:
          type: integer
          description: "order_statuses.id, stored as project order_status_id"
        client_notes:
          type: string
          nullable: true
          description: "Shown on invoice; maps to invoice note"
        tax_id:
          type: integer
          description: "Tax row id on the default invoice (0 or omit for none, per your data)"
        tax_exempt:
          type: boolean
          description: Invoice tax exempt flag
        tax_exempt_reason:
          type: string
          nullable: true
        discount_amount:
          type: number
          description: "Discount amount; interpreted together with discount_amount_type"
        discount_amount_type:
          type: string
          enum: [percentage, fixed_amount]
          description: "percentage vs fixed_amount (fixed currency amount)"
        discount_type:
          type: string
          enum: [before_tax, after_tax]
        quickbooks_enabled:
          type: boolean
          description: Override QuickBooks sync for this request; defaults to company flag when omitted
  securitySchemes:
    bearerAuth:
      type: http
      scheme: bearer
      bearerFormat: 64 hex (no prefix)
