> ## Documentation Index
> Fetch the complete documentation index at: https://docs.printools.io/llms.txt
> Use this file to discover all available pages before exploring further.

# One quote and everything hanging off it, in one call

> Requires ALL of the scopes quotes.read, customers.read, garments.read, decorations.read.

Required scopes: `quotes.read`, `customers.read`, `garments.read`, `decorations.read`.


## OpenAPI

````yaml GET /v1/quotes/{quoteId}/data
openapi: 3.1.0
info:
  title: PRINTOOLS API
  version: 1.0.0
  description: >-
    The PRINTOOLS public REST API. This document is generated from the live
    route table and service GraphQL SDL; do not edit it by hand.


    Every 2xx response has {status, message, data}. Every non-2xx response has
    {error, message, status: false}.


    Tenant credential: send X-API-Key only. It is bound to one shop.
    X-PrintTools-Organization-Id is optional for that credential and, if sent,
    must match its shop.


    Partner credential: send X-API-Key, X-API-Secret, Authorization: Bearer
    <Cognito access token>, and X-PrintTools-Organization-Id. The OAuth flow is
    documented at https://app.printools.io/api/index.html#partner-auth.


    Read live limits from GET /v1/reference/limits. Field shapes are projected
    from operation declarations; local endpoints have explicit handler-derived
    schemas.
servers:
  - url: https://api.printools.io
    description: Production
security: []
externalDocs:
  description: Getting started, OAuth and webhook guidance
  url: https://app.printools.io/api/index.html
paths:
  /v1/quotes/{quoteId}/data:
    get:
      tags:
        - quotes
      summary: One quote and everything hanging off it, in one call.
      description: >-
        Requires ALL of the scopes quotes.read, customers.read, garments.read,
        decorations.read.
      operationId: get_v1_quotes_quoteId_data
      parameters:
        - name: quoteId
          in: path
          required: true
          schema:
            type: string
            format: uuid
            example: 11111111-2222-3333-4444-555555555555
          description: UUID.
        - name: X-PrintTools-Organization-Id
          in: header
          required: false
          schema:
            type: string
            format: uuid
          description: >-
            Tenant credential: optional and, when sent, must equal the shop
            bound to the key. Partner credential: required; it selects an
            organisation the user previously authorised.
      responses:
        '200':
          description: Successful response.
          content:
            application/json:
              schema:
                allOf:
                  - $ref: '#/components/schemas/Success'
                  - type: object
                    properties:
                      data:
                        type: object
                        required:
                          - quote
                          - customer
                          - mocks
                          - invoices
                        properties:
                          quote:
                            anyOf:
                              - $ref: '#/components/schemas/Output_Quote'
                              - type: 'null'
                          customer:
                            anyOf:
                              - anyOf:
                                  - $ref: '#/components/schemas/Output_Customer'
                                  - type: 'null'
                              - type: 'null'
                          mocks:
                            type: array
                            items:
                              $ref: '#/components/schemas/Output_Mock'
                          invoices:
                            type: array
                            items:
                              $ref: '#/components/schemas/Output_Invoice'
        '400':
          description: >-
            Validation or request-shape error. Correct the request before
            retrying.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
        '401':
          description: Credential authentication failed.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
          headers:
            WWW-Authenticate:
              schema:
                type: string
                example: ApiKey
        '403':
          description: >-
            The credential lacks a required scope, or its actor lacks
            permission.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
        '404':
          description: The resource or endpoint was not found.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
        '409':
          description: >-
            Conflict, including idempotency-key reuse or an in-flight idempotent
            request.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
        '413':
          description: The JSON request body exceeds the published size limit.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
        '429':
          description: Rate limited. Retry after the returned delay.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
          headers:
            Retry-After:
              description: Whole seconds to wait.
              schema:
                type: integer
                minimum: 0
        '500':
          description: An unexpected server error. A retry can be appropriate.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
      security:
        - tenantApiKey: []
        - partnerApiKey: []
          partnerApiSecret: []
          partnerBearer: []
      x-codeSamples:
        - lang: curl
          label: Tenant credential
          source: >-
            curl --request GET
            'https://api.printools.io/v1/quotes/11111111-2222-3333-4444-555555555555/data'
            \
              --header 'X-API-Key: $PRINTOOLS_API_KEY'
components:
  schemas:
    Success:
      type: object
      required:
        - status
        - message
        - data
      properties:
        status:
          type: boolean
          description: >-
            The actual resolver result. It can be false when an operation
            returns false or null without throwing.
        message:
          type: string
          example: OK
        data: {}
      description: Every 2xx response uses this envelope.
    Output_Quote:
      type: object
      properties:
        id:
          type: string
        quoteRef:
          type:
            - string
            - 'null'
        reference:
          type:
            - string
            - 'null'
        status:
          type: string
        clientId:
          type:
            - string
            - 'null'
        contactId:
          type:
            - string
            - 'null'
        shippingAddressId:
          type:
            - string
            - 'null'
        salesRepEmail:
          type:
            - string
            - 'null'
        deliveryMethod:
          type:
            - string
            - 'null'
        deliveryDate:
          type:
            - string
            - 'null'
          format: date
        productionDeadline:
          type:
            - string
            - 'null'
          format: date
        invoiceDueDate:
          type:
            - string
            - 'null'
          format: date
        notes:
          type:
            - string
            - 'null'
        subtotalExGst:
          type: number
        gstAmount:
          type: number
        totalIncGst:
          type: number
        taxRateId:
          type:
            - string
            - 'null'
        taxName:
          type:
            - string
            - 'null'
        taxRatePct:
          type:
            - number
            - 'null'
        mockId:
          type:
            - string
            - 'null'
        jobId:
          type:
            - string
            - 'null'
        jobRef:
          type:
            - string
            - 'null'
          description: >-
            The linked job's own `jobRef` (CB-05), resolved server-side from the
            job row —

            never derived from `jobId`. Null when the quote has no job, when
            `jobId` points

            at a row that no longer exists (these link columns carry no FK) or
            is not

            visible to the caller's tenant, and when the linked job's own ref is
            null.

            Read-only: no mutation input sets it.
        invoiceId:
          type:
            - string
            - 'null'
        invoiceStatus:
          type:
            - string
            - 'null'
        sentToEmail:
          type:
            - string
            - 'null'
        sentAt:
          type:
            - string
            - 'null'
          format: date-time
        createdAt:
          type: string
          format: date-time
        updatedAt:
          type: string
          format: date-time
        groups:
          type: array
          items:
            $ref: '#/components/schemas/Output_QuoteGroup'
      required:
        - id
        - status
        - subtotalExGst
        - gstAmount
        - totalIncGst
        - createdAt
        - updatedAt
        - groups
    Output_Customer:
      type: object
      properties:
        id:
          type: string
        kind:
          $ref: '#/components/schemas/Enum_CustomerKind'
        name:
          type:
            - string
            - 'null'
        businessName:
          type:
            - string
            - 'null'
        firstName:
          type:
            - string
            - 'null'
        lastName:
          type:
            - string
            - 'null'
        email:
          type:
            - string
            - 'null'
        phone:
          type:
            - string
            - 'null'
        abn:
          type:
            - string
            - 'null'
        tier:
          type:
            - string
            - 'null'
        notes:
          type:
            - string
            - 'null'
        isActive:
          type: boolean
        portalToken:
          type:
            - string
            - 'null'
        defaultBillingAddressId:
          type:
            - string
            - 'null'
        defaultShippingAddressId:
          type:
            - string
            - 'null'
        primaryContactId:
          type:
            - string
            - 'null'
        archivedAt:
          type:
            - string
            - 'null'
          format: date-time
        createdAt:
          type: string
          format: date-time
        updatedAt:
          type: string
          format: date-time
        contacts:
          type: array
          items:
            $ref: '#/components/schemas/Output_Contact'
        addresses:
          type: array
          items:
            $ref: '#/components/schemas/Output_CustomerAddress'
      required:
        - id
        - kind
        - isActive
        - createdAt
        - updatedAt
        - contacts
        - addresses
    Output_Mock:
      type: object
      properties:
        id:
          type: string
        jobId:
          type:
            - string
            - 'null'
        quoteId:
          type:
            - string
            - 'null'
        quoteGroupId:
          type:
            - string
            - 'null'
          description: >-
            The quote group this mock illustrates. Multi-option quotes carry one
            mock per option; `Quote.mockId` stays the quote-level primary.
        quoteLineId:
          type:
            - string
            - 'null'
          description: >-
            The quote line this mock's composite mirrors onto (via
            QuoteLine.customImageUrl), so the mock reaches the quote document
            and quotePdf.
        name:
          type:
            - string
            - 'null'
        garmentAssetId:
          type:
            - string
            - 'null'
        baseImageUrl:
          type:
            - string
            - 'null'
        status:
          type: string
        version:
          type: integer
        parentMockId:
          type:
            - string
            - 'null'
        approvedAt:
          type:
            - string
            - 'null'
          format: date-time
        approvedByName:
          type:
            - string
            - 'null'
        approvedByEmail:
          type:
            - string
            - 'null'
        rejectionReason:
          type:
            - string
            - 'null'
        previewUrl:
          type:
            - string
            - 'null'
        layers:
          description: >-
            Arbitrary JSON. Its structure is application-defined and is
            intentionally not presented as a closed object.
        createdAt:
          type: string
          format: date-time
        updatedAt:
          type: string
          format: date-time
      required:
        - id
        - status
        - version
        - layers
        - createdAt
        - updatedAt
    Output_Invoice:
      type: object
      properties:
        id:
          type: string
        quoteId:
          type: string
        clientId:
          type:
            - string
            - 'null'
        contactId:
          type:
            - string
            - 'null'
        invoiceRef:
          type: string
          description: INV-YYYY-NNNN, per-org sequence.
        currency:
          type: string
        subtotalCents:
          type: integer
          description: Money in cents (dollars -> cents, round half-up).
        gstCents:
          type: integer
        amountCents:
          type: integer
        status:
          type: string
          description: >-
            Draft | AwaitingPayment | Pending | Paid | PaymentFailed | Refunded
            | Void
        dueDate:
          type:
            - string
            - 'null'
          format: date
        lineItemsSnapshot:
          description: Locked copy of the quote tree at generation time.
        notes:
          type:
            - string
            - 'null'
        pinchPaymentLinkUrl:
          type:
            - string
            - 'null'
          description: >-
            Hosted Pinch payment link; null for manual-payment invoices.


            Also null — for every read and every mutation return — while the
            org's

            `payments` settings area has `paymentLinksEnabled: false`. The
            column is left

            intact underneath, so re-enabling the toggle restores the link
            without

            re-minting it at Pinch.
        pinchPaymentId:
          type:
            - string
            - 'null'
          description: Pinch payment id (pmt_...) once a platform payment lands (C3).
        xeroInvoiceId:
          type:
            - string
            - 'null'
          description: >-
            Accounting-sync receipts (Epic D): set once pushed; null = not
            synced.
        qboSalesReceiptId:
          type:
            - string
            - 'null'
        xeroPaymentId:
          type:
            - string
            - 'null'
          description: >-
            Xero payment id once the payment is recorded in Xero; null = invoice
            pushed but payment not synced.
        xeroInvoiceUrl:
          type:
            - string
            - 'null'
          description: Deep link to this invoice in Xero; null until pushed.
        qboInvoiceId:
          type:
            - string
            - 'null'
          description: >-
            QB-01 — the COLLECTABLE QuickBooks Invoice, set by
            pushInvoiceToQuickBooks.

            Distinct from qboSalesReceiptId, which is the paid-record document
            the

            dispatcher writes for a shop that never presses the manual action;
            an

            invoice carries one or the other, never both.
        qboPaymentId:
          type:
            - string
            - 'null'
          description: >-
            QuickBooks payment id once the payment is booked there; null =
            invoice pushed but payment not synced.
        qboInvoiceUrl:
          type:
            - string
            - 'null'
          description: Deep link to this invoice in QuickBooks; null until pushed.
        paidMethod:
          type:
            - string
            - 'null'
          description: >-
            pinch | bank_transfer | cash | card | other | xero | quickbooks (how
            Paid happened).
        paidReference:
          type:
            - string
            - 'null'
        paidAt:
          type:
            - string
            - 'null'
          format: date-time
        sentAt:
          type:
            - string
            - 'null'
          format: date-time
        pinchRefundId:
          type:
            - string
            - 'null'
          description: Pinch refund id (rfd_...); null for off-platform refunds.
        refundedAmountCents:
          type:
            - integer
            - 'null'
        refundReason:
          type:
            - string
            - 'null'
        refundedAt:
          type:
            - string
            - 'null'
          format: date-time
        voidReason:
          type:
            - string
            - 'null'
          description: Why the invoice was voided; null unless status is Void.
        voidedAt:
          type:
            - string
            - 'null'
          format: date-time
        paymentClaimedAt:
          type:
            - string
            - 'null'
          format: date-time
          description: >-
            Client-declared off-platform payment ("remittance advice"),
            submitted from the portal.

            NOT a payment record — the shop must verify it. Cleared by
            markInvoicePaid (confirmed),

            dismissPaymentClaim (rejected) and the Pinch webhook `paid` path (a
            real payment makes

            the claim moot).
        paymentClaimedMethod:
          type:
            - string
            - 'null'
          description: bank_transfer | cash | other — the portal's claim vocabulary.
        paymentClaimedReference:
          type:
            - string
            - 'null'
        paymentClaimedNote:
          type:
            - string
            - 'null'
        createdAt:
          type: string
          format: date-time
        updatedAt:
          type: string
          format: date-time
      required:
        - id
        - quoteId
        - invoiceRef
        - currency
        - subtotalCents
        - gstCents
        - amountCents
        - status
        - lineItemsSnapshot
        - createdAt
        - updatedAt
    Error:
      type: object
      required:
        - error
        - message
        - status
      properties:
        error:
          type: string
          description: Stable machine-readable error code.
        message:
          type: string
        status:
          type: boolean
          const: false
      description: Every non-2xx response uses this envelope.
    Output_QuoteGroup:
      type: object
      properties:
        id:
          type: string
        quoteId:
          type: string
        name:
          type: string
        totalQuantity:
          type: integer
        sortOrder:
          type: integer
        rollUpDecoration:
          type: boolean
        rollUpSetup:
          type: boolean
        createdAt:
          type: string
          format: date-time
        updatedAt:
          type: string
          format: date-time
        lines:
          type: array
          items:
            $ref: '#/components/schemas/Output_QuoteLine'
          description: >-
            `lines` and `decorations` are two collections sharing ONE
            `sortOrder`

            sequence (F-26). To render a group in the operator's order, merge
            them and

            sort by `sortOrder` — do not concatenate lines-then-decorations,
            which is

            what made a surcharge unable to sit below a decoration. To reorder,
            renumber

            across BOTH collections.
        decorations:
          type: array
          items:
            $ref: '#/components/schemas/Output_QuoteGroupDecoration'
      required:
        - id
        - quoteId
        - name
        - totalQuantity
        - sortOrder
        - rollUpDecoration
        - rollUpSetup
        - createdAt
        - updatedAt
        - lines
        - decorations
    Enum_CustomerKind:
      type: string
      enum:
        - Business
        - Individual
    Output_Contact:
      type: object
      properties:
        id:
          type: string
        customerId:
          type: string
        isPrimary:
          type: boolean
        fullName:
          type: string
        roleTitle:
          type:
            - string
            - 'null'
        email:
          type:
            - string
            - 'null'
        phone:
          type:
            - string
            - 'null'
        notes:
          type:
            - string
            - 'null'
        createdAt:
          type: string
          format: date-time
        updatedAt:
          type: string
          format: date-time
        includeInAccountingEmails:
          type: boolean
          description: >-
            Copy this person on the accounting provider's outgoing documents.
            Maps onto

            Xero's `ContactPerson.IncludeInEmails` — the only recipient switch
            Xero

            exposes, and a property of the PERSON on the business record because
            a Xero

            invoice has no per-invoice recipient field.


            Defaults false: only the business's own address is written to until
            the shop

            deliberately opts somebody in.
      required:
        - id
        - customerId
        - isPrimary
        - fullName
        - createdAt
        - updatedAt
        - includeInAccountingEmails
    Output_CustomerAddress:
      type: object
      properties:
        id:
          type: string
        customerId:
          type: string
        kind:
          $ref: '#/components/schemas/Enum_AddressKind'
        fullAddress:
          type:
            - string
            - 'null'
        line1:
          type:
            - string
            - 'null'
        line2:
          type:
            - string
            - 'null'
        suburb:
          type:
            - string
            - 'null'
        state:
          type:
            - string
            - 'null'
        postcode:
          type:
            - string
            - 'null'
        country:
          type: string
        isDefault:
          type: boolean
        createdAt:
          type: string
          format: date-time
        updatedAt:
          type: string
          format: date-time
      required:
        - id
        - customerId
        - kind
        - country
        - isDefault
        - createdAt
        - updatedAt
    Output_QuoteLine:
      type: object
      properties:
        id:
          type: string
        quoteGroupId:
          type: string
        lineType:
          $ref: '#/components/schemas/Enum_QuoteLineType'
        garmentCatalogueItemId:
          type:
            - string
            - 'null'
        productName:
          type: string
        productSku:
          type:
            - string
            - 'null'
        title:
          type:
            - string
            - 'null'
        description:
          type:
            - string
            - 'null'
        quantity:
          type: integer
        unitPriceExGst:
          type: number
        valueType:
          anyOf:
            - $ref: '#/components/schemas/Enum_LineValueType'
            - type: 'null'
          description: >-
            Flat or Percentage. Applies to a Discount AND (F-26) a Surcharge — a
            rate on

            a line, whichever direction it points.
        percentage:
          type:
            - number
            - 'null'
          description: The rate, when valueType is Percentage. Null otherwise.
        discountValueType:
          anyOf:
            - $ref: '#/components/schemas/Enum_DiscountValueType'
            - type: 'null'
        discountPercentage:
          type:
            - number
            - 'null'
        customImageUrl:
          type:
            - string
            - 'null'
          description: Per-line image override; null falls back to the catalogue thumbnail.
        sortOrder:
          type: integer
          description: >-
            Position in the group's row sequence — shared with the group's
            DECORATIONS

            since F-26, so a surcharge or discount can sit below a decoration.
            Omit on

            create to append.
        createdAt:
          type: string
          format: date-time
        updatedAt:
          type: string
          format: date-time
        variants:
          type: array
          items:
            $ref: '#/components/schemas/Output_QuoteVariant'
      required:
        - id
        - quoteGroupId
        - lineType
        - productName
        - quantity
        - unitPriceExGst
        - sortOrder
        - createdAt
        - updatedAt
        - variants
    Output_QuoteGroupDecoration:
      type: object
      properties:
        id:
          type: string
        quoteGroupId:
          type: string
        decorationItemId:
          type:
            - string
            - 'null'
        method:
          anyOf:
            - $ref: '#/components/schemas/Enum_JobDecorationMethod'
            - type: 'null'
          description: >-
            The decoration technique. Recorded from the quote stage since
            CU-86d43a1qr —

            it is agreed with the customer when the price is, not discovered on
            the floor.

            Same enum as `JobGroupDecoration.method`, and literally the same
            Postgres

            type, so conversion copies it.
        position:
          type:
            - string
            - 'null'
          description: >-
            Where the print goes — FREE TEXT (CU-86d4571me).


            Was a nine-value enum whose escape hatch was `Other`, which could
            not carry

            what the "other" actually was: "2cm down from neckline", "side of
            caps",

            "over pocket", "under collar". An escape hatch that cannot hold the

            explanation is not one, and the shop floor was reading a bare
            "Other".


            It also ABSORBS the old `placement`, which was a second free-text
            field for

            the same fact ("2cm below collar", "centred on pocket"). Two fields
            meant two

            places to look and two chances to disagree; there is one now.


            The nine former values (Front, Back, Left Sleeve, Right Sleeve,
            Neck,

            Lower Back, Left Chest, Right Chest, Other) are CLIENT-SIDE
            SUGGESTIONS

            ONLY — the server constrains nothing but trims the value and caps
            its

            length. Stored as the display label, so the job, the proof, the PDF,
            the

            portal and the job sheet all show the same string.


            Null means no position has been named. Empty is refused rather than
            stored.
        quantity:
          type: integer
        setupCost:
          type: number
          description: Present only when the credential includes the costs.read scope.
        unitCost:
          type: number
          description: Present only when the credential includes the costs.read scope.
        baseUnitCost:
          type: number
        baseSetupCost:
          type: number
        includeSetupCost:
          type: boolean
        markupType:
          anyOf:
            - $ref: '#/components/schemas/Enum_MarkupType'
            - type: 'null'
          description: >-
            The markup connecting the per-unit COST to the per-unit CHARGE,
            stored so it

            survives a reopen (CU-86d46hu37).


            A decoration used to persist nothing here: the client back-solved a
            markup

            from cost and charge every time the dialog opened, which is why a
            decoration

            saved with a PERCENTAGE markup always reopened showing a flat dollar
            amount —

            a back-solved percentage is rarely exact, so the back-solve had to
            prefer

            flat. Null on rows written before these columns existed; those still

            back-solve, exactly as every row did before.
        markupValue:
          type:
            - number
            - 'null'
        markupSource:
          $ref: '#/components/schemas/Enum_MarkupSource'
          description: >-
            Who owns the per-unit markup — see `MarkupSource`. `group` re-bands
            on every

            quantity or cost move; `manual` never does.
        setupMarkupType:
          anyOf:
            - $ref: '#/components/schemas/Enum_MarkupType'
            - type: 'null'
          description: >-
            The same three facts for the SETUP lane, which is a genuinely
            separate

            markup: a setup markup group is matched by `appliesToSetupCost`
            (exclusive of

            the per-unit groups) and bands on the SETUP cost, not the per-unit
            one. One

            lane can be group-owned while the other is not.


            Inert unless `includeSetupCost` — there is no charge to mark up when
            the

            setup is not being charged.
        setupMarkupValue:
          type:
            - number
            - 'null'
        setupMarkupSource:
          $ref: '#/components/schemas/Enum_MarkupSource'
        rollUp:
          type: boolean
          description: >-
            Fold this decoration's UNIT charge into the garment unit price
            rather than

            pricing it on its own row (CU-86d416yvz).


            PER DECORATION. `QuoteGroup.rollUpDecoration` folded every
            decoration in the

            group or none of them, which made the dialog's per-decoration toggle
            a lie —

            turning it on for the embroidery also folded the screen print. The
            group

            flags are still readable this release and no longer feed the
            calculation.


            Display only: the group subtotal, the quote total and the margin are
            the

            same number either way. What changes is where the customer reads the
            money.
        rollUpSetup:
          type: boolean
          description: >-
            Same for the SETUP charge. Inert unless `includeSetupCost` is true —
            there is

            nothing to fold when the setup is not being charged, the pairing the
            group

            flag already had.
        showOnDocuments:
          type: boolean
          description: >-
            Draw this decoration's row on customer-facing documents at all.


            Distinct from `rollUp`, and they answer different questions — the
            quote

            document, the PDF and the portal all honour both:

              rollUp only        the row is listed, its price reads "Included"
              rollUp + hidden    no row anywhere; the money is inside the garment price

            Hidden with `rollUp: false` would leave the itemised rows summing to
            less

            than the customer pays, so hiding a row implies the fold.


            Defaults true — every decoration prints today and this must not
            change how a

            live quote reads. Note the opposite default to
            `QuoteVariant.showOnDocuments`,

            which folds by default because a courier fee itemised as a garment
            variant

            was the complaint there.
        description:
          type:
            - string
            - 'null'
        widthCm:
          type:
            - number
            - 'null'
          description: >-
            Print dimensions in CENTIMETRES (BE-04 / Q-24 / CU-86d43a1qr). The
            unit is

            fixed by the schema, not stored per row — renderers print "30 × 25
            cm"; the

            column only ever holds the number. Nullable: most decorations carry
            no

            dimensions, and 0 is a measurement, not an absence.


            These were `width`/`height` in millimetres until CU-86d43a1qr, while
            the proof

            spec describing the same print stored centimetres — so one job
            printed

            `300 × 250 mm` on the quote and `30 × 25 cm` on the proof. Every
            stage stores

            cm now and nothing converts.
        lengthCm:
          type:
            - number
            - 'null'
        necklineDropCm:
          type:
            - number
            - 'null'
          description: How far below the collar the print starts, in cm.
        screenCount:
          type:
            - integer
            - 'null'
          description: Screens to burn — screen printing only. Null when it does not apply.
        screenMeshGrade:
          type:
            - string
            - 'null'
          description: Mesh grade for those screens, e.g. `156`.
        colourCount:
          type:
            - integer
            - 'null'
          description: >-
            How many ink colours the print uses. Free of `colours` — an operator
            can price a 4-colour print before naming the inks.
        colours:
          type: array
          items:
            $ref: '#/components/schemas/Output_DecorationColour'
          description: >-
            The inks, in the operator's order. At most one is flagged `isBase`,
            and it renders first.
        specialInstructions:
          type:
            - string
            - 'null'
          description: >-
            Internal shop notes — **never customer-facing**. Named to match

            `JobGroupDecoration.specialInstructions` so quote and job share one

            vocabulary. `description` is the Title and prints on the quote
            document, so

            it could not be reused for this. Deliberately absent from

            `PortalQuoteDecoration` and from the PDF renderer — internal by

            construction, the way `Mock.layers` is withheld from `PortalMock`.
        artworkFileUrl:
          type:
            - string
            - 'null'
        separationFileUrl:
          type:
            - string
            - 'null'
        sortOrder:
          type: integer
          description: >-
            Position in the group's row sequence — the SAME sequence as the
            group's

            LINES since F-26, not a separate one. Omit on create to append.
        createdAt:
          type: string
          format: date-time
        updatedAt:
          type: string
          format: date-time
      required:
        - id
        - quoteGroupId
        - quantity
        - baseUnitCost
        - baseSetupCost
        - includeSetupCost
        - markupSource
        - setupMarkupSource
        - rollUp
        - rollUpSetup
        - showOnDocuments
        - colours
        - sortOrder
        - createdAt
        - updatedAt
    Enum_AddressKind:
      type: string
      enum:
        - Billing
        - Shipping
        - Other
    Enum_QuoteLineType:
      type: string
      enum:
        - Garment
        - Decoration
        - Service
        - Cost
        - Discount
        - Surcharge
        - Other
    Enum_LineValueType:
      type: string
      enum:
        - Flat
        - Percentage
    Enum_DiscountValueType:
      type: string
      enum:
        - Flat
        - Percentage
    Output_QuoteVariant:
      type: object
      properties:
        id:
          type: string
        quoteLineId:
          type: string
        size:
          type:
            - string
            - 'null'
        colour:
          type:
            - string
            - 'null'
        quantity:
          type: integer
        unitPriceExGst:
          type: number
        buyPrice:
          type: number
          description: Present only when the credential includes the costs.read scope.
        extraCost:
          type: number
          description: Present only when the credential includes the costs.read scope.
        markupType:
          anyOf:
            - $ref: '#/components/schemas/Enum_MarkupType'
            - type: 'null'
          description: >-
            The markup that connects cost to sell, stored so it survives a
            reopen

            (BE-01 / Q-16). Cost is `buyPrice + extraCost`; sell is

            `percentage ? cost + cost·value/100 : cost + value`. Null on rows
            written

            before the column existed — read those back by back-solving from
            cost and

            sell, as the client did for every row previously. `MarkupType` is
            the enum

            catalog-service already declares.
        markupValue:
          type:
            - number
            - 'null'
        markupSource:
          $ref: '#/components/schemas/Enum_MarkupSource'
          description: >-
            Whether the markup above belongs to a markup GROUP or to the
            operator

            (CU-86d46hu37). `group` re-resolves through the group's matrix
            whenever the

            quantity or the cost moves — on reopen as much as on create — while
            `manual`

            is left alone and holds its saved sell price through a cost edit.


            Never null: rows written before the column existed read `manual`,
            which is

            precisely how they behaved.
        isCustomLine:
          type: boolean
          description: >-
            A cost that belongs to the line but is not per-garment — freight is
            the

            example (BE-02 / Q-14). Stored as a variant with no size and no
            colour; the

            flag is what tells one from a half-filled variant, since the size
            sort, the

            quote document and the PDF all walk these rows.
        showOnDocuments:
          type: boolean
          description: >-
            Whether a CUSTOM variant is itemised on the documents the customer
            sees —

            quote, invoice, PDF, portal (QU-02).


            False, the default, folds its money into the garment line's total
            and draws

            no row anywhere. True prints one row reading `Custom variant`,
            quantity 1,

            with size and colour blank. Either way the money is identical: a
            line's

            total already sums every variant beneath it, custom rows included.


            Ignored on a real size/colour variant — that row IS the itemisation.
        sortOrder:
          type: integer
        createdAt:
          type: string
          format: date-time
        updatedAt:
          type: string
          format: date-time
      required:
        - id
        - quoteLineId
        - quantity
        - unitPriceExGst
        - markupSource
        - isCustomLine
        - showOnDocuments
        - sortOrder
        - createdAt
        - updatedAt
    Enum_JobDecorationMethod:
      type: string
      enum:
        - ScreenPrinting
        - Embroidery
        - DtfTransfer
        - Dtg
        - Vinyl
        - Sublimation
        - HeatTransfer
        - Outsourced
        - Other
    Enum_MarkupType:
      type: string
      enum:
        - percentage
        - flat_rate
    Enum_MarkupSource:
      type: string
      description: >-
        WHO owns a markup, and therefore whether it may be re-resolved through
        its

        markup group's matrix (CU-86d46hu37).


        `group` — a markup group resolved the number, so it re-bands on every
        quantity

        or cost change, on create and on reopen alike; the sell price moves with
        it.

        `manual` — an operator typed the markup, flipped its \$/% toggle, or
        overrode

        the sell price. Never re-banded, and a cost edit back-solves to hold the
        saved

        sell fixed, which is the edit-lock that stops a live quote repricing
        itself.


        Stored `not null default 'manual'`, so every row written before this
        existed is

        operator-owned and behaves exactly as it did. A create path that means
        `group`

        must SAY `group` — relying on the default silently reintroduces the bug
        the

        enum exists to fix.
      enum:
        - group
        - manual
    Output_DecorationColour:
      type: object
      properties:
        name:
          type: string
        isBase:
          type: boolean
      required:
        - name
        - isBase
  securitySchemes:
    tenantApiKey:
      type: apiKey
      in: header
      name: X-API-Key
      description: Tenant credential. This is sufficient by itself.
    partnerApiKey:
      type: apiKey
      in: header
      name: X-API-Key
      description: Partner application client ID.
    partnerApiSecret:
      type: apiKey
      in: header
      name: X-API-Secret
      description: Partner application secret.
    partnerBearer:
      type: http
      scheme: bearer
      bearerFormat: JWT
      description: Cognito access token obtained through OAuth.

````