{
  "swagger": "2.0",
  "info": {
    "title": "Factory Sales API",
    "description": "Create and read sales orders and quotes, customers, and the supporting product catalogue, for your company.\n\nEvery request is authenticated with a bearer token and is scoped to the authenticated company.\n\nNumbers and money: monetary amounts are Money objects — an integer number of micros (millionths of the currency's major unit) in amountMicros (1.00 = 1,000,000 micros, so $12.34 is \"12340000\") plus an ISO 4217 currency code. Quantities, discounts, percentages, and markups are decimal strings (a quantity of \"2.5\", a discount of \"10.00\" meaning 10%). All 64-bit integer fields — including amountMicros and orderNumber — are sent and returned as JSON strings, not JSON numbers, to avoid precision loss in JavaScript clients.\n\nEnums: enum fields take the upper-case value names listed on each field (for example \"FULFILMENT_METHOD_DELIVERY\"). The string name is the canonical wire form: responses always return the name, and requests should send it (integer values are also accepted, but names are recommended).\n\nErrors: every error response, whatever the HTTP status, has the same body — an error envelope with a short stable \"type\" naming the kind of failure, a human-readable \"message\", the request's idempotency key echoed back as \"requestId\" when the failing request carried one, and an \"errors\" array. The \"type\" values are \"validation_failure\" (HTTP 400 — a malformed or invalid request), \"failed_precondition\" (HTTP 400 — the request was valid but the resource's state does not allow it, for example writing a line on an invoiced order), \"unauthenticated\" (HTTP 401), \"permission_denied\" (HTTP 403), \"not_found\" (HTTP 404), \"conflict\" (HTTP 409), \"rate_limited\" (HTTP 429 — you are being throttled, and the Retry-After header, when present, says when to come back), \"resource_exhausted\" (HTTP 429 — a limit other than a request rate was exceeded, in practice a request or a response too large to carry; the message names the limit, and retrying unchanged will fail the same way — send less, or ask for a smaller page), \"internal\" (HTTP 500), \"not_implemented\" (HTTP 501 — a valid request the API does not support yet), \"unavailable\" (HTTP 503), and \"timeout\" (HTTP 504); treat an unrecognised type as a generic error. On a validation failure the errors array carries one entry per field-level problem — each with a stable error code, a human-readable message, and a pointer to the offending field (for example \"lines[2].catalogue.product_id\"). On an idempotency conflict (HTTP 409, type \"conflict\") it carries one entry whose code says which kind: \"request_replayed\" (the original request completed — \"resourceId\" on the envelope holds the id it created; read it instead of retrying), \"request_id_reused\" (the key was reused with a different body), \"request_attempt_failed\" (the original attempt failed and may still have written) or \"request_in_progress\" (its outcome is unknown); verify with a read before sending a fresh key. For every other type the array is empty. Stable validation codes defined so far include \"customer_not_found\" (the customer reference on a create could not be resolved) and \"product_not_found\" (a catalogue line references a product id that does not exist). The list is not exhaustive — more codes will be added as validations grow, so treat an unrecognised code as a generic validation failure rather than an error.\n\nUnknown request fields: a JSON property the API does not recognise — a misspelt or removed field name — is silently ignored, not rejected, so check field names against this document.\n\nA request without a valid bearer token returns HTTP 401 (unauthenticated). Retrieving a single resource by an id that does not exist in your company returns HTTP 404 (not_found).\n\nRate limits: requests are rate limited. A request over the limit returns HTTP 429 (rate_limited) with a Retry-After header saying how long to wait before retrying; concrete limits will be announced.\n\nField filtering: reduce response payload size by requesting only the fields you need. Pass a `fields` query parameter with a comma-separated list of camelCase JSON names — e.g. `?fields=orderId,total.amountMicros`. Dot paths reach into nested objects and map transparently across arrays. Paths are relative to the resource, not the response envelope; envelope keys like `nextPageToken` are always preserved. To keep everything except a few heavy fields, use `excludeFields` instead. If both are provided, `fields` takes precedence. Filtering applies to successful (2xx) JSON responses only — error bodies are never filtered. Unknown field names are silently ignored.\n\nVersioning: the major version is carried in the path (\"/v1/\"). v1 is a stable contract — every published operation, field, and value keeps its meaning and wire form for the life of the version, subject only to the additive changes and the deprecation process described here. Releases are tagged from one mainline, and every change is listed in the changelog.\n\nAdditive changes may ship at any time without notice: new endpoints; new optional request fields and query parameters; new fields in responses; a list operation starting to populate a field it previously omitted; new values in an enum; new error \"type\" values and error codes; new webhook event types; limits raised or patterns widened; and new items marked Preview. Build clients that stay correct under all of these: ignore fields you do not recognise, treat an enum value you do not recognise as \"other\" rather than failing, treat an unrecognised error type as a generic error of its HTTP status, treat page tokens as opaque, and never parse an id beyond its prefix.\n\nBreaking changes never ship within v1. They are: removing or renaming an endpoint, field, parameter, or enum value; adding a required request field; changing a field's type, JSON name, or wire form; changing the meaning of an existing field or value; changing a default; changing which HTTP status or error type an existing outcome returns; making a field that is always present conditional or redacted; reducing the fields a list operation populates; tightening validation (lower limits, narrower patterns) on inputs that succeed today; and changing the documented pagination or ordering behaviour of an operation. Anything on this list arrives only under a new version path (\"/v2/\") or, for a removal, at the end of a deprecation period.\n\nDeprecation: a deprecated endpoint, field, or value is announced in the changelog and marked Deprecated in its description, together with its replacement and its end date. It keeps working for at least 12 months after the announcement. When a new major version ships, v1 remains served for at least 12 months after that version is generally available.\n\nPreview: an endpoint or field whose description begins with \"Preview:\" is published for early use but is not yet covered by these guarantees — it may change, and it may be withdrawn, with 30 days' notice in the changelog. A preview item is promoted by removing the marker, at which point the full policy applies to it.\n\nNot covered by compatibility: the order of keys in JSON, whitespace and formatting, the wording of human-readable messages, the contents of page tokens, concrete rate-limit numbers, response timing, and any behaviour this documentation does not describe.\n\nWebhooks: subscribe with WebhookService to be notified when orders and quotes change. Deliveries are thin events: the signed JSON body names the event (`id`, `type`, `occurred_at`) and the resource by id (`data.order.id` or `data.quote.id`, plus `data.previous` on transitions) — it never carries the resource itself. Fetch current state with GetOrder / GetQuote, deduplicate on the event `id` (delivery is at-least-once), and verify the `X-Factory-Signature` header with the secret CreateWebhook returns once. The change feeds (QueryOrders, QueryQuotes with an updatedSince checkpoint) remain the way to backfill and reconcile.",
    "version": "v1"
  },
  "tags": [
    {
      "name": "CatalogueService",
      "description": "Your product catalogue — the products, kits, and flashings you sell."
    },
    {
      "name": "CompanyService",
      "description": "Your company's settings and custom-field definitions — identity, currency and locale, the measurement system quantities are expressed in, tax context, and the custom-field keys that write endpoints accept."
    },
    {
      "name": "CustomerService",
      "description": "Your company's customers — the businesses you quote and sell to."
    },
    {
      "name": "InventoryService",
      "description": "Read and update stock levels for your catalogue products. Stock is tracked per product variant row, optionally split by colour. Each entry carries on-hand, promised, on-the-way, and available quantities for that one variant — never a cumulative product-level total — plus the variant's identifying attributes (thickness and name/value pairs), so entries are self-describing. An entry's productRowId is the same id the catalogue product's rows carry."
    },
    {
      "name": "QuoteService",
      "description": "Create quotes, read them, add, update, and remove their line items, upload flashing-drawing SVGs, and work with each quote's conversation (messages and file attachments) — the only surface for creating and reading quotes; confirmed orders live on OrderService. A quote's line items can be added, updated, and removed individually after creation; the server recomputes totals on every change. A quote created as a draft is finalised and sent from the Factory app. The quote itself is not voided or deleted through this API, and turning an accepted quote into a confirmed order happens in the Factory app."
    },
    {
      "name": "LabelService",
      "description": "The labels your company uses to tag orders."
    },
    {
      "name": "OrderService",
      "description": "Create sales orders, read and list them, upload flashing-drawing SVGs, and work with each order's conversation (messages and file attachments) — the order surface of the API. An order is created either as a draft or submitted immediately; after creation this API reads orders but does not update them — workflow status, fulfilment, and payment changes happen in the Factory app and are reflected on subsequent reads. Quotes live on QuoteService."
    },
    {
      "name": "OrderStatusService",
      "description": "The workflow statuses your company's orders move through."
    },
    {
      "name": "SearchService",
      "description": "Preview: free-text search across your catalogue. This service is in\npreview — published for early use, not yet covered by the compatibility\nguarantees in the API conventions (see Versioning); it may change or be\nwithdrawn with 30 days' notice."
    },
    {
      "name": "SupplierService",
      "description": "List your company's suppliers. Suppliers are managed in the Factory purchasing UI — this read-only surface lets API consumers discover supplier ids for use as inventory list filters."
    },
    {
      "name": "UserService",
      "description": "The people in your company, for assigning work to."
    },
    {
      "name": "WebhookService",
      "description": "Register HTTPS endpoints to be notified when orders and quotes change, and inspect what was delivered. Every endpoint here requires the administrator role — tokens bound to any other user role are refused with 403. Deliveries are thin events: a signed JSON body `{id, type, occurred_at, data}` (snake_case keys) where `data` names the resource by id — `data.order.id` or `data.quote.id` (an order for `quote.converted_to_order`) — plus `data.previous` on transitions (`status_id`, `quote`, or `archived`). The body never carries the resource itself: read current state with GetOrder / GetQuote by that id (`order.archived` needs `includeArchived`). Every POST carries `Content-Type: application/json`, `X-Factory-Event` (the type), `X-Factory-Signature` (`t=\u003cunix seconds\u003e,v1=\u003chex HMAC-SHA256 of \"\u003ct\u003e.\u003craw body\u003e\"\u003e`, keyed by the full `whsec_…` secret CreateWebhook returns once), `Date` (the same instant as `t`, as an HTTP-date) and `User-Agent: Factory-API/v1`. Delivery is at-least-once and never lost — deduplicate on `id`; answer 2xx within 10 seconds and do the work asynchronously; failed deliveries retry over ~10.6 hours, then exhaust."
    }
  ],
  "host": "api.factory.app",
  "schemes": [
    "https"
  ],
  "consumes": [
    "application/json"
  ],
  "produces": [
    "application/json"
  ],
  "paths": {
    "/v1/catalogue/flashings": {
      "get": {
        "summary": "List the flashing product templates for your company.",
        "description": "Returns a paginated list of flashing templates. A flashing line item must\nreference one of these templates by id, so use this list to find the\ntemplate for the thickness you need and its selectable colours. Templates are\nreturned ordered by flashing name, then thickness.",
        "operationId": "CatalogueService_ListFlashings",
        "responses": {
          "200": {
            "description": "A successful response.",
            "schema": {
              "$ref": "#/definitions/factory.api.v1.catalogue.ListFlashingsResponse"
            }
          },
          "400": {
            "description": "The request was rejected because it was malformed — for example an unusable page token or an invalid filter value. The body is a validation-failure envelope: an overall type and message and one entry per problem (each with a stable code, a message, and a param pointing at the offending parameter).",
            "schema": {
              "$ref": "#/definitions/factory.api.v1.Error"
            }
          },
          "401": {
            "description": "The request is missing a valid bearer token, or the token is invalid or expired.",
            "schema": {
              "$ref": "#/definitions/factory.api.v1.Error"
            }
          },
          "default": {
            "description": "Any other error. The body is the same error envelope every error uses: a short stable type identifying the kind of failure (for example \"rate_limited\" or \"internal\"), a human-readable message, and the request's idempotency key echoed back when one was supplied.",
            "schema": {
              "$ref": "#/definitions/factory.api.v1.Error"
            }
          }
        },
        "parameters": [
          {
            "name": "pageSize",
            "description": "Maximum number of templates to return per page. 0 uses the server default;\nthe server may cap the value.",
            "in": "query",
            "required": false,
            "type": "integer",
            "format": "int32"
          },
          {
            "name": "pageToken",
            "description": "Opaque page token from a previous response, used to fetch the next page.",
            "in": "query",
            "required": false,
            "type": "string"
          },
          {
            "name": "productType",
            "description": "Optional: only return templates whose `productType` equals this value.\nEvery flashing template currently reports \"Flashing\", so the filter\nnarrows nothing today. There is no filter by name, thickness or colour:\nto find a template, page through the list and match on `flashingName`,\n`thickness` and `colours`.",
            "in": "query",
            "required": false,
            "type": "string"
          },
          {
            "name": "fields",
            "description": "Comma-separated response fields to include, using camelCase JSON names\n(e.g. `orderId,total.amountMicros`). Dot paths reach into nested objects\nand map transparently across arrays. Paths are relative to the resource,\nnot the response envelope; envelope keys like `nextPageToken` are always\npreserved. Only 2xx JSON responses are filtered; error bodies pass through\nunmodified. Unknown names are silently ignored. Takes precedence over\n`excludeFields` when both are provided.",
            "in": "query",
            "required": false,
            "type": "string"
          },
          {
            "name": "excludeFields",
            "description": "Comma-separated response fields to exclude, using camelCase JSON names.\nDot paths and array-transparency work the same as `fields`. Paths are\nrelative to the resource, not the response envelope. Only 2xx JSON\nresponses are filtered; error bodies pass through unmodified. Ignored\nwhen `fields` is also provided.",
            "in": "query",
            "required": false,
            "type": "string"
          }
        ],
        "tags": [
          "CatalogueService"
        ]
      }
    },
    "/v1/catalogue/flashings/{templateId}": {
      "get": {
        "summary": "Retrieve a single flashing product template by its id.",
        "description": "Returns the full flashing template, including its thickness and selectable\ncolours.",
        "operationId": "CatalogueService_GetFlashing",
        "responses": {
          "200": {
            "description": "A successful response.",
            "schema": {
              "$ref": "#/definitions/factory.api.v1.catalogue.GetFlashingResponse"
            }
          },
          "400": {
            "description": "The request was rejected because it was malformed — for example a badly formed id. The body is a validation-failure envelope: an overall type and message and one entry per problem (each with a stable code, a message, and a param pointing at the offending parameter).",
            "schema": {
              "$ref": "#/definitions/factory.api.v1.Error"
            }
          },
          "401": {
            "description": "The request is missing a valid bearer token, or the token is invalid or expired.",
            "schema": {
              "$ref": "#/definitions/factory.api.v1.Error"
            }
          },
          "404": {
            "description": "No flashing template with the given id exists for the authenticated company.",
            "schema": {
              "$ref": "#/definitions/factory.api.v1.Error"
            }
          },
          "default": {
            "description": "Any other error. The body is the same error envelope every error uses: a short stable type identifying the kind of failure (for example \"rate_limited\" or \"internal\"), a human-readable message, and the request's idempotency key echoed back when one was supplied.",
            "schema": {
              "$ref": "#/definitions/factory.api.v1.Error"
            }
          }
        },
        "parameters": [
          {
            "name": "templateId",
            "description": "The id of the flashing template to retrieve. Required.",
            "in": "path",
            "required": true,
            "type": "string"
          },
          {
            "name": "fields",
            "description": "Comma-separated response fields to include, using camelCase JSON names\n(e.g. `orderId,total.amountMicros`). Dot paths reach into nested objects\nand map transparently across arrays. Paths are relative to the resource,\nnot the response envelope; envelope keys are always preserved. Only 2xx\nJSON responses are filtered; error bodies pass through unmodified. Unknown\nnames are silently ignored. Takes precedence over `excludeFields` when\nboth are provided.",
            "in": "query",
            "required": false,
            "type": "string"
          },
          {
            "name": "excludeFields",
            "description": "Comma-separated response fields to exclude, using camelCase JSON names.\nDot paths and array-transparency work the same as `fields`. Paths are\nrelative to the resource, not the response envelope. Only 2xx JSON\nresponses are filtered; error bodies pass through unmodified. Ignored\nwhen `fields` is also provided.",
            "in": "query",
            "required": false,
            "type": "string"
          }
        ],
        "tags": [
          "CatalogueService"
        ]
      }
    },
    "/v1/catalogue/kits": {
      "get": {
        "summary": "List the configured product kits for your company.",
        "description": "Returns a paginated list of product kit summaries. This is a lightweight\nlisting: rows, components, and sub-assemblies are not populated — use\nGetKit to fetch a single kit's priced rows and full component tree. Kits\nare returned alphabetically by name.",
        "operationId": "CatalogueService_ListKits",
        "responses": {
          "200": {
            "description": "A successful response.",
            "schema": {
              "$ref": "#/definitions/factory.api.v1.catalogue.ListKitsResponse"
            }
          },
          "400": {
            "description": "The request was rejected because it was malformed — for example an unusable page token or an invalid filter value. The body is a validation-failure envelope: an overall type and message and one entry per problem (each with a stable code, a message, and a param pointing at the offending parameter).",
            "schema": {
              "$ref": "#/definitions/factory.api.v1.Error"
            }
          },
          "401": {
            "description": "The request is missing a valid bearer token, or the token is invalid or expired.",
            "schema": {
              "$ref": "#/definitions/factory.api.v1.Error"
            }
          },
          "default": {
            "description": "Any other error. The body is the same error envelope every error uses: a short stable type identifying the kind of failure (for example \"rate_limited\" or \"internal\"), a human-readable message, and the request's idempotency key echoed back when one was supplied.",
            "schema": {
              "$ref": "#/definitions/factory.api.v1.Error"
            }
          }
        },
        "parameters": [
          {
            "name": "pageSize",
            "description": "Maximum number of kits to return per page. 0 uses the server default; the\nserver may cap the value.",
            "in": "query",
            "required": false,
            "type": "integer",
            "format": "int32"
          },
          {
            "name": "pageToken",
            "description": "Opaque page token from a previous response, used to fetch the next page.",
            "in": "query",
            "required": false,
            "type": "string"
          },
          {
            "name": "name",
            "description": "Optional: only return kits whose name matches this text.",
            "in": "query",
            "required": false,
            "type": "string"
          },
          {
            "name": "fields",
            "description": "Comma-separated response fields to include, using camelCase JSON names\n(e.g. `orderId,total.amountMicros`). Dot paths reach into nested objects\nand map transparently across arrays. Paths are relative to the resource,\nnot the response envelope; envelope keys like `nextPageToken` are always\npreserved. Only 2xx JSON responses are filtered; error bodies pass through\nunmodified. Unknown names are silently ignored. Takes precedence over\n`excludeFields` when both are provided.",
            "in": "query",
            "required": false,
            "type": "string"
          },
          {
            "name": "excludeFields",
            "description": "Comma-separated response fields to exclude, using camelCase JSON names.\nDot paths and array-transparency work the same as `fields`. Paths are\nrelative to the resource, not the response envelope. Only 2xx JSON\nresponses are filtered; error bodies pass through unmodified. Ignored\nwhen `fields` is also provided.",
            "in": "query",
            "required": false,
            "type": "string"
          }
        ],
        "tags": [
          "CatalogueService"
        ]
      }
    },
    "/v1/catalogue/kits/{kitId}": {
      "get": {
        "summary": "Retrieve a single product kit by its id, with its full component tree.",
        "description": "Returns the kit with its priced rows, top-level component products, and\nsub-assemblies — the single call that lets you drill into one kit's full\ndetail.",
        "operationId": "CatalogueService_GetKit",
        "responses": {
          "200": {
            "description": "A successful response.",
            "schema": {
              "$ref": "#/definitions/factory.api.v1.catalogue.GetKitResponse"
            }
          },
          "400": {
            "description": "The request was rejected because it was malformed — for example a badly formed id. The body is a validation-failure envelope: an overall type and message and one entry per problem (each with a stable code, a message, and a param pointing at the offending parameter).",
            "schema": {
              "$ref": "#/definitions/factory.api.v1.Error"
            }
          },
          "401": {
            "description": "The request is missing a valid bearer token, or the token is invalid or expired.",
            "schema": {
              "$ref": "#/definitions/factory.api.v1.Error"
            }
          },
          "404": {
            "description": "No product kit with the given id exists for the authenticated company.",
            "schema": {
              "$ref": "#/definitions/factory.api.v1.Error"
            }
          },
          "default": {
            "description": "Any other error. The body is the same error envelope every error uses: a short stable type identifying the kind of failure (for example \"rate_limited\" or \"internal\"), a human-readable message, and the request's idempotency key echoed back when one was supplied.",
            "schema": {
              "$ref": "#/definitions/factory.api.v1.Error"
            }
          }
        },
        "parameters": [
          {
            "name": "kitId",
            "description": "The id of the kit to retrieve. Required.",
            "in": "path",
            "required": true,
            "type": "string"
          },
          {
            "name": "fields",
            "description": "Comma-separated response fields to include, using camelCase JSON names\n(e.g. `orderId,total.amountMicros`). Dot paths reach into nested objects\nand map transparently across arrays. Paths are relative to the resource,\nnot the response envelope; envelope keys are always preserved. Only 2xx\nJSON responses are filtered; error bodies pass through unmodified. Unknown\nnames are silently ignored. Takes precedence over `excludeFields` when\nboth are provided.",
            "in": "query",
            "required": false,
            "type": "string"
          },
          {
            "name": "excludeFields",
            "description": "Comma-separated response fields to exclude, using camelCase JSON names.\nDot paths and array-transparency work the same as `fields`. Paths are\nrelative to the resource, not the response envelope. Only 2xx JSON\nresponses are filtered; error bodies pass through unmodified. Ignored\nwhen `fields` is also provided.",
            "in": "query",
            "required": false,
            "type": "string"
          }
        ],
        "tags": [
          "CatalogueService"
        ]
      }
    },
    "/v1/catalogue/products": {
      "get": {
        "summary": "List the orderable catalogue products for your company.",
        "description": "Returns a paginated list of catalogue product summaries. The listing is\nlightweight: `rows` is not populated — retrieve a product with GetProduct\nto get its priced variant rows. Product kits are not included here; list\nthem with ListKits. Products are returned alphabetically by name.",
        "operationId": "CatalogueService_ListProducts",
        "responses": {
          "200": {
            "description": "A successful response.",
            "schema": {
              "$ref": "#/definitions/factory.api.v1.catalogue.ListProductsResponse"
            }
          },
          "400": {
            "description": "The request was rejected because it was malformed — for example an unusable page token or an invalid filter value. The body is a validation-failure envelope: an overall type and message and one entry per problem (each with a stable code, a message, and a param pointing at the offending parameter).",
            "schema": {
              "$ref": "#/definitions/factory.api.v1.Error"
            }
          },
          "401": {
            "description": "The request is missing a valid bearer token, or the token is invalid or expired.",
            "schema": {
              "$ref": "#/definitions/factory.api.v1.Error"
            }
          },
          "default": {
            "description": "Any other error. The body is the same error envelope every error uses: a short stable type identifying the kind of failure (for example \"rate_limited\" or \"internal\"), a human-readable message, and the request's idempotency key echoed back when one was supplied.",
            "schema": {
              "$ref": "#/definitions/factory.api.v1.Error"
            }
          }
        },
        "parameters": [
          {
            "name": "pageSize",
            "description": "Maximum number of products to return per page. 0 uses the server default;\nthe server may cap the value.",
            "in": "query",
            "required": false,
            "type": "integer",
            "format": "int32"
          },
          {
            "name": "pageToken",
            "description": "Opaque page token from a previous response, used to fetch the next page.",
            "in": "query",
            "required": false,
            "type": "string"
          },
          {
            "name": "name",
            "description": "Optional: only return products whose name contains this text. A browse aid,\nnot a precise lookup key.",
            "in": "query",
            "required": false,
            "type": "string"
          },
          {
            "name": "categoryName",
            "description": "Optional: only return products in this category, by category name.",
            "in": "query",
            "required": false,
            "type": "string"
          },
          {
            "name": "fields",
            "description": "Comma-separated response fields to include, using camelCase JSON names\n(e.g. `orderId,total.amountMicros`). Dot paths reach into nested objects\nand map transparently across arrays. Paths are relative to the resource,\nnot the response envelope; envelope keys like `nextPageToken` are always\npreserved. Only 2xx JSON responses are filtered; error bodies pass through\nunmodified. Unknown names are silently ignored. Takes precedence over\n`excludeFields` when both are provided.",
            "in": "query",
            "required": false,
            "type": "string"
          },
          {
            "name": "excludeFields",
            "description": "Comma-separated response fields to exclude, using camelCase JSON names.\nDot paths and array-transparency work the same as `fields`. Paths are\nrelative to the resource, not the response envelope. Only 2xx JSON\nresponses are filtered; error bodies pass through unmodified. Ignored\nwhen `fields` is also provided.",
            "in": "query",
            "required": false,
            "type": "string"
          }
        ],
        "tags": [
          "CatalogueService"
        ]
      }
    },
    "/v1/catalogue/products/{productId}": {
      "get": {
        "summary": "Retrieve a single catalogue product by its id.",
        "description": "Returns the full product, including all of its priced variant rows.",
        "operationId": "CatalogueService_GetProduct",
        "responses": {
          "200": {
            "description": "A successful response.",
            "schema": {
              "$ref": "#/definitions/factory.api.v1.catalogue.GetProductResponse"
            }
          },
          "400": {
            "description": "The request was rejected because it was malformed — for example a badly formed id. The body is a validation-failure envelope: an overall type and message and one entry per problem (each with a stable code, a message, and a param pointing at the offending parameter).",
            "schema": {
              "$ref": "#/definitions/factory.api.v1.Error"
            }
          },
          "401": {
            "description": "The request is missing a valid bearer token, or the token is invalid or expired.",
            "schema": {
              "$ref": "#/definitions/factory.api.v1.Error"
            }
          },
          "404": {
            "description": "No catalogue product with the given id exists for the authenticated company.",
            "schema": {
              "$ref": "#/definitions/factory.api.v1.Error"
            }
          },
          "default": {
            "description": "Any other error. The body is the same error envelope every error uses: a short stable type identifying the kind of failure (for example \"rate_limited\" or \"internal\"), a human-readable message, and the request's idempotency key echoed back when one was supplied.",
            "schema": {
              "$ref": "#/definitions/factory.api.v1.Error"
            }
          }
        },
        "parameters": [
          {
            "name": "productId",
            "description": "The id of the product to retrieve. Required.",
            "in": "path",
            "required": true,
            "type": "string"
          },
          {
            "name": "fields",
            "description": "Comma-separated response fields to include, using camelCase JSON names\n(e.g. `orderId,total.amountMicros`). Dot paths reach into nested objects\nand map transparently across arrays. Paths are relative to the resource,\nnot the response envelope; envelope keys are always preserved. Only 2xx\nJSON responses are filtered; error bodies pass through unmodified. Unknown\nnames are silently ignored. Takes precedence over `excludeFields` when\nboth are provided.",
            "in": "query",
            "required": false,
            "type": "string"
          },
          {
            "name": "excludeFields",
            "description": "Comma-separated response fields to exclude, using camelCase JSON names.\nDot paths and array-transparency work the same as `fields`. Paths are\nrelative to the resource, not the response envelope. Only 2xx JSON\nresponses are filtered; error bodies pass through unmodified. Ignored\nwhen `fields` is also provided.",
            "in": "query",
            "required": false,
            "type": "string"
          }
        ],
        "tags": [
          "CatalogueService"
        ]
      }
    },
    "/v1/catalogue:search": {
      "get": {
        "summary": "Preview: search your catalogue.",
        "description": "Searches products, kits, and flashings by text query and returns one\nranked list of minimal hits, most relevant first. Every whitespace-\nseparated word in the query must match — against an entry's name, its\ncategory, a material, or a product attribute value (for example an Item\nCode). Fetch full detail with GetProduct, GetKit, or GetFlashing using\nthe hit's id. Result order is relevance-based and may change between\nrequests; it is not a stable contract.\n\nThis operation is in preview and not yet covered by the compatibility\nguarantees (see Versioning in the API conventions). Known limitations\ntoday: a page-by-page walk of a large result set can drop or repeat a\nrow between pages, so fetch one large page when completeness matters;\nand a flashing hit's id cannot yet be passed to GetFlashing (see the\nhit's id field).",
        "operationId": "SearchService_SearchCatalogue",
        "responses": {
          "200": {
            "description": "A successful response.",
            "schema": {
              "$ref": "#/definitions/factory.api.v1.search.SearchCatalogueResponse"
            }
          },
          "400": {
            "description": "The request was rejected because it was malformed — for example an empty query or an unusable page token. The body is a validation-failure envelope: an overall type and message and one entry per problem (each with a stable code, a message, and a param pointing at the offending parameter).",
            "schema": {
              "$ref": "#/definitions/factory.api.v1.Error"
            }
          },
          "401": {
            "description": "Authentication failed: the bearer token is missing, invalid, or expired.",
            "schema": {
              "$ref": "#/definitions/factory.api.v1.Error"
            }
          },
          "default": {
            "description": "Any other error. The body is the same error envelope every error uses: a short stable type identifying the kind of failure (for example \"rate_limited\" or \"internal\"), a human-readable message, and the request's idempotency key echoed back when one was supplied.",
            "schema": {
              "$ref": "#/definitions/factory.api.v1.Error"
            }
          }
        },
        "parameters": [
          {
            "name": "query",
            "description": "The text to search for. Every whitespace-separated word must match.",
            "in": "query",
            "required": false,
            "type": "string"
          },
          {
            "name": "pageSize",
            "description": "Maximum number of hits to return per page. 0 uses the server default;\nthe server may cap the value.",
            "in": "query",
            "required": false,
            "type": "integer",
            "format": "int32"
          },
          {
            "name": "pageToken",
            "description": "Opaque page token from a previous response, used to fetch the next page.\nA token is bound to the query it was minted for.",
            "in": "query",
            "required": false,
            "type": "string"
          },
          {
            "name": "fields",
            "description": "Comma-separated response fields to include, using camelCase JSON names\n(e.g. `orderId,total.amountMicros`). Dot paths reach into nested objects\nand map transparently across arrays. Paths are relative to the resource,\nnot the response envelope; envelope keys like `nextPageToken` are always\npreserved. Only 2xx JSON responses are filtered; error bodies pass through\nunmodified. Unknown names are silently ignored. Takes precedence over\n`excludeFields` when both are provided.",
            "in": "query",
            "required": false,
            "type": "string"
          },
          {
            "name": "excludeFields",
            "description": "Comma-separated response fields to exclude, using camelCase JSON names.\nDot paths and array-transparency work the same as `fields`. Paths are\nrelative to the resource, not the response envelope. Only 2xx JSON\nresponses are filtered; error bodies pass through unmodified. Ignored\nwhen `fields` is also provided.",
            "in": "query",
            "required": false,
            "type": "string"
          }
        ],
        "tags": [
          "SearchService"
        ]
      }
    },
    "/v1/company": {
      "get": {
        "summary": "Get your company's settings.",
        "description": "A singleton read keyed by the bearer token. Returns the company's\nidentity, locale and currency settings, the measurement system its\nquantities and dimensions are expressed in, its tax context, contact and\naddress details, and logo.",
        "operationId": "CompanyService_GetCompany",
        "responses": {
          "200": {
            "description": "A successful response.",
            "schema": {
              "$ref": "#/definitions/factory.api.v1.company.GetCompanyResponse"
            }
          },
          "401": {
            "description": "The request is missing a valid bearer token, or the token is invalid or expired.",
            "schema": {
              "$ref": "#/definitions/factory.api.v1.Error"
            }
          },
          "default": {
            "description": "Any other error. The body is the same error envelope every error uses: a short stable type identifying the kind of failure (for example \"rate_limited\" or \"internal\"), a human-readable message, and the request's idempotency key echoed back when one was supplied.",
            "schema": {
              "$ref": "#/definitions/factory.api.v1.Error"
            }
          }
        },
        "parameters": [
          {
            "name": "fields",
            "description": "Comma-separated response fields to include, using camelCase JSON names\n(e.g. `orderId,total.amountMicros`). Dot paths reach into nested objects\nand map transparently across arrays. Paths are relative to the resource,\nnot the response envelope; envelope keys are always preserved. Only 2xx\nJSON responses are filtered; error bodies pass through unmodified. Unknown\nnames are silently ignored. Takes precedence over `excludeFields` when\nboth are provided.",
            "in": "query",
            "required": false,
            "type": "string"
          },
          {
            "name": "excludeFields",
            "description": "Comma-separated response fields to exclude, using camelCase JSON names.\nDot paths and array-transparency work the same as `fields`. Paths are\nrelative to the resource, not the response envelope. Only 2xx JSON\nresponses are filtered; error bodies pass through unmodified. Ignored\nwhen `fields` is also provided.",
            "in": "query",
            "required": false,
            "type": "string"
          }
        ],
        "tags": [
          "CompanyService"
        ]
      }
    },
    "/v1/company/custom-fields": {
      "get": {
        "summary": "List your company's custom field definitions.",
        "description": "Custom fields are account-configured, typed fields on sales documents,\ncustomers, and suppliers. The `key` returned here is the identifier write\nendpoints accept — for example `customFields` on CreateQuoteRequest is an\nobject keyed by these keys. For select and multi-select fields, the value\na write must carry is an option `key` from the definition's options (not\nthe option's label). Hidden definitions are included — existing documents\ncan still carry values for fields that were hidden after being set.\nOptionally filter by module.",
        "operationId": "CompanyService_ListCompanyCustomFields",
        "responses": {
          "200": {
            "description": "A successful response.",
            "schema": {
              "$ref": "#/definitions/factory.api.v1.company.ListCompanyCustomFieldsResponse"
            }
          },
          "400": {
            "description": "The request was rejected because it was malformed — for example an unknown module filter value. The body is a validation-failure envelope.",
            "schema": {
              "$ref": "#/definitions/factory.api.v1.Error"
            }
          },
          "401": {
            "description": "The request is missing a valid bearer token, or the token is invalid or expired.",
            "schema": {
              "$ref": "#/definitions/factory.api.v1.Error"
            }
          },
          "default": {
            "description": "Any other error. The body is the same error envelope every error uses: a short stable type identifying the kind of failure (for example \"rate_limited\" or \"internal\"), a human-readable message, and the request's idempotency key echoed back when one was supplied.",
            "schema": {
              "$ref": "#/definitions/factory.api.v1.Error"
            }
          }
        },
        "parameters": [
          {
            "name": "module",
            "description": "Only return definitions for this module. Optional; unspecified returns\nevery module's definitions.\n\n - CUSTOM_FIELD_MODULE_SALES_DOCUMENTS: Quotes and orders share one set of sales-document fields.",
            "in": "query",
            "required": false,
            "type": "string",
            "enum": [
              "CUSTOM_FIELD_MODULE_UNSPECIFIED",
              "CUSTOM_FIELD_MODULE_SALES_DOCUMENTS",
              "CUSTOM_FIELD_MODULE_CUSTOMERS",
              "CUSTOM_FIELD_MODULE_SUPPLIERS",
              "CUSTOM_FIELD_MODULE_PURCHASE_ORDERS"
            ],
            "default": "CUSTOM_FIELD_MODULE_UNSPECIFIED"
          },
          {
            "name": "fields",
            "description": "Comma-separated response fields to include, using camelCase JSON names\n(e.g. `orderId,total.amountMicros`). Dot paths reach into nested objects\nand map transparently across arrays. Paths are relative to the resource,\nnot the response envelope; envelope keys like `nextPageToken` are always\npreserved. Only 2xx JSON responses are filtered; error bodies pass through\nunmodified. Unknown names are silently ignored. Takes precedence over\n`excludeFields` when both are provided.",
            "in": "query",
            "required": false,
            "type": "string"
          },
          {
            "name": "excludeFields",
            "description": "Comma-separated response fields to exclude, using camelCase JSON names.\nDot paths and array-transparency work the same as `fields`. Paths are\nrelative to the resource, not the response envelope. Only 2xx JSON\nresponses are filtered; error bodies pass through unmodified. Ignored\nwhen `fields` is also provided.",
            "in": "query",
            "required": false,
            "type": "string"
          }
        ],
        "tags": [
          "CompanyService"
        ]
      }
    },
    "/v1/company/labels": {
      "get": {
        "summary": "List your company's labels, sorted by name.",
        "description": "Returns the labels configured for the authenticated company. Use the\nreturned label ids when replacing an order's labels with SetOrderLabels.",
        "operationId": "LabelService_ListLabels",
        "responses": {
          "200": {
            "description": "A successful response.",
            "schema": {
              "$ref": "#/definitions/factory.api.v1.salesorders.ListLabelsResponse"
            }
          },
          "400": {
            "description": "The request was rejected because it was malformed — for example an unusable page token. The body is a validation-failure envelope: an overall type and message and one entry per problem (each with a stable code, a message, and a param pointing at the offending parameter).",
            "schema": {
              "$ref": "#/definitions/factory.api.v1.Error"
            }
          },
          "401": {
            "description": "The request is missing a valid bearer token, or the token is invalid or expired.",
            "schema": {
              "$ref": "#/definitions/factory.api.v1.Error"
            }
          },
          "default": {
            "description": "Any other error. The body is the same error envelope every error uses: a short stable type identifying the kind of failure (for example \"rate_limited\" or \"internal\"), a human-readable message, and the request's idempotency key echoed back when one was supplied.",
            "schema": {
              "$ref": "#/definitions/factory.api.v1.Error"
            }
          }
        },
        "parameters": [
          {
            "name": "pageSize",
            "description": "The maximum number of labels to return in one page. Optional; a server\ndefault applies when unset.",
            "in": "query",
            "required": false,
            "type": "integer",
            "format": "int32"
          },
          {
            "name": "pageToken",
            "description": "Opaque page token from a previous response, used to fetch the next page.",
            "in": "query",
            "required": false,
            "type": "string"
          },
          {
            "name": "fields",
            "description": "Comma-separated response fields to include, using camelCase JSON names\n(e.g. `orderId,total.amountMicros`). Dot paths reach into nested objects\nand map transparently across arrays. Paths are relative to the resource,\nnot the response envelope; envelope keys like `nextPageToken` are always\npreserved. Only 2xx JSON responses are filtered; error bodies pass through\nunmodified. Unknown names are silently ignored. Takes precedence over\n`excludeFields` when both are provided.",
            "in": "query",
            "required": false,
            "type": "string"
          },
          {
            "name": "excludeFields",
            "description": "Comma-separated response fields to exclude, using camelCase JSON names.\nDot paths and array-transparency work the same as `fields`. Paths are\nrelative to the resource, not the response envelope. Only 2xx JSON\nresponses are filtered; error bodies pass through unmodified. Ignored\nwhen `fields` is also provided.",
            "in": "query",
            "required": false,
            "type": "string"
          }
        ],
        "tags": [
          "LabelService"
        ]
      }
    },
    "/v1/company/order-statuses": {
      "get": {
        "summary": "List your company's order statuses, in column order.",
        "description": "Returns the order statuses (workflow columns) configured for the\nauthenticated company, in display order. Use the returned status ids when\nsetting an order's status on CreateOrder.",
        "operationId": "OrderStatusService_ListOrderStatuses",
        "responses": {
          "200": {
            "description": "A successful response.",
            "schema": {
              "$ref": "#/definitions/factory.api.v1.salesorders.ListOrderStatusesResponse"
            }
          },
          "400": {
            "description": "The request was rejected because it was malformed — for example an unusable page token. The body is a validation-failure envelope: an overall type and message and one entry per problem (each with a stable code, a message, and a param pointing at the offending parameter).",
            "schema": {
              "$ref": "#/definitions/factory.api.v1.Error"
            }
          },
          "401": {
            "description": "The request is missing a valid bearer token, or the token is invalid or expired.",
            "schema": {
              "$ref": "#/definitions/factory.api.v1.Error"
            }
          },
          "default": {
            "description": "Any other error. The body is the same error envelope every error uses: a short stable type identifying the kind of failure (for example \"rate_limited\" or \"internal\"), a human-readable message, and the request's idempotency key echoed back when one was supplied.",
            "schema": {
              "$ref": "#/definitions/factory.api.v1.Error"
            }
          }
        },
        "parameters": [
          {
            "name": "pageSize",
            "description": "Maximum number of statuses to return per page. 0 uses the server default.",
            "in": "query",
            "required": false,
            "type": "integer",
            "format": "int32"
          },
          {
            "name": "pageToken",
            "description": "Opaque page token from a previous response, used to fetch the next page.",
            "in": "query",
            "required": false,
            "type": "string"
          },
          {
            "name": "fields",
            "description": "Comma-separated response fields to include, using camelCase JSON names\n(e.g. `orderId,total.amountMicros`). Dot paths reach into nested objects\nand map transparently across arrays. Paths are relative to the resource,\nnot the response envelope; envelope keys like `nextPageToken` are always\npreserved. Only 2xx JSON responses are filtered; error bodies pass through\nunmodified. Unknown names are silently ignored. Takes precedence over\n`excludeFields` when both are provided.",
            "in": "query",
            "required": false,
            "type": "string"
          },
          {
            "name": "excludeFields",
            "description": "Comma-separated response fields to exclude, using camelCase JSON names.\nDot paths and array-transparency work the same as `fields`. Paths are\nrelative to the resource, not the response envelope. Only 2xx JSON\nresponses are filtered; error bodies pass through unmodified. Ignored\nwhen `fields` is also provided.",
            "in": "query",
            "required": false,
            "type": "string"
          }
        ],
        "tags": [
          "OrderStatusService"
        ]
      }
    },
    "/v1/customers": {
      "get": {
        "summary": "List or search the customers in your company.",
        "description": "A company-scoped list of customers, optionally filtered by company name\n(a case-insensitive contains-match). Use it to resolve a customer by name\nbefore referencing it on an order — compare each hit's full `companyName`\nwith the name you hold, since a single hit can still be a partial match.\nCustomers are returned alphabetically by company\nname. List entries are summaries — id, company name, billing address, and\nlast order time; fetch the full record (email, contacts, tax identifier,\nprice level) with GetCustomer.",
        "operationId": "CustomerService_ListCustomers",
        "responses": {
          "200": {
            "description": "A successful response.",
            "schema": {
              "$ref": "#/definitions/factory.api.v1.customers.ListCustomersResponse"
            }
          },
          "400": {
            "description": "The request was rejected because it was malformed — for example an unusable page token or an invalid filter value. The body is a validation-failure envelope: an overall type and message and one entry per problem (each with a stable code, a message, and a param pointing at the offending parameter).",
            "schema": {
              "$ref": "#/definitions/factory.api.v1.Error"
            }
          },
          "401": {
            "description": "The request is missing a valid bearer token, or the token is invalid or expired.",
            "schema": {
              "$ref": "#/definitions/factory.api.v1.Error"
            }
          },
          "default": {
            "description": "Any other error. The body is the same error envelope every error uses: a short stable type identifying the kind of failure (for example \"rate_limited\" or \"internal\"), a human-readable message, and the request's idempotency key echoed back when one was supplied.",
            "schema": {
              "$ref": "#/definitions/factory.api.v1.Error"
            }
          }
        },
        "parameters": [
          {
            "name": "pageSize",
            "description": "Maximum number of customers to return per page. 0 uses the server default;\nthe server may cap the value.",
            "in": "query",
            "required": false,
            "type": "integer",
            "format": "int32"
          },
          {
            "name": "pageToken",
            "description": "Opaque page token from a previous response, used to fetch the next page.",
            "in": "query",
            "required": false,
            "type": "string"
          },
          {
            "name": "companyName",
            "description": "Optional: only return customers whose company name contains this text,\ncompared case-insensitively — \"Apex\" and \"ex In\" both match \"Apex\nIndustrial Solutions\". To resolve a customer by name, compare each hit's\n`companyName` with the full name before taking its id; a single hit is not\nproof of an exact match. Pages are filtered after they are fetched, so a\npage may hold fewer than `pageSize` entries while `nextPageToken` is still\nset.",
            "in": "query",
            "required": false,
            "type": "string"
          },
          {
            "name": "fields",
            "description": "Comma-separated response fields to include, using camelCase JSON names\n(e.g. `orderId,total.amountMicros`). Dot paths reach into nested objects\nand map transparently across arrays. Paths are relative to the resource,\nnot the response envelope; envelope keys like `nextPageToken` are always\npreserved. Only 2xx JSON responses are filtered; error bodies pass through\nunmodified. Unknown names are silently ignored. Takes precedence over\n`excludeFields` when both are provided.",
            "in": "query",
            "required": false,
            "type": "string"
          },
          {
            "name": "excludeFields",
            "description": "Comma-separated response fields to exclude, using camelCase JSON names.\nDot paths and array-transparency work the same as `fields`. Paths are\nrelative to the resource, not the response envelope. Only 2xx JSON\nresponses are filtered; error bodies pass through unmodified. Ignored\nwhen `fields` is also provided.",
            "in": "query",
            "required": false,
            "type": "string"
          }
        ],
        "tags": [
          "CustomerService"
        ]
      },
      "post": {
        "summary": "Create a customer for the authenticated company.",
        "description": "Adds a new customer — company details, addresses, and contacts — to your\ncompany. Company name and email are required; a tax identifier is optional\n(in v1 that means an ABN). The company name must be\nunique within your company (matched case-insensitively); a duplicate is\nrejected with HTTP 400 (validation_failure). To make a retry safe, send\nan idempotency\nkey in `requestId`; a repeat with the same key can never create a second\ncustomer — it is rejected with HTTP 409 carrying the original customer's\nid. Reference the created customer by its id when creating an order or\nquote.",
        "operationId": "CustomerService_CreateCustomer",
        "responses": {
          "200": {
            "description": "A successful response.",
            "schema": {
              "$ref": "#/definitions/factory.api.v1.customers.CreateCustomerResponse"
            }
          },
          "400": {
            "description": "The request was rejected because it failed validation (for example a missing required field or a duplicate company name). The body is a validation-failure envelope: an overall type and message, the echoed `requestId`, and one entry per field-level problem (each with a stable code, a message, and a param pointing at the offending field).",
            "schema": {
              "$ref": "#/definitions/factory.api.v1.Error"
            }
          },
          "401": {
            "description": "The request is missing a valid bearer token, or the token is invalid or expired.",
            "schema": {
              "$ref": "#/definitions/factory.api.v1.Error"
            }
          },
          "409": {
            "description": "The idempotency key in `requestId` conflicts with an earlier use: the original request already completed (`resourceId` in the error body carries the id of the customer it created), its outcome is still unknown, or the key was reused with a materially different body. Verify the existing customer with ListCustomers (filter by `companyName` and compare the full name), then send a new `requestId` to create a new customer.",
            "schema": {
              "$ref": "#/definitions/factory.api.v1.Error"
            }
          },
          "default": {
            "description": "Any other error. The body is the same error envelope every error uses: a short stable type identifying the kind of failure (for example \"rate_limited\" or \"internal\"), a human-readable message, and the request's idempotency key echoed back when one was supplied.",
            "schema": {
              "$ref": "#/definitions/factory.api.v1.Error"
            }
          }
        },
        "parameters": [
          {
            "name": "body",
            "description": "Input for creating a customer: company details, addresses, and contacts. Company\nname and email are required; a tax identifier is optional.",
            "in": "body",
            "required": true,
            "schema": {
              "$ref": "#/definitions/factory.api.v1.customers.CreateCustomerRequest"
            }
          }
        ],
        "tags": [
          "CustomerService"
        ]
      }
    },
    "/v1/customers/{customerId}": {
      "get": {
        "summary": "Retrieve a single customer by its id.",
        "description": "Returns the full customer record — company details, addresses, contacts,\nand account flags — for the given id, scoped to your company.",
        "operationId": "CustomerService_GetCustomer",
        "responses": {
          "200": {
            "description": "A successful response.",
            "schema": {
              "$ref": "#/definitions/factory.api.v1.customers.GetCustomerResponse"
            }
          },
          "400": {
            "description": "The request was rejected because it was malformed — for example a badly formed id. The body is a validation-failure envelope: an overall type and message and one entry per problem (each with a stable code, a message, and a param pointing at the offending parameter).",
            "schema": {
              "$ref": "#/definitions/factory.api.v1.Error"
            }
          },
          "401": {
            "description": "The request is missing a valid bearer token, or the token is invalid or expired.",
            "schema": {
              "$ref": "#/definitions/factory.api.v1.Error"
            }
          },
          "404": {
            "description": "No customer with the given id exists for the authenticated company.",
            "schema": {
              "$ref": "#/definitions/factory.api.v1.Error"
            }
          },
          "default": {
            "description": "Any other error. The body is the same error envelope every error uses: a short stable type identifying the kind of failure (for example \"rate_limited\" or \"internal\"), a human-readable message, and the request's idempotency key echoed back when one was supplied.",
            "schema": {
              "$ref": "#/definitions/factory.api.v1.Error"
            }
          }
        },
        "parameters": [
          {
            "name": "customerId",
            "description": "The id of the customer to retrieve. Required.",
            "in": "path",
            "required": true,
            "type": "string"
          },
          {
            "name": "fields",
            "description": "Comma-separated response fields to include, using camelCase JSON names\n(e.g. `orderId,total.amountMicros`). Dot paths reach into nested objects\nand map transparently across arrays. Paths are relative to the resource,\nnot the response envelope; envelope keys are always preserved. Only 2xx\nJSON responses are filtered; error bodies pass through unmodified. Unknown\nnames are silently ignored. Takes precedence over `excludeFields` when\nboth are provided.",
            "in": "query",
            "required": false,
            "type": "string"
          },
          {
            "name": "excludeFields",
            "description": "Comma-separated response fields to exclude, using camelCase JSON names.\nDot paths and array-transparency work the same as `fields`. Paths are\nrelative to the resource, not the response envelope. Only 2xx JSON\nresponses are filtered; error bodies pass through unmodified. Ignored\nwhen `fields` is also provided.",
            "in": "query",
            "required": false,
            "type": "string"
          }
        ],
        "tags": [
          "CustomerService"
        ]
      }
    },
    "/v1/inventory": {
      "get": {
        "summary": "List stock levels for your inventory-tracked products.",
        "description": "Returns a paginated list of stock entries — one per product row per colour\n— for every catalogue product that has inventory tracking enabled.\nEvery entry is the stock of a single variant row, not a product total:\na product with several rows appears once per row, and a single-row\nproduct yields one entry. Each entry carries its variant's identifying\nattributes (thickness and name/value pairs), so entries are tellable\napart on their own; sum a product's entries when you need its overall\nposition. Filterable by product name, category, and supplier. Products\nare ordered alphabetically by name; entries within a product follow the\ncatalogue's row order. Non-tracked products never appear.",
        "operationId": "InventoryService_ListInventory",
        "responses": {
          "200": {
            "description": "A successful response.",
            "schema": {
              "$ref": "#/definitions/factory.api.v1.inventory.ListInventoryResponse"
            }
          },
          "400": {
            "description": "The request was rejected because it was malformed — for example an unusable page token or an invalid filter value. The body is a validation-failure envelope: an overall type and message and one entry per problem (each with a stable code, a message, and a param pointing at the offending parameter).",
            "schema": {
              "$ref": "#/definitions/factory.api.v1.Error"
            }
          },
          "401": {
            "description": "The request is missing a valid bearer token, or the token is invalid or expired.",
            "schema": {
              "$ref": "#/definitions/factory.api.v1.Error"
            }
          },
          "default": {
            "description": "Any other error. The body is the same error envelope every error uses: a short stable type identifying the kind of failure (for example \"rate_limited\" or \"internal\"), a human-readable message, and the request's idempotency key echoed back when one was supplied.",
            "schema": {
              "$ref": "#/definitions/factory.api.v1.Error"
            }
          }
        },
        "parameters": [
          {
            "name": "pageSize",
            "description": "Maximum number of stock entries to return per page. 0 uses the server\ndefault; the server may cap the value.",
            "in": "query",
            "required": false,
            "type": "integer",
            "format": "int32"
          },
          {
            "name": "pageToken",
            "description": "Opaque page token from a previous response, used to fetch the next page.",
            "in": "query",
            "required": false,
            "type": "string"
          },
          {
            "name": "name",
            "description": "Optional: only return entries for products whose name contains this text\n(case-insensitive substring match).",
            "in": "query",
            "required": false,
            "type": "string"
          },
          {
            "name": "categoryId",
            "description": "Optional: only return entries for products in this category.",
            "in": "query",
            "required": false,
            "type": "string"
          },
          {
            "name": "supplierId",
            "description": "Optional: only return entries for products linked to any of these\nsuppliers. A product appears if it is linked to at least one of the\nlisted suppliers. Use ListSuppliers on the SupplierService to discover\nvalid ids.",
            "in": "query",
            "required": false,
            "type": "array",
            "items": {
              "type": "string"
            },
            "collectionFormat": "multi"
          },
          {
            "name": "fields",
            "description": "Comma-separated response fields to include, using camelCase JSON names\n(e.g. `orderId,total.amountMicros`). Dot paths reach into nested objects\nand map transparently across arrays. Paths are relative to the resource,\nnot the response envelope; envelope keys like `nextPageToken` are always\npreserved. Only 2xx JSON responses are filtered; error bodies pass through\nunmodified. Unknown names are silently ignored. Takes precedence over\n`excludeFields` when both are provided.",
            "in": "query",
            "required": false,
            "type": "string"
          },
          {
            "name": "excludeFields",
            "description": "Comma-separated response fields to exclude, using camelCase JSON names.\nDot paths and array-transparency work the same as `fields`. Paths are\nrelative to the resource, not the response envelope. Only 2xx JSON\nresponses are filtered; error bodies pass through unmodified. Ignored\nwhen `fields` is also provided.",
            "in": "query",
            "required": false,
            "type": "string"
          }
        ],
        "tags": [
          "InventoryService"
        ]
      }
    },
    "/v1/inventory/{productId}": {
      "get": {
        "summary": "Retrieve stock levels for a single catalogue product.",
        "description": "Returns every stock entry for the given product — one per row per colour.\nEach entry is the stock of a single variant row (and colour), not a\nproduct total; sum the entries when you need the product's overall\nposition. Entries carry their variant's identifying attributes, and row\nids match the rows returned by the catalogue GetProduct endpoint. The\nproduct must have inventory tracking enabled; a non-tracked product\nreturns HTTP 404.",
        "operationId": "InventoryService_GetProductInventory",
        "responses": {
          "200": {
            "description": "A successful response.",
            "schema": {
              "$ref": "#/definitions/factory.api.v1.inventory.GetProductInventoryResponse"
            }
          },
          "400": {
            "description": "The request was rejected because it was malformed — for example a badly formed id. The body is a validation-failure envelope: an overall type and message and one entry per problem (each with a stable code, a message, and a param pointing at the offending parameter).",
            "schema": {
              "$ref": "#/definitions/factory.api.v1.Error"
            }
          },
          "401": {
            "description": "The request is missing a valid bearer token, or the token is invalid or expired.",
            "schema": {
              "$ref": "#/definitions/factory.api.v1.Error"
            }
          },
          "404": {
            "description": "No catalogue product with the given id exists for the authenticated company, or the product does not have inventory tracking enabled.",
            "schema": {
              "$ref": "#/definitions/factory.api.v1.Error"
            }
          },
          "default": {
            "description": "Any other error. The body is the same error envelope every error uses: a short stable type identifying the kind of failure (for example \"rate_limited\" or \"internal\"), a human-readable message, and the request's idempotency key echoed back when one was supplied.",
            "schema": {
              "$ref": "#/definitions/factory.api.v1.Error"
            }
          }
        },
        "parameters": [
          {
            "name": "productId",
            "description": "The id of the product to retrieve inventory for. Required.",
            "in": "path",
            "required": true,
            "type": "string"
          },
          {
            "name": "fields",
            "description": "Comma-separated response fields to include, using camelCase JSON names\n(e.g. `orderId,total.amountMicros`). Dot paths reach into nested objects\nand map transparently across arrays. Paths are relative to the resource,\nnot the response envelope; envelope keys like `nextPageToken` are always\npreserved. Only 2xx JSON responses are filtered; error bodies pass through\nunmodified. Unknown names are silently ignored. Takes precedence over\n`excludeFields` when both are provided.",
            "in": "query",
            "required": false,
            "type": "string"
          },
          {
            "name": "excludeFields",
            "description": "Comma-separated response fields to exclude, using camelCase JSON names.\nDot paths and array-transparency work the same as `fields`. Paths are\nrelative to the resource, not the response envelope. Only 2xx JSON\nresponses are filtered; error bodies pass through unmodified. Ignored\nwhen `fields` is also provided.",
            "in": "query",
            "required": false,
            "type": "string"
          }
        ],
        "tags": [
          "InventoryService"
        ]
      },
      "put": {
        "summary": "Set on-hand stock levels for a tracked product's rows.",
        "description": "Replaces the on-hand quantity for each row+colour combination you\ninclude. Entries you omit are left unchanged — this is a partial update,\nnot a full replace. The response echoes the product's complete stock\nposition (all entries, including unchanged ones) with recomputed\npromised, on-the-way, and available. The product must have inventory\ntracking enabled; a non-tracked product returns HTTP 404.",
        "operationId": "InventoryService_SetStockLevels",
        "responses": {
          "200": {
            "description": "A successful response.",
            "schema": {
              "$ref": "#/definitions/factory.api.v1.inventory.SetStockLevelsResponse"
            }
          },
          "400": {
            "description": "The request was rejected because it failed validation — for example a row id that does not belong to this product, a colour not in the product's material, or a malformed `onHand` value. The body is a validation-failure envelope.",
            "schema": {
              "$ref": "#/definitions/factory.api.v1.Error"
            }
          },
          "401": {
            "description": "The request is missing a valid bearer token, or the token is invalid or expired.",
            "schema": {
              "$ref": "#/definitions/factory.api.v1.Error"
            }
          },
          "404": {
            "description": "No catalogue product with the given id exists for the authenticated company, or the product does not have inventory tracking enabled.",
            "schema": {
              "$ref": "#/definitions/factory.api.v1.Error"
            }
          },
          "default": {
            "description": "Any other error. The body is the same error envelope every error uses: a short stable type identifying the kind of failure (for example \"rate_limited\" or \"internal\"), a human-readable message, and the request's idempotency key echoed back when one was supplied.",
            "schema": {
              "$ref": "#/definitions/factory.api.v1.Error"
            }
          }
        },
        "parameters": [
          {
            "name": "productId",
            "description": "The product to update. Required.",
            "in": "path",
            "required": true,
            "type": "string"
          },
          {
            "name": "body",
            "in": "body",
            "required": true,
            "schema": {
              "$ref": "#/definitions/factory.api.v1.inventory.InventoryService.SetStockLevelsBody"
            }
          }
        ],
        "tags": [
          "InventoryService"
        ]
      }
    },
    "/v1/orders": {
      "get": {
        "summary": "List the authenticated company's orders, with pagination and optional filters.",
        "description": "A flat, company-scoped list of orders. Confirmed orders only: quotes are\nread through QuoteService, and work-in-progress documents that have not yet\nbecome a quote or an order never appear here. Orders are returned most\nrecently updated first; because that order shifts as orders change, use\nQueryOrders for a stable ordering suitable for incremental sync. Paginated\nvia `pageSize` / `pageToken`.\n\nOrders carry three independent status dimensions, each filterable here:\n\n * Workflow status (`statusId` or `statusName`) — the order's position on\n   your company's kanban board. Statuses are company-configured: discover\n   yours with GET /v1/company/order-statuses, then filter by id or exact\n   name. Filter by one, not both.\n * Payment status (`paymentStatus`) — whether the order is unpaid, partially\n   paid, or paid. A fixed enum.\n * Received status (`receivedStatus`) — whether the order has been\n   delivered, picked up, installed, or not yet received. A fixed enum.\n\nAdditional filters: `customerId`, reference (substring), `isSubmitted`\n(draft/submitted), `labelIds`, `fulfilmentMethod`, `requiredAfter`/`requiredBefore`\n(required-by window), and archived (live vs archived view).",
        "operationId": "OrderService_ListOrders",
        "responses": {
          "200": {
            "description": "A successful response.",
            "schema": {
              "$ref": "#/definitions/factory.api.v1.salesorders.ListOrdersResponse"
            }
          },
          "400": {
            "description": "The request was rejected because it was malformed — for example an unusable page token or an invalid filter value. The body is a validation-failure envelope: an overall type and message and one entry per problem (each with a stable code, a message, and a param pointing at the offending parameter).",
            "schema": {
              "$ref": "#/definitions/factory.api.v1.Error"
            }
          },
          "401": {
            "description": "The request is missing a valid bearer token, or the token is invalid or expired.",
            "schema": {
              "$ref": "#/definitions/factory.api.v1.Error"
            }
          },
          "default": {
            "description": "Any other error. The body is the same error envelope every error uses: a short stable type identifying the kind of failure (for example \"rate_limited\" or \"internal\"), a human-readable message, and the request's idempotency key echoed back when one was supplied.",
            "schema": {
              "$ref": "#/definitions/factory.api.v1.Error"
            }
          }
        },
        "parameters": [
          {
            "name": "pageSize",
            "description": "Maximum number of orders to return per page. 0 uses the server default; the\nserver may cap the value.",
            "in": "query",
            "required": false,
            "type": "integer",
            "format": "int32"
          },
          {
            "name": "pageToken",
            "description": "Opaque page token from a previous response, used to fetch the next page.",
            "in": "query",
            "required": false,
            "type": "string"
          },
          {
            "name": "customerId",
            "description": "Optional: only return orders for this customer id.",
            "in": "query",
            "required": false,
            "type": "string"
          },
          {
            "name": "reference",
            "description": "Optional: only return orders whose external reference contains this text,\ncompared case-insensitively (a substring match, not exact-equals) — e.g. to\nfind a previously submitted order by part of its reference.",
            "in": "query",
            "required": false,
            "type": "string"
          },
          {
            "name": "isSubmitted",
            "description": "Optional: filter by whether the order has been submitted. Leave unset to\nreturn all orders; set to false to return only drafts; set to true to return\nonly submitted orders.",
            "in": "query",
            "required": false,
            "type": "boolean"
          },
          {
            "name": "archived",
            "description": "Optional: return the archived set instead of the live one. False (the\ndefault) lists live orders, as always; true lists ONLY archived orders —\na separate view, not a superset. Each returned order carries\n`isArchived`: true. Changing this flag mid-pagination invalidates the page\ntoken.",
            "in": "query",
            "required": false,
            "type": "boolean"
          },
          {
            "name": "labelIds",
            "description": "Optional: only return orders carrying at least one of these labels (by\nid — discover your labels with GET /v1/company/labels). An id not in\nyour label set matches no orders. Combines with the other filters.\nChanging the label filter mid-pagination invalidates the page token.\nDuplicate ids are rejected.",
            "in": "query",
            "required": false,
            "type": "array",
            "items": {
              "type": "string"
            },
            "collectionFormat": "multi"
          },
          {
            "name": "statusId",
            "description": "Optional: only return orders whose current workflow status matches this id.\nWorkflow statuses are company-configured kanban columns — every company has\ndifferent statuses. Call GET /v1/company/order-statuses to discover your\ncompany's statuses and their ids. Use either `statusId` or `statusName` to\nfilter, not both.",
            "in": "query",
            "required": false,
            "type": "string"
          },
          {
            "name": "statusName",
            "description": "Optional: only return orders whose current workflow status has this exact\nname (case-sensitive). An alternative to `statusId` when you know the name\nbut not the id — for example `statusName=Submitted`. Call\nGET /v1/company/order-statuses to see available names.",
            "in": "query",
            "required": false,
            "type": "string"
          },
          {
            "name": "paymentStatus",
            "description": "Optional: only return orders with this payment status. Unlike workflow\nstatus, payment status is a fixed set: UNPAID, PARTIALLY_PAID, or PAID.\nLeave unset to include all.\n\n - PAYMENT_STATUS_UNSPECIFIED: Default, unset value.\n - PAYMENT_STATUS_UNPAID: No payment has been received.\n - PAYMENT_STATUS_PARTIALLY_PAID: Part of the order total has been paid.\n - PAYMENT_STATUS_PAID: The order has been paid in full.",
            "in": "query",
            "required": false,
            "type": "string",
            "enum": [
              "PAYMENT_STATUS_UNSPECIFIED",
              "PAYMENT_STATUS_UNPAID",
              "PAYMENT_STATUS_PARTIALLY_PAID",
              "PAYMENT_STATUS_PAID"
            ],
            "default": "PAYMENT_STATUS_UNSPECIFIED"
          },
          {
            "name": "receivedStatus",
            "description": "Optional: only return orders with this received status. Unlike workflow\nstatus, received status is a fixed set: DELIVERED, PICKED_UP,\nNOT_RECEIVED, or INSTALLED. Leave unset to include all.\n\n - RECEIVED_STATUS_UNSPECIFIED: Default, unset value.\n - RECEIVED_STATUS_DELIVERED: The order was delivered.\n - RECEIVED_STATUS_PICKED_UP: The order was picked up by the customer.\n - RECEIVED_STATUS_NOT_RECEIVED: The order has not yet reached the customer.\n - RECEIVED_STATUS_INSTALLED: The order was installed.",
            "in": "query",
            "required": false,
            "type": "string",
            "enum": [
              "RECEIVED_STATUS_UNSPECIFIED",
              "RECEIVED_STATUS_DELIVERED",
              "RECEIVED_STATUS_PICKED_UP",
              "RECEIVED_STATUS_NOT_RECEIVED",
              "RECEIVED_STATUS_INSTALLED"
            ],
            "default": "RECEIVED_STATUS_UNSPECIFIED"
          },
          {
            "name": "fulfilmentMethod",
            "description": "Optional: only return orders with this fulfilment method. A fixed set:\nDELIVER, PICK_UP, or INSTALL. Leave unset to include all.\n\n - FULFILMENT_METHOD_UNSPECIFIED: Default, unset value.\n - FULFILMENT_METHOD_PICKUP: The customer collects the goods themselves.\n - FULFILMENT_METHOD_DELIVERY: The goods are delivered to the delivery address.\n - FULFILMENT_METHOD_INSTALL: The goods are installed at the install address.",
            "in": "query",
            "required": false,
            "type": "string",
            "enum": [
              "FULFILMENT_METHOD_UNSPECIFIED",
              "FULFILMENT_METHOD_PICKUP",
              "FULFILMENT_METHOD_DELIVERY",
              "FULFILMENT_METHOD_INSTALL"
            ],
            "default": "FULFILMENT_METHOD_UNSPECIFIED"
          },
          {
            "name": "requiredAfter",
            "description": "Only orders required at or after this time. Optional; filters on the\norder's required-by date, independent of the other filters.",
            "in": "query",
            "required": false,
            "type": "string",
            "format": "date-time"
          },
          {
            "name": "requiredBefore",
            "description": "Only orders required strictly before this time. Optional; combine with\n`requiredAfter` to bound a required-by window.",
            "in": "query",
            "required": false,
            "type": "string",
            "format": "date-time"
          },
          {
            "name": "fields",
            "description": "Comma-separated response fields to include, using camelCase JSON names\n(e.g. `orderId,total.amountMicros`). Dot paths reach into nested objects\nand map transparently across arrays. Paths are relative to the resource,\nnot the response envelope; envelope keys like `nextPageToken` are always\npreserved. Only 2xx JSON responses are filtered; error bodies pass through\nunmodified. Unknown names are silently ignored. Takes precedence over\n`excludeFields` when both are provided.",
            "in": "query",
            "required": false,
            "type": "string"
          },
          {
            "name": "excludeFields",
            "description": "Comma-separated response fields to exclude, using camelCase JSON names.\nDot paths and array-transparency work the same as `fields`. Paths are\nrelative to the resource, not the response envelope. Only 2xx JSON\nresponses are filtered; error bodies pass through unmodified. Ignored\nwhen `fields` is also provided.",
            "in": "query",
            "required": false,
            "type": "string"
          }
        ],
        "tags": [
          "OrderService"
        ]
      },
      "post": {
        "summary": "Create a sales order for the authenticated company.",
        "description": "The primary write endpoint: submit a complete order — header, line items,\nadjustments, and addresses — in one call, either saved as a draft or\nsubmitted immediately. The order belongs to the company identified by the\nbearer token. To make a retry safe, send an idempotency key in `requestId`;\na repeat with the same key can never create a second order — it is rejected\nwith HTTP 409 carrying the original order's id. A customer that can't be\nresolved or an unknown catalogue id is\nrejected with HTTP 400 (validation_failure).\n\nTax and currency follow your account's settings: a single tax rate and a\nsingle currency. You may send a `tax` descriptor (rate, code, jurisdiction)\nand amounts in any currency, but in this version they are advisory — the\nserver applies your account's configured rate to taxable lines (mark a line\nexempt with `isTaxFree`, or a customer with `disableTaxByDefault`) and\ntreats every amount as being in your account's currency: amounts are\nstored as the numbers you send and never converted, so an amount sent in\nanother currency reads back as the same number in your account's currency.\nIt can't yet apply a\ndifferent rate or currency per line, order, or jurisdiction, such as VAT or a\n15% rate. The created order echoes back the tax and currency actually applied\n(`tax`, `taxAmount`), so compare them to your input to detect any override;\nif your source system priced the order differently, the recomputed total can\ndiffer from your source document.",
        "operationId": "OrderService_CreateOrder",
        "responses": {
          "200": {
            "description": "A successful response.",
            "schema": {
              "$ref": "#/definitions/factory.api.v1.salesorders.CreateOrderResponse"
            }
          },
          "400": {
            "description": "The request was rejected because it failed validation. The body is a validation-failure envelope: an overall type and message, the echoed `requestId`, and one entry per field-level problem (each with a stable code, a message, and a param pointing at the offending field).",
            "schema": {
              "$ref": "#/definitions/factory.api.v1.Error"
            }
          },
          "401": {
            "description": "The request is missing a valid bearer token, or the token is invalid or expired.",
            "schema": {
              "$ref": "#/definitions/factory.api.v1.Error"
            }
          },
          "409": {
            "description": "The idempotency key in `requestId` conflicts with an earlier use: the original request already completed (`resourceId` in the error body carries the id of the order it created), its outcome is still unknown, or the key was reused with a materially different body. Verify the existing order with GetOrder or ListOrders, then send a new `requestId` to create a new order.",
            "schema": {
              "$ref": "#/definitions/factory.api.v1.Error"
            }
          },
          "default": {
            "description": "Any other error. The body is the same error envelope every error uses: a short stable type identifying the kind of failure (for example \"rate_limited\" or \"internal\"), a human-readable message, and the request's idempotency key echoed back when one was supplied.",
            "schema": {
              "$ref": "#/definitions/factory.api.v1.Error"
            }
          }
        },
        "parameters": [
          {
            "name": "body",
            "description": "Input for creating an order: the customer, line items, adjustments,\naddresses, and delivery/submission options. Order totals are derived by the\nserver, not supplied here.",
            "in": "body",
            "required": true,
            "schema": {
              "$ref": "#/definitions/factory.api.v1.salesorders.CreateOrderRequest"
            }
          }
        ],
        "tags": [
          "OrderService"
        ]
      }
    },
    "/v1/orders/{orderId}": {
      "get": {
        "summary": "Retrieve a single order by its id.",
        "description": "Returns the full order — header, lines, adjustments, totals, and\nserver-derived fields — for the given id, scoped to the authenticated\ncompany. Resolves confirmed orders only: an id that belongs to a quote\nreturns HTTP 404 (not_found) — read quotes with GetQuote.",
        "operationId": "OrderService_GetOrder",
        "responses": {
          "200": {
            "description": "A successful response.",
            "schema": {
              "$ref": "#/definitions/factory.api.v1.salesorders.GetOrderResponse"
            }
          },
          "400": {
            "description": "The request was rejected because it was malformed — for example a badly formed id. The body is a validation-failure envelope: an overall type and message and one entry per problem (each with a stable code, a message, and a param pointing at the offending parameter).",
            "schema": {
              "$ref": "#/definitions/factory.api.v1.Error"
            }
          },
          "401": {
            "description": "The request is missing a valid bearer token, or the token is invalid or expired.",
            "schema": {
              "$ref": "#/definitions/factory.api.v1.Error"
            }
          },
          "404": {
            "description": "No order with the given id exists for the authenticated company, the order is archived (pass `includeArchived=true` to read it anyway), or the document is still a quote — read it with GetQuote, swapping the id's order_ prefix for quote_ (the 26-character suffix stays the same).",
            "schema": {
              "$ref": "#/definitions/factory.api.v1.Error"
            }
          },
          "default": {
            "description": "Any other error. The body is the same error envelope every error uses: a short stable type identifying the kind of failure (for example \"rate_limited\" or \"internal\"), a human-readable message, and the request's idempotency key echoed back when one was supplied.",
            "schema": {
              "$ref": "#/definitions/factory.api.v1.Error"
            }
          }
        },
        "parameters": [
          {
            "name": "orderId",
            "description": "The id of the order to retrieve. Required.",
            "in": "path",
            "required": true,
            "type": "string"
          },
          {
            "name": "includeArchived",
            "description": "Optional: also resolve the order if it is archived. By default an archived\norder is not found (404); set this to true to read it anyway — the\nreturned order carries `isArchived`: true. Archived orders stay read-only:\nthis flag never makes them writable.",
            "in": "query",
            "required": false,
            "type": "boolean"
          },
          {
            "name": "fields",
            "description": "Comma-separated response fields to include, using camelCase JSON names\n(e.g. `orderId,total.amountMicros`). Dot paths reach into nested objects\nand map transparently across arrays. Paths are relative to the resource,\nnot the response envelope; envelope keys are always preserved. Only 2xx\nJSON responses are filtered; error bodies pass through unmodified. Unknown\nnames are silently ignored. Takes precedence over `excludeFields` when\nboth are provided.",
            "in": "query",
            "required": false,
            "type": "string"
          },
          {
            "name": "excludeFields",
            "description": "Comma-separated response fields to exclude, using camelCase JSON names.\nDot paths and array-transparency work the same as `fields`. Paths are\nrelative to the resource, not the response envelope. Only 2xx JSON\nresponses are filtered; error bodies pass through unmodified. Ignored\nwhen `fields` is also provided.",
            "in": "query",
            "required": false,
            "type": "string"
          }
        ],
        "tags": [
          "OrderService"
        ]
      }
    },
    "/v1/orders/{orderId}/attachments": {
      "get": {
        "summary": "List an order's attachments.",
        "description": "Returns every file on the order's conversation as a flat list, oldest\nfirst: files your team attached and documents the Factory app generated\n(`isGenerated` distinguishes them). Each attachment carries a presigned\ndownload URL. Signature captures collected in the app are not returned.",
        "operationId": "OrderService_ListAttachments",
        "responses": {
          "200": {
            "description": "A successful response.",
            "schema": {
              "$ref": "#/definitions/factory.api.v1.salesorders.ListAttachmentsResponse"
            }
          },
          "400": {
            "description": "The request was rejected because it failed validation. The body is a validation-failure envelope: an overall type and message and one entry per field-level problem (each with a stable code, a message, and a param pointing at the offending field).",
            "schema": {
              "$ref": "#/definitions/factory.api.v1.Error"
            }
          },
          "401": {
            "description": "The request is missing a valid bearer token, or the token is invalid or expired.",
            "schema": {
              "$ref": "#/definitions/factory.api.v1.Error"
            }
          },
          "404": {
            "description": "No order with the given id exists for the authenticated company, or the document is still a quote — read its conversation with the QuoteService endpoint, swapping the id's order_ prefix for quote_ (the 26-character suffix stays the same).",
            "schema": {
              "$ref": "#/definitions/factory.api.v1.Error"
            }
          },
          "default": {
            "description": "Any other error. The body is the same error envelope every error uses: a short stable type identifying the kind of failure (for example \"rate_limited\" or \"internal\"), a human-readable message, and the request's idempotency key echoed back when one was supplied.",
            "schema": {
              "$ref": "#/definitions/factory.api.v1.Error"
            }
          }
        },
        "parameters": [
          {
            "name": "orderId",
            "description": "The id of the order whose attachments to list. Required.",
            "in": "path",
            "required": true,
            "type": "string"
          },
          {
            "name": "pageSize",
            "description": "Maximum number of attachments to return per page. 0 uses the server\ndefault; the server may cap the value.",
            "in": "query",
            "required": false,
            "type": "integer",
            "format": "int32"
          },
          {
            "name": "pageToken",
            "description": "Opaque page token from a previous response, used to fetch the next page.",
            "in": "query",
            "required": false,
            "type": "string"
          },
          {
            "name": "fields",
            "description": "Comma-separated response fields to include, using camelCase JSON names\n(e.g. `orderId,total.amountMicros`). Dot paths reach into nested objects\nand map transparently across arrays. Paths are relative to the resource,\nnot the response envelope; envelope keys like `nextPageToken` are always\npreserved. Only 2xx JSON responses are filtered; error bodies pass through\nunmodified. Unknown names are silently ignored. Takes precedence over\n`excludeFields` when both are provided.",
            "in": "query",
            "required": false,
            "type": "string"
          },
          {
            "name": "excludeFields",
            "description": "Comma-separated response fields to exclude, using camelCase JSON names.\nDot paths and array-transparency work the same as `fields`. Paths are\nrelative to the resource, not the response envelope. Only 2xx JSON\nresponses are filtered; error bodies pass through unmodified. Ignored\nwhen `fields` is also provided.",
            "in": "query",
            "required": false,
            "type": "string"
          }
        ],
        "tags": [
          "OrderService"
        ]
      },
      "post": {
        "summary": "Attach a file to an order.",
        "description": "Uploads one file, up to 20 MB, into the order's conversation, where it\nappears immediately in the Factory app's Collaborate tab. Send the file\ncontent (base64-encoded over REST) with its filename and, optionally, a\nplain-text caption — with a caption the upload reads as a message\ncarrying a file; without one it is a bare attachment, which is how most\nfiles are posted. The created conversation message is returned,\nincluding the attachment's id and a presigned download URL. HEIC and\nHEIF images are converted to JPEG on upload. To make a retry safe, send\nan idempotency key in `requestId`; a repeat with the same key can never\nattach the file twice — it is rejected with HTTP 409 carrying the\noriginal message's id.",
        "operationId": "OrderService_UploadAttachment",
        "responses": {
          "200": {
            "description": "A successful response.",
            "schema": {
              "$ref": "#/definitions/factory.api.v1.salesorders.UploadAttachmentResponse"
            }
          },
          "400": {
            "description": "The request was rejected because it failed validation — the body is a validation-failure envelope: an overall type and message, the echoed `requestId`, and one entry per field-level problem (each with a stable code, a message, and a param pointing at the offending field). This includes a file over the 20 MB limit and a HEIC image that could not be converted.",
            "schema": {
              "$ref": "#/definitions/factory.api.v1.Error"
            }
          },
          "401": {
            "description": "The request is missing a valid bearer token, or the token is invalid or expired.",
            "schema": {
              "$ref": "#/definitions/factory.api.v1.Error"
            }
          },
          "404": {
            "description": "No order with the given id exists for the authenticated company, or the document is still a quote — attach via the QuoteService endpoint, swapping the id's order_ prefix for quote_ (the 26-character suffix stays the same).",
            "schema": {
              "$ref": "#/definitions/factory.api.v1.Error"
            }
          },
          "409": {
            "description": "The idempotency key in `requestId` conflicts with an earlier use: the original request already completed (`resourceId` in the error body carries the id of the message it created), its outcome is still unknown, or the key was reused with a materially different body. Verify the conversation with ListMessages or ListAttachments, then send a new `requestId` to attach a new file.",
            "schema": {
              "$ref": "#/definitions/factory.api.v1.Error"
            }
          },
          "default": {
            "description": "Any other error. The body is the same error envelope every error uses: a short stable type identifying the kind of failure (for example \"rate_limited\" or \"internal\"), a human-readable message, and the request's idempotency key echoed back when one was supplied.",
            "schema": {
              "$ref": "#/definitions/factory.api.v1.Error"
            }
          }
        },
        "parameters": [
          {
            "name": "orderId",
            "description": "The id of the order to attach to. Required.",
            "in": "path",
            "required": true,
            "type": "string"
          },
          {
            "name": "body",
            "in": "body",
            "required": true,
            "schema": {
              "$ref": "#/definitions/factory.api.v1.salesorders.OrderService.UploadAttachmentBody"
            }
          }
        ],
        "tags": [
          "OrderService"
        ]
      }
    },
    "/v1/orders/{orderId}/attachments/{attachmentId}": {
      "get": {
        "summary": "Retrieve one attachment on an order.",
        "description": "Returns the attachment's details and a fresh presigned download URL.\nUse this when a stored URL from an earlier read has expired.",
        "operationId": "OrderService_GetAttachment",
        "responses": {
          "200": {
            "description": "A successful response.",
            "schema": {
              "$ref": "#/definitions/factory.api.v1.salesorders.GetAttachmentResponse"
            }
          },
          "400": {
            "description": "The request was rejected because it failed validation. The body is a validation-failure envelope: an overall type and message and one entry per field-level problem (each with a stable code, a message, and a param pointing at the offending field).",
            "schema": {
              "$ref": "#/definitions/factory.api.v1.Error"
            }
          },
          "401": {
            "description": "The request is missing a valid bearer token, or the token is invalid or expired.",
            "schema": {
              "$ref": "#/definitions/factory.api.v1.Error"
            }
          },
          "404": {
            "description": "No order with the given id exists for the authenticated company, the attachment is not part of this order's conversation, or it has been removed.",
            "schema": {
              "$ref": "#/definitions/factory.api.v1.Error"
            }
          },
          "default": {
            "description": "Any other error. The body is the same error envelope every error uses: a short stable type identifying the kind of failure (for example \"rate_limited\" or \"internal\"), a human-readable message, and the request's idempotency key echoed back when one was supplied.",
            "schema": {
              "$ref": "#/definitions/factory.api.v1.Error"
            }
          }
        },
        "parameters": [
          {
            "name": "orderId",
            "description": "The id of the order the attachment belongs to. Required.",
            "in": "path",
            "required": true,
            "type": "string"
          },
          {
            "name": "attachmentId",
            "description": "The id of the attachment to retrieve. Required.",
            "in": "path",
            "required": true,
            "type": "string"
          },
          {
            "name": "fields",
            "description": "Comma-separated response fields to include, using camelCase JSON names\n(e.g. `orderId,total.amountMicros`). Dot paths reach into nested objects\nand map transparently across arrays. Paths are relative to the resource,\nnot the response envelope; envelope keys are always preserved. Only 2xx\nJSON responses are filtered; error bodies pass through unmodified. Unknown\nnames are silently ignored. Takes precedence over `excludeFields` when\nboth are provided.",
            "in": "query",
            "required": false,
            "type": "string"
          },
          {
            "name": "excludeFields",
            "description": "Comma-separated response fields to exclude, using camelCase JSON names.\nDot paths and array-transparency work the same as `fields`. Paths are\nrelative to the resource, not the response envelope. Only 2xx JSON\nresponses are filtered; error bodies pass through unmodified. Ignored\nwhen `fields` is also provided.",
            "in": "query",
            "required": false,
            "type": "string"
          }
        ],
        "tags": [
          "OrderService"
        ]
      },
      "delete": {
        "summary": "Remove an attachment from an order.",
        "description": "Removes the file from the order's conversation and from attachment\nlists. The removal is reversible by a Factory app administrator. Only\nthe person who posted the file or an administrator can remove it; for an\nAPI key, the acting user is the person the key belongs to, so a key can\nalways remove files it uploaded. Documents generated by the Factory app\n(`isGenerated`) cannot be removed.",
        "operationId": "OrderService_DeleteAttachment",
        "responses": {
          "200": {
            "description": "A successful response.",
            "schema": {
              "$ref": "#/definitions/factory.api.v1.salesorders.DeleteAttachmentResponse"
            }
          },
          "400": {
            "description": "The request was rejected because it failed validation, or the attachment is a document generated by the Factory app, which cannot be removed (failed_precondition). The body is a validation-failure envelope: an overall type and message and one entry per field-level problem (each with a stable code, a message, and a param pointing at the offending field).",
            "schema": {
              "$ref": "#/definitions/factory.api.v1.Error"
            }
          },
          "401": {
            "description": "The request is missing a valid bearer token, or the token is invalid or expired.",
            "schema": {
              "$ref": "#/definitions/factory.api.v1.Error"
            }
          },
          "403": {
            "description": "The acting user did not post this file and is not an administrator.",
            "schema": {
              "$ref": "#/definitions/factory.api.v1.Error"
            }
          },
          "404": {
            "description": "No order with the given id exists for the authenticated company, the attachment is not part of this order's conversation, or it has already been removed.",
            "schema": {
              "$ref": "#/definitions/factory.api.v1.Error"
            }
          },
          "default": {
            "description": "Any other error. The body is the same error envelope every error uses: a short stable type identifying the kind of failure (for example \"rate_limited\" or \"internal\"), a human-readable message, and the request's idempotency key echoed back when one was supplied.",
            "schema": {
              "$ref": "#/definitions/factory.api.v1.Error"
            }
          }
        },
        "parameters": [
          {
            "name": "orderId",
            "description": "The id of the order the attachment belongs to. Required.",
            "in": "path",
            "required": true,
            "type": "string"
          },
          {
            "name": "attachmentId",
            "description": "The id of the attachment to remove. Required.",
            "in": "path",
            "required": true,
            "type": "string"
          }
        ],
        "tags": [
          "OrderService"
        ]
      }
    },
    "/v1/orders/{orderId}/drawings:uploadSvg": {
      "post": {
        "summary": "Attach client-rendered SVG images to an order's flashing drawings.",
        "description": "Drawings exist only on flashing lines: a drawing is the folded profile of\na sheet-metal flashing, not a general image or file attachment for the\norder. An order with no flashing lines has no drawings, and this is the\nonly surface that accepts an upload.\n\nCall this after creating an order: map each drawing's `tempId` to the\n`drawingId` returned on the created order, then upload the rendered SVG for\neach `drawingId`. Stored SVGs are returned as a presigned URL\n(`svgUrl` on the FlashingDrawing) on later reads. Each drawing's outcome is reported\nindividually; a drawing is skipped if it isn't part of this order, isn't an\nimage/svg+xml, or has been deleted.",
        "operationId": "OrderService_UploadDrawingSvg",
        "responses": {
          "200": {
            "description": "A successful response.",
            "schema": {
              "$ref": "#/definitions/factory.api.v1.salesorders.UploadDrawingSvgResponse"
            }
          },
          "400": {
            "description": "The request was rejected because it failed validation. The body is a validation-failure envelope: an overall type and message, the echoed `requestId`, and one entry per field-level problem (each with a stable code, a message, and a param pointing at the offending field).",
            "schema": {
              "$ref": "#/definitions/factory.api.v1.Error"
            }
          },
          "401": {
            "description": "The request is missing a valid bearer token, or the token is invalid or expired.",
            "schema": {
              "$ref": "#/definitions/factory.api.v1.Error"
            }
          },
          "default": {
            "description": "Any other error. The body is the same error envelope every error uses: a short stable type identifying the kind of failure (for example \"rate_limited\" or \"internal\"), a human-readable message, and the request's idempotency key echoed back when one was supplied.",
            "schema": {
              "$ref": "#/definitions/factory.api.v1.Error"
            }
          }
        },
        "parameters": [
          {
            "name": "orderId",
            "description": "The id of the order whose flashing drawings are being uploaded. Required.",
            "in": "path",
            "required": true,
            "type": "string"
          },
          {
            "name": "body",
            "in": "body",
            "required": true,
            "schema": {
              "$ref": "#/definitions/factory.api.v1.salesorders.OrderService.UploadDrawingSvgBody"
            }
          }
        ],
        "tags": [
          "OrderService"
        ]
      }
    },
    "/v1/orders/{orderId}/labels": {
      "put": {
        "summary": "Replace the set of labels on an order.",
        "description": "The list you send becomes the order's complete label set: labels not\nlisted are removed, and an empty list removes them all. Look up label\nids with ListLabels. A label id that does not exist in your company is\nrejected with HTTP 404 (not_found) and nothing is changed.",
        "operationId": "OrderService_SetOrderLabels",
        "responses": {
          "200": {
            "description": "A successful response.",
            "schema": {
              "$ref": "#/definitions/factory.api.v1.salesorders.SetOrderLabelsResponse"
            }
          },
          "400": {
            "description": "The request was rejected because it failed validation — for example a badly formed or duplicated label id. The body is a validation-failure envelope: an overall type and message and one entry per field-level problem (each with a stable code, a message, and a param pointing at the offending field).",
            "schema": {
              "$ref": "#/definitions/factory.api.v1.Error"
            }
          },
          "401": {
            "description": "The request is missing a valid bearer token, or the token is invalid or expired.",
            "schema": {
              "$ref": "#/definitions/factory.api.v1.Error"
            }
          },
          "404": {
            "description": "No order with the given id exists for the authenticated company, the order is archived (archived orders are read-only), or the document is still a quote — label it with SetQuoteLabels. Also returned when a label id does not exist in your company: nothing is changed and the order's labels are left as they were.",
            "schema": {
              "$ref": "#/definitions/factory.api.v1.Error"
            }
          },
          "default": {
            "description": "Any other error. The body is the same error envelope every error uses: a short stable type identifying the kind of failure (for example \"rate_limited\" or \"internal\"), a human-readable message, and the request's idempotency key echoed back when one was supplied.",
            "schema": {
              "$ref": "#/definitions/factory.api.v1.Error"
            }
          }
        },
        "parameters": [
          {
            "name": "orderId",
            "description": "The id of the order, as returned by CreateOrder.",
            "in": "path",
            "required": true,
            "type": "string"
          },
          {
            "name": "body",
            "in": "body",
            "required": true,
            "schema": {
              "$ref": "#/definitions/factory.api.v1.salesorders.OrderService.SetOrderLabelsBody"
            }
          }
        ],
        "tags": [
          "OrderService"
        ]
      }
    },
    "/v1/orders/{orderId}/lines": {
      "post": {
        "summary": "Add a line item to an existing order.",
        "description": "Build an order up over time: each call creates one line under the order and\nrecomputes the order's totals. One line per call; add several lines with\nseveral calls. The new line's position among the existing lines is not\ncontrollable and is not guaranteed to be last; read the order for its\nfinal order. Adding a line does not change whether the order is\nsubmitted.\n\nFlashing lines cannot yet be added to an existing order and are rejected\nwith HTTP 501 (not_implemented): include them when creating the order, or\nadd them to the quote before conversion.",
        "operationId": "OrderService_AddOrderLineItem",
        "responses": {
          "200": {
            "description": "A successful response.",
            "schema": {
              "$ref": "#/definitions/factory.api.v1.salesorders.AddOrderLineItemResponse"
            }
          },
          "400": {
            "description": "The request was rejected because it failed validation — the body is a validation-failure envelope: an overall type and message, the echoed `requestId`, and one entry per field-level problem (each with a stable code, a message, and a param pointing at the offending field) — or because the order is not modifiable: an invoiced or accounting-locked order refuses line changes (failed_precondition).",
            "schema": {
              "$ref": "#/definitions/factory.api.v1.Error"
            }
          },
          "401": {
            "description": "The request is missing a valid bearer token, or the token is invalid or expired.",
            "schema": {
              "$ref": "#/definitions/factory.api.v1.Error"
            }
          },
          "404": {
            "description": "No order with the given id exists for the authenticated company, the order is archived (archived orders are read-only), or the document is still a quote — manage its lines with the QuoteService line endpoints, swapping the id's order_ prefix for quote_ (the 26-character suffix stays the same).",
            "schema": {
              "$ref": "#/definitions/factory.api.v1.Error"
            }
          },
          "409": {
            "description": "The idempotency key in `requestId` conflicts with an earlier use: the original request already completed (`resourceId` in the error body carries the id of the order it updated), its outcome is still unknown, or the key was reused with a materially different body. Verify the order's lines with GetOrder, then send a new `requestId` to add a new line.",
            "schema": {
              "$ref": "#/definitions/factory.api.v1.Error"
            }
          },
          "default": {
            "description": "Any other error. The body is the same error envelope every error uses: a short stable type identifying the kind of failure (for example \"rate_limited\" or \"internal\"), a human-readable message, and the request's idempotency key echoed back when one was supplied.",
            "schema": {
              "$ref": "#/definitions/factory.api.v1.Error"
            }
          }
        },
        "parameters": [
          {
            "name": "orderId",
            "description": "The id of the order to add the line item to, as returned by CreateOrder.",
            "in": "path",
            "required": true,
            "type": "string"
          },
          {
            "name": "body",
            "in": "body",
            "required": true,
            "schema": {
              "$ref": "#/definitions/factory.api.v1.salesorders.OrderService.AddOrderLineItemBody"
            }
          }
        ],
        "tags": [
          "OrderService"
        ]
      }
    },
    "/v1/orders/{orderId}/lines/{lineId}": {
      "delete": {
        "summary": "Remove a single line item from an order.",
        "description": "Identify the line by the server-assigned id returned when you read the\norder; the order's totals are recomputed after removal. A line id that does\nnot belong to the order is rejected with HTTP 404 (not_found). An order that\nhas been invoiced is rejected with HTTP 400 (failed_precondition). A\nflashing line cannot yet be removed and is rejected with HTTP 501\n(not_implemented).\n\nConcurrent writes to the same line are not detected: the last write wins.\nRe-read the order after writing if other clients may be editing it.",
        "operationId": "OrderService_DeleteOrderLineItem",
        "responses": {
          "200": {
            "description": "A successful response.",
            "schema": {
              "$ref": "#/definitions/factory.api.v1.salesorders.DeleteOrderLineItemResponse"
            }
          },
          "400": {
            "description": "The request was rejected because it was malformed — for example a badly formed id (the body is a validation-failure envelope: an overall type and message and one entry per problem, each with a stable code, a message, and a param pointing at the offending parameter) — or because the order is not modifiable: an invoiced or accounting-locked order refuses line changes (failed_precondition).",
            "schema": {
              "$ref": "#/definitions/factory.api.v1.Error"
            }
          },
          "401": {
            "description": "The request is missing a valid bearer token, or the token is invalid or expired.",
            "schema": {
              "$ref": "#/definitions/factory.api.v1.Error"
            }
          },
          "404": {
            "description": "No order with the given id exists for the authenticated company, the order is archived (archived orders are read-only), the document is still a quote — manage its lines with the QuoteService line endpoints — or the line id does not identify a line on this order.",
            "schema": {
              "$ref": "#/definitions/factory.api.v1.Error"
            }
          },
          "default": {
            "description": "Any other error. The body is the same error envelope every error uses: a short stable type identifying the kind of failure (for example \"rate_limited\" or \"internal\"), a human-readable message, and the request's idempotency key echoed back when one was supplied.",
            "schema": {
              "$ref": "#/definitions/factory.api.v1.Error"
            }
          }
        },
        "parameters": [
          {
            "name": "orderId",
            "description": "The id of the order the line belongs to, as returned by CreateOrder.",
            "in": "path",
            "required": true,
            "type": "string"
          },
          {
            "name": "lineId",
            "description": "The server-assigned id of the line to remove, as returned when the order is\nread. This has a different form from the order id.",
            "in": "path",
            "required": true,
            "type": "string"
          },
          {
            "name": "fields",
            "description": "Comma-separated response fields to include, using camelCase JSON names\n(e.g. `orderId,total.amountMicros`). Dot paths reach into nested objects\nand map transparently across arrays. Paths are relative to the resource,\nnot the response envelope; envelope keys are always preserved. Only 2xx\nJSON responses are filtered; error bodies pass through unmodified. Unknown\nnames are silently ignored. Takes precedence over `excludeFields` when\nboth are provided.",
            "in": "query",
            "required": false,
            "type": "string"
          },
          {
            "name": "excludeFields",
            "description": "Comma-separated response fields to exclude, using camelCase JSON names.\nDot paths and array-transparency work the same as `fields`. Paths are\nrelative to the resource, not the response envelope. Only 2xx JSON\nresponses are filtered; error bodies pass through unmodified. Ignored\nwhen `fields` is also provided.",
            "in": "query",
            "required": false,
            "type": "string"
          }
        ],
        "tags": [
          "OrderService"
        ]
      },
      "put": {
        "summary": "Replace a single line item on an order.",
        "description": "Send the full replacement line content; the whole line is replaced and the\norder's totals are recomputed. Identify the line by the server-assigned id\nreturned when you read the order. A line id that does not belong to the\norder is rejected with HTTP 404 (not_found). An order that has been invoiced\nis rejected with HTTP 400 (failed_precondition). A flashing line cannot yet\nbe replaced and is rejected with HTTP 501 (not_implemented).\n\nConcurrent writes to the same line are not detected: the last write wins.\nRe-read the order after writing if other clients may be editing it.",
        "operationId": "OrderService_UpdateOrderLineItem",
        "responses": {
          "200": {
            "description": "A successful response.",
            "schema": {
              "$ref": "#/definitions/factory.api.v1.salesorders.UpdateOrderLineItemResponse"
            }
          },
          "400": {
            "description": "The request was rejected because it failed validation — the body is a validation-failure envelope: an overall type and message and one entry per field-level problem (each with a stable code, a message, and a param pointing at the offending field) — or because the order is not modifiable: an invoiced or accounting-locked order refuses line changes (failed_precondition).",
            "schema": {
              "$ref": "#/definitions/factory.api.v1.Error"
            }
          },
          "401": {
            "description": "The request is missing a valid bearer token, or the token is invalid or expired.",
            "schema": {
              "$ref": "#/definitions/factory.api.v1.Error"
            }
          },
          "404": {
            "description": "No order with the given id exists for the authenticated company, the order is archived (archived orders are read-only), the document is still a quote — manage its lines with the QuoteService line endpoints — or the line id does not identify a line on this order.",
            "schema": {
              "$ref": "#/definitions/factory.api.v1.Error"
            }
          },
          "default": {
            "description": "Any other error. The body is the same error envelope every error uses: a short stable type identifying the kind of failure (for example \"rate_limited\" or \"internal\"), a human-readable message, and the request's idempotency key echoed back when one was supplied.",
            "schema": {
              "$ref": "#/definitions/factory.api.v1.Error"
            }
          }
        },
        "parameters": [
          {
            "name": "orderId",
            "description": "The id of the order the line belongs to, as returned by CreateOrder.",
            "in": "path",
            "required": true,
            "type": "string"
          },
          {
            "name": "lineId",
            "description": "The server-assigned id of the line to replace, as returned when the order is\nread. This has a different form from the order id.",
            "in": "path",
            "required": true,
            "type": "string"
          },
          {
            "name": "line",
            "description": "The replacement line content. The whole line is replaced with this — its kind\n(on-the-fly, catalogue, labour, notes, or product kit) and all its fields\ntake effect as sent.",
            "in": "body",
            "required": true,
            "schema": {
              "$ref": "#/definitions/factory.api.v1.salesdocuments.SalesLine"
            }
          },
          {
            "name": "fields",
            "description": "Comma-separated response fields to include, using camelCase JSON names\n(e.g. `orderId,total.amountMicros`). Dot paths reach into nested objects\nand map transparently across arrays. Paths are relative to the resource,\nnot the response envelope; envelope keys are always preserved. Only 2xx\nJSON responses are filtered; error bodies pass through unmodified. Unknown\nnames are silently ignored. Takes precedence over `excludeFields` when\nboth are provided.",
            "in": "query",
            "required": false,
            "type": "string"
          },
          {
            "name": "excludeFields",
            "description": "Comma-separated response fields to exclude, using camelCase JSON names.\nDot paths and array-transparency work the same as `fields`. Paths are\nrelative to the resource, not the response envelope. Only 2xx JSON\nresponses are filtered; error bodies pass through unmodified. Ignored\nwhen `fields` is also provided.",
            "in": "query",
            "required": false,
            "type": "string"
          }
        ],
        "tags": [
          "OrderService"
        ]
      }
    },
    "/v1/orders/{orderId}/messages": {
      "get": {
        "summary": "List an order's conversation.",
        "description": "Returns the order's conversation — the same thread shown in the Factory\napp's Collaborate tab — oldest message first: text written by your team,\nfiles they attached, and documents the app generated. Each message\ncarries its attachments, each with a presigned download URL. Most\nattachments are posted without any text, so expect many file-only\nmessages. Signature captures collected in the app are not returned.",
        "operationId": "OrderService_ListMessages",
        "responses": {
          "200": {
            "description": "A successful response.",
            "schema": {
              "$ref": "#/definitions/factory.api.v1.salesorders.ListMessagesResponse"
            }
          },
          "400": {
            "description": "The request was rejected because it failed validation. The body is a validation-failure envelope: an overall type and message and one entry per field-level problem (each with a stable code, a message, and a param pointing at the offending field).",
            "schema": {
              "$ref": "#/definitions/factory.api.v1.Error"
            }
          },
          "401": {
            "description": "The request is missing a valid bearer token, or the token is invalid or expired.",
            "schema": {
              "$ref": "#/definitions/factory.api.v1.Error"
            }
          },
          "404": {
            "description": "No order with the given id exists for the authenticated company, or the document is still a quote — read its conversation with the QuoteService endpoint, swapping the id's order_ prefix for quote_ (the 26-character suffix stays the same).",
            "schema": {
              "$ref": "#/definitions/factory.api.v1.Error"
            }
          },
          "default": {
            "description": "Any other error. The body is the same error envelope every error uses: a short stable type identifying the kind of failure (for example \"rate_limited\" or \"internal\"), a human-readable message, and the request's idempotency key echoed back when one was supplied.",
            "schema": {
              "$ref": "#/definitions/factory.api.v1.Error"
            }
          }
        },
        "parameters": [
          {
            "name": "orderId",
            "description": "The id of the order whose conversation to list. Required.",
            "in": "path",
            "required": true,
            "type": "string"
          },
          {
            "name": "pageSize",
            "description": "Maximum number of messages to return per page. 0 uses the server\ndefault; the server may cap the value.",
            "in": "query",
            "required": false,
            "type": "integer",
            "format": "int32"
          },
          {
            "name": "pageToken",
            "description": "Opaque page token from a previous response, used to fetch the next page.",
            "in": "query",
            "required": false,
            "type": "string"
          },
          {
            "name": "fields",
            "description": "Comma-separated response fields to include, using camelCase JSON names\n(e.g. `orderId,total.amountMicros`). Dot paths reach into nested objects\nand map transparently across arrays. Paths are relative to the resource,\nnot the response envelope; envelope keys like `nextPageToken` are always\npreserved. Only 2xx JSON responses are filtered; error bodies pass through\nunmodified. Unknown names are silently ignored. Takes precedence over\n`excludeFields` when both are provided.",
            "in": "query",
            "required": false,
            "type": "string"
          },
          {
            "name": "excludeFields",
            "description": "Comma-separated response fields to exclude, using camelCase JSON names.\nDot paths and array-transparency work the same as `fields`. Paths are\nrelative to the resource, not the response envelope. Only 2xx JSON\nresponses are filtered; error bodies pass through unmodified. Ignored\nwhen `fields` is also provided.",
            "in": "query",
            "required": false,
            "type": "string"
          }
        ],
        "tags": [
          "OrderService"
        ]
      },
      "post": {
        "summary": "Post a text message to an order's conversation.",
        "description": "Adds a plain-text message to the order's conversation, visible\nimmediately in the Factory app's Collaborate tab. The message is stored\nas an HTML fragment — the posted text HTML-escaped and `\u003cp\u003e`-wrapped —\nand reads back in that form. There is no way to @-mention a user through\nthis API: because text is escaped, mention markup renders as literal\ntext and notifies no one. PostMessage sends text on its own; to send\na file with text, use the caption on UploadAttachment.",
        "operationId": "OrderService_PostMessage",
        "responses": {
          "200": {
            "description": "A successful response.",
            "schema": {
              "$ref": "#/definitions/factory.api.v1.salesorders.PostMessageResponse"
            }
          },
          "400": {
            "description": "The request was rejected because it failed validation. The body is a validation-failure envelope: an overall type and message, the echoed `requestId`, and one entry per field-level problem (each with a stable code, a message, and a param pointing at the offending field).",
            "schema": {
              "$ref": "#/definitions/factory.api.v1.Error"
            }
          },
          "401": {
            "description": "The request is missing a valid bearer token, or the token is invalid or expired.",
            "schema": {
              "$ref": "#/definitions/factory.api.v1.Error"
            }
          },
          "404": {
            "description": "No order with the given id exists for the authenticated company, or the document is still a quote — post to the QuoteService endpoint, swapping the id's order_ prefix for quote_ (the 26-character suffix stays the same).",
            "schema": {
              "$ref": "#/definitions/factory.api.v1.Error"
            }
          },
          "409": {
            "description": "The idempotency key in `requestId` conflicts with an earlier use: the original request already completed (`resourceId` in the error body carries the id of the message it posted), its outcome is still unknown, or the key was reused with a materially different body. Verify the conversation with ListMessages, then send a new `requestId` to post a new message.",
            "schema": {
              "$ref": "#/definitions/factory.api.v1.Error"
            }
          },
          "default": {
            "description": "Any other error. The body is the same error envelope every error uses: a short stable type identifying the kind of failure (for example \"rate_limited\" or \"internal\"), a human-readable message, and the request's idempotency key echoed back when one was supplied.",
            "schema": {
              "$ref": "#/definitions/factory.api.v1.Error"
            }
          }
        },
        "parameters": [
          {
            "name": "orderId",
            "description": "The id of the order to post to. Required.",
            "in": "path",
            "required": true,
            "type": "string"
          },
          {
            "name": "body",
            "in": "body",
            "required": true,
            "schema": {
              "$ref": "#/definitions/factory.api.v1.salesorders.OrderService.PostMessageBody"
            }
          }
        ],
        "tags": [
          "OrderService"
        ]
      }
    },
    "/v1/orders:query": {
      "get": {
        "summary": "Query the company's orders as a change feed, for incremental sync.",
        "description": "Like ListOrders, but ordered by when each order last changed (oldest first)\nand filterable by an `updatedSince` / `updatedBefore` time window — so a\npoller fetches only what changed since its last run and checkpoints on the\nmost recent order it saw. Use ListOrders for simple lookups; use this to\nkeep an external system (CRM, reporting, accounting) in sync. Paginated\nvia `pageSize` / `pageToken`.\n\nCarries confirmed orders only — quotes are read through QuoteService, and\nwork-in-progress documents never appear. One migration to expect while\nsyncing: an order that began as a quote enters this feed at conversion as\nan order id that shares the quote id's 26-character suffix —\n\"quote_\u003csuffix\u003e\" becomes \"order_\u003csuffix\u003e\" — so a document you first saw on\nthe quote feed is recognised here by swapping the prefix.\n\nThe same three status dimensions as ListOrders are filterable here:\n\n * Workflow status (`statusId` or `statusName`) — the order's position on\n   your company's kanban board. Discover statuses with\n   GET /v1/company/order-statuses, then filter by id or exact name.\n * Payment status (`paymentStatus`) — unpaid, partially paid, or paid.\n * Received status (`receivedStatus`) — delivered, picked up, installed, or\n   not yet received.\n\nAdditional filters: `customerId`, reference (substring), `isSubmitted`,\n`fulfilmentMethod`, `createdAfter`/`createdBefore` (creation window),\n`requiredAfter`/`requiredBefore` (required-by window), and `includeArchived`.",
        "operationId": "OrderService_QueryOrders",
        "responses": {
          "200": {
            "description": "A successful response.",
            "schema": {
              "$ref": "#/definitions/factory.api.v1.salesorders.QueryOrdersResponse"
            }
          },
          "400": {
            "description": "The request was rejected because it was malformed — for example an unusable page token or an invalid filter value. The body is a validation-failure envelope: an overall type and message and one entry per problem (each with a stable code, a message, and a param pointing at the offending parameter).",
            "schema": {
              "$ref": "#/definitions/factory.api.v1.Error"
            }
          },
          "401": {
            "description": "The request is missing a valid bearer token, or the token is invalid or expired.",
            "schema": {
              "$ref": "#/definitions/factory.api.v1.Error"
            }
          },
          "default": {
            "description": "Any other error. The body is the same error envelope every error uses: a short stable type identifying the kind of failure (for example \"rate_limited\" or \"internal\"), a human-readable message, and the request's idempotency key echoed back when one was supplied.",
            "schema": {
              "$ref": "#/definitions/factory.api.v1.Error"
            }
          }
        },
        "parameters": [
          {
            "name": "updatedSince",
            "description": "Return only orders updated at or after this time. Pass your last sync time\nhere to fetch just what changed since then; omit to start from the\nbeginning. This is the primary filter for incremental sync.",
            "in": "query",
            "required": false,
            "type": "string",
            "format": "date-time"
          },
          {
            "name": "updatedBefore",
            "description": "Optional upper bound: return only orders updated strictly before this time.\nCombine with `updatedSince` to page through a bounded window; omit for \"up to\nnow\".",
            "in": "query",
            "required": false,
            "type": "string",
            "format": "date-time"
          },
          {
            "name": "pageSize",
            "description": "Maximum number of orders to return per page. 0 uses the server default; the\nserver may cap the value.",
            "in": "query",
            "required": false,
            "type": "integer",
            "format": "int32"
          },
          {
            "name": "pageToken",
            "description": "Opaque page token from a previous response, used to fetch the next page.",
            "in": "query",
            "required": false,
            "type": "string"
          },
          {
            "name": "customerId",
            "description": "Optional: only return orders for this customer id.",
            "in": "query",
            "required": false,
            "type": "string"
          },
          {
            "name": "reference",
            "description": "Optional: only return orders whose external reference contains this text,\ncompared case-insensitively (a substring match, not exact-equals).",
            "in": "query",
            "required": false,
            "type": "string"
          },
          {
            "name": "statusId",
            "description": "Optional: only return orders whose current workflow status matches this id.\nWorkflow statuses are company-configured kanban columns — every company has\ndifferent statuses. Call GET /v1/company/order-statuses to discover your\ncompany's statuses and their ids. Use either `statusId` or `statusName` to\nfilter, not both.",
            "in": "query",
            "required": false,
            "type": "string"
          },
          {
            "name": "paymentStatus",
            "description": "Optional: only return orders with this payment status. Unlike workflow\nstatus, payment status is a fixed set: UNPAID, PARTIALLY_PAID, or PAID.\nLeave unset to include all.\n\n - PAYMENT_STATUS_UNSPECIFIED: Default, unset value.\n - PAYMENT_STATUS_UNPAID: No payment has been received.\n - PAYMENT_STATUS_PARTIALLY_PAID: Part of the order total has been paid.\n - PAYMENT_STATUS_PAID: The order has been paid in full.",
            "in": "query",
            "required": false,
            "type": "string",
            "enum": [
              "PAYMENT_STATUS_UNSPECIFIED",
              "PAYMENT_STATUS_UNPAID",
              "PAYMENT_STATUS_PARTIALLY_PAID",
              "PAYMENT_STATUS_PAID"
            ],
            "default": "PAYMENT_STATUS_UNSPECIFIED"
          },
          {
            "name": "isSubmitted",
            "description": "Optional: filter by whether the order has been submitted. Leave unset to\nreturn all orders; set to false to return only drafts; set to true to return\nonly submitted orders.",
            "in": "query",
            "required": false,
            "type": "boolean"
          },
          {
            "name": "includeArchived",
            "description": "Optional: widen the feed to include archived orders. False (the default)\nkeeps today's feed of live orders; true returns live AND archived orders\nin one feed. Archiving stamps the order's update time, so with this flag\na poller sees the archive happen in-band: the order reappears with\n`isArchived`: true and a fresh `lastUpdatedAt` (and a restore reappears\nwith false) — no separate feed to reconcile. Changing this flag\nmid-pagination invalidates the page token.",
            "in": "query",
            "required": false,
            "type": "boolean"
          },
          {
            "name": "createdAfter",
            "description": "Only orders created at or after this time (inclusive). Optional; filters on\nthe order's creation time, independent of the updated window — combine the\ntwo windows to sync recent changes for orders created in a period. Note:\nthis parameter is named `createdAfter`, not `createdSince` — it is a range\nfilter, not a sync checkpoint.",
            "in": "query",
            "required": false,
            "type": "string",
            "format": "date-time"
          },
          {
            "name": "createdBefore",
            "description": "Only orders created strictly before this time (exclusive). Optional;\ncombine with `createdAfter` to bound a creation window.",
            "in": "query",
            "required": false,
            "type": "string",
            "format": "date-time"
          },
          {
            "name": "statusName",
            "description": "Optional: only return orders whose current workflow status has this exact\nname (case-sensitive). An alternative to `statusId` when you know the name\nbut not the id — for example `statusName=Submitted`. Call\nGET /v1/company/order-statuses to see available names.",
            "in": "query",
            "required": false,
            "type": "string"
          },
          {
            "name": "receivedStatus",
            "description": "Optional: only return orders with this received status. Unlike workflow\nstatus, received status is a fixed set: DELIVERED, PICKED_UP,\nNOT_RECEIVED, or INSTALLED. Leave unset to include all.\n\n - RECEIVED_STATUS_UNSPECIFIED: Default, unset value.\n - RECEIVED_STATUS_DELIVERED: The order was delivered.\n - RECEIVED_STATUS_PICKED_UP: The order was picked up by the customer.\n - RECEIVED_STATUS_NOT_RECEIVED: The order has not yet reached the customer.\n - RECEIVED_STATUS_INSTALLED: The order was installed.",
            "in": "query",
            "required": false,
            "type": "string",
            "enum": [
              "RECEIVED_STATUS_UNSPECIFIED",
              "RECEIVED_STATUS_DELIVERED",
              "RECEIVED_STATUS_PICKED_UP",
              "RECEIVED_STATUS_NOT_RECEIVED",
              "RECEIVED_STATUS_INSTALLED"
            ],
            "default": "RECEIVED_STATUS_UNSPECIFIED"
          },
          {
            "name": "fulfilmentMethod",
            "description": "Optional: only return orders with this fulfilment method. A fixed set:\nDELIVER, PICK_UP, or INSTALL. Leave unset to include all.\n\n - FULFILMENT_METHOD_UNSPECIFIED: Default, unset value.\n - FULFILMENT_METHOD_PICKUP: The customer collects the goods themselves.\n - FULFILMENT_METHOD_DELIVERY: The goods are delivered to the delivery address.\n - FULFILMENT_METHOD_INSTALL: The goods are installed at the install address.",
            "in": "query",
            "required": false,
            "type": "string",
            "enum": [
              "FULFILMENT_METHOD_UNSPECIFIED",
              "FULFILMENT_METHOD_PICKUP",
              "FULFILMENT_METHOD_DELIVERY",
              "FULFILMENT_METHOD_INSTALL"
            ],
            "default": "FULFILMENT_METHOD_UNSPECIFIED"
          },
          {
            "name": "requiredAfter",
            "description": "Only orders required at or after this time. Optional; filters on the\norder's required-by date, independent of the other filters.",
            "in": "query",
            "required": false,
            "type": "string",
            "format": "date-time"
          },
          {
            "name": "requiredBefore",
            "description": "Only orders required strictly before this time. Optional; combine with\n`requiredAfter` to bound a required-by window.",
            "in": "query",
            "required": false,
            "type": "string",
            "format": "date-time"
          },
          {
            "name": "fields",
            "description": "Comma-separated response fields to include, using camelCase JSON names\n(e.g. `orderId,total.amountMicros`). Dot paths reach into nested objects\nand map transparently across arrays. Paths are relative to the resource,\nnot the response envelope; envelope keys like `nextPageToken` are always\npreserved. Only 2xx JSON responses are filtered; error bodies pass through\nunmodified. Unknown names are silently ignored. Takes precedence over\n`excludeFields` when both are provided.",
            "in": "query",
            "required": false,
            "type": "string"
          },
          {
            "name": "excludeFields",
            "description": "Comma-separated response fields to exclude, using camelCase JSON names.\nDot paths and array-transparency work the same as `fields`. Paths are\nrelative to the resource, not the response envelope. Only 2xx JSON\nresponses are filtered; error bodies pass through unmodified. Ignored\nwhen `fields` is also provided.",
            "in": "query",
            "required": false,
            "type": "string"
          }
        ],
        "tags": [
          "OrderService"
        ]
      }
    },
    "/v1/quotes": {
      "get": {
        "summary": "List the authenticated company's quotes, with pagination and optional filters.",
        "description": "A flat, company-scoped list of quotes. Quotes only — confirmed orders are\nread through OrderService. Quotes are returned most recently updated first;\nbecause that order shifts as quotes change, use QueryQuotes for a stable\nordering suitable for incremental sync. List entries are summaries — the\nquote header and its totals without the line items; fetch the full document\nwith GetQuote. Paginated via `pageSize` / `pageToken`.\n\nQuotes have no workflow or payment status. Available filters: `customerId`,\nreference (substring), `isSubmitted` (draft/sent), `labelIds`,\n`fulfilmentMethod`, `requiredAfter`/`requiredBefore` (required-by window), and\narchived (live vs archived view).",
        "operationId": "QuoteService_ListQuotes",
        "responses": {
          "200": {
            "description": "A successful response.",
            "schema": {
              "$ref": "#/definitions/factory.api.v1.quotes.ListQuotesResponse"
            }
          },
          "400": {
            "description": "The request was rejected because it was malformed — for example an unusable page token or an invalid filter value. The body is a validation-failure envelope: an overall type and message and one entry per problem (each with a stable code, a message, and a param pointing at the offending parameter).",
            "schema": {
              "$ref": "#/definitions/factory.api.v1.Error"
            }
          },
          "401": {
            "description": "The request is missing a valid bearer token, or the token is invalid or expired.",
            "schema": {
              "$ref": "#/definitions/factory.api.v1.Error"
            }
          },
          "default": {
            "description": "Any other error. The body is the same error envelope every error uses: a short stable type identifying the kind of failure (for example \"rate_limited\" or \"internal\"), a human-readable message, and the request's idempotency key echoed back when one was supplied.",
            "schema": {
              "$ref": "#/definitions/factory.api.v1.Error"
            }
          }
        },
        "parameters": [
          {
            "name": "pageSize",
            "description": "Maximum number of quotes to return per page. 0 uses the server default; the\nserver may cap the value.",
            "in": "query",
            "required": false,
            "type": "integer",
            "format": "int32"
          },
          {
            "name": "pageToken",
            "description": "Opaque page token from a previous response, used to fetch the next page.",
            "in": "query",
            "required": false,
            "type": "string"
          },
          {
            "name": "customerId",
            "description": "Optional: only return quotes for this customer id.",
            "in": "query",
            "required": false,
            "type": "string"
          },
          {
            "name": "reference",
            "description": "Optional: only return quotes whose external reference contains this text,\ncompared case-insensitively (a substring match, not exact-equals) — e.g. to\nfind a previously submitted quote by part of its reference.",
            "in": "query",
            "required": false,
            "type": "string"
          },
          {
            "name": "isSubmitted",
            "description": "Optional: filter by whether the quote has been sent. Leave unset to return\nall quotes; set to false to return only drafts; set to true to return only\nsent quotes.",
            "in": "query",
            "required": false,
            "type": "boolean"
          },
          {
            "name": "labelIds",
            "description": "Optional: only return quotes carrying at least one of these labels (by\nid — discover your labels with GET /v1/company/labels). An id not in\nyour label set matches no quotes. Combines with the other filters.\nChanging the label filter mid-pagination invalidates the page token.\nDuplicate ids are rejected.",
            "in": "query",
            "required": false,
            "type": "array",
            "items": {
              "type": "string"
            },
            "collectionFormat": "multi"
          },
          {
            "name": "fulfilmentMethod",
            "description": "Optional: only return quotes with this fulfilment method. Leave unset to\ninclude all.\n\n - FULFILMENT_METHOD_UNSPECIFIED: Default, unset value.\n - FULFILMENT_METHOD_PICKUP: The customer collects the goods themselves.\n - FULFILMENT_METHOD_DELIVERY: The goods are delivered to the delivery address.\n - FULFILMENT_METHOD_INSTALL: The goods are installed at the install address.",
            "in": "query",
            "required": false,
            "type": "string",
            "enum": [
              "FULFILMENT_METHOD_UNSPECIFIED",
              "FULFILMENT_METHOD_PICKUP",
              "FULFILMENT_METHOD_DELIVERY",
              "FULFILMENT_METHOD_INSTALL"
            ],
            "default": "FULFILMENT_METHOD_UNSPECIFIED"
          },
          {
            "name": "requiredAfter",
            "description": "Only quotes required at or after this time. Optional; filters on the\nquote's required-by date, independent of the other filters.",
            "in": "query",
            "required": false,
            "type": "string",
            "format": "date-time"
          },
          {
            "name": "requiredBefore",
            "description": "Only quotes required strictly before this time. Optional; combine with\n`requiredAfter` to bound a required-by window.",
            "in": "query",
            "required": false,
            "type": "string",
            "format": "date-time"
          },
          {
            "name": "fields",
            "description": "Comma-separated response fields to include, using camelCase JSON names\n(e.g. `orderId,total.amountMicros`). Dot paths reach into nested objects\nand map transparently across arrays. Paths are relative to the resource,\nnot the response envelope; envelope keys like `nextPageToken` are always\npreserved. Only 2xx JSON responses are filtered; error bodies pass through\nunmodified. Unknown names are silently ignored. Takes precedence over\n`excludeFields` when both are provided.",
            "in": "query",
            "required": false,
            "type": "string"
          },
          {
            "name": "excludeFields",
            "description": "Comma-separated response fields to exclude, using camelCase JSON names.\nDot paths and array-transparency work the same as `fields`. Paths are\nrelative to the resource, not the response envelope. Only 2xx JSON\nresponses are filtered; error bodies pass through unmodified. Ignored\nwhen `fields` is also provided.",
            "in": "query",
            "required": false,
            "type": "string"
          }
        ],
        "tags": [
          "QuoteService"
        ]
      },
      "post": {
        "summary": "Create a quote for the authenticated company.",
        "description": "Submit a complete quote — customer, line items, adjustments, and addresses —\nin one call, either saved as a draft or finalised and sent. The quote\nbelongs to the company identified by the bearer token. To make a retry safe,\nsend an idempotency key in `requestId`; a repeat with the same key can never\ncreate a second quote — it is rejected with HTTP 409 carrying the original\nquote's id. A customer that can't be\nresolved or an unknown catalogue id is rejected with HTTP 400\n(validation_failure).\n\nQuote totals (subtotal, tax amount, total, margin) are derived by the server\nfrom the line items, not supplied here. You may send a `tax` descriptor (rate,\ncode, jurisdiction); in this version it is advisory — the server applies your\naccount's configured rate to taxable lines (mark a line exempt with\n`isTaxFree`) and echoes the tax actually applied back on the quote\n(`tax`, `taxAmount`). Every amount is treated as being in your account's\ncurrency: amounts are stored as the numbers you send and never converted,\nso an amount sent in another currency reads back as the same number in\nyour account's currency.",
        "operationId": "QuoteService_CreateQuote",
        "responses": {
          "200": {
            "description": "A successful response.",
            "schema": {
              "$ref": "#/definitions/factory.api.v1.quotes.CreateQuoteResponse"
            }
          },
          "400": {
            "description": "The request was rejected because it failed validation. The body is a validation-failure envelope: an overall type and message, the echoed `requestId`, and one entry per field-level problem (each with a stable code, a message, and a param pointing at the offending field).",
            "schema": {
              "$ref": "#/definitions/factory.api.v1.Error"
            }
          },
          "401": {
            "description": "The request is missing a valid bearer token, or the token is invalid or expired.",
            "schema": {
              "$ref": "#/definitions/factory.api.v1.Error"
            }
          },
          "409": {
            "description": "The idempotency key in `requestId` conflicts with an earlier use: the original request already completed (`resourceId` in the error body carries the id of the quote it created), its outcome is still unknown, or the key was reused with a materially different body. Verify the existing quote with GetQuote or ListQuotes, then send a new `requestId` to create a new quote.",
            "schema": {
              "$ref": "#/definitions/factory.api.v1.Error"
            }
          },
          "default": {
            "description": "Any other error. The body is the same error envelope every error uses: a short stable type identifying the kind of failure (for example \"rate_limited\" or \"internal\"), a human-readable message, and the request's idempotency key echoed back when one was supplied.",
            "schema": {
              "$ref": "#/definitions/factory.api.v1.Error"
            }
          }
        },
        "parameters": [
          {
            "name": "body",
            "description": "Input for creating a quote: the customer, line items, adjustments, addresses,\nand draft/sent option. Quote totals are derived by the server, not supplied\nhere.",
            "in": "body",
            "required": true,
            "schema": {
              "$ref": "#/definitions/factory.api.v1.quotes.CreateQuoteRequest"
            }
          }
        ],
        "tags": [
          "QuoteService"
        ]
      }
    },
    "/v1/quotes/{quoteId}": {
      "get": {
        "summary": "Retrieve a single quote by its id.",
        "description": "Returns the full quote — header, lines, adjustments, totals, and\nserver-derived fields — for the given id, scoped to the authenticated\ncompany. Resolves quotes only: once a quote has been accepted and converted\nto an order in the Factory app, its id returns HTTP 404 (not_found) here and\nthe document is read with GetOrder instead.",
        "operationId": "QuoteService_GetQuote",
        "responses": {
          "200": {
            "description": "A successful response.",
            "schema": {
              "$ref": "#/definitions/factory.api.v1.quotes.GetQuoteResponse"
            }
          },
          "400": {
            "description": "The request was rejected because it was malformed — for example a badly formed id. The body is a validation-failure envelope: an overall type and message and one entry per problem (each with a stable code, a message, and a param pointing at the offending parameter).",
            "schema": {
              "$ref": "#/definitions/factory.api.v1.Error"
            }
          },
          "401": {
            "description": "The request is missing a valid bearer token, or the token is invalid or expired.",
            "schema": {
              "$ref": "#/definitions/factory.api.v1.Error"
            }
          },
          "404": {
            "description": "No quote with the given id exists for the authenticated company, or the quote has been accepted and converted to an order — read it with GetOrder, swapping the id's quote_ prefix for order_ (the 26-character suffix stays the same).",
            "schema": {
              "$ref": "#/definitions/factory.api.v1.Error"
            }
          },
          "default": {
            "description": "Any other error. The body is the same error envelope every error uses: a short stable type identifying the kind of failure (for example \"rate_limited\" or \"internal\"), a human-readable message, and the request's idempotency key echoed back when one was supplied.",
            "schema": {
              "$ref": "#/definitions/factory.api.v1.Error"
            }
          }
        },
        "parameters": [
          {
            "name": "quoteId",
            "description": "The id of the quote to retrieve. Required.",
            "in": "path",
            "required": true,
            "type": "string"
          },
          {
            "name": "fields",
            "description": "Comma-separated response fields to include, using camelCase JSON names\n(e.g. `orderId,total.amountMicros`). Dot paths reach into nested objects\nand map transparently across arrays. Paths are relative to the resource,\nnot the response envelope; envelope keys are always preserved. Only 2xx\nJSON responses are filtered; error bodies pass through unmodified. Unknown\nnames are silently ignored. Takes precedence over `excludeFields` when\nboth are provided.",
            "in": "query",
            "required": false,
            "type": "string"
          },
          {
            "name": "excludeFields",
            "description": "Comma-separated response fields to exclude, using camelCase JSON names.\nDot paths and array-transparency work the same as `fields`. Paths are\nrelative to the resource, not the response envelope. Only 2xx JSON\nresponses are filtered; error bodies pass through unmodified. Ignored\nwhen `fields` is also provided.",
            "in": "query",
            "required": false,
            "type": "string"
          }
        ],
        "tags": [
          "QuoteService"
        ]
      }
    },
    "/v1/quotes/{quoteId}/attachments": {
      "get": {
        "summary": "List a quote's attachments.",
        "description": "Returns every file on the quote's conversation as a flat list, oldest\nfirst: files your team attached and documents the Factory app generated\n(`isGenerated` distinguishes them). Each attachment carries a presigned\ndownload URL. Signature captures collected in the app are not returned.",
        "operationId": "QuoteService_ListAttachments",
        "responses": {
          "200": {
            "description": "A successful response.",
            "schema": {
              "$ref": "#/definitions/factory.api.v1.quotes.ListAttachmentsResponse"
            }
          },
          "400": {
            "description": "The request was rejected because it failed validation. The body is a validation-failure envelope: an overall type and message and one entry per field-level problem (each with a stable code, a message, and a param pointing at the offending field).",
            "schema": {
              "$ref": "#/definitions/factory.api.v1.Error"
            }
          },
          "401": {
            "description": "The request is missing a valid bearer token, or the token is invalid or expired.",
            "schema": {
              "$ref": "#/definitions/factory.api.v1.Error"
            }
          },
          "404": {
            "description": "No quote with the given id exists for the authenticated company, or the quote has been accepted and converted to an order — its conversation carries over: read it with the order-side endpoint, swapping the id's quote_ prefix for order_ (the 26-character suffix stays the same).",
            "schema": {
              "$ref": "#/definitions/factory.api.v1.Error"
            }
          },
          "default": {
            "description": "Any other error. The body is the same error envelope every error uses: a short stable type identifying the kind of failure (for example \"rate_limited\" or \"internal\"), a human-readable message, and the request's idempotency key echoed back when one was supplied.",
            "schema": {
              "$ref": "#/definitions/factory.api.v1.Error"
            }
          }
        },
        "parameters": [
          {
            "name": "quoteId",
            "description": "The id of the quote whose attachments to list. Required.",
            "in": "path",
            "required": true,
            "type": "string"
          },
          {
            "name": "pageSize",
            "description": "Maximum number of attachments to return per page. 0 uses the server\ndefault; the server may cap the value.",
            "in": "query",
            "required": false,
            "type": "integer",
            "format": "int32"
          },
          {
            "name": "pageToken",
            "description": "Opaque page token from a previous response, used to fetch the next page.",
            "in": "query",
            "required": false,
            "type": "string"
          },
          {
            "name": "fields",
            "description": "Comma-separated response fields to include, using camelCase JSON names\n(e.g. `orderId,total.amountMicros`). Dot paths reach into nested objects\nand map transparently across arrays. Paths are relative to the resource,\nnot the response envelope; envelope keys like `nextPageToken` are always\npreserved. Only 2xx JSON responses are filtered; error bodies pass through\nunmodified. Unknown names are silently ignored. Takes precedence over\n`excludeFields` when both are provided.",
            "in": "query",
            "required": false,
            "type": "string"
          },
          {
            "name": "excludeFields",
            "description": "Comma-separated response fields to exclude, using camelCase JSON names.\nDot paths and array-transparency work the same as `fields`. Paths are\nrelative to the resource, not the response envelope. Only 2xx JSON\nresponses are filtered; error bodies pass through unmodified. Ignored\nwhen `fields` is also provided.",
            "in": "query",
            "required": false,
            "type": "string"
          }
        ],
        "tags": [
          "QuoteService"
        ]
      },
      "post": {
        "summary": "Attach a file to a quote.",
        "description": "Uploads one file, up to 20 MB, into the quote's conversation, where it\nappears immediately in the Factory app's Collaborate tab. Send the file\ncontent (base64-encoded over REST) with its filename and, optionally, a\nplain-text caption — with a caption the upload reads as a message\ncarrying a file; without one it is a bare attachment, which is how most\nfiles are posted. The created conversation message is returned,\nincluding the attachment's id and a presigned download URL. HEIC and\nHEIF images are converted to JPEG on upload. To make a retry safe, send\nan idempotency key in `requestId`; a repeat with the same key can never\nattach the file twice — it is rejected with HTTP 409 carrying the\noriginal message's id.",
        "operationId": "QuoteService_UploadAttachment",
        "responses": {
          "200": {
            "description": "A successful response.",
            "schema": {
              "$ref": "#/definitions/factory.api.v1.quotes.UploadAttachmentResponse"
            }
          },
          "400": {
            "description": "The request was rejected because it failed validation — the body is a validation-failure envelope: an overall type and message, the echoed `requestId`, and one entry per field-level problem (each with a stable code, a message, and a param pointing at the offending field). This includes a file over the 20 MB limit and a HEIC image that could not be converted.",
            "schema": {
              "$ref": "#/definitions/factory.api.v1.Error"
            }
          },
          "401": {
            "description": "The request is missing a valid bearer token, or the token is invalid or expired.",
            "schema": {
              "$ref": "#/definitions/factory.api.v1.Error"
            }
          },
          "404": {
            "description": "No quote with the given id exists for the authenticated company, or the quote has been accepted and converted to an order — its conversation carries over: attach via the order-side endpoint, swapping the id's quote_ prefix for order_ (the 26-character suffix stays the same).",
            "schema": {
              "$ref": "#/definitions/factory.api.v1.Error"
            }
          },
          "409": {
            "description": "The idempotency key in `requestId` conflicts with an earlier use: the original request already completed (`resourceId` in the error body carries the id of the message it created), its outcome is still unknown, or the key was reused with a materially different body. Verify the conversation with ListMessages or ListAttachments, then send a new `requestId` to attach a new file.",
            "schema": {
              "$ref": "#/definitions/factory.api.v1.Error"
            }
          },
          "default": {
            "description": "Any other error. The body is the same error envelope every error uses: a short stable type identifying the kind of failure (for example \"rate_limited\" or \"internal\"), a human-readable message, and the request's idempotency key echoed back when one was supplied.",
            "schema": {
              "$ref": "#/definitions/factory.api.v1.Error"
            }
          }
        },
        "parameters": [
          {
            "name": "quoteId",
            "description": "The id of the quote to attach to. Required.",
            "in": "path",
            "required": true,
            "type": "string"
          },
          {
            "name": "body",
            "in": "body",
            "required": true,
            "schema": {
              "$ref": "#/definitions/factory.api.v1.quotes.QuoteService.UploadAttachmentBody"
            }
          }
        ],
        "tags": [
          "QuoteService"
        ]
      }
    },
    "/v1/quotes/{quoteId}/attachments/{attachmentId}": {
      "get": {
        "summary": "Retrieve one attachment on a quote.",
        "description": "Returns the attachment's details and a fresh presigned download URL.\nUse this when a stored URL from an earlier read has expired.",
        "operationId": "QuoteService_GetAttachment",
        "responses": {
          "200": {
            "description": "A successful response.",
            "schema": {
              "$ref": "#/definitions/factory.api.v1.quotes.GetAttachmentResponse"
            }
          },
          "400": {
            "description": "The request was rejected because it failed validation. The body is a validation-failure envelope: an overall type and message and one entry per field-level problem (each with a stable code, a message, and a param pointing at the offending field).",
            "schema": {
              "$ref": "#/definitions/factory.api.v1.Error"
            }
          },
          "401": {
            "description": "The request is missing a valid bearer token, or the token is invalid or expired.",
            "schema": {
              "$ref": "#/definitions/factory.api.v1.Error"
            }
          },
          "404": {
            "description": "No quote with the given id exists for the authenticated company, the attachment is not part of this quote's conversation, or it has been removed.",
            "schema": {
              "$ref": "#/definitions/factory.api.v1.Error"
            }
          },
          "default": {
            "description": "Any other error. The body is the same error envelope every error uses: a short stable type identifying the kind of failure (for example \"rate_limited\" or \"internal\"), a human-readable message, and the request's idempotency key echoed back when one was supplied.",
            "schema": {
              "$ref": "#/definitions/factory.api.v1.Error"
            }
          }
        },
        "parameters": [
          {
            "name": "quoteId",
            "description": "The id of the quote the attachment belongs to. Required.",
            "in": "path",
            "required": true,
            "type": "string"
          },
          {
            "name": "attachmentId",
            "description": "The id of the attachment to retrieve. Required.",
            "in": "path",
            "required": true,
            "type": "string"
          },
          {
            "name": "fields",
            "description": "Comma-separated response fields to include, using camelCase JSON names\n(e.g. `orderId,total.amountMicros`). Dot paths reach into nested objects\nand map transparently across arrays. Paths are relative to the resource,\nnot the response envelope; envelope keys are always preserved. Only 2xx\nJSON responses are filtered; error bodies pass through unmodified. Unknown\nnames are silently ignored. Takes precedence over `excludeFields` when\nboth are provided.",
            "in": "query",
            "required": false,
            "type": "string"
          },
          {
            "name": "excludeFields",
            "description": "Comma-separated response fields to exclude, using camelCase JSON names.\nDot paths and array-transparency work the same as `fields`. Paths are\nrelative to the resource, not the response envelope. Only 2xx JSON\nresponses are filtered; error bodies pass through unmodified. Ignored\nwhen `fields` is also provided.",
            "in": "query",
            "required": false,
            "type": "string"
          }
        ],
        "tags": [
          "QuoteService"
        ]
      },
      "delete": {
        "summary": "Remove an attachment from a quote.",
        "description": "Removes the file from the quote's conversation and from attachment\nlists. The removal is reversible by a Factory app administrator. Only\nthe person who posted the file or an administrator can remove it; for an\nAPI key, the acting user is the person the key belongs to, so a key can\nalways remove files it uploaded. Documents generated by the Factory app\n(`isGenerated`) cannot be removed.",
        "operationId": "QuoteService_DeleteAttachment",
        "responses": {
          "200": {
            "description": "A successful response.",
            "schema": {
              "$ref": "#/definitions/factory.api.v1.quotes.DeleteAttachmentResponse"
            }
          },
          "400": {
            "description": "The request was rejected because it failed validation, or the attachment is a document generated by the Factory app, which cannot be removed (failed_precondition). The body is a validation-failure envelope: an overall type and message and one entry per field-level problem (each with a stable code, a message, and a param pointing at the offending field).",
            "schema": {
              "$ref": "#/definitions/factory.api.v1.Error"
            }
          },
          "401": {
            "description": "The request is missing a valid bearer token, or the token is invalid or expired.",
            "schema": {
              "$ref": "#/definitions/factory.api.v1.Error"
            }
          },
          "403": {
            "description": "The acting user did not post this file and is not an administrator.",
            "schema": {
              "$ref": "#/definitions/factory.api.v1.Error"
            }
          },
          "404": {
            "description": "No quote with the given id exists for the authenticated company, the attachment is not part of this quote's conversation, or it has already been removed.",
            "schema": {
              "$ref": "#/definitions/factory.api.v1.Error"
            }
          },
          "default": {
            "description": "Any other error. The body is the same error envelope every error uses: a short stable type identifying the kind of failure (for example \"rate_limited\" or \"internal\"), a human-readable message, and the request's idempotency key echoed back when one was supplied.",
            "schema": {
              "$ref": "#/definitions/factory.api.v1.Error"
            }
          }
        },
        "parameters": [
          {
            "name": "quoteId",
            "description": "The id of the quote the attachment belongs to. Required.",
            "in": "path",
            "required": true,
            "type": "string"
          },
          {
            "name": "attachmentId",
            "description": "The id of the attachment to remove. Required.",
            "in": "path",
            "required": true,
            "type": "string"
          }
        ],
        "tags": [
          "QuoteService"
        ]
      }
    },
    "/v1/quotes/{quoteId}/drawings:uploadSvg": {
      "post": {
        "summary": "Attach client-rendered SVG images to a quote's flashing drawings.",
        "description": "Drawings exist only on flashing lines: a drawing is the folded profile of\na sheet-metal flashing, not a general image or file attachment for the\nquote. A quote with no flashing lines has no drawings, and this is the\nonly surface that accepts an upload.\n\nCall this after creating a quote: map each drawing's `tempId` to the\n`drawingId` returned on the created quote, then upload the rendered SVG for\neach `drawingId`. Stored SVGs are returned as a presigned URL\n(`svgUrl` on the FlashingDrawing) on later reads. Each drawing's outcome is reported\nindividually; a drawing is skipped if it isn't part of this quote, isn't an\nimage/svg+xml, or has been deleted.",
        "operationId": "QuoteService_UploadDrawingSvg",
        "responses": {
          "200": {
            "description": "A successful response.",
            "schema": {
              "$ref": "#/definitions/factory.api.v1.quotes.UploadDrawingSvgResponse"
            }
          },
          "400": {
            "description": "The request was rejected because it failed validation. The body is a validation-failure envelope: an overall type and message, the echoed `requestId`, and one entry per field-level problem (each with a stable code, a message, and a param pointing at the offending field).",
            "schema": {
              "$ref": "#/definitions/factory.api.v1.Error"
            }
          },
          "401": {
            "description": "The request is missing a valid bearer token, or the token is invalid or expired.",
            "schema": {
              "$ref": "#/definitions/factory.api.v1.Error"
            }
          },
          "default": {
            "description": "Any other error. The body is the same error envelope every error uses: a short stable type identifying the kind of failure (for example \"rate_limited\" or \"internal\"), a human-readable message, and the request's idempotency key echoed back when one was supplied.",
            "schema": {
              "$ref": "#/definitions/factory.api.v1.Error"
            }
          }
        },
        "parameters": [
          {
            "name": "quoteId",
            "description": "The id of the quote whose flashing drawings are being uploaded. Required.",
            "in": "path",
            "required": true,
            "type": "string"
          },
          {
            "name": "body",
            "in": "body",
            "required": true,
            "schema": {
              "$ref": "#/definitions/factory.api.v1.quotes.QuoteService.UploadDrawingSvgBody"
            }
          }
        ],
        "tags": [
          "QuoteService"
        ]
      }
    },
    "/v1/quotes/{quoteId}/labels": {
      "put": {
        "summary": "Replace the set of labels on a quote.",
        "description": "The list you send becomes the quote's complete label set: labels not\nlisted are removed, and an empty list removes them all. Look up label\nids with ListLabels. A label id that does not exist in your company is\nrejected with HTTP 404 (not_found) and nothing is changed.",
        "operationId": "QuoteService_SetQuoteLabels",
        "responses": {
          "200": {
            "description": "A successful response.",
            "schema": {
              "$ref": "#/definitions/factory.api.v1.quotes.SetQuoteLabelsResponse"
            }
          },
          "400": {
            "description": "The request was rejected because it failed validation — for example a badly formed or duplicated label id. The body is a validation-failure envelope: an overall type and message and one entry per field-level problem (each with a stable code, a message, and a param pointing at the offending field).",
            "schema": {
              "$ref": "#/definitions/factory.api.v1.Error"
            }
          },
          "401": {
            "description": "The request is missing a valid bearer token, or the token is invalid or expired.",
            "schema": {
              "$ref": "#/definitions/factory.api.v1.Error"
            }
          },
          "404": {
            "description": "No quote with the given id exists for the authenticated company, or the quote has been accepted and converted to an order — label it with SetOrderLabels, swapping the id's quote_ prefix for order_ (the 26-character suffix stays the same). Also returned when a label id does not exist in your company: nothing is changed and the quote's labels are left as they were.",
            "schema": {
              "$ref": "#/definitions/factory.api.v1.Error"
            }
          },
          "default": {
            "description": "Any other error. The body is the same error envelope every error uses: a short stable type identifying the kind of failure (for example \"rate_limited\" or \"internal\"), a human-readable message, and the request's idempotency key echoed back when one was supplied.",
            "schema": {
              "$ref": "#/definitions/factory.api.v1.Error"
            }
          }
        },
        "parameters": [
          {
            "name": "quoteId",
            "description": "The id of the quote, as returned by CreateQuote.",
            "in": "path",
            "required": true,
            "type": "string"
          },
          {
            "name": "body",
            "in": "body",
            "required": true,
            "schema": {
              "$ref": "#/definitions/factory.api.v1.quotes.QuoteService.SetQuoteLabelsBody"
            }
          }
        ],
        "tags": [
          "QuoteService"
        ]
      }
    },
    "/v1/quotes/{quoteId}/lines": {
      "post": {
        "summary": "Add a line item to an existing quote.",
        "description": "Build a quote up over time: each call creates one line under the quote and\nrecomputes the quote's totals. One line per call; add several lines with\nseveral calls. The new line's position among the existing lines is not\ncontrollable and is not guaranteed to be last; read the quote for its\nfinal order. Adding a line does not change whether the quote is\nsubmitted. Pair with CreateQuote, which imports a whole quote in one\natomic call (or creates an empty quote you then add lines to with this).",
        "operationId": "QuoteService_AddQuoteLineItem",
        "responses": {
          "200": {
            "description": "A successful response.",
            "schema": {
              "$ref": "#/definitions/factory.api.v1.quotes.AddQuoteLineItemResponse"
            }
          },
          "400": {
            "description": "The request was rejected because it failed validation. The body is a validation-failure envelope: an overall type and message, the echoed `requestId`, and one entry per field-level problem (each with a stable code, a message, and a param pointing at the offending field).",
            "schema": {
              "$ref": "#/definitions/factory.api.v1.Error"
            }
          },
          "401": {
            "description": "The request is missing a valid bearer token, or the token is invalid or expired.",
            "schema": {
              "$ref": "#/definitions/factory.api.v1.Error"
            }
          },
          "404": {
            "description": "No quote with the given id exists for the authenticated company, or the quote has been accepted and converted to an order — manage its lines with the OrderService line endpoints, swapping the id's quote_ prefix for order_ (the 26-character suffix stays the same).",
            "schema": {
              "$ref": "#/definitions/factory.api.v1.Error"
            }
          },
          "409": {
            "description": "The idempotency key in `requestId` conflicts with an earlier use: the original request already completed (`resourceId` in the error body carries the id of the quote it updated), its outcome is still unknown, or the key was reused with a materially different body. Verify the quote's lines with GetQuote, then send a new `requestId` to add a new line.",
            "schema": {
              "$ref": "#/definitions/factory.api.v1.Error"
            }
          },
          "default": {
            "description": "Any other error. The body is the same error envelope every error uses: a short stable type identifying the kind of failure (for example \"rate_limited\" or \"internal\"), a human-readable message, and the request's idempotency key echoed back when one was supplied.",
            "schema": {
              "$ref": "#/definitions/factory.api.v1.Error"
            }
          }
        },
        "parameters": [
          {
            "name": "quoteId",
            "description": "The id of the quote to add the line item to, as returned by CreateQuote.",
            "in": "path",
            "required": true,
            "type": "string"
          },
          {
            "name": "body",
            "in": "body",
            "required": true,
            "schema": {
              "$ref": "#/definitions/factory.api.v1.quotes.QuoteService.AddQuoteLineItemBody"
            }
          }
        ],
        "tags": [
          "QuoteService"
        ]
      }
    },
    "/v1/quotes/{quoteId}/lines/{lineId}": {
      "delete": {
        "summary": "Remove a single line item from a quote.",
        "description": "Identify the line by the server-assigned id returned when you read the quote;\nthe quote's totals are recomputed after removal. A line id that does not\nbelong to the quote is rejected with HTTP 404 (not_found). A flashing line\ncannot yet be removed and is rejected with HTTP 501 (not_implemented).\n\nConcurrent writes to the same line are not detected: the last write wins.\nRe-read the quote after writing if other clients may be editing it.",
        "operationId": "QuoteService_DeleteQuoteLineItem",
        "responses": {
          "200": {
            "description": "A successful response.",
            "schema": {
              "$ref": "#/definitions/factory.api.v1.quotes.DeleteQuoteLineItemResponse"
            }
          },
          "400": {
            "description": "The request was rejected because it was malformed — for example a badly formed id. The body is a validation-failure envelope: an overall type and message and one entry per problem (each with a stable code, a message, and a param pointing at the offending parameter).",
            "schema": {
              "$ref": "#/definitions/factory.api.v1.Error"
            }
          },
          "401": {
            "description": "The request is missing a valid bearer token, or the token is invalid or expired.",
            "schema": {
              "$ref": "#/definitions/factory.api.v1.Error"
            }
          },
          "404": {
            "description": "No quote with the given id exists for the authenticated company, the quote has been accepted and converted to an order — manage its lines with the OrderService line endpoints — or the line id does not identify a line on this quote.",
            "schema": {
              "$ref": "#/definitions/factory.api.v1.Error"
            }
          },
          "default": {
            "description": "Any other error. The body is the same error envelope every error uses: a short stable type identifying the kind of failure (for example \"rate_limited\" or \"internal\"), a human-readable message, and the request's idempotency key echoed back when one was supplied.",
            "schema": {
              "$ref": "#/definitions/factory.api.v1.Error"
            }
          }
        },
        "parameters": [
          {
            "name": "quoteId",
            "description": "The id of the quote the line belongs to, as returned by CreateQuote.",
            "in": "path",
            "required": true,
            "type": "string"
          },
          {
            "name": "lineId",
            "description": "The server-assigned id of the line to remove, as returned when the quote is\nread. This has a different form from the quote id.",
            "in": "path",
            "required": true,
            "type": "string"
          },
          {
            "name": "fields",
            "description": "Comma-separated response fields to include, using camelCase JSON names\n(e.g. `orderId,total.amountMicros`). Dot paths reach into nested objects\nand map transparently across arrays. Paths are relative to the resource,\nnot the response envelope; envelope keys are always preserved. Only 2xx\nJSON responses are filtered; error bodies pass through unmodified. Unknown\nnames are silently ignored. Takes precedence over `excludeFields` when\nboth are provided.",
            "in": "query",
            "required": false,
            "type": "string"
          },
          {
            "name": "excludeFields",
            "description": "Comma-separated response fields to exclude, using camelCase JSON names.\nDot paths and array-transparency work the same as `fields`. Paths are\nrelative to the resource, not the response envelope. Only 2xx JSON\nresponses are filtered; error bodies pass through unmodified. Ignored\nwhen `fields` is also provided.",
            "in": "query",
            "required": false,
            "type": "string"
          }
        ],
        "tags": [
          "QuoteService"
        ]
      },
      "put": {
        "summary": "Replace a single line item on a quote.",
        "description": "Send the full replacement line content; the whole line is replaced and the\nquote's totals are recomputed. Identify the line by the server-assigned id\nreturned when you read the quote. A line id that does not belong to the quote\nis rejected with HTTP 404 (not_found). A flashing line cannot yet be\nreplaced and is rejected with HTTP 501 (not_implemented).\n\nConcurrent writes to the same line are not detected: the last write wins.\nRe-read the quote after writing if other clients may be editing it.",
        "operationId": "QuoteService_UpdateQuoteLineItem",
        "responses": {
          "200": {
            "description": "A successful response.",
            "schema": {
              "$ref": "#/definitions/factory.api.v1.quotes.UpdateQuoteLineItemResponse"
            }
          },
          "400": {
            "description": "The request was rejected because it failed validation. The body is a validation-failure envelope: an overall type and message and one entry per field-level problem (each with a stable code, a message, and a param pointing at the offending field).",
            "schema": {
              "$ref": "#/definitions/factory.api.v1.Error"
            }
          },
          "401": {
            "description": "The request is missing a valid bearer token, or the token is invalid or expired.",
            "schema": {
              "$ref": "#/definitions/factory.api.v1.Error"
            }
          },
          "404": {
            "description": "No quote with the given id exists for the authenticated company, the quote has been accepted and converted to an order — manage its lines with the OrderService line endpoints — or the line id does not identify a line on this quote.",
            "schema": {
              "$ref": "#/definitions/factory.api.v1.Error"
            }
          },
          "default": {
            "description": "Any other error. The body is the same error envelope every error uses: a short stable type identifying the kind of failure (for example \"rate_limited\" or \"internal\"), a human-readable message, and the request's idempotency key echoed back when one was supplied.",
            "schema": {
              "$ref": "#/definitions/factory.api.v1.Error"
            }
          }
        },
        "parameters": [
          {
            "name": "quoteId",
            "description": "The id of the quote the line belongs to, as returned by CreateQuote.",
            "in": "path",
            "required": true,
            "type": "string"
          },
          {
            "name": "lineId",
            "description": "The server-assigned id of the line to replace, as returned when the quote is\nread. This has a different form from the quote id.",
            "in": "path",
            "required": true,
            "type": "string"
          },
          {
            "name": "line",
            "description": "The replacement line content. The whole line is replaced with this — its kind\n(on-the-fly, catalogue, labour, notes, flashing, or product kit) and all its\nfields take effect as sent.",
            "in": "body",
            "required": true,
            "schema": {
              "$ref": "#/definitions/factory.api.v1.salesdocuments.SalesLine"
            }
          },
          {
            "name": "fields",
            "description": "Comma-separated response fields to include, using camelCase JSON names\n(e.g. `orderId,total.amountMicros`). Dot paths reach into nested objects\nand map transparently across arrays. Paths are relative to the resource,\nnot the response envelope; envelope keys are always preserved. Only 2xx\nJSON responses are filtered; error bodies pass through unmodified. Unknown\nnames are silently ignored. Takes precedence over `excludeFields` when\nboth are provided.",
            "in": "query",
            "required": false,
            "type": "string"
          },
          {
            "name": "excludeFields",
            "description": "Comma-separated response fields to exclude, using camelCase JSON names.\nDot paths and array-transparency work the same as `fields`. Paths are\nrelative to the resource, not the response envelope. Only 2xx JSON\nresponses are filtered; error bodies pass through unmodified. Ignored\nwhen `fields` is also provided.",
            "in": "query",
            "required": false,
            "type": "string"
          }
        ],
        "tags": [
          "QuoteService"
        ]
      }
    },
    "/v1/quotes/{quoteId}/messages": {
      "get": {
        "summary": "List a quote's conversation.",
        "description": "Returns the quote's conversation — the same thread shown in the Factory\napp's Collaborate tab — oldest message first: text written by your team,\nfiles they attached, and documents the app generated. Each message\ncarries its attachments, each with a presigned download URL. Most\nattachments are posted without any text, so expect many file-only\nmessages. Signature captures collected in the app are not returned.",
        "operationId": "QuoteService_ListMessages",
        "responses": {
          "200": {
            "description": "A successful response.",
            "schema": {
              "$ref": "#/definitions/factory.api.v1.quotes.ListMessagesResponse"
            }
          },
          "400": {
            "description": "The request was rejected because it failed validation. The body is a validation-failure envelope: an overall type and message and one entry per field-level problem (each with a stable code, a message, and a param pointing at the offending field).",
            "schema": {
              "$ref": "#/definitions/factory.api.v1.Error"
            }
          },
          "401": {
            "description": "The request is missing a valid bearer token, or the token is invalid or expired.",
            "schema": {
              "$ref": "#/definitions/factory.api.v1.Error"
            }
          },
          "404": {
            "description": "No quote with the given id exists for the authenticated company, or the quote has been accepted and converted to an order — its conversation carries over: read it with the order-side endpoint, swapping the id's quote_ prefix for order_ (the 26-character suffix stays the same).",
            "schema": {
              "$ref": "#/definitions/factory.api.v1.Error"
            }
          },
          "default": {
            "description": "Any other error. The body is the same error envelope every error uses: a short stable type identifying the kind of failure (for example \"rate_limited\" or \"internal\"), a human-readable message, and the request's idempotency key echoed back when one was supplied.",
            "schema": {
              "$ref": "#/definitions/factory.api.v1.Error"
            }
          }
        },
        "parameters": [
          {
            "name": "quoteId",
            "description": "The id of the quote whose conversation to list. Required.",
            "in": "path",
            "required": true,
            "type": "string"
          },
          {
            "name": "pageSize",
            "description": "Maximum number of messages to return per page. 0 uses the server\ndefault; the server may cap the value.",
            "in": "query",
            "required": false,
            "type": "integer",
            "format": "int32"
          },
          {
            "name": "pageToken",
            "description": "Opaque page token from a previous response, used to fetch the next page.",
            "in": "query",
            "required": false,
            "type": "string"
          },
          {
            "name": "fields",
            "description": "Comma-separated response fields to include, using camelCase JSON names\n(e.g. `orderId,total.amountMicros`). Dot paths reach into nested objects\nand map transparently across arrays. Paths are relative to the resource,\nnot the response envelope; envelope keys like `nextPageToken` are always\npreserved. Only 2xx JSON responses are filtered; error bodies pass through\nunmodified. Unknown names are silently ignored. Takes precedence over\n`excludeFields` when both are provided.",
            "in": "query",
            "required": false,
            "type": "string"
          },
          {
            "name": "excludeFields",
            "description": "Comma-separated response fields to exclude, using camelCase JSON names.\nDot paths and array-transparency work the same as `fields`. Paths are\nrelative to the resource, not the response envelope. Only 2xx JSON\nresponses are filtered; error bodies pass through unmodified. Ignored\nwhen `fields` is also provided.",
            "in": "query",
            "required": false,
            "type": "string"
          }
        ],
        "tags": [
          "QuoteService"
        ]
      },
      "post": {
        "summary": "Post a text message to a quote's conversation.",
        "description": "Adds a plain-text message to the quote's conversation, visible\nimmediately in the Factory app's Collaborate tab. The message is stored\nas an HTML fragment — the posted text HTML-escaped and `\u003cp\u003e`-wrapped —\nand reads back in that form. There is no way to @-mention a user through\nthis API: because text is escaped, mention markup renders as literal\ntext and notifies no one. PostMessage sends text on its own; to send\na file with text, use the caption on UploadAttachment.",
        "operationId": "QuoteService_PostMessage",
        "responses": {
          "200": {
            "description": "A successful response.",
            "schema": {
              "$ref": "#/definitions/factory.api.v1.quotes.PostMessageResponse"
            }
          },
          "400": {
            "description": "The request was rejected because it failed validation. The body is a validation-failure envelope: an overall type and message, the echoed `requestId`, and one entry per field-level problem (each with a stable code, a message, and a param pointing at the offending field).",
            "schema": {
              "$ref": "#/definitions/factory.api.v1.Error"
            }
          },
          "401": {
            "description": "The request is missing a valid bearer token, or the token is invalid or expired.",
            "schema": {
              "$ref": "#/definitions/factory.api.v1.Error"
            }
          },
          "404": {
            "description": "No quote with the given id exists for the authenticated company, or the quote has been accepted and converted to an order — its conversation carries over: post to the order-side endpoint, swapping the id's quote_ prefix for order_ (the 26-character suffix stays the same).",
            "schema": {
              "$ref": "#/definitions/factory.api.v1.Error"
            }
          },
          "409": {
            "description": "The idempotency key in `requestId` conflicts with an earlier use: the original request already completed (`resourceId` in the error body carries the id of the message it posted), its outcome is still unknown, or the key was reused with a materially different body. Verify the conversation with ListMessages, then send a new `requestId` to post a new message.",
            "schema": {
              "$ref": "#/definitions/factory.api.v1.Error"
            }
          },
          "default": {
            "description": "Any other error. The body is the same error envelope every error uses: a short stable type identifying the kind of failure (for example \"rate_limited\" or \"internal\"), a human-readable message, and the request's idempotency key echoed back when one was supplied.",
            "schema": {
              "$ref": "#/definitions/factory.api.v1.Error"
            }
          }
        },
        "parameters": [
          {
            "name": "quoteId",
            "description": "The id of the quote to post to. Required.",
            "in": "path",
            "required": true,
            "type": "string"
          },
          {
            "name": "body",
            "in": "body",
            "required": true,
            "schema": {
              "$ref": "#/definitions/factory.api.v1.quotes.QuoteService.PostMessageBody"
            }
          }
        ],
        "tags": [
          "QuoteService"
        ]
      }
    },
    "/v1/quotes:query": {
      "get": {
        "summary": "Query the company's quotes as a change feed, for incremental sync.",
        "description": "Like ListQuotes, but ordered by when each quote last changed (oldest first)\nand filterable by an `updatedSince` / `updatedBefore` time window — so a\npoller fetches only what changed since its last run and checkpoints on the\nmost recent quote it saw. Use ListQuotes for simple lookups; use this to\nkeep an external system (CRM, reporting) in sync. Paginated via `pageSize` /\n`pageToken`.\n\nQuotes have no workflow or payment status. Available filters: `customerId`,\nreference (substring), `isSubmitted` (draft/sent), `labelIds`,\n`fulfilmentMethod`, `createdAfter`/`createdBefore` (creation window),\n`requiredAfter`/`requiredBefore` (required-by window), and `includeArchived`.\n\nOne migration to expect while syncing: a quote accepted in the Factory app\nbecomes an order. It leaves this feed without a tombstone entry — there is\nno final \"deleted\" record — and reappears in QueryOrders as an order id.\nThe two ids share their 26-character suffix and differ only in prefix: a\nquote \"quote_\u003csuffix\u003e\" becomes the order \"order_\u003csuffix\u003e\". Reconcile a\nquote that stops updating here by swapping the prefix and checking the\norder feed for that id.",
        "operationId": "QuoteService_QueryQuotes",
        "responses": {
          "200": {
            "description": "A successful response.",
            "schema": {
              "$ref": "#/definitions/factory.api.v1.quotes.QueryQuotesResponse"
            }
          },
          "400": {
            "description": "The request was rejected because it was malformed — for example an unusable page token or an invalid filter value. The body is a validation-failure envelope: an overall type and message and one entry per problem (each with a stable code, a message, and a param pointing at the offending parameter).",
            "schema": {
              "$ref": "#/definitions/factory.api.v1.Error"
            }
          },
          "401": {
            "description": "The request is missing a valid bearer token, or the token is invalid or expired.",
            "schema": {
              "$ref": "#/definitions/factory.api.v1.Error"
            }
          },
          "default": {
            "description": "Any other error. The body is the same error envelope every error uses: a short stable type identifying the kind of failure (for example \"rate_limited\" or \"internal\"), a human-readable message, and the request's idempotency key echoed back when one was supplied.",
            "schema": {
              "$ref": "#/definitions/factory.api.v1.Error"
            }
          }
        },
        "parameters": [
          {
            "name": "updatedSince",
            "description": "Return only quotes updated at or after this time. Pass your last sync time\nhere to fetch just what changed since then; omit to start from the\nbeginning. This is the primary filter for incremental sync.",
            "in": "query",
            "required": false,
            "type": "string",
            "format": "date-time"
          },
          {
            "name": "updatedBefore",
            "description": "Optional upper bound: return only quotes updated strictly before this time.\nCombine with `updatedSince` to page through a bounded window; omit for \"up to\nnow\".",
            "in": "query",
            "required": false,
            "type": "string",
            "format": "date-time"
          },
          {
            "name": "pageSize",
            "description": "Maximum number of quotes to return per page. 0 uses the server default; the\nserver may cap the value.",
            "in": "query",
            "required": false,
            "type": "integer",
            "format": "int32"
          },
          {
            "name": "pageToken",
            "description": "Opaque page token from a previous response, used to fetch the next page.",
            "in": "query",
            "required": false,
            "type": "string"
          },
          {
            "name": "customerId",
            "description": "Optional: only return quotes for this customer id.",
            "in": "query",
            "required": false,
            "type": "string"
          },
          {
            "name": "reference",
            "description": "Optional: only return quotes whose external reference contains this text,\ncompared case-insensitively (a substring match, not exact-equals).",
            "in": "query",
            "required": false,
            "type": "string"
          },
          {
            "name": "isSubmitted",
            "description": "Optional: filter by whether the quote has been sent. Leave unset to return\nall quotes; set to false to return only drafts; set to true to return only\nsent quotes.",
            "in": "query",
            "required": false,
            "type": "boolean"
          },
          {
            "name": "createdAfter",
            "description": "Only quotes created at or after this time (inclusive). Optional; filters on\nthe quote's creation time, independent of the updated window — combine the\ntwo windows to sync recent changes for quotes created in a period. Note:\nthis parameter is named `createdAfter`, not `createdSince` — it is a range\nfilter, not a sync checkpoint.",
            "in": "query",
            "required": false,
            "type": "string",
            "format": "date-time"
          },
          {
            "name": "createdBefore",
            "description": "Only quotes created strictly before this time (exclusive). Optional;\ncombine with `createdAfter` to bound a creation window.",
            "in": "query",
            "required": false,
            "type": "string",
            "format": "date-time"
          },
          {
            "name": "fulfilmentMethod",
            "description": "Optional: only return quotes with this fulfilment method. Leave unset to\ninclude all.\n\n - FULFILMENT_METHOD_UNSPECIFIED: Default, unset value.\n - FULFILMENT_METHOD_PICKUP: The customer collects the goods themselves.\n - FULFILMENT_METHOD_DELIVERY: The goods are delivered to the delivery address.\n - FULFILMENT_METHOD_INSTALL: The goods are installed at the install address.",
            "in": "query",
            "required": false,
            "type": "string",
            "enum": [
              "FULFILMENT_METHOD_UNSPECIFIED",
              "FULFILMENT_METHOD_PICKUP",
              "FULFILMENT_METHOD_DELIVERY",
              "FULFILMENT_METHOD_INSTALL"
            ],
            "default": "FULFILMENT_METHOD_UNSPECIFIED"
          },
          {
            "name": "requiredAfter",
            "description": "Only quotes required at or after this time. Optional; filters on the\nquote's required-by date, independent of the other filters.",
            "in": "query",
            "required": false,
            "type": "string",
            "format": "date-time"
          },
          {
            "name": "requiredBefore",
            "description": "Only quotes required strictly before this time. Optional; combine with\n`requiredAfter` to bound a required-by window.",
            "in": "query",
            "required": false,
            "type": "string",
            "format": "date-time"
          },
          {
            "name": "fields",
            "description": "Comma-separated response fields to include, using camelCase JSON names\n(e.g. `orderId,total.amountMicros`). Dot paths reach into nested objects\nand map transparently across arrays. Paths are relative to the resource,\nnot the response envelope; envelope keys like `nextPageToken` are always\npreserved. Only 2xx JSON responses are filtered; error bodies pass through\nunmodified. Unknown names are silently ignored. Takes precedence over\n`excludeFields` when both are provided.",
            "in": "query",
            "required": false,
            "type": "string"
          },
          {
            "name": "excludeFields",
            "description": "Comma-separated response fields to exclude, using camelCase JSON names.\nDot paths and array-transparency work the same as `fields`. Paths are\nrelative to the resource, not the response envelope. Only 2xx JSON\nresponses are filtered; error bodies pass through unmodified. Ignored\nwhen `fields` is also provided.",
            "in": "query",
            "required": false,
            "type": "string"
          }
        ],
        "tags": [
          "QuoteService"
        ]
      }
    },
    "/v1/suppliers": {
      "get": {
        "summary": "List suppliers in your company.",
        "description": "Returns a paginated list of your company's suppliers. Useful for\ndiscovering supplier ids to pass as filters on the inventory list.\nSuppliers are ordered alphabetically by name.",
        "operationId": "SupplierService_ListSuppliers",
        "responses": {
          "200": {
            "description": "A successful response.",
            "schema": {
              "$ref": "#/definitions/factory.api.v1.suppliers.ListSuppliersResponse"
            }
          },
          "400": {
            "description": "The request was rejected because it was malformed — for example an unusable page token or an invalid filter value. The body is a validation-failure envelope: an overall type and message and one entry per problem (each with a stable code, a message, and a param pointing at the offending parameter).",
            "schema": {
              "$ref": "#/definitions/factory.api.v1.Error"
            }
          },
          "401": {
            "description": "The request is missing a valid bearer token, or the token is invalid or expired.",
            "schema": {
              "$ref": "#/definitions/factory.api.v1.Error"
            }
          },
          "default": {
            "description": "Any other error. The body is the same error envelope every error uses: a short stable type identifying the kind of failure (for example \"rate_limited\" or \"internal\"), a human-readable message, and the request's idempotency key echoed back when one was supplied.",
            "schema": {
              "$ref": "#/definitions/factory.api.v1.Error"
            }
          }
        },
        "parameters": [
          {
            "name": "pageSize",
            "description": "Maximum number of suppliers to return per page. 0 uses the server\ndefault; the server may cap the value.",
            "in": "query",
            "required": false,
            "type": "integer",
            "format": "int32"
          },
          {
            "name": "pageToken",
            "description": "Opaque page token from a previous response, used to fetch the next page.",
            "in": "query",
            "required": false,
            "type": "string"
          },
          {
            "name": "name",
            "description": "Optional: only return suppliers whose name contains this text\n(case-insensitive substring match).",
            "in": "query",
            "required": false,
            "type": "string"
          },
          {
            "name": "fields",
            "description": "Comma-separated response fields to include, using camelCase JSON names\n(e.g. `orderId,total.amountMicros`). Dot paths reach into nested objects\nand map transparently across arrays. Paths are relative to the resource,\nnot the response envelope; envelope keys like `nextPageToken` are always\npreserved. Only 2xx JSON responses are filtered; error bodies pass through\nunmodified. Unknown names are silently ignored. Takes precedence over\n`excludeFields` when both are provided.",
            "in": "query",
            "required": false,
            "type": "string"
          },
          {
            "name": "excludeFields",
            "description": "Comma-separated response fields to exclude, using camelCase JSON names.\nDot paths and array-transparency work the same as `fields`. Paths are\nrelative to the resource, not the response envelope. Only 2xx JSON\nresponses are filtered; error bodies pass through unmodified. Ignored\nwhen `fields` is also provided.",
            "in": "query",
            "required": false,
            "type": "string"
          }
        ],
        "tags": [
          "SupplierService"
        ]
      }
    },
    "/v1/suppliers/{supplierId}": {
      "get": {
        "summary": "Retrieve a single supplier by id.",
        "description": "Returns the supplier matching the given id. Useful when you already\nhave a supplier id (for example from an inventory filter) and need\nits details without listing.",
        "operationId": "SupplierService_GetSupplier",
        "responses": {
          "200": {
            "description": "A successful response.",
            "schema": {
              "$ref": "#/definitions/factory.api.v1.suppliers.GetSupplierResponse"
            }
          },
          "400": {
            "description": "The request was rejected because it was malformed — for example a badly formed id. The body is a validation-failure envelope: an overall type and message and one entry per problem (each with a stable code, a message, and a param pointing at the offending parameter).",
            "schema": {
              "$ref": "#/definitions/factory.api.v1.Error"
            }
          },
          "401": {
            "description": "The request is missing a valid bearer token, or the token is invalid or expired.",
            "schema": {
              "$ref": "#/definitions/factory.api.v1.Error"
            }
          },
          "404": {
            "description": "No supplier with the given id exists for the authenticated company.",
            "schema": {
              "$ref": "#/definitions/factory.api.v1.Error"
            }
          },
          "default": {
            "description": "Any other error. The body is the same error envelope every error uses: a short stable type identifying the kind of failure (for example \"rate_limited\" or \"internal\"), a human-readable message, and the request's idempotency key echoed back when one was supplied.",
            "schema": {
              "$ref": "#/definitions/factory.api.v1.Error"
            }
          }
        },
        "parameters": [
          {
            "name": "supplierId",
            "description": "The id of the supplier to retrieve. Required.",
            "in": "path",
            "required": true,
            "type": "string"
          }
        ],
        "tags": [
          "SupplierService"
        ]
      }
    },
    "/v1/users": {
      "get": {
        "summary": "List the users in your company.",
        "description": "A company-scoped list of users, returning active users by default. Use it\nto find a user to assign to a labour line on an order. Users are returned in\na stable order, sorted by username (the login name). User emails are\npopulated only when the caller is a company administrator; other callers\nreceive users without email addresses.",
        "operationId": "UserService_ListUsers",
        "responses": {
          "200": {
            "description": "A successful response.",
            "schema": {
              "$ref": "#/definitions/factory.api.v1.users.ListUsersResponse"
            }
          },
          "400": {
            "description": "The request was rejected because it was malformed — for example an unusable page token or an invalid filter value. The body is a validation-failure envelope: an overall type and message and one entry per problem (each with a stable code, a message, and a param pointing at the offending parameter).",
            "schema": {
              "$ref": "#/definitions/factory.api.v1.Error"
            }
          },
          "401": {
            "description": "The request is missing a valid bearer token, or the token is invalid or expired.",
            "schema": {
              "$ref": "#/definitions/factory.api.v1.Error"
            }
          },
          "default": {
            "description": "Any other error. The body is the same error envelope every error uses: a short stable type identifying the kind of failure (for example \"rate_limited\" or \"internal\"), a human-readable message, and the request's idempotency key echoed back when one was supplied.",
            "schema": {
              "$ref": "#/definitions/factory.api.v1.Error"
            }
          }
        },
        "parameters": [
          {
            "name": "pageSize",
            "description": "Maximum number of users to return per page. 0 uses the server default; the\nserver may cap the value.",
            "in": "query",
            "required": false,
            "type": "integer",
            "format": "int32"
          },
          {
            "name": "pageToken",
            "description": "Opaque page token from a previous response, used to fetch the next page.",
            "in": "query",
            "required": false,
            "type": "string"
          },
          {
            "name": "activeOnly",
            "description": "Whether to return only active, non-deleted users. When omitted, defaults\nto true; set it explicitly to false to include inactive users.",
            "in": "query",
            "required": false,
            "type": "boolean"
          },
          {
            "name": "fields",
            "description": "Comma-separated response fields to include, using camelCase JSON names\n(e.g. `orderId,total.amountMicros`). Dot paths reach into nested objects\nand map transparently across arrays. Paths are relative to the resource,\nnot the response envelope; envelope keys like `nextPageToken` are always\npreserved. Only 2xx JSON responses are filtered; error bodies pass through\nunmodified. Unknown names are silently ignored. Takes precedence over\n`excludeFields` when both are provided.",
            "in": "query",
            "required": false,
            "type": "string"
          },
          {
            "name": "excludeFields",
            "description": "Comma-separated response fields to exclude, using camelCase JSON names.\nDot paths and array-transparency work the same as `fields`. Paths are\nrelative to the resource, not the response envelope. Only 2xx JSON\nresponses are filtered; error bodies pass through unmodified. Ignored\nwhen `fields` is also provided.",
            "in": "query",
            "required": false,
            "type": "string"
          }
        ],
        "tags": [
          "UserService"
        ]
      }
    },
    "/v1/users/{userId}": {
      "get": {
        "summary": "Retrieve a single company user by id.",
        "description": "Returns the user record — name, email, and active status — for the given\nid, scoped to your company.",
        "operationId": "UserService_GetUser",
        "responses": {
          "200": {
            "description": "A successful response.",
            "schema": {
              "$ref": "#/definitions/factory.api.v1.users.GetUserResponse"
            }
          },
          "400": {
            "description": "The request was rejected because it was malformed — for example a badly formed id. The body is a validation-failure envelope: an overall type and message and one entry per problem (each with a stable code, a message, and a param pointing at the offending parameter).",
            "schema": {
              "$ref": "#/definitions/factory.api.v1.Error"
            }
          },
          "401": {
            "description": "The request is missing a valid bearer token, or the token is invalid or expired.",
            "schema": {
              "$ref": "#/definitions/factory.api.v1.Error"
            }
          },
          "403": {
            "description": "The authenticated user is not a company administrator. Retrieving a user by id is an administrator-only operation.",
            "schema": {
              "$ref": "#/definitions/factory.api.v1.Error"
            }
          },
          "404": {
            "description": "No user with the given id exists in the authenticated company.",
            "schema": {
              "$ref": "#/definitions/factory.api.v1.Error"
            }
          },
          "default": {
            "description": "Any other error. The body is the same error envelope every error uses: a short stable type identifying the kind of failure (for example \"rate_limited\" or \"internal\"), a human-readable message, and the request's idempotency key echoed back when one was supplied.",
            "schema": {
              "$ref": "#/definitions/factory.api.v1.Error"
            }
          }
        },
        "parameters": [
          {
            "name": "userId",
            "description": "The id of the user to retrieve. Required.",
            "in": "path",
            "required": true,
            "type": "string"
          },
          {
            "name": "fields",
            "description": "Comma-separated response fields to include, using camelCase JSON names\n(e.g. `orderId,total.amountMicros`). Dot paths reach into nested objects\nand map transparently across arrays. Paths are relative to the resource,\nnot the response envelope; envelope keys are always preserved. Only 2xx\nJSON responses are filtered; error bodies pass through unmodified. Unknown\nnames are silently ignored. Takes precedence over `excludeFields` when\nboth are provided.",
            "in": "query",
            "required": false,
            "type": "string"
          },
          {
            "name": "excludeFields",
            "description": "Comma-separated response fields to exclude, using camelCase JSON names.\nDot paths and array-transparency work the same as `fields`. Paths are\nrelative to the resource, not the response envelope. Only 2xx JSON\nresponses are filtered; error bodies pass through unmodified. Ignored\nwhen `fields` is also provided.",
            "in": "query",
            "required": false,
            "type": "string"
          }
        ],
        "tags": [
          "UserService"
        ]
      }
    },
    "/v1/webhook-deliveries": {
      "get": {
        "summary": "List delivery records, newest first.",
        "description": "Records cover the last 30 days. Filter by subscription, state, or a\ncreation-time window; combine filters freely.",
        "operationId": "WebhookService_ListWebhookDeliveries",
        "responses": {
          "200": {
            "description": "A successful response.",
            "schema": {
              "$ref": "#/definitions/factory.api.v1.webhooks.ListWebhookDeliveriesResponse"
            }
          },
          "400": {
            "description": "The request was rejected because it was malformed — for example an unusable page token or a filter id with the wrong prefix. The body is a validation-failure envelope: an overall type and message and one entry per problem (each with a stable code, a message, and a param pointing at the offending parameter).",
            "schema": {
              "$ref": "#/definitions/factory.api.v1.Error"
            }
          },
          "401": {
            "description": "The request is missing a valid bearer token, or the token is invalid or expired.",
            "schema": {
              "$ref": "#/definitions/factory.api.v1.Error"
            }
          },
          "default": {
            "description": "Any other error. The body is the same error envelope every error uses: a short stable type identifying the kind of failure, a human-readable message, and the request's idempotency key echoed back when one was supplied.",
            "schema": {
              "$ref": "#/definitions/factory.api.v1.Error"
            }
          }
        },
        "parameters": [
          {
            "name": "webhookId",
            "description": "Only deliveries for this subscription. Optional.",
            "in": "query",
            "required": false,
            "type": "string"
          },
          {
            "name": "state",
            "description": "Only deliveries in this state. Optional.\n\n - DELIVERY_STATE_PENDING: Waiting for its first or next attempt (see `nextAttemptAt`).\n - DELIVERY_STATE_SUCCEEDED: The endpoint acknowledged the delivery with a 2xx response.\n - DELIVERY_STATE_EXHAUSTED: All retry attempts were used without a 2xx (about 10.6 hours across 6\nattempts). Exhausted deliveries are not retried again.",
            "in": "query",
            "required": false,
            "type": "string",
            "enum": [
              "DELIVERY_STATE_UNSPECIFIED",
              "DELIVERY_STATE_PENDING",
              "DELIVERY_STATE_SUCCEEDED",
              "DELIVERY_STATE_EXHAUSTED"
            ],
            "default": "DELIVERY_STATE_UNSPECIFIED"
          },
          {
            "name": "createdAfter",
            "description": "Only deliveries created at or after this instant. Optional.",
            "in": "query",
            "required": false,
            "type": "string",
            "format": "date-time"
          },
          {
            "name": "createdBefore",
            "description": "Only deliveries created before this instant. Optional.",
            "in": "query",
            "required": false,
            "type": "string",
            "format": "date-time"
          },
          {
            "name": "pageSize",
            "description": "The maximum number of records to return in one page. Optional; a server\ndefault applies when unset.",
            "in": "query",
            "required": false,
            "type": "integer",
            "format": "int32"
          },
          {
            "name": "pageToken",
            "description": "Opaque page token from a previous response, used to fetch the next page.",
            "in": "query",
            "required": false,
            "type": "string"
          },
          {
            "name": "fields",
            "description": "Comma-separated response fields to include, using camelCase JSON names\n(e.g. `orderId,total.amountMicros`). Dot paths reach into nested objects\nand map transparently across arrays. Paths are relative to the resource,\nnot the response envelope; envelope keys like `nextPageToken` are always\npreserved. Only 2xx JSON responses are filtered; error bodies pass through\nunmodified. Unknown names are silently ignored. Takes precedence over\n`excludeFields` when both are provided.",
            "in": "query",
            "required": false,
            "type": "string"
          },
          {
            "name": "excludeFields",
            "description": "Comma-separated response fields to exclude, using camelCase JSON names.\nDot paths and array-transparency work the same as `fields`. Paths are\nrelative to the resource, not the response envelope. Only 2xx JSON\nresponses are filtered; error bodies pass through unmodified. Ignored\nwhen `fields` is also provided.",
            "in": "query",
            "required": false,
            "type": "string"
          }
        ],
        "tags": [
          "WebhookService"
        ]
      }
    },
    "/v1/webhook-deliveries/{deliveryId}": {
      "get": {
        "summary": "Fetch one delivery record.",
        "operationId": "WebhookService_GetWebhookDelivery",
        "responses": {
          "200": {
            "description": "A successful response.",
            "schema": {
              "$ref": "#/definitions/factory.api.v1.webhooks.GetWebhookDeliveryResponse"
            }
          },
          "400": {
            "description": "The request was rejected because it was malformed — for example a delivery id that is not a whdel_ id. The body is a validation-failure envelope: an overall type and message and one entry per problem (each with a stable code, a message, and a param pointing at the offending parameter).",
            "schema": {
              "$ref": "#/definitions/factory.api.v1.Error"
            }
          },
          "401": {
            "description": "The request is missing a valid bearer token, or the token is invalid or expired.",
            "schema": {
              "$ref": "#/definitions/factory.api.v1.Error"
            }
          },
          "404": {
            "description": "No delivery record with the given id exists for the authenticated company — including records past their 30-day retention.",
            "schema": {
              "$ref": "#/definitions/factory.api.v1.Error"
            }
          },
          "default": {
            "description": "Any other error. The body is the same error envelope every error uses: a short stable type identifying the kind of failure, a human-readable message, and the request's idempotency key echoed back when one was supplied.",
            "schema": {
              "$ref": "#/definitions/factory.api.v1.Error"
            }
          }
        },
        "parameters": [
          {
            "name": "deliveryId",
            "description": "The delivery record to fetch.",
            "in": "path",
            "required": true,
            "type": "string"
          },
          {
            "name": "fields",
            "description": "Comma-separated response fields to include, using camelCase JSON names\n(e.g. `orderId,total.amountMicros`). Dot paths reach into nested objects\nand map transparently across arrays. Paths are relative to the resource,\nnot the response envelope; envelope keys like `nextPageToken` are always\npreserved. Only 2xx JSON responses are filtered; error bodies pass through\nunmodified. Unknown names are silently ignored. Takes precedence over\n`excludeFields` when both are provided.",
            "in": "query",
            "required": false,
            "type": "string"
          },
          {
            "name": "excludeFields",
            "description": "Comma-separated response fields to exclude, using camelCase JSON names.\nDot paths and array-transparency work the same as `fields`. Paths are\nrelative to the resource, not the response envelope. Only 2xx JSON\nresponses are filtered; error bodies pass through unmodified. Ignored\nwhen `fields` is also provided.",
            "in": "query",
            "required": false,
            "type": "string"
          }
        ],
        "tags": [
          "WebhookService"
        ]
      }
    },
    "/v1/webhooks": {
      "get": {
        "summary": "List your company's webhook subscriptions.",
        "description": "Sorted by creation time, newest first. Secrets are never included.",
        "operationId": "WebhookService_ListWebhooks",
        "responses": {
          "200": {
            "description": "A successful response.",
            "schema": {
              "$ref": "#/definitions/factory.api.v1.webhooks.ListWebhooksResponse"
            }
          },
          "400": {
            "description": "The request was rejected because it was malformed — for example an unusable page token. The body is a validation-failure envelope: an overall type and message and one entry per problem (each with a stable code, a message, and a param pointing at the offending parameter).",
            "schema": {
              "$ref": "#/definitions/factory.api.v1.Error"
            }
          },
          "401": {
            "description": "The request is missing a valid bearer token, or the token is invalid or expired.",
            "schema": {
              "$ref": "#/definitions/factory.api.v1.Error"
            }
          },
          "default": {
            "description": "Any other error. The body is the same error envelope every error uses: a short stable type identifying the kind of failure, a human-readable message, and the request's idempotency key echoed back when one was supplied.",
            "schema": {
              "$ref": "#/definitions/factory.api.v1.Error"
            }
          }
        },
        "parameters": [
          {
            "name": "pageSize",
            "description": "The maximum number of subscriptions to return in one page. Optional; a\nserver default applies when unset.",
            "in": "query",
            "required": false,
            "type": "integer",
            "format": "int32"
          },
          {
            "name": "pageToken",
            "description": "Opaque page token from a previous response, used to fetch the next page.",
            "in": "query",
            "required": false,
            "type": "string"
          },
          {
            "name": "fields",
            "description": "Comma-separated response fields to include, using camelCase JSON names\n(e.g. `orderId,total.amountMicros`). Dot paths reach into nested objects\nand map transparently across arrays. Paths are relative to the resource,\nnot the response envelope; envelope keys like `nextPageToken` are always\npreserved. Only 2xx JSON responses are filtered; error bodies pass through\nunmodified. Unknown names are silently ignored. Takes precedence over\n`excludeFields` when both are provided.",
            "in": "query",
            "required": false,
            "type": "string"
          },
          {
            "name": "excludeFields",
            "description": "Comma-separated response fields to exclude, using camelCase JSON names.\nDot paths and array-transparency work the same as `fields`. Paths are\nrelative to the resource, not the response envelope. Only 2xx JSON\nresponses are filtered; error bodies pass through unmodified. Ignored\nwhen `fields` is also provided.",
            "in": "query",
            "required": false,
            "type": "string"
          }
        ],
        "tags": [
          "WebhookService"
        ]
      },
      "post": {
        "summary": "Register a webhook subscription.",
        "description": "Creates a subscription delivering the requested event types to an HTTPS\nendpoint. The response is the only place the signing secret ever\nappears — store it before returning; it cannot be retrieved again.",
        "operationId": "WebhookService_CreateWebhook",
        "responses": {
          "200": {
            "description": "A successful response.",
            "schema": {
              "$ref": "#/definitions/factory.api.v1.webhooks.CreateWebhookResponse"
            }
          },
          "400": {
            "description": "The request was rejected because it was malformed — for example a non-HTTPS url, an endpoint resolving to private address space, an unknown event type (the message names it), or no event types at all. The body is a validation-failure envelope: an overall type and message and one entry per problem (each with a stable code, a message, and a param pointing at the offending parameter).",
            "schema": {
              "$ref": "#/definitions/factory.api.v1.Error"
            }
          },
          "401": {
            "description": "The request is missing a valid bearer token, or the token is invalid or expired.",
            "schema": {
              "$ref": "#/definitions/factory.api.v1.Error"
            }
          },
          "default": {
            "description": "Any other error. The body is the same error envelope every error uses: a short stable type identifying the kind of failure (for example \"rate_limited\" or \"internal\"), a human-readable message, and the request's idempotency key echoed back when one was supplied.",
            "schema": {
              "$ref": "#/definitions/factory.api.v1.Error"
            }
          }
        },
        "parameters": [
          {
            "name": "body",
            "description": "Input for registering a webhook subscription.",
            "in": "body",
            "required": true,
            "schema": {
              "$ref": "#/definitions/factory.api.v1.webhooks.CreateWebhookRequest"
            }
          }
        ],
        "tags": [
          "WebhookService"
        ]
      }
    },
    "/v1/webhooks/{webhookId}": {
      "get": {
        "summary": "Fetch one webhook subscription.",
        "description": "The secret is never included — it is returned only by CreateWebhook.",
        "operationId": "WebhookService_GetWebhook",
        "responses": {
          "200": {
            "description": "A successful response.",
            "schema": {
              "$ref": "#/definitions/factory.api.v1.webhooks.GetWebhookResponse"
            }
          },
          "400": {
            "description": "The request was rejected because it was malformed — for example a webhook id that is not a whsub_ id. The body is a validation-failure envelope: an overall type and message and one entry per problem (each with a stable code, a message, and a param pointing at the offending parameter).",
            "schema": {
              "$ref": "#/definitions/factory.api.v1.Error"
            }
          },
          "401": {
            "description": "The request is missing a valid bearer token, or the token is invalid or expired.",
            "schema": {
              "$ref": "#/definitions/factory.api.v1.Error"
            }
          },
          "404": {
            "description": "No webhook subscription with the given id exists for the authenticated company.",
            "schema": {
              "$ref": "#/definitions/factory.api.v1.Error"
            }
          },
          "default": {
            "description": "Any other error. The body is the same error envelope every error uses: a short stable type identifying the kind of failure, a human-readable message, and the request's idempotency key echoed back when one was supplied.",
            "schema": {
              "$ref": "#/definitions/factory.api.v1.Error"
            }
          }
        },
        "parameters": [
          {
            "name": "webhookId",
            "description": "The subscription to fetch.",
            "in": "path",
            "required": true,
            "type": "string"
          },
          {
            "name": "fields",
            "description": "Comma-separated response fields to include, using camelCase JSON names\n(e.g. `orderId,total.amountMicros`). Dot paths reach into nested objects\nand map transparently across arrays. Paths are relative to the resource,\nnot the response envelope; envelope keys like `nextPageToken` are always\npreserved. Only 2xx JSON responses are filtered; error bodies pass through\nunmodified. Unknown names are silently ignored. Takes precedence over\n`excludeFields` when both are provided.",
            "in": "query",
            "required": false,
            "type": "string"
          },
          {
            "name": "excludeFields",
            "description": "Comma-separated response fields to exclude, using camelCase JSON names.\nDot paths and array-transparency work the same as `fields`. Paths are\nrelative to the resource, not the response envelope. Only 2xx JSON\nresponses are filtered; error bodies pass through unmodified. Ignored\nwhen `fields` is also provided.",
            "in": "query",
            "required": false,
            "type": "string"
          }
        ],
        "tags": [
          "WebhookService"
        ]
      },
      "delete": {
        "summary": "Delete a webhook subscription.",
        "description": "Deliveries stop immediately. Existing delivery records remain readable\nuntil their 30-day retention lapses.",
        "operationId": "WebhookService_DeleteWebhook",
        "responses": {
          "200": {
            "description": "A successful response.",
            "schema": {
              "$ref": "#/definitions/factory.api.v1.webhooks.DeleteWebhookResponse"
            }
          },
          "400": {
            "description": "The request was rejected because it was malformed — for example a webhook id that is not a whsub_ id. The body is a validation-failure envelope: an overall type and message and one entry per problem (each with a stable code, a message, and a param pointing at the offending parameter).",
            "schema": {
              "$ref": "#/definitions/factory.api.v1.Error"
            }
          },
          "401": {
            "description": "The request is missing a valid bearer token, or the token is invalid or expired.",
            "schema": {
              "$ref": "#/definitions/factory.api.v1.Error"
            }
          },
          "404": {
            "description": "No webhook subscription with the given id exists for the authenticated company.",
            "schema": {
              "$ref": "#/definitions/factory.api.v1.Error"
            }
          },
          "default": {
            "description": "Any other error. The body is the same error envelope every error uses: a short stable type identifying the kind of failure, a human-readable message, and the request's idempotency key echoed back when one was supplied.",
            "schema": {
              "$ref": "#/definitions/factory.api.v1.Error"
            }
          }
        },
        "parameters": [
          {
            "name": "webhookId",
            "description": "The subscription to delete.",
            "in": "path",
            "required": true,
            "type": "string"
          }
        ],
        "tags": [
          "WebhookService"
        ]
      },
      "patch": {
        "summary": "Change a webhook subscription.",
        "description": "Fields left unset keep their current value. The secret cannot be\nchanged (rotation is not available in v1); to stop deliveries set\nstatus to disabled, to retire an endpoint delete the subscription and\ncreate a new one.",
        "operationId": "WebhookService_UpdateWebhook",
        "responses": {
          "200": {
            "description": "A successful response.",
            "schema": {
              "$ref": "#/definitions/factory.api.v1.webhooks.UpdateWebhookResponse"
            }
          },
          "400": {
            "description": "The request was rejected because it was malformed — for example a non-HTTPS url, an endpoint resolving to private address space, an unknown event type (the message names it), or an empty event-type list. The body is a validation-failure envelope: an overall type and message and one entry per problem (each with a stable code, a message, and a param pointing at the offending parameter).",
            "schema": {
              "$ref": "#/definitions/factory.api.v1.Error"
            }
          },
          "401": {
            "description": "The request is missing a valid bearer token, or the token is invalid or expired.",
            "schema": {
              "$ref": "#/definitions/factory.api.v1.Error"
            }
          },
          "404": {
            "description": "No webhook subscription with the given id exists for the authenticated company.",
            "schema": {
              "$ref": "#/definitions/factory.api.v1.Error"
            }
          },
          "default": {
            "description": "Any other error. The body is the same error envelope every error uses: a short stable type identifying the kind of failure, a human-readable message, and the request's idempotency key echoed back when one was supplied.",
            "schema": {
              "$ref": "#/definitions/factory.api.v1.Error"
            }
          }
        },
        "parameters": [
          {
            "name": "webhookId",
            "description": "The subscription to change.",
            "in": "path",
            "required": true,
            "type": "string"
          },
          {
            "name": "body",
            "in": "body",
            "required": true,
            "schema": {
              "$ref": "#/definitions/factory.api.v1.webhooks.WebhookService.UpdateWebhookBody"
            }
          }
        ],
        "tags": [
          "WebhookService"
        ]
      }
    },
    "/v1/webhooks/{webhookId}:test": {
      "post": {
        "summary": "Send a signed test event to a subscription's endpoint, synchronously.",
        "description": "POSTs a synthetic `ping` event through the real signing and delivery\npath and reports what the endpoint answered. The ping is recorded as a\n`webhook.test` delivery (visible in the delivery log, resendable from\nthe Factory UI) but retries do not apply — this is a live round trip for\nverifying an endpoint and its signature check.",
        "operationId": "WebhookService_TestWebhook",
        "responses": {
          "200": {
            "description": "A successful response.",
            "schema": {
              "$ref": "#/definitions/factory.api.v1.webhooks.TestWebhookResponse"
            }
          },
          "400": {
            "description": "The request was rejected because it was malformed — for example a webhook id that is not a whsub_ id. The body is a validation-failure envelope: an overall type and message and one entry per problem (each with a stable code, a message, and a param pointing at the offending parameter).",
            "schema": {
              "$ref": "#/definitions/factory.api.v1.Error"
            }
          },
          "401": {
            "description": "The request is missing a valid bearer token, or the token is invalid or expired.",
            "schema": {
              "$ref": "#/definitions/factory.api.v1.Error"
            }
          },
          "404": {
            "description": "No webhook subscription with the given id exists for the authenticated company.",
            "schema": {
              "$ref": "#/definitions/factory.api.v1.Error"
            }
          },
          "default": {
            "description": "Any other error. The body is the same error envelope every error uses: a short stable type identifying the kind of failure, a human-readable message, and the request's idempotency key echoed back when one was supplied.",
            "schema": {
              "$ref": "#/definitions/factory.api.v1.Error"
            }
          }
        },
        "parameters": [
          {
            "name": "webhookId",
            "description": "The subscription whose endpoint to ping.",
            "in": "path",
            "required": true,
            "type": "string"
          },
          {
            "name": "body",
            "in": "body",
            "required": true,
            "schema": {
              "$ref": "#/definitions/factory.api.v1.webhooks.WebhookService.TestWebhookBody"
            }
          }
        ],
        "tags": [
          "WebhookService"
        ]
      }
    }
  },
  "definitions": {
    "factory.api.v1.Address": {
      "type": "object",
      "properties": {
        "address1": {
          "type": "string",
          "description": "First address line (street number and name)."
        },
        "address2": {
          "type": "string",
          "description": "Second address line (unit, suite, or similar)."
        },
        "city": {
          "type": "string",
          "description": "City or suburb."
        },
        "state": {
          "type": "string",
          "description": "State, province, or region."
        },
        "postalCode": {
          "type": "string",
          "description": "Postal code (ZIP code, postcode)."
        },
        "countryCode": {
          "type": "string",
          "description": "Country, as an uppercase ISO 3166-1 alpha-2 code (e.g. \"AU\"). Optional;\nwhen supplied, any other form is rejected."
        }
      },
      "description": "A single postal address."
    },
    "factory.api.v1.Error": {
      "type": "object",
      "properties": {
        "type": {
          "type": "string",
          "description": "A short, stable identifier for the kind of failure (for example\n\"validation_failure\", \"not_found\", \"conflict\", \"rate_limited\"). Treat an\nunrecognised type as a generic error."
        },
        "message": {
          "type": "string",
          "description": "A human-readable summary of the failure."
        },
        "requestId": {
          "type": "string",
          "description": "The request's idempotency key, echoed back when one was supplied."
        },
        "errors": {
          "type": "array",
          "items": {
            "type": "object",
            "$ref": "#/definitions/factory.api.v1.ErrorDetail"
          },
          "description": "The individual problems that caused the request to fail. Populated on\nvalidation failures (one entry per field-level problem) and on idempotency\nconflicts (one entry whose `code` says which kind, see `resourceId`);\nempty for every other kind of failure."
        },
        "resourceId": {
          "type": "string",
          "description": "On an idempotency conflict (HTTP 409) whose original request completed\n(`errors[0].code` is \"request_replayed\"), the id of the resource that\nrequest created — read it instead of retrying. Empty otherwise."
        }
      },
      "description": "The error envelope.\n\nThe body of every error response, whatever the HTTP status. It names the\nkind of failure, summarises it for a human, echoes the request's\nidempotency key when one was supplied, and — for validation failures —\nlists the individual field-level problems."
    },
    "factory.api.v1.ErrorDetail": {
      "type": "object",
      "properties": {
        "code": {
          "type": "string",
          "description": "A short, stable error code identifying the problem (e.g.\n\"customer_not_found\", \"product_not_found\")."
        },
        "message": {
          "type": "string",
          "description": "A human-readable explanation of the problem."
        },
        "param": {
          "type": "string",
          "description": "A pointer to the offending field (e.g.\n\"lines[2].catalogue.product_id\")."
        }
      },
      "description": "A single problem with one request field."
    },
    "factory.api.v1.catalogue.Flashing": {
      "type": "object",
      "properties": {
        "templateId": {
          "type": "string",
          "description": "The template's unique id. Reference this on a flashing line item."
        },
        "productType": {
          "type": "string",
          "description": "The catalogue product type of this template. Flashing templates report\n\"Flashing\"; it marks them apart from other kinds of catalogue product."
        },
        "thickness": {
          "type": "string",
          "description": "The thickness this template is for, as a decimal string in the template's\nunits (e.g. \"0.55\", millimetres for metric accounts). Fixed for the\ntemplate: each thickness is a separate template with its own id, so to\norder a different thickness, select the template (`templateId`) that has it."
        },
        "flashingId": {
          "type": "string",
          "description": "The id of the underlying flashing."
        },
        "flashingName": {
          "type": "string",
          "description": "The display name of the underlying flashing."
        },
        "materialId": {
          "type": "string",
          "description": "The id of the template's material selection. Not populated yet: empty for\nevery template today, and may start being returned in a later release\nwithout a version change. Use `flashingId` to identify the flashing."
        },
        "materialName": {
          "type": "string",
          "description": "The display name of the template's material selection. Empty today, like\n`materialId`."
        },
        "colours": {
          "type": "array",
          "items": {
            "type": "string"
          },
          "description": "The colours you can select for a flashing line built on this template. A\nline's colour must be one of these. Colour is the per-line choice; the\nthickness is fixed by the template id (see `thickness`)."
        }
      },
      "description": "A flashing product template — one flashing at one thickness. A flashing line\nitem must reference one of these by id; it also surfaces the selectable\ncolours you choose from when building that line."
    },
    "factory.api.v1.catalogue.GetFlashingResponse": {
      "type": "object",
      "properties": {
        "flashing": {
          "$ref": "#/definitions/factory.api.v1.catalogue.Flashing",
          "description": "The requested flashing template."
        }
      },
      "description": "Result of retrieving a single flashing template."
    },
    "factory.api.v1.catalogue.GetKitResponse": {
      "type": "object",
      "properties": {
        "kit": {
          "$ref": "#/definitions/factory.api.v1.catalogue.Kit",
          "description": "The requested kit, including its rows and full component tree."
        }
      },
      "description": "Result of retrieving a single product kit."
    },
    "factory.api.v1.catalogue.GetProductResponse": {
      "type": "object",
      "properties": {
        "product": {
          "$ref": "#/definitions/factory.api.v1.catalogue.Product",
          "description": "The requested product."
        }
      },
      "description": "Result of retrieving a single catalogue product."
    },
    "factory.api.v1.catalogue.Kit": {
      "type": "object",
      "properties": {
        "kitId": {
          "type": "string",
          "description": "The kit's id — the id of the catalogue product that fronts this kit. This\nis the id a kit line item references, and the id GetKit accepts."
        },
        "name": {
          "type": "string",
          "description": "The kit's display name."
        },
        "rows": {
          "type": "array",
          "items": {
            "type": "object",
            "$ref": "#/definitions/factory.api.v1.catalogue.KitRow"
          },
          "description": "The kit's priced variant rows. Populated by GetKit; the kit listing leaves\nthis empty."
        },
        "components": {
          "type": "array",
          "items": {
            "type": "object",
            "$ref": "#/definitions/factory.api.v1.catalogue.KitProductComponent"
          },
          "description": "The kit's top-level component products (those not inside a sub-assembly).\nPopulated by GetKit; the kit listing leaves this empty."
        },
        "subKits": {
          "type": "array",
          "items": {
            "type": "object",
            "$ref": "#/definitions/factory.api.v1.catalogue.KitSubAssembly"
          },
          "description": "The kit's sub-assemblies, each holding its own component products. Nested\none level deep (a sub-assembly cannot itself contain sub-assemblies)."
        }
      },
      "description": "A configured product kit — a product assembled from component products,\noptionally grouped into sub-assemblies."
    },
    "factory.api.v1.catalogue.KitProductComponent": {
      "type": "object",
      "properties": {
        "kitProductId": {
          "type": "string",
          "description": "The component's unique id within the kit."
        },
        "productId": {
          "type": "string",
          "description": "The id of the catalogue product used as this component. Additional-product\ncomponents only."
        },
        "productRowId": {
          "type": "string",
          "description": "The id of the specific product variant row used as this component.\nCatalogue-product components only."
        },
        "colour": {
          "type": "string",
          "description": "The component's colour. Catalogue-product components only."
        },
        "material": {
          "type": "string",
          "description": "The component's material. Catalogue-product components only."
        },
        "quantity": {
          "type": "string",
          "description": "The component's quantity, as a decimal string."
        },
        "productType": {
          "$ref": "#/definitions/factory.common.v1.KitComponentType",
          "description": "The component's kind: catalogue product, notes, or labour. Determines which\nof the fields below are populated. On-the-fly is never a kit-template\ncomponent, so it is not part of this set."
        },
        "notes": {
          "type": "string",
          "description": "Free-text note. Populated when this is a notes component."
        },
        "notesType": {
          "$ref": "#/definitions/factory.common.v1.NotesType",
          "description": "Whether the note is internal-only or customer-visible. Notes components\nonly."
        },
        "labourUserId": {
          "type": "string",
          "description": "The assigned company user's id. Populated when this is a labour component."
        },
        "hourlyRateCharged": {
          "$ref": "#/definitions/factory.common.v1.Money",
          "description": "The labour charge-out rate per hour. Labour components only."
        },
        "hourlyRateCost": {
          "$ref": "#/definitions/factory.common.v1.Money",
          "description": "The labour cost rate per hour. Labour components only."
        },
        "customHourlyRateCharged": {
          "$ref": "#/definitions/factory.common.v1.Money",
          "description": "A custom override of the charge-out rate, when set. Labour components only."
        },
        "customHourlyRateCost": {
          "$ref": "#/definitions/factory.common.v1.Money",
          "description": "A custom override of the cost rate, when set. Labour components only."
        },
        "productName": {
          "type": "string",
          "description": "The display name of the catalogue product used as this component.\nCatalogue-product components only. Echo this into the kit component's\n`productName` when building an order or quote kit line from this template —\ncomponent names are stored as given and are not derived from product\nreferences."
        }
      },
      "description": "One component within a kit or sub-assembly — the default contents of a kit. A\ncomponent is typed by `productType`: by default a catalogue product, but it\nmay instead be a labour or notes entry. The same component may appear under\nmore than one sub-assembly."
    },
    "factory.api.v1.catalogue.KitRow": {
      "type": "object",
      "properties": {
        "kitRowId": {
          "type": "string",
          "description": "The kit variant row's unique id."
        },
        "attributes": {
          "type": "array",
          "items": {
            "type": "object",
            "$ref": "#/definitions/factory.api.v1.catalogue.RowAttribute"
          },
          "description": "Attribute pairs that identify this kit variant."
        },
        "markedUpPrice": {
          "$ref": "#/definitions/factory.common.v1.Money",
          "description": "The computed marked-up sell price, for reference."
        },
        "prices": {
          "type": "array",
          "items": {
            "type": "object",
            "$ref": "#/definitions/factory.api.v1.catalogue.RowPrice"
          },
          "description": "The variant's price at each of your price levels."
        }
      },
      "description": "One priced variant of a product kit, identified by its attributes."
    },
    "factory.api.v1.catalogue.KitSubAssembly": {
      "type": "object",
      "properties": {
        "subKitId": {
          "type": "string",
          "description": "The sub-assembly's unique id."
        },
        "title": {
          "type": "string",
          "description": "The sub-assembly's display title."
        },
        "index": {
          "type": "integer",
          "format": "int32",
          "description": "The sub-assembly's position in display order within the kit."
        },
        "components": {
          "type": "array",
          "items": {
            "type": "object",
            "$ref": "#/definitions/factory.api.v1.catalogue.KitProductComponent"
          },
          "description": "The component products that make up this sub-assembly."
        }
      },
      "description": "A sub-assembly within a product kit, holding its own component products. A\nsub-assembly is nested one level deep and cannot itself contain\nsub-assemblies."
    },
    "factory.api.v1.catalogue.ListFlashingsResponse": {
      "type": "object",
      "properties": {
        "flashings": {
          "type": "array",
          "items": {
            "type": "object",
            "$ref": "#/definitions/factory.api.v1.catalogue.Flashing"
          },
          "description": "The flashing templates in this page."
        },
        "nextPageToken": {
          "type": "string",
          "description": "Token to pass as `pageToken` to fetch the next page; empty when there are no\nmore results."
        }
      },
      "description": "A page of flashing templates."
    },
    "factory.api.v1.catalogue.ListKitsResponse": {
      "type": "object",
      "properties": {
        "kits": {
          "type": "array",
          "items": {
            "type": "object",
            "$ref": "#/definitions/factory.api.v1.catalogue.Kit"
          },
          "description": "The kits in this page."
        },
        "nextPageToken": {
          "type": "string",
          "description": "Token to pass as `pageToken` to fetch the next page; empty when there are no\nmore results."
        }
      },
      "description": "A page of product kits."
    },
    "factory.api.v1.catalogue.ListProductsResponse": {
      "type": "object",
      "properties": {
        "products": {
          "type": "array",
          "items": {
            "type": "object",
            "$ref": "#/definitions/factory.api.v1.catalogue.Product"
          },
          "description": "The products in this page."
        },
        "nextPageToken": {
          "type": "string",
          "description": "Token to pass as `pageToken` to fetch the next page; empty when there are no\nmore results."
        }
      },
      "description": "A page of catalogue products."
    },
    "factory.api.v1.catalogue.Product": {
      "type": "object",
      "properties": {
        "productId": {
          "type": "string",
          "description": "The product's unique id. Use this to reference the product on a line item."
        },
        "name": {
          "type": "string",
          "description": "The product's display name. Not guaranteed unique; not a lookup key."
        },
        "categoryId": {
          "type": "string",
          "description": "The id of the category this product belongs to."
        },
        "categoryName": {
          "type": "string",
          "description": "The display name of the product's category."
        },
        "pricingStrategy": {
          "$ref": "#/definitions/factory.common.v1.PricingStrategy",
          "description": "How this product's quantity is interpreted for pricing."
        },
        "isTaxFree": {
          "type": "boolean",
          "description": "Whether this product is exempt from tax."
        },
        "rows": {
          "type": "array",
          "items": {
            "type": "object",
            "$ref": "#/definitions/factory.api.v1.catalogue.ProductRow"
          },
          "description": "The product's priced variant rows."
        },
        "isInventoryTracked": {
          "type": "boolean",
          "description": "Whether this product participates in inventory tracking. When true,\nstock levels for its rows are available from the Inventory API."
        }
      },
      "description": "A catalogue product you can order. Identified by its id; the name is not\nguaranteed to be unique, so always resolve by id."
    },
    "factory.api.v1.catalogue.ProductRow": {
      "type": "object",
      "properties": {
        "productRowId": {
          "type": "string",
          "description": "The variant row's unique id. Reference this on a catalogue line item."
        },
        "colour": {
          "type": "string",
          "description": "The variant's own colour. Usually empty — colour is normally chosen per line\nfrom `colourOptions` rather than baked into the row."
        },
        "thickness": {
          "type": "string",
          "description": "The variant's thickness."
        },
        "attributes": {
          "type": "array",
          "items": {
            "type": "object",
            "$ref": "#/definitions/factory.api.v1.catalogue.RowAttribute"
          },
          "description": "Additional attribute pairs that identify this variant, e.g.\n[{name:\"Size\", value:\"A4\"}]."
        },
        "markedUpPrice": {
          "$ref": "#/definitions/factory.common.v1.Money",
          "description": "The computed marked-up sell price, for reference."
        },
        "prices": {
          "type": "array",
          "items": {
            "type": "object",
            "$ref": "#/definitions/factory.api.v1.catalogue.RowPrice"
          },
          "description": "The variant's price at each of your price levels."
        },
        "colourOptions": {
          "type": "array",
          "items": {
            "type": "string"
          },
          "description": "The colours a catalogue line built on this row may use, when the product's\nmaterial offers a colour selection. A line's colour must be one of these.\nThey come from the row's material, so every row sharing that material offers\nthe same colours; the list is empty when the product has no colour choice.\nColour is the per-line choice — the attributes above identify the variant."
        }
      },
      "description": "One priced variant of a product, identified by attributes such as thickness\nand size. Colour is usually not a variant attribute but a per-line choice —\nsee `colourOptions` for the colours a line built on this row may use."
    },
    "factory.api.v1.catalogue.RowAttribute": {
      "type": "object",
      "properties": {
        "name": {
          "type": "string",
          "description": "The attribute name, e.g. \"Size\"."
        },
        "value": {
          "type": "string",
          "description": "The attribute value, e.g. \"A4\"."
        }
      },
      "description": "A single attribute name/value pair describing a product variant, e.g.\n{name:\"Size\", value:\"A4\"}."
    },
    "factory.api.v1.catalogue.RowPrice": {
      "type": "object",
      "properties": {
        "priceLevelId": {
          "type": "string",
          "description": "The id of the price level (the level's own identity)."
        },
        "priceLevelName": {
          "type": "string",
          "description": "The price level's display name, e.g. \"Account\" or \"Standard\"."
        },
        "price": {
          "$ref": "#/definitions/factory.common.v1.Money",
          "description": "The price for this variant at this level."
        },
        "rowPriceId": {
          "type": "string",
          "description": "The id to reference on a catalogue line item to select this variant at this\nprice level."
        }
      },
      "description": "A product variant's price at one of your price levels.\n\nCarries two ids: `priceLevelId` identifies the price level itself, while\n`rowPriceId` identifies this specific row-at-this-level entry. A catalogue\nline item references `rowPriceId`; the two use different id prefixes, so\nsending the wrong one is rejected."
    },
    "factory.api.v1.collaborate.Attachment": {
      "type": "object",
      "properties": {
        "id": {
          "type": "string",
          "description": "The id of this attachment. Use it to retrieve or delete the attachment."
        },
        "messageId": {
          "type": "string",
          "description": "The id of the conversation message this attachment belongs to."
        },
        "filename": {
          "type": "string",
          "description": "The file's name, as uploaded."
        },
        "contentType": {
          "type": "string",
          "description": "The file's media type (for example `image/png` or `application/pdf`)."
        },
        "sizeBytes": {
          "type": "string",
          "format": "int64",
          "description": "The file's size in bytes, rounded to kilobyte precision. Files smaller\nthan one kilobyte read as 0."
        },
        "url": {
          "type": "string",
          "description": "A presigned URL to download the file. The URL expires; re-read the\nattachment for a fresh one rather than storing it."
        },
        "thumbnailUrl": {
          "type": "string",
          "description": "A presigned URL to a small preview image, for image attachments. Empty\nwhen no preview exists. Expires like `url`."
        },
        "isGenerated": {
          "type": "boolean",
          "description": "True when this file is a document the Factory app generated (for example\na quote or invoice PDF) rather than a file somebody uploaded. Generated\ndocuments cannot be deleted through this API."
        },
        "generatedType": {
          "type": "string",
          "description": "For generated documents, the kind of document (for example `quote` or\n`invoice`). Not a fixed list; new kinds may appear. Empty for uploaded\nfiles."
        }
      },
      "description": "A file attached to an order or quote conversation."
    },
    "factory.api.v1.collaborate.Message": {
      "type": "object",
      "properties": {
        "id": {
          "type": "string",
          "description": "The id of this message."
        },
        "authorName": {
          "type": "string",
          "description": "The display name of the person who wrote the message. Empty when the\nauthor's account no longer exists."
        },
        "authorEmail": {
          "type": "string",
          "description": "The email address of the person who wrote the message. Empty when the\nauthor's account no longer exists."
        },
        "text": {
          "type": "string",
          "description": "The message text, as an HTML fragment. Messages written in the Factory\napp carry the app's markup (including @-mentions); plain text posted\nthrough this API is HTML-escaped and `\u003cp\u003e`-wrapped on write, and reads\nback in that stored form. Empty for file-only messages — most\nattachments are posted without any text."
        },
        "createdAt": {
          "type": "string",
          "format": "date-time",
          "description": "When the message was posted."
        },
        "attachments": {
          "type": "array",
          "items": {
            "type": "object",
            "$ref": "#/definitions/factory.api.v1.collaborate.Attachment"
          },
          "description": "The files attached to this message, if any."
        }
      },
      "description": "One entry in an order or quote conversation: a text message, one or more\nattached files, or both."
    },
    "factory.api.v1.company.Company": {
      "type": "object",
      "properties": {
        "companyId": {
          "type": "string",
          "description": "The company's unique id.",
          "readOnly": true
        },
        "name": {
          "type": "string",
          "description": "The company's display name."
        },
        "domain": {
          "type": "string",
          "description": "The company's Factory domain."
        },
        "countryCode": {
          "type": "string",
          "description": "The company's country code, as an uppercase ISO 3166-1 alpha-2 code\n(e.g. \"AU\"). Empty when not set."
        },
        "timeZone": {
          "type": "string",
          "description": "The company's IANA time zone name (e.g. \"Australia/Sydney\")."
        },
        "currency": {
          "type": "string",
          "description": "The ISO 4217 currency code all monetary amounts use. Currently one of\n\"AUD\", \"CAD\", \"NZD\", \"USD\", \"GBP\", or \"ZAR\"; the set may grow."
        },
        "currencySymbol": {
          "type": "string",
          "description": "The currency's display symbol (e.g. \"$\")."
        },
        "measurementSystem": {
          "$ref": "#/definitions/factory.api.v1.company.MeasurementSystem",
          "description": "The measurement system quantities and dimensions are expressed in."
        },
        "taxRate": {
          "type": "string",
          "description": "The tax rate applied to the company's sales documents, as a decimal\nfraction (e.g. \"0.10\" means 10%). The server computes document tax with\nthis rate; it cannot be set per document."
        },
        "taxIdentifier": {
          "$ref": "#/definitions/factory.common.v1.TaxIdentifier",
          "description": "The company's tax identifier, if set. On read the scheme is ABN for\nAustralian accounts."
        },
        "email": {
          "type": "string",
          "description": "The company's contact email address."
        },
        "phone": {
          "type": "string",
          "description": "The company's contact phone number."
        },
        "website": {
          "type": "string",
          "description": "The company's website URL."
        },
        "address": {
          "type": "string",
          "description": "The company's street address (first line)."
        },
        "address2": {
          "type": "string",
          "description": "The company's street address (second line)."
        },
        "city": {
          "type": "string",
          "description": "The company's city or suburb."
        },
        "state": {
          "type": "string",
          "description": "The company's state or region code (e.g. \"VIC\")."
        },
        "postalCode": {
          "type": "string",
          "description": "The company's postal code (ZIP code, postcode)."
        },
        "logoUrl": {
          "type": "string",
          "description": "A URL to the company's logo image, if one is set. Served from Factory's\nmedia CDN; the URL is stable and safe to cache."
        },
        "pricingSettings": {
          "$ref": "#/definitions/factory.api.v1.company.PricingSettings",
          "description": "The account settings that affect how lines are priced. Always present on\nreads."
        }
      },
      "description": "A company's settings and configuration."
    },
    "factory.api.v1.company.CustomFieldDefinition": {
      "type": "object",
      "properties": {
        "key": {
          "type": "string",
          "description": "The field's key — the identifier write endpoints accept.",
          "readOnly": true
        },
        "name": {
          "type": "string",
          "description": "The field's display name."
        },
        "module": {
          "$ref": "#/definitions/factory.api.v1.company.CustomFieldModule",
          "description": "Which record type the field is defined for."
        },
        "type": {
          "$ref": "#/definitions/factory.api.v1.company.CustomFieldType",
          "description": "The field's value type."
        },
        "options": {
          "type": "array",
          "items": {
            "type": "object",
            "$ref": "#/definitions/factory.api.v1.company.CustomFieldOption"
          },
          "description": "The permitted choices, for select and multi-select fields. A written\nvalue must be an option `key` (multi-select: a list of option keys)."
        },
        "readOnly": {
          "type": "boolean",
          "description": "The field's value is computed and cannot be written."
        },
        "isHidden": {
          "type": "boolean",
          "description": "Whether the field is hidden in the Factory app. Hidden fields are still\nreturned here so values on existing documents can be interpreted."
        }
      },
      "description": "One account-configured custom field definition."
    },
    "factory.api.v1.company.CustomFieldModule": {
      "type": "string",
      "enum": [
        "CUSTOM_FIELD_MODULE_UNSPECIFIED",
        "CUSTOM_FIELD_MODULE_SALES_DOCUMENTS",
        "CUSTOM_FIELD_MODULE_CUSTOMERS",
        "CUSTOM_FIELD_MODULE_SUPPLIERS",
        "CUSTOM_FIELD_MODULE_PURCHASE_ORDERS"
      ],
      "default": "CUSTOM_FIELD_MODULE_UNSPECIFIED",
      "description": "The record type a custom field is defined for.\n\n - CUSTOM_FIELD_MODULE_SALES_DOCUMENTS: Quotes and orders share one set of sales-document fields."
    },
    "factory.api.v1.company.CustomFieldOption": {
      "type": "object",
      "properties": {
        "key": {
          "type": "string",
          "description": "The option's key — what a written value must contain."
        },
        "label": {
          "type": "string",
          "description": "The option's display label."
        },
        "isDefault": {
          "type": "boolean",
          "description": "Whether this option is the field's default."
        }
      },
      "description": "One permitted choice of a select or multi-select custom field."
    },
    "factory.api.v1.company.CustomFieldType": {
      "type": "string",
      "enum": [
        "CUSTOM_FIELD_TYPE_UNSPECIFIED",
        "CUSTOM_FIELD_TYPE_TEXT",
        "CUSTOM_FIELD_TYPE_NUMBER",
        "CUSTOM_FIELD_TYPE_CURRENCY",
        "CUSTOM_FIELD_TYPE_DATE",
        "CUSTOM_FIELD_TYPE_DATETIME",
        "CUSTOM_FIELD_TYPE_SELECT",
        "CUSTOM_FIELD_TYPE_CHECKBOX",
        "CUSTOM_FIELD_TYPE_MULTISELECT",
        "CUSTOM_FIELD_TYPE_FORMULA",
        "CUSTOM_FIELD_TYPE_RATING",
        "CUSTOM_FIELD_TYPE_EMAIL",
        "CUSTOM_FIELD_TYPE_PHONE",
        "CUSTOM_FIELD_TYPE_URL",
        "CUSTOM_FIELD_TYPE_FILE"
      ],
      "default": "CUSTOM_FIELD_TYPE_UNSPECIFIED",
      "description": "A custom field's value type."
    },
    "factory.api.v1.company.GetCompanyResponse": {
      "type": "object",
      "properties": {
        "company": {
          "$ref": "#/definitions/factory.api.v1.company.Company",
          "description": "Your company's settings."
        }
      }
    },
    "factory.api.v1.company.ListCompanyCustomFieldsResponse": {
      "type": "object",
      "properties": {
        "customFields": {
          "type": "array",
          "items": {
            "type": "object",
            "$ref": "#/definitions/factory.api.v1.company.CustomFieldDefinition"
          },
          "description": "The company's custom field definitions."
        }
      }
    },
    "factory.api.v1.company.MeasurementSystem": {
      "type": "string",
      "enum": [
        "MEASUREMENT_SYSTEM_UNSPECIFIED",
        "MEASUREMENT_SYSTEM_METRIC",
        "MEASUREMENT_SYSTEM_IMPERIAL",
        "MEASUREMENT_SYSTEM_BOTH"
      ],
      "default": "MEASUREMENT_SYSTEM_UNSPECIFIED",
      "description": "The measurement system a company's quantities are expressed in.\n\n - MEASUREMENT_SYSTEM_BOTH: The company works in both systems; each document line's pricing strategy\ndeclares its own unit."
    },
    "factory.api.v1.company.PricingSettings": {
      "type": "object",
      "properties": {
        "minimumLinealMetreLengthMm": {
          "type": "string",
          "description": "The minimum length charged on lines priced per lineal metre, in\nmillimetres, as a decimal string (for example \"1000\"). Each measured piece\nshorter than this is priced as this length. Empty when not enabled."
        },
        "minimumLinealFeetLengthMm": {
          "type": "string",
          "description": "The minimum length charged on lines priced per lineal foot, in\nmillimetres, as a decimal string (for example \"304.8\", one foot). Each\nmeasured piece shorter than this is priced as this length. Empty when not\nenabled."
        },
        "minimumFlashingLengthMm": {
          "type": "string",
          "description": "The minimum length charged on each piece of a flashing, in millimetres, as\na decimal string. Factory prices a flashing piece shorter than this as\nthis length; the API itself never prices flashings, so apply it when you\ncompute a flashing line's `totalPrice`. Empty when not enabled."
        },
        "crushFoldBendCount": {
          "type": "integer",
          "format": "int32",
          "description": "The number of bends a crush fold counts as when a flashing's `bends` are\ncounted. 0 means the default of 2."
        }
      },
      "description": "The account settings that change how the server, and Factory, price lines.\nEach minimum length is in millimetres and is empty when the setting is off."
    },
    "factory.api.v1.customers.CreateCustomerRequest": {
      "type": "object",
      "properties": {
        "companyName": {
          "type": "string",
          "description": "The customer's company name. Required, and must be unique within your company\n(matched case-insensitively); a duplicate is rejected."
        },
        "taxIdentifier": {
          "$ref": "#/definitions/factory.common.v1.TaxIdentifier",
          "description": "The customer's tax identifier. Optional. In v1 the only accepted scheme is\nABN (Australian Business Number); supplying any other scheme is rejected."
        },
        "email": {
          "type": "string",
          "description": "The customer's email address. Required; must be a valid email address."
        },
        "phone": {
          "type": "string",
          "description": "The customer's phone number."
        },
        "billingAddress": {
          "$ref": "#/definitions/factory.api.v1.Address",
          "description": "The customer's billing address."
        },
        "deliveryAddresses": {
          "type": "array",
          "items": {
            "type": "object",
            "$ref": "#/definitions/factory.api.v1.Address"
          },
          "description": "The customer's delivery addresses."
        },
        "contacts": {
          "type": "array",
          "items": {
            "type": "object",
            "$ref": "#/definitions/factory.api.v1.customers.CustomerContact"
          },
          "description": "The customer's contacts, at most 50 on a create. Each contact's name is\nrequired; its id is assigned by the server, so omit it on create."
        },
        "requestId": {
          "type": "string",
          "description": "Optional idempotency key for this request: an opaque, client-generated\nUUID, scoped to the API key that sends it. Requests carrying the same key\nare executed at most once, so a retry can never create a second copy. Any\nrepeat is rejected with HTTP 409: if the original request completed, the\nbody's `resourceId` carries the id it created; if its outcome is\nstill unknown, or the key is reused with a materially different body,\nverify with a read before retrying with a fresh key. Keys are retained\nfor at least 24 hours. If the idempotency store is unavailable, requests\ncarrying a key are rejected with HTTP 503 (requests without a key are\nunaffected). Omit the key for no idempotency guarantee."
        },
        "fields": {
          "type": "string",
          "description": "Comma-separated response fields to include, using camelCase JSON names\n(e.g. `orderId,total.amountMicros`). Dot paths reach into nested objects\nand map transparently across arrays. Paths are relative to the resource,\nnot the response envelope; envelope keys are always preserved. Only 2xx\nJSON responses are filtered; error bodies pass through unmodified. Unknown\nnames are silently ignored. Takes precedence over `excludeFields` when\nboth are provided."
        },
        "excludeFields": {
          "type": "string",
          "description": "Comma-separated response fields to exclude, using camelCase JSON names.\nDot paths and array-transparency work the same as `fields`. Paths are\nrelative to the resource, not the response envelope. Only 2xx JSON\nresponses are filtered; error bodies pass through unmodified. Ignored\nwhen `fields` is also provided."
        }
      },
      "description": "Input for creating a customer: company details, addresses, and contacts. Company\nname and email are required; a tax identifier is optional.",
      "required": [
        "companyName",
        "email"
      ]
    },
    "factory.api.v1.customers.CreateCustomerResponse": {
      "type": "object",
      "properties": {
        "customer": {
          "$ref": "#/definitions/factory.api.v1.customers.Customer",
          "description": "The created customer."
        }
      },
      "description": "Result of creating a customer: the created customer with server-assigned fields."
    },
    "factory.api.v1.customers.Customer": {
      "type": "object",
      "properties": {
        "customerId": {
          "type": "string",
          "description": "The customer's unique id."
        },
        "companyName": {
          "type": "string",
          "description": "The customer's company name. Unique within your company and matched\ncase-insensitively when resolving."
        },
        "taxIdentifier": {
          "$ref": "#/definitions/factory.common.v1.TaxIdentifier",
          "description": "The customer's tax identifier, if set. Not required to be unique. On read the\nscheme is ABN for existing (Australian) customers."
        },
        "email": {
          "type": "string",
          "description": "The customer's email address, if set."
        },
        "phone": {
          "type": "string",
          "description": "The customer's phone number."
        },
        "defaultPriceLevelId": {
          "type": "string",
          "description": "The customer's default price level, by id. Read reference only; line\npricing is supplied by the caller on each order line."
        },
        "defaultPriceLevelName": {
          "type": "string",
          "description": "The name of the customer's default price level."
        },
        "billingAddress": {
          "$ref": "#/definitions/factory.api.v1.Address",
          "description": "The customer's billing address."
        },
        "deliveryAddresses": {
          "type": "array",
          "items": {
            "type": "object",
            "$ref": "#/definitions/factory.api.v1.Address"
          },
          "description": "The customer's delivery addresses."
        },
        "contacts": {
          "type": "array",
          "items": {
            "type": "object",
            "$ref": "#/definitions/factory.api.v1.customers.CustomerContact"
          },
          "description": "The customer's contacts."
        },
        "isOnCreditHold": {
          "type": "boolean",
          "description": "Whether the customer is currently on credit hold."
        },
        "disableTaxByDefault": {
          "type": "boolean",
          "description": "Whether tax is disabled by default for this customer."
        },
        "lastOrderAt": {
          "type": "string",
          "format": "date-time",
          "description": "When the customer most recently placed an order. Populated on ListCustomers\nentries; not currently returned by GetCustomer."
        }
      },
      "description": "A customer belonging to your company."
    },
    "factory.api.v1.customers.CustomerContact": {
      "type": "object",
      "properties": {
        "contactId": {
          "type": "string",
          "description": "The contact's unique id."
        },
        "name": {
          "type": "string",
          "description": "The contact's name."
        },
        "email": {
          "type": "string",
          "description": "The contact's email address."
        },
        "phone": {
          "type": "string",
          "description": "The contact's phone number."
        },
        "mobile": {
          "type": "string",
          "description": "The contact's mobile number."
        }
      },
      "description": "A contact person associated with a customer."
    },
    "factory.api.v1.customers.GetCustomerResponse": {
      "type": "object",
      "properties": {
        "customer": {
          "$ref": "#/definitions/factory.api.v1.customers.Customer",
          "description": "The requested customer."
        }
      },
      "description": "Result of retrieving a single customer."
    },
    "factory.api.v1.customers.ListCustomersResponse": {
      "type": "object",
      "properties": {
        "customers": {
          "type": "array",
          "items": {
            "type": "object",
            "$ref": "#/definitions/factory.api.v1.customers.Customer"
          },
          "description": "The customers in this page."
        },
        "nextPageToken": {
          "type": "string",
          "description": "Token to pass as `pageToken` to fetch the next page; empty when there are no\nmore results."
        }
      },
      "description": "A page of customers."
    },
    "factory.api.v1.inventory.GetProductInventoryResponse": {
      "type": "object",
      "properties": {
        "productName": {
          "type": "string",
          "description": "The product's display name.",
          "readOnly": true
        },
        "isColourTracked": {
          "type": "boolean",
          "description": "Whether the product is colour-tracked. When true, entries are split by\ncolour; when false, there is one entry per row.",
          "readOnly": true
        },
        "entries": {
          "type": "array",
          "items": {
            "type": "object",
            "$ref": "#/definitions/factory.api.v1.inventory.StockEntry"
          },
          "description": "The stock entries for this product — one per row per colour."
        }
      },
      "description": "Result of retrieving inventory for a single product."
    },
    "factory.api.v1.inventory.InventoryService.SetStockLevelsBody": {
      "type": "object",
      "properties": {
        "entries": {
          "type": "array",
          "items": {
            "type": "object",
            "$ref": "#/definitions/factory.api.v1.inventory.StockLevelInput"
          },
          "description": "The stock levels to set. At least one entry is required. Each identifies\na product row and optional colour, with the new on-hand quantity.\nEntries you omit are left unchanged."
        }
      },
      "description": "Input for setting on-hand stock levels on a tracked product.",
      "required": [
        "entries"
      ]
    },
    "factory.api.v1.inventory.ListInventoryResponse": {
      "type": "object",
      "properties": {
        "entries": {
          "type": "array",
          "items": {
            "type": "object",
            "$ref": "#/definitions/factory.api.v1.inventory.StockEntry"
          },
          "description": "The stock entries in this page."
        },
        "nextPageToken": {
          "type": "string",
          "description": "Token to pass as `pageToken` to fetch the next page; empty when there are\nno more results."
        }
      },
      "description": "A page of stock entries."
    },
    "factory.api.v1.inventory.SetStockLevelsResponse": {
      "type": "object",
      "properties": {
        "entries": {
          "type": "array",
          "items": {
            "type": "object",
            "$ref": "#/definitions/factory.api.v1.inventory.StockEntry"
          },
          "description": "The stock entries for the product after the update — all entries,\nincluding those not changed by this call, with recomputed promised,\non-the-way, and available."
        }
      },
      "description": "Result of setting stock levels: the product's complete stock position."
    },
    "factory.api.v1.inventory.StockEntry": {
      "type": "object",
      "properties": {
        "productId": {
          "type": "string",
          "description": "The catalogue product this entry belongs to.",
          "readOnly": true
        },
        "productName": {
          "type": "string",
          "description": "The product's display name.",
          "readOnly": true
        },
        "productRowId": {
          "type": "string",
          "description": "The product variant row this entry is for — the same id the catalogue\nproduct's rows carry. Look the row up there when you need more than\nthis entry shows, such as the variant's prices or colour options.",
          "readOnly": true
        },
        "colour": {
          "type": "string",
          "description": "The colour this entry is for. Empty when the product is not\ncolour-tracked — in that case there is one entry per row.",
          "readOnly": true
        },
        "thickness": {
          "type": "string",
          "description": "The variant's thickness, when its catalogue row carries one. Empty\notherwise.",
          "readOnly": true
        },
        "attributes": {
          "type": "array",
          "items": {
            "type": "object",
            "$ref": "#/definitions/factory.api.v1.catalogue.RowAttribute"
          },
          "description": "The attribute pairs identifying this variant, e.g.\n[{name:\"Size\", value:\"A4\"}] — the same attributes the catalogue\nproduct's row carries. Together with thickness and colour they label\nthe entry; a single-row product has no attributes to distinguish, so\nthe list may be empty.",
          "readOnly": true
        },
        "onHand": {
          "type": "string",
          "description": "Quantity currently in stock (on hand). The only directly maintained\nvalue; the others are derived from it and from order/purchase data.",
          "readOnly": true
        },
        "promised": {
          "type": "string",
          "description": "Quantity committed to submitted orders that have not yet been received\nby the customer. Computed from open order line items.",
          "readOnly": true
        },
        "onTheWay": {
          "type": "string",
          "description": "Quantity on submitted purchase orders that have not yet arrived.\nComputed from open purchase order items.",
          "readOnly": true
        },
        "available": {
          "type": "string",
          "description": "Quantity available for new orders: `onHand` minus promised. Can be\nnegative when more stock is committed than is on hand.",
          "readOnly": true
        }
      },
      "description": "One stock entry: the inventory for a single product variant row at a\nsingle colour (or colourless) — the atomic unit of inventory in the\nsystem. Quantities are for that one row+colour combination alone; the API\nnever reports cumulative product-level totals. Sum a product's entries to\ncompute them. Each entry carries its variant's identifying attributes\n(thickness and name/value pairs), so entries are tellable apart without a\ncatalogue lookup."
    },
    "factory.api.v1.inventory.StockLevelInput": {
      "type": "object",
      "properties": {
        "productRowId": {
          "type": "string",
          "description": "The product variant row to set stock for. Must belong to the product\nidentified in the request."
        },
        "colour": {
          "type": "string",
          "description": "The colour to set stock for. Required for colour-tracked products (one\nof the row's `colourOptions`); leave empty for non-colour-tracked products."
        },
        "onHand": {
          "type": "string",
          "description": "The on-hand quantity to record, as a decimal string. Replaces the\ncurrent value."
        }
      },
      "description": "A stock level to set: identifies a product row and optional colour, and\nthe on-hand quantity to record.",
      "required": [
        "productRowId",
        "onHand"
      ]
    },
    "factory.api.v1.quotes.AddQuoteLineItemResponse": {
      "type": "object",
      "properties": {
        "quote": {
          "$ref": "#/definitions/factory.api.v1.quotes.models.Quote",
          "description": "The quote's id and recomputed totals (`subtotal`, `taxAmount`, `total`,\n`totalCost`, `margin`, `labourTotal`). No other field is populated —\n`lines` is empty and `isSubmitted` is absent regardless of the quote's\nstate — so read the quote with GetQuote for the new line's id and\nposition."
        },
        "drawings": {
          "type": "array",
          "items": {
            "type": "object",
            "$ref": "#/definitions/factory.api.v1.salesdocuments.DrawingIdMapping"
          },
          "description": "When the added line was a flashing line: the server identities of its\ncreated drawings (your `tempId` paired with the assigned `drawingId`, the\nother sides in the order you sent them) — use them to address\nUploadDrawingSvg. Empty for every other line kind."
        }
      },
      "description": "Result of adding a line item to a quote: the quote's recomputed totals."
    },
    "factory.api.v1.quotes.CreateQuoteRequest": {
      "type": "object",
      "properties": {
        "customerId": {
          "type": "string",
          "description": "The customer's unique id, as returned by the Customers API. Exactly one of\n`customerId` or `companyName` must be set; in this version only\n`customerId` is accepted."
        },
        "companyName": {
          "type": "string",
          "description": "The customer's company name. Not yet supported: a request that sets\n`companyName` instead of `customerId` is rejected with HTTP 501\n(not_implemented). Resolve the name with ListCustomers and send\n`customerId`."
        },
        "lines": {
          "type": "array",
          "items": {
            "type": "object",
            "$ref": "#/definitions/factory.api.v1.salesdocuments.SalesLine"
          },
          "description": "The quote's line items. Each line is one of the supported line types\n(on-the-fly, catalogue, labour, notes, flashing, or product kit)."
        },
        "adjustments": {
          "type": "array",
          "items": {
            "type": "object",
            "$ref": "#/definitions/factory.api.v1.salesdocuments.Adjustment"
          },
          "description": "Quote-level fees, discounts, and markups applied across the whole quote."
        },
        "isSubmitted": {
          "type": "boolean",
          "description": "Finalise and send the quote immediately (true) or save it as a draft (false)."
        },
        "fulfilmentMethod": {
          "$ref": "#/definitions/factory.api.v1.salesdocuments.FulfilmentMethod",
          "description": "How the customer would receive the order if the quote is accepted: pickup,\ndelivery, or installation. Determines which address below applies."
        },
        "billingAddress": {
          "$ref": "#/definitions/factory.api.v1.Address",
          "description": "Billing address for the quote."
        },
        "deliveryAddress": {
          "$ref": "#/definitions/factory.api.v1.Address",
          "description": "Delivery address. Used only when `fulfilmentMethod` is DELIVERY."
        },
        "installAddress": {
          "$ref": "#/definitions/factory.api.v1.Address",
          "description": "Installation address. Used only when `fulfilmentMethod` is INSTALL."
        },
        "reference": {
          "type": "string",
          "description": "Free-text external reference for this quote — e.g. the quote id or purchase\norder (PO) number from your own system. Shown as \"PO #\" in the Factory app\nand as \"PO\" on the quote and order documents. Stored for lookup and audit;\ndistinct from `requestId` (the retry key) and from the read-only\n`orderNumber` (Factory's own number for the quote)."
        },
        "customFields": {
          "type": "object",
          "description": "Values for your account's custom-defined sales-document fields, keyed by\neach field's configured key. Unknown keys, keys of other modules, and\nvalues that don't match the field's type are rejected. Send number and\ncurrency values as numbers or decimal strings, checkboxes as booleans,\nmulti-selects as arrays of strings, and everything else as strings.\nSelect and multi-select values must be option keys from the field's\ndefinition, never display labels; discover your account's field keys and\noptions with GET /v1/company/custom-fields. Formula fields are computed\nand cannot be written."
        },
        "requiredAt": {
          "type": "string",
          "format": "date-time",
          "description": "The date the customer needs the order by, if the quote is accepted."
        },
        "contact": {
          "$ref": "#/definitions/factory.api.v1.salesdocuments.Contact",
          "description": "The point-of-contact person for this quote, distinct from the customer."
        },
        "notes": {
          "type": "string",
          "description": "Free-text notes about the quote."
        },
        "quotedAt": {
          "type": "string",
          "format": "date-time",
          "description": "The date the quote was issued. If omitted, the server sets it."
        },
        "tax": {
          "$ref": "#/definitions/factory.common.v1.TaxDetail",
          "description": "The tax to apply, as a rate/code/jurisdiction descriptor. Optional and\nadvisory in this version: the server applies your account's configured tax\nsettings and echoes the values actually applied (`tax` and `taxAmount` on\nthe Quote) — compare them to detect an override."
        },
        "requestId": {
          "type": "string",
          "description": "Optional idempotency key for this request: an opaque, client-generated\nUUID, scoped to the API key that sends it. Requests carrying the same key\nare executed at most once, so a retry can never create a second copy. Any\nrepeat is rejected with HTTP 409: if the original request completed, the\nbody's `resourceId` carries the id it created; if its outcome is\nstill unknown, or the key is reused with a materially different body,\nverify with a read before retrying with a fresh key. Keys are retained\nfor at least 24 hours. If the idempotency store is unavailable, requests\ncarrying a key are rejected with HTTP 503 (requests without a key are\nunaffected). Omit the key for no idempotency guarantee."
        },
        "labelIds": {
          "type": "array",
          "items": {
            "type": "string"
          },
          "description": "Labels to attach to the quote as part of creating it, by id. Every id\nmust exist in your company's label set (discover them with\nGET /v1/company/labels): an unknown id fails the whole call with HTTP\n404 before the quote is created. Labelling happens with the create but\nnot atomically inside it — in the rare case the quote is created and\nlabelling then fails, the error names the created quote id and the quote\nexists WITHOUT labels; attach them with SetQuoteLabels. The created\nquote echoes the attached labels. Duplicate ids are rejected."
        },
        "fields": {
          "type": "string",
          "description": "Comma-separated response fields to include, using camelCase JSON names\n(e.g. `orderId,total.amountMicros`). Dot paths reach into nested objects\nand map transparently across arrays. Paths are relative to the resource,\nnot the response envelope; envelope keys are always preserved. Only 2xx\nJSON responses are filtered; error bodies pass through unmodified. Unknown\nnames are silently ignored. Takes precedence over `excludeFields` when\nboth are provided."
        },
        "excludeFields": {
          "type": "string",
          "description": "Comma-separated response fields to exclude, using camelCase JSON names.\nDot paths and array-transparency work the same as `fields`. Paths are\nrelative to the resource, not the response envelope. Only 2xx JSON\nresponses are filtered; error bodies pass through unmodified. Ignored\nwhen `fields` is also provided."
        }
      },
      "description": "Input for creating a quote: the customer, line items, adjustments, addresses,\nand draft/sent option. Quote totals are derived by the server, not supplied\nhere."
    },
    "factory.api.v1.quotes.CreateQuoteResponse": {
      "type": "object",
      "properties": {
        "quote": {
          "$ref": "#/definitions/factory.api.v1.quotes.models.Quote",
          "description": "The created quote, including server-set fields and assigned identifiers."
        },
        "drawings": {
          "type": "array",
          "items": {
            "type": "object",
            "$ref": "#/definitions/factory.api.v1.salesdocuments.DrawingIdMapping"
          },
          "description": "The server identities of the flashing drawings created by this call, one\nentry per flashing line in the order you sent them, each pairing your\n`tempId` with the assigned `drawingId` — use them to address\nUploadDrawingSvg. Only flashing lines carry drawings; empty when the\nquote had no flashing lines."
        }
      },
      "description": "Result of creating a quote: the created quote with all server-assigned fields\npopulated."
    },
    "factory.api.v1.quotes.DeleteAttachmentResponse": {
      "type": "object",
      "description": "Result of removing an attachment. Empty on success."
    },
    "factory.api.v1.quotes.DeleteQuoteLineItemResponse": {
      "type": "object",
      "properties": {
        "quote": {
          "$ref": "#/definitions/factory.api.v1.quotes.models.Quote",
          "description": "The updated quote, with the line removed and totals recomputed."
        }
      },
      "description": "Result of removing a line item: the quote with recomputed totals."
    },
    "factory.api.v1.quotes.GetAttachmentResponse": {
      "type": "object",
      "properties": {
        "attachment": {
          "$ref": "#/definitions/factory.api.v1.collaborate.Attachment",
          "description": "The requested attachment, with a fresh presigned download URL."
        }
      },
      "description": "Result of retrieving a single attachment."
    },
    "factory.api.v1.quotes.GetQuoteResponse": {
      "type": "object",
      "properties": {
        "quote": {
          "$ref": "#/definitions/factory.api.v1.quotes.models.Quote",
          "description": "The requested quote."
        }
      },
      "description": "Result of retrieving a single quote."
    },
    "factory.api.v1.quotes.ListAttachmentsResponse": {
      "type": "object",
      "properties": {
        "attachments": {
          "type": "array",
          "items": {
            "type": "object",
            "$ref": "#/definitions/factory.api.v1.collaborate.Attachment"
          },
          "description": "The attachments in this page, oldest first."
        },
        "nextPageToken": {
          "type": "string",
          "description": "Token to pass as `pageToken` to fetch the next page; empty when there are\nno more results."
        }
      },
      "description": "A page of a quote's attachments."
    },
    "factory.api.v1.quotes.ListMessagesResponse": {
      "type": "object",
      "properties": {
        "messages": {
          "type": "array",
          "items": {
            "type": "object",
            "$ref": "#/definitions/factory.api.v1.collaborate.Message"
          },
          "description": "The messages in this page, oldest first."
        },
        "nextPageToken": {
          "type": "string",
          "description": "Token to pass as `pageToken` to fetch the next page; empty when there are\nno more results."
        }
      },
      "description": "A page of a quote's conversation."
    },
    "factory.api.v1.quotes.ListQuotesResponse": {
      "type": "object",
      "properties": {
        "quotes": {
          "type": "array",
          "items": {
            "type": "object",
            "$ref": "#/definitions/factory.api.v1.quotes.models.Quote"
          },
          "description": "The quotes in this page."
        },
        "nextPageToken": {
          "type": "string",
          "description": "Token to pass as `pageToken` to fetch the next page; empty when there are no\nmore results."
        }
      },
      "description": "A page of quotes."
    },
    "factory.api.v1.quotes.PostMessageResponse": {
      "type": "object",
      "properties": {
        "message": {
          "$ref": "#/definitions/factory.api.v1.collaborate.Message",
          "description": "The created message."
        }
      },
      "description": "Result of posting a message: the created conversation entry."
    },
    "factory.api.v1.quotes.QueryQuotesResponse": {
      "type": "object",
      "properties": {
        "quotes": {
          "type": "array",
          "items": {
            "type": "object",
            "$ref": "#/definitions/factory.api.v1.quotes.models.Quote"
          },
          "description": "The quotes in this page, oldest change first. Use the last quote's update\ntime as the `updatedSince` for your next call."
        },
        "nextPageToken": {
          "type": "string",
          "description": "Token to pass as `pageToken` to fetch the next page; empty when there are no\nmore results."
        }
      },
      "description": "A page of quotes from the change feed, ordered by when each quote last changed\n(oldest first). The lower bound (`updatedSince`) is inclusive, so the quote you\ncheckpoint on reappears as the first item of the next page — dedupe on `quoteId`\n+ `lastUpdatedAt`, or skip ids you have already seen at the window boundary."
    },
    "factory.api.v1.quotes.QuoteService.AddQuoteLineItemBody": {
      "type": "object",
      "properties": {
        "line": {
          "$ref": "#/definitions/factory.api.v1.salesdocuments.SalesLine",
          "description": "The line item to add: one of the supported line types (on-the-fly,\ncatalogue, labour, notes, or product kit)."
        },
        "requestId": {
          "type": "string",
          "description": "Optional idempotency key for this request: an opaque, client-generated\nUUID, scoped to the API key that sends it. Requests carrying the same key\nare executed at most once, so a retry can never create a second copy. Any\nrepeat is rejected with HTTP 409: if the original request completed, the\nbody's `resourceId` carries the id it created; if its outcome is\nstill unknown, or the key is reused with a materially different body,\nverify with a read before retrying with a fresh key. Keys are retained\nfor at least 24 hours. If the idempotency store is unavailable, requests\ncarrying a key are rejected with HTTP 503 (requests without a key are\nunaffected). Omit the key for no idempotency guarantee."
        },
        "fields": {
          "type": "string",
          "description": "Comma-separated response fields to include, using camelCase JSON names\n(e.g. `orderId,total.amountMicros`). Dot paths reach into nested objects\nand map transparently across arrays. Paths are relative to the resource,\nnot the response envelope; envelope keys are always preserved. Only 2xx\nJSON responses are filtered; error bodies pass through unmodified. Unknown\nnames are silently ignored. Takes precedence over `excludeFields` when\nboth are provided."
        },
        "excludeFields": {
          "type": "string",
          "description": "Comma-separated response fields to exclude, using camelCase JSON names.\nDot paths and array-transparency work the same as `fields`. Paths are\nrelative to the resource, not the response envelope. Only 2xx JSON\nresponses are filtered; error bodies pass through unmodified. Ignored\nwhen `fields` is also provided."
        }
      },
      "description": "Input for adding a single line item to an existing quote (build a quote up\nover time, one line per call).",
      "required": [
        "line"
      ]
    },
    "factory.api.v1.quotes.QuoteService.PostMessageBody": {
      "type": "object",
      "properties": {
        "text": {
          "type": "string",
          "description": "The message text, as plain text. Required."
        },
        "requestId": {
          "type": "string",
          "description": "Optional idempotency key for this request: an opaque, client-generated\nUUID, scoped to the API key that sends it. Requests carrying the same key\nare executed at most once, so a retry can never create a second copy. Any\nrepeat is rejected with HTTP 409: if the original request completed, the\nbody's `resourceId` carries the id it created; if its outcome is\nstill unknown, or the key is reused with a materially different body,\nverify with a read before retrying with a fresh key. Keys are retained\nfor at least 24 hours. If the idempotency store is unavailable, requests\ncarrying a key are rejected with HTTP 503 (requests without a key are\nunaffected). Omit the key for no idempotency guarantee."
        },
        "fields": {
          "type": "string",
          "description": "Comma-separated response fields to include, using camelCase JSON names\n(e.g. `orderId,total.amountMicros`). Dot paths reach into nested objects\nand map transparently across arrays. Paths are relative to the resource,\nnot the response envelope; envelope keys are always preserved. Only 2xx\nJSON responses are filtered; error bodies pass through unmodified. Unknown\nnames are silently ignored. Takes precedence over `excludeFields` when\nboth are provided."
        },
        "excludeFields": {
          "type": "string",
          "description": "Comma-separated response fields to exclude, using camelCase JSON names.\nDot paths and array-transparency work the same as `fields`. Paths are\nrelative to the resource, not the response envelope. Only 2xx JSON\nresponses are filtered; error bodies pass through unmodified. Ignored\nwhen `fields` is also provided."
        }
      },
      "description": "Input for posting a text message to a quote's conversation.",
      "required": [
        "text"
      ]
    },
    "factory.api.v1.quotes.QuoteService.SetQuoteLabelsBody": {
      "type": "object",
      "properties": {
        "labelIds": {
          "type": "array",
          "items": {
            "type": "string"
          },
          "description": "The quote's desired complete label set. An empty list removes every\nlabel. Duplicate ids are rejected."
        },
        "fields": {
          "type": "string",
          "description": "Comma-separated response fields to include, using camelCase JSON names\n(e.g. `orderId,total.amountMicros`). Dot paths reach into nested objects\nand map transparently across arrays. Paths are relative to the resource,\nnot the response envelope; envelope keys are always preserved. Only 2xx\nJSON responses are filtered; error bodies pass through unmodified. Unknown\nnames are silently ignored. Takes precedence over `excludeFields` when\nboth are provided."
        },
        "excludeFields": {
          "type": "string",
          "description": "Comma-separated response fields to exclude, using camelCase JSON names.\nDot paths and array-transparency work the same as `fields`. Paths are\nrelative to the resource, not the response envelope. Only 2xx JSON\nresponses are filtered; error bodies pass through unmodified. Ignored\nwhen `fields` is also provided."
        }
      },
      "description": "Input for replacing the labels on a quote."
    },
    "factory.api.v1.quotes.QuoteService.UploadAttachmentBody": {
      "type": "object",
      "properties": {
        "file": {
          "type": "string",
          "format": "byte",
          "description": "The file content, up to 20 MB. Required."
        },
        "filename": {
          "type": "string",
          "description": "The file's name, including its extension. Required."
        },
        "contentType": {
          "type": "string",
          "description": "The file's media type (for example `image/png`). Optional; derived from\nthe filename when omitted."
        },
        "caption": {
          "type": "string",
          "description": "Optional plain-text caption. The upload always creates one conversation\nmessage carrying the file; the caption becomes that message's text,\nstored in the same HTML-escaped, `\u003cp\u003e`-wrapped form as a posted message."
        },
        "requestId": {
          "type": "string",
          "description": "Optional idempotency key for this request: an opaque, client-generated\nUUID, scoped to the API key that sends it. Requests carrying the same key\nare executed at most once, so a retry can never create a second copy. Any\nrepeat is rejected with HTTP 409: if the original request completed, the\nbody's `resourceId` carries the id it created; if its outcome is\nstill unknown, or the key is reused with a materially different body,\nverify with a read before retrying with a fresh key. Keys are retained\nfor at least 24 hours. If the idempotency store is unavailable, requests\ncarrying a key are rejected with HTTP 503 (requests without a key are\nunaffected). Omit the key for no idempotency guarantee."
        },
        "fields": {
          "type": "string",
          "description": "Comma-separated response fields to include, using camelCase JSON names\n(e.g. `orderId,total.amountMicros`). Dot paths reach into nested objects\nand map transparently across arrays. Paths are relative to the resource,\nnot the response envelope; envelope keys are always preserved. Only 2xx\nJSON responses are filtered; error bodies pass through unmodified. Unknown\nnames are silently ignored. Takes precedence over `excludeFields` when\nboth are provided."
        },
        "excludeFields": {
          "type": "string",
          "description": "Comma-separated response fields to exclude, using camelCase JSON names.\nDot paths and array-transparency work the same as `fields`. Paths are\nrelative to the resource, not the response envelope. Only 2xx JSON\nresponses are filtered; error bodies pass through unmodified. Ignored\nwhen `fields` is also provided."
        }
      },
      "description": "Input for attaching one file to a quote.",
      "required": [
        "file",
        "filename"
      ]
    },
    "factory.api.v1.quotes.QuoteService.UploadDrawingSvgBody": {
      "type": "object",
      "properties": {
        "svgs": {
          "type": "array",
          "items": {
            "type": "object",
            "$ref": "#/definitions/factory.api.v1.salesdocuments.DrawingSvg"
          },
          "description": "The flashing drawings and their rendered SVGs. At least one is required."
        },
        "fields": {
          "type": "string",
          "description": "Comma-separated response fields to include, using camelCase JSON names\n(e.g. `orderId,total.amountMicros`). Dot paths reach into nested objects\nand map transparently across arrays. Paths are relative to the resource,\nnot the response envelope; envelope keys are always preserved. Only 2xx\nJSON responses are filtered; error bodies pass through unmodified. Unknown\nnames are silently ignored. Takes precedence over `excludeFields` when\nboth are provided."
        },
        "excludeFields": {
          "type": "string",
          "description": "Comma-separated response fields to exclude, using camelCase JSON names.\nDot paths and array-transparency work the same as `fields`. Paths are\nrelative to the resource, not the response envelope. Only 2xx JSON\nresponses are filtered; error bodies pass through unmodified. Ignored\nwhen `fields` is also provided."
        }
      },
      "description": "Input for uploading rendered SVGs for a quote's flashing drawings (the only\nline kind that carries drawings).",
      "required": [
        "svgs"
      ]
    },
    "factory.api.v1.quotes.SetQuoteLabelsResponse": {
      "type": "object",
      "properties": {
        "labels": {
          "type": "array",
          "items": {
            "type": "object",
            "$ref": "#/definitions/factory.api.v1.salesdocuments.Label"
          },
          "description": "The quote's labels after the change, oldest attachment first."
        }
      },
      "description": "Result of replacing the labels on a quote: the resulting label set."
    },
    "factory.api.v1.quotes.UpdateQuoteLineItemResponse": {
      "type": "object",
      "properties": {
        "quote": {
          "$ref": "#/definitions/factory.api.v1.quotes.models.Quote",
          "description": "The updated quote, including the recomputed totals."
        }
      },
      "description": "Result of replacing a line item: the quote with recomputed totals."
    },
    "factory.api.v1.quotes.UploadAttachmentResponse": {
      "type": "object",
      "properties": {
        "message": {
          "$ref": "#/definitions/factory.api.v1.collaborate.Message",
          "description": "The created message, carrying the uploaded file as its single\nattachment (with its id and a presigned download URL)."
        }
      },
      "description": "Result of attaching a file: the created conversation entry."
    },
    "factory.api.v1.quotes.UploadDrawingSvgResponse": {
      "type": "object",
      "properties": {
        "results": {
          "type": "array",
          "items": {
            "type": "object",
            "$ref": "#/definitions/factory.api.v1.salesdocuments.DrawingSvgResult"
          },
          "description": "One result per submitted flashing drawing, in request order."
        }
      },
      "description": "Result of an SVG upload: one outcome per submitted flashing drawing."
    },
    "factory.api.v1.quotes.models.Quote": {
      "type": "object",
      "properties": {
        "quoteId": {
          "type": "string",
          "description": "The quote's unique id. Read-only.",
          "readOnly": true
        },
        "isSubmitted": {
          "type": "boolean",
          "description": "Whether the quote has been finalised and sent (true) or is still a draft\n(false)."
        },
        "customerId": {
          "type": "string",
          "description": "The id of the customer this quote belongs to. Always present. Read-only.",
          "readOnly": true
        },
        "customerCompanyName": {
          "type": "string",
          "description": "The customer's company name. Read-only.",
          "readOnly": true
        },
        "subtotal": {
          "$ref": "#/definitions/factory.common.v1.Money",
          "description": "Quote subtotal — the sum of line totals before tax. Derived by the server\nfrom the line items; read-only.",
          "readOnly": true
        },
        "taxAmount": {
          "$ref": "#/definitions/factory.common.v1.Money",
          "description": "Total tax for the quote, as an absolute money amount (the tax `amount`, not a\nrate). Derived by the server from your account's tax settings; read-only. See\n`tax` for the rate, code, and jurisdiction that produced it.",
          "readOnly": true
        },
        "total": {
          "$ref": "#/definitions/factory.common.v1.Money",
          "description": "Quote grand total. Derived by the server; read-only.",
          "readOnly": true
        },
        "margin": {
          "$ref": "#/definitions/factory.common.v1.Money",
          "description": "Quote margin, computed by the server from the line items. Read-only.",
          "readOnly": true
        },
        "totalCost": {
          "$ref": "#/definitions/factory.common.v1.Money",
          "description": "Total cost of the quote, computed by the server from the line items. Read-only.",
          "readOnly": true
        },
        "discountAmount": {
          "$ref": "#/definitions/factory.common.v1.Money",
          "description": "The quote-level discount total, rolled up from the per-line discounts and the\ndiscount entries in `adjustments`. Read-only.",
          "readOnly": true
        },
        "lines": {
          "type": "array",
          "items": {
            "type": "object",
            "$ref": "#/definitions/factory.api.v1.salesdocuments.SalesLine"
          },
          "description": "The quote's line items."
        },
        "adjustments": {
          "type": "array",
          "items": {
            "type": "object",
            "$ref": "#/definitions/factory.api.v1.salesdocuments.Adjustment"
          },
          "description": "Quote-level fees, discounts, and markups. At most one fee per quote is allowed."
        },
        "fulfilmentMethod": {
          "$ref": "#/definitions/factory.api.v1.salesdocuments.FulfilmentMethod",
          "description": "How the customer would receive the order if the quote is accepted: pickup,\ndelivery, or installation."
        },
        "billingAddress": {
          "$ref": "#/definitions/factory.api.v1.Address",
          "description": "Billing address for the quote."
        },
        "deliveryAddress": {
          "$ref": "#/definitions/factory.api.v1.Address",
          "description": "Delivery address. Applies when `fulfilmentMethod` is DELIVERY."
        },
        "installAddress": {
          "$ref": "#/definitions/factory.api.v1.Address",
          "description": "Installation address. Applies when `fulfilmentMethod` is INSTALL."
        },
        "deliveryFee": {
          "$ref": "#/definitions/factory.common.v1.Money",
          "description": "The delivery fee quoted on the order. Read-only.",
          "readOnly": true
        },
        "reference": {
          "type": "string",
          "description": "Free-text external reference, e.g. the quote id or purchase order (PO)\nnumber from your own system. Shown as \"PO #\" in the Factory app and as \"PO\"\non the quote and order documents."
        },
        "orderNumber": {
          "type": "string",
          "format": "int64",
          "description": "Factory's number for this quote. Assigned by the server when the quote is\ncreated, sequential within your company, and never changed — the quote keeps\nthis number if it becomes an order. Shown as \"Quote #\" on the quote PDF and\nemail, as \"Order #\" in the Factory app (order list, order page, workflow\nboard) and on invoices and delivery documents, and used as the document\nreference in the accounting integrations. Read-only.",
          "readOnly": true
        },
        "customFields": {
          "type": "object",
          "description": "The document's custom-field values, keyed by each field's configured key."
        },
        "contact": {
          "$ref": "#/definitions/factory.api.v1.salesdocuments.Contact",
          "description": "The quote's point-of-contact person."
        },
        "notes": {
          "type": "string",
          "description": "Free-text notes about the quote."
        },
        "requiredAt": {
          "type": "string",
          "format": "date-time",
          "description": "The date the customer needs the order by, if the quote is accepted."
        },
        "quotedAt": {
          "type": "string",
          "format": "date-time",
          "description": "The date the quote was issued."
        },
        "createdAt": {
          "type": "string",
          "format": "date-time",
          "description": "When the quote was created. Read-only.",
          "readOnly": true
        },
        "submittedAt": {
          "type": "string",
          "format": "date-time",
          "description": "When the quote was sent (finalised). Read-only.",
          "readOnly": true
        },
        "lastUpdatedAt": {
          "type": "string",
          "format": "date-time",
          "description": "When the quote was last updated. Read-only.",
          "readOnly": true
        },
        "createdByUserId": {
          "type": "string",
          "description": "The id of the user who created the quote, empty when unset. Resolve the user\nvia the Users API (which can return users who have since left). Read-only.",
          "readOnly": true
        },
        "submittedByUserId": {
          "type": "string",
          "description": "The id of the user who sent the quote, empty when unset. Resolve the user via\nthe Users API (which can return users who have since left). Read-only.",
          "readOnly": true
        },
        "tax": {
          "$ref": "#/definitions/factory.common.v1.TaxDetail",
          "description": "The tax applied to the quote: rate, code, and jurisdiction. Reflects your\naccount's tax settings — the values actually applied — and is the companion\ndescriptor to the `taxAmount` amount. Read-only.",
          "readOnly": true
        },
        "labels": {
          "type": "array",
          "items": {
            "type": "object",
            "$ref": "#/definitions/factory.api.v1.salesdocuments.Label"
          },
          "description": "The labels attached to this quote, oldest attachment first. Present on\ncreate responses when `labelIds` are supplied; absent from line-write\nresponses.",
          "readOnly": true
        },
        "labourTotal": {
          "$ref": "#/definitions/factory.common.v1.Money",
          "description": "The total charged for labour on the quote: the sum of the labour line\ntotals, including labour components inside product kits. Derived by the\nserver from the line items on every write; read-only.",
          "readOnly": true
        }
      },
      "description": "A quote, as returned by read endpoints. Includes the quote header, its line\nitems, adjustments, addresses, contact, and server-derived totals. The same\nshape is returned whether the quote was just created or fetched later."
    },
    "factory.api.v1.salesdocuments.Adjustment": {
      "type": "object",
      "properties": {
        "type": {
          "$ref": "#/definitions/factory.api.v1.salesdocuments.AdjustmentType",
          "description": "The kind of adjustment: a fee, discount, or markup. Required."
        },
        "title": {
          "type": "string",
          "description": "A label describing the adjustment."
        },
        "amount": {
          "$ref": "#/definitions/factory.common.v1.Money",
          "description": "The adjustment as a fixed money amount. Exactly one of `amount` or\n`percent` must be set. A fee must use this form."
        },
        "percent": {
          "type": "string",
          "description": "The adjustment as a percentage (for example, \"10.00\"). Exactly one of\n`amount` or `percent` must be set. For a discount, the percentage is\ntaken of the lines' subtotal (see AdjustmentType); a fee given as a\npercent is rejected with HTTP 400 (validation_failure)."
        }
      },
      "description": "A fee, discount, or markup applied across the whole order or quote.",
      "required": [
        "type"
      ]
    },
    "factory.api.v1.salesdocuments.AdjustmentType": {
      "type": "string",
      "enum": [
        "ADJUSTMENT_TYPE_UNSPECIFIED",
        "ADJUSTMENT_TYPE_FEE",
        "ADJUSTMENT_TYPE_DISCOUNT",
        "ADJUSTMENT_TYPE_MARKUP"
      ],
      "default": "ADJUSTMENT_TYPE_UNSPECIFIED",
      "description": "The kind of order- or quote-level adjustment: a fee, a discount, or a markup.\nOnly fees and discounts change the document's totals.\n\n - ADJUSTMENT_TYPE_UNSPECIFIED: Default, unset value. Not a valid choice when setting an adjustment.\n - ADJUSTMENT_TYPE_FEE: A fee added to the order or quote after any discounts, and taxed at the\naccount's rate. It must be a fixed `amount`, not a `percent`, and at most\none fee is allowed per order or quote.\n - ADJUSTMENT_TYPE_DISCOUNT: A discount subtracted from the lines' subtotal before tax and before any\nfee. A `percent` discount is taken of the lines' subtotal as it stands\nwhen the discount is applied; with several discounts, each applies to the\nsubtotal left by the earlier ones, in the order they were added.\n - ADJUSTMENT_TYPE_MARKUP: A markup recorded against the order or quote. It is stored and returned on\nreads but changes no total: to mark up a document, raise its line prices.\nAny number of markups may be recorded."
    },
    "factory.api.v1.salesdocuments.AngleCell": {
      "type": "object",
      "properties": {
        "angle": {
          "type": "number",
          "format": "double",
          "description": "The angle at this vertex, in degrees. Omitted when no angle is set."
        },
        "hidden": {
          "type": "boolean",
          "description": "Whether this vertex's angle label is hidden. Omitted when not set."
        }
      },
      "description": "The angle entry for a single point (vertex) in the drawing."
    },
    "factory.api.v1.salesdocuments.AngleSet": {
      "type": "object",
      "properties": {
        "values": {
          "type": "object",
          "additionalProperties": {
            "$ref": "#/definitions/factory.api.v1.salesdocuments.AngleCell"
          },
          "description": "The angle entry for each vertex, keyed by point id."
        },
        "positions": {
          "type": "object",
          "additionalProperties": {
            "$ref": "#/definitions/factory.api.v1.salesdocuments.LabelPosition"
          },
          "description": "The label box for each vertex's angle label, keyed by point id. Optional:\nFactory places any label without a box itself."
        }
      },
      "description": "The collection of vertex angles and their label positions for a drawing."
    },
    "factory.api.v1.salesdocuments.Arrow": {
      "type": "object",
      "properties": {
        "x": {
          "type": "number",
          "format": "double",
          "description": "The x coordinate of the arrow, in canvas space. Omitted until placed."
        },
        "y": {
          "type": "number",
          "format": "double",
          "description": "The y coordinate of the arrow, in canvas space. Omitted until placed."
        },
        "angle": {
          "type": "number",
          "format": "double",
          "description": "The direction the arrow points, in degrees, turning clockwise on screen\nfrom 0 = left: 90 points up, 180 right, 270 down."
        }
      },
      "description": "The arrow that indicates the front-facing direction of the flashing."
    },
    "factory.api.v1.salesdocuments.CatalogueLine": {
      "type": "object",
      "properties": {
        "productId": {
          "type": "string",
          "description": "The id of the catalogue product for this line. Required, and must match an\nexisting catalogue product; an unknown id rejects the whole request."
        },
        "productRowId": {
          "type": "string",
          "description": "Optional id of a specific row within the catalogue product. If set, it must\nresolve."
        },
        "rowPriceId": {
          "type": "string",
          "description": "Optional id selecting the row's price at one of your price levels — the\n`rowPriceId` returned by the catalogue (not the price level's own id).\nIf set, it must resolve."
        },
        "productName": {
          "type": "string",
          "description": "Display name for the line."
        },
        "productDescription": {
          "type": "string",
          "description": "Longer description for the line."
        },
        "categoryName": {
          "type": "string",
          "description": "Category name for the line."
        },
        "priceName": {
          "type": "string",
          "description": "Price name shown for the line."
        },
        "params": {
          "type": "array",
          "items": {
            "type": "object"
          },
          "description": "Product parameters as a JSON array of {name, value} entries (for example\n[{\"name\": \"Colour\", \"value\": \"Monument\"}]). The colour is taken from the\n\"Colour\" parameter and validated against the product's available colours."
        },
        "attributes": {
          "type": "array",
          "items": {
            "type": "object"
          },
          "description": "Additional named attributes for the line, as a JSON array of {name, value}\nentries (for example {name: \"Size\", value: \"76x38mm\"}). An entry may also\ncarry flags that hide it from specific documents or views, such as the\ninvoice PDF or the customer view."
        },
        "pricing": {
          "$ref": "#/definitions/factory.api.v1.salesdocuments.LinePricing",
          "description": "Pricing for this line. The catalogue does not price the line; you supply it."
        }
      },
      "description": "A catalogue line: a line for a product from your catalogue. You provide the\ncatalogue product id and the line's pricing.",
      "required": [
        "productId"
      ]
    },
    "factory.api.v1.salesdocuments.Contact": {
      "type": "object",
      "properties": {
        "name": {
          "type": "string",
          "description": "The contact person's name."
        },
        "email": {
          "type": "string",
          "description": "The contact's email address. Optional; when provided, it must be a valid\nemail address."
        },
        "phone": {
          "type": "string",
          "description": "The contact's landline phone number."
        },
        "mobile": {
          "type": "string",
          "description": "The contact's mobile number, normalized to E.164 format (e.g. +61400000000)."
        }
      },
      "description": "The person to contact about this order or quote. Belongs to the order or quote\nitself, separate from the customer and from any address."
    },
    "factory.api.v1.salesdocuments.DrawingIdMapping": {
      "type": "object",
      "properties": {
        "tempId": {
          "type": "string",
          "description": "The `tempId` you supplied on the drawing, echoed verbatim; empty when you\ndid not supply one (position in the list still identifies the drawing)."
        },
        "drawingId": {
          "type": "string",
          "description": "The server-assigned id of the drawing."
        },
        "otherSides": {
          "type": "array",
          "items": {
            "type": "object",
            "$ref": "#/definitions/factory.api.v1.salesdocuments.DrawingIdMapping"
          },
          "description": "The identities of the drawing's other sides, in the order you sent them."
        }
      },
      "description": "The server identity assigned to one flashing drawing you sent in a write,\npaired with the `tempId` you supplied. Only flashing lines create drawings;\nthe writes that carry them (CreateQuote, CreateOrder, AddQuoteLineItem)\nreturn one entry per flashing line, in the order you sent the lines — use\nit to address UploadDrawingSvg without re-reading the document."
    },
    "factory.api.v1.salesdocuments.DrawingSide": {
      "type": "string",
      "enum": [
        "DRAWING_SIDE_UNSPECIFIED",
        "DRAWING_SIDE_FAR",
        "DRAWING_SIDE_NEAR"
      ],
      "default": "DRAWING_SIDE_UNSPECIFIED",
      "description": "Which face of the flashing a drawing represents. When not specified, a\nflashing line's main `drawing` is the far side and each of its `otherSides`\nis the near side.\n\n - DRAWING_SIDE_UNSPECIFIED: Default, unset value.\n - DRAWING_SIDE_FAR: The far side of the flashing.\n - DRAWING_SIDE_NEAR: The near side of the flashing."
    },
    "factory.api.v1.salesdocuments.DrawingSvg": {
      "type": "object",
      "properties": {
        "drawingId": {
          "type": "string",
          "description": "The id of the flashing drawing this SVG belongs to. Required."
        },
        "svg": {
          "type": "string",
          "format": "byte",
          "description": "The rendered SVG image content, sent base64-encoded in JSON. The decoded\nimage must be at most 2,000,000 bytes (the base64 text is about a third\nlarger). Required."
        }
      },
      "description": "One flashing drawing's rendered SVG image.",
      "required": [
        "drawingId",
        "svg"
      ]
    },
    "factory.api.v1.salesdocuments.DrawingSvgResult": {
      "type": "object",
      "properties": {
        "drawingId": {
          "type": "string",
          "description": "The drawing this result refers to.",
          "readOnly": true
        },
        "uploaded": {
          "type": "boolean",
          "description": "Whether the SVG was stored successfully.",
          "readOnly": true
        },
        "svgUrl": {
          "type": "string",
          "description": "On success, a presigned URL to the stored SVG.",
          "readOnly": true
        },
        "error": {
          "type": "string",
          "description": "On failure, the reason: the drawing isn't part of this order or quote, isn't\nan image/svg+xml, or has been deleted.",
          "readOnly": true
        }
      },
      "description": "The outcome of uploading one flashing drawing's SVG."
    },
    "factory.api.v1.salesdocuments.Finish": {
      "type": "object",
      "properties": {
        "type": {
          "$ref": "#/definitions/factory.api.v1.salesdocuments.FinishType",
          "description": "The kind of end treatment."
        },
        "size": {
          "type": "number",
          "format": "double",
          "description": "The size of the end treatment in millimetres. It counts toward the\nflashing's `totalGirth`."
        },
        "label": {
          "type": "string",
          "description": "The display label for this finish: the type's code followed by the size,\ne.g. \"cf10\" for a 10 mm crush fold or \"oh25\" for a 25 mm open hook."
        },
        "flip": {
          "type": "boolean",
          "description": "Which side of the edge the finish folds to. Looking from the finished end\nalong its edge (screen coordinates, y pointing down), `false` puts the\nfold on the right-hand side and `true` on the left. A feathered edge is\ndrawn mirrored relative to the other types."
        },
        "position": {
          "$ref": "#/definitions/factory.api.v1.salesdocuments.LabelPosition",
          "description": "Optional bounding box for this finish's label."
        }
      },
      "description": "An end treatment applied to a point on a flashing edge (for example a hook or\nfold). Optional on a point."
    },
    "factory.api.v1.salesdocuments.FinishType": {
      "type": "string",
      "enum": [
        "FINISH_TYPE_UNSPECIFIED",
        "FINISH_TYPE_CRUSH_FOLD",
        "FINISH_TYPE_OPEN_HOOK",
        "FINISH_TYPE_FEATHER",
        "FINISH_TYPE_DRIP_EDGE"
      ],
      "default": "FINISH_TYPE_UNSPECIFIED",
      "description": "The end-treatment applied to a flashing edge. Each type has a short code\nused in the finish's `label`, adds its `size` to the flashing's total girth,\nand counts a fixed number of bends.\n\n - FINISH_TYPE_UNSPECIFIED: Unspecified end treatment.\n - FINISH_TYPE_CRUSH_FOLD: A crush fold. Code \"cf\"; counts 2 bends (or the account's\n`crushFoldBendCount` when set).\n - FINISH_TYPE_OPEN_HOOK: An open hook. Code \"oh\"; counts 2 bends.\n - FINISH_TYPE_FEATHER: A feathered edge. Code \"fe\"; counts 1 bend.\n - FINISH_TYPE_DRIP_EDGE: A drip edge. Code \"de\"; counts 1 bend."
    },
    "factory.api.v1.salesdocuments.FlashingDrawing": {
      "type": "object",
      "properties": {
        "drawingId": {
          "type": "string",
          "description": "Server-assigned unique id of the drawing. Read-only output.",
          "readOnly": true
        },
        "tempId": {
          "type": "string",
          "description": "A client-assigned correlation id supplied when creating the drawing. The\nwrite that creates the drawing returns it paired with the assigned\n`drawingId` in the response's `drawings` mapping (CreateQuote,\nCreateOrder, AddQuoteLineItem) — use that to address UploadDrawingSvg.\nIt must be unique across every drawing in the request, other sides\nincluded; a repeated id is rejected with HTTP 400 (validation_failure).\nOmit it and the server assigns unique ids for you. It is not stored, so\nreads do not return it."
        },
        "drawingNumber": {
          "type": "integer",
          "format": "int32",
          "description": "The drawing's display number within the order or quote. Not assigned by\nthe server: it is stored as sent (0 when omitted), and reads and printed\ndocuments order drawings by it, so number them 0, 1, 2… in line order."
        },
        "side": {
          "$ref": "#/definitions/factory.api.v1.salesdocuments.DrawingSide",
          "description": "Which face of the flashing this drawing represents. When not specified, a\nline's main `drawing` is the far side and each of its `otherSides` is the\nnear side."
        },
        "isFreeDrawing": {
          "type": "boolean",
          "description": "Whether the edge sizes in `lines.values` are the drawing's dimensions.\nSend `true`: when false, Factory measures each edge from the points'\ncanvas coordinates instead, so girth, bends and the displayed sizes come\nfrom the sketch rather than from the sizes you supplied."
        },
        "isLargeBoxSize": {
          "type": "boolean",
          "description": "Whether the drawing uses the large box size."
        },
        "isDeleted": {
          "type": "boolean",
          "description": "Whether the drawing has been deleted."
        },
        "points": {
          "type": "object",
          "additionalProperties": {
            "$ref": "#/definitions/factory.api.v1.salesdocuments.Point"
          },
          "description": "The vertices of the drawing, keyed by point id. Record each edge at both\nends: `p1.vectors` lists `p2` exactly when `p2.connect` lists `p1`.\nRequired when the order or quote is submitted."
        },
        "lines": {
          "$ref": "#/definitions/factory.api.v1.salesdocuments.LineSet",
          "description": "The edge sizes between points in millimetres, with their optional label\npositions. Give every edge a size — Factory counts the girth of a drawing\nwith a missing size as 0. When omitted, the drawing is stored with an\nempty set and reads return `lines` with empty `values` and `positions`."
        },
        "angles": {
          "$ref": "#/definitions/factory.api.v1.salesdocuments.AngleSet",
          "description": "The vertex angles in degrees, with their optional label positions. When\nomitted, the drawing is stored with an empty set and reads return\n`angles` with empty `values` and `positions`."
        },
        "annotations": {
          "type": "object",
          "additionalProperties": {
            "$ref": "#/definitions/factory.api.v1.salesdocuments.TextBox"
          },
          "description": "Free-text annotations on the drawing, keyed by annotation id."
        },
        "frontArrow": {
          "$ref": "#/definitions/factory.api.v1.salesdocuments.Arrow",
          "description": "The arrow marking the front face of the flashing. Optional."
        },
        "squareAngle": {
          "$ref": "#/definitions/factory.api.v1.salesdocuments.SquareAngle",
          "description": "The right-angle (90°) marker placed on the drawing."
        },
        "svgUrl": {
          "type": "string",
          "description": "A presigned URL to the rendered SVG image of the drawing. Read-only output;\npopulated after the SVG is uploaded via UploadDrawingSvg on OrderService or\nQuoteService.",
          "readOnly": true
        }
      },
      "description": "A flashing drawing: its identity, metadata, geometry, and the URL of its\nrendered SVG image. Carried only by flashing lines (`drawing` / `otherSides`\non FlashingLine) — drawings describe a flashing's folded profile and are not a\ngeneral-purpose image or attachment type. Geometry is sent in the same shape\nthe drawing tool holds it: `points` are the vertices, joined by the edges\nlisted in each point's `vectors` and `connect`; `lines.values` gives each\nedge's real size in millimetres; `angles.values` gives the angle at each\nvertex. Point coordinates only lay out the sketch — the sizes in `lines`\nare the dimensions. A drawing whose `points`, `lines` or `angles` exceeds\nabout 15,000 characters of JSON is rejected with HTTP 400\n(validation_failure)."
    },
    "factory.api.v1.salesdocuments.FlashingLine": {
      "type": "object",
      "properties": {
        "templateId": {
          "type": "string",
          "description": "The id of the flashing material or template. Required when the order or\nquote is submitted."
        },
        "productName": {
          "type": "string",
          "description": "The display name of the flashing material."
        },
        "priceLevel": {
          "type": "string",
          "description": "The name of the price level applied to this line (for example \"A\"), not a\nprice level id. Required when the order or quote is submitted; an empty\nvalue is stored as \"A\"."
        },
        "colour": {
          "type": "string",
          "description": "The flashing colour. Required when the order or quote is submitted, and\nvalidated against the colours available for the template when `templateId`\nis set; without a template it is stored unchecked."
        },
        "thickness": {
          "type": "string",
          "description": "The material thickness, as a decimal string in millimetres (for example\n\"0.55\"). Required when the order or quote is submitted. Send the thickness\nof the selected template (`templateId`) — each thickness is a distinct\ntemplate. It is stored as sent and not compared with the template, so a\nmismatch is saved silently."
        },
        "bends": {
          "type": "string",
          "description": "The number of bends in the flashing, as a decimal string. Count one bend\nfor each point that joins two edges, plus the bends each finish adds\n(crush fold 2, open hook 2, feather 1, drip edge 1; the account's\n`crushFoldBendCount` setting, when set, replaces the crush-fold count).\nWith `otherSides`, take the count from the side with the largest girth.\nRequired when the order or quote is submitted, and must then be greater\nthan zero. Stored as sent; not checked against the drawing."
        },
        "totalGirth": {
          "type": "string",
          "description": "The total girth of the flashing in millimetres, as a decimal string: the\nsum of every edge size in the drawing's `lines.values` (hidden edges\nexcluded) plus the `size` of every finish. Edges of 110, 320 and 10 with a\n10 mm crush fold give \"450\". With `otherSides`, use the side with the\nlargest girth. Required when the order or quote is submitted, and must\nthen be greater than zero. Stored as sent; not checked against the\ndrawing."
        },
        "totalLength": {
          "type": "string",
          "description": "The total length of the flashing in metres, as a decimal string: the sum\nover `subitems` of amount × length ÷ 1000. Three pieces at 1200 plus two\nat 925 give \"5.45\". Stored as sent; not checked against `subitems`."
        },
        "unitPrice": {
          "$ref": "#/definitions/factory.common.v1.Money",
          "description": "The price per metre of the flashing. Required when the order or quote is\nsubmitted."
        },
        "totalPrice": {
          "$ref": "#/definitions/factory.common.v1.Money",
          "description": "The total price for this line. Stored as sent and never derived or\nchecked. Factory prices a flashing as `unitPrice` × the priced length,\nwhere each piece shorter than the account's `minimumFlashingLengthMm` is\ncharged at that minimum, rounded to the cent."
        },
        "customPrice": {
          "$ref": "#/definitions/factory.common.v1.Money",
          "description": "A custom total price for the line. When set, the document's subtotal uses\nit in place of `totalPrice`; `unitPrice` and `customPricePerLength` never\nenter the subtotal directly."
        },
        "customPricePerLength": {
          "$ref": "#/definitions/factory.common.v1.Money",
          "description": "A custom price per metre. Factory uses it in place of `unitPrice` when\ncomputing `totalPrice`; send the resulting `totalPrice` yourself, as the\nAPI does not recompute it."
        },
        "subitems": {
          "type": "array",
          "items": {
            "type": "object"
          },
          "description": "The cut list: one entry per piece length, each an object with `amount`\n(the number of pieces) and `length` (the piece length in millimetres),\nboth JSON numbers — for example `[{\"amount\": 3, \"length\": 1200},\n{\"amount\": 2, \"length\": 925}]`. Stored as sent and not validated; a\nflashing with no lengths cannot be completed in Factory. Unlike\n`measurements` on other line kinds, lengths here are millimetre numbers,\nnot metre strings."
        },
        "isTaxFree": {
          "type": "boolean",
          "description": "Whether this line is exempt from tax."
        },
        "drawing": {
          "$ref": "#/definitions/factory.api.v1.salesdocuments.FlashingDrawing",
          "description": "The main drawing for the flashing: the folded profile whose edge sizes\ngive `totalGirth` and `bends`. Required — a flashing line without a\ndrawing is rejected with HTTP 400 (validation_failure). Its `side`\ndefaults to the far side."
        },
        "otherSides": {
          "type": "array",
          "items": {
            "type": "object",
            "$ref": "#/definitions/factory.api.v1.salesdocuments.FlashingDrawing"
          },
          "description": "Additional side drawings of the same flashing, used when the profile\ntapers: Factory compares each side's edge sizes with the main drawing's,\nedge by edge, and counts every differing edge as a taper. Their `side`\ndefaults to the near side. When present, `totalGirth` and `bends` come\nfrom whichever side has the largest girth."
        },
        "tax": {
          "$ref": "#/definitions/factory.common.v1.TaxDetail",
          "description": "The tax applied to this line: rate, code, and jurisdiction. Complements the\n`isTaxFree` flag. In v1 the server applies your account's tax settings to\ntaxable lines; this object reserves the shape for richer per-line tax later."
        }
      },
      "description": "A sheet-metal flashing line item: the flashing material, its specification,\npricing, and its drawings.\n\nA flashing line is created with its document (CreateQuote, CreateOrder) or\nadded whole to a quote (AddQuoteLineItem); it cannot yet be added to an\nexisting order, updated, or removed individually — those calls answer HTTP\n501 (not_implemented). Its specification (`bends`, `totalGirth`,\n`totalLength`, `subitems`) and its prices are stored exactly as you send them:\nthe API never derives, prices, or cross-checks a flashing, so compute them\nyourself from the drawing and cut list as described on each field. Lengths\nand girths are always millimetres (metres for `totalLength`), whatever the\naccount's measurement system.",
      "required": [
        "drawing"
      ]
    },
    "factory.api.v1.salesdocuments.FulfilmentMethod": {
      "type": "string",
      "enum": [
        "FULFILMENT_METHOD_UNSPECIFIED",
        "FULFILMENT_METHOD_PICKUP",
        "FULFILMENT_METHOD_DELIVERY",
        "FULFILMENT_METHOD_INSTALL"
      ],
      "default": "FULFILMENT_METHOD_UNSPECIFIED",
      "description": "How the customer receives the goods on an order or quote. Determines which\naddress is used: delivery uses the delivery address, installation uses the\ninstall address.\n\n - FULFILMENT_METHOD_UNSPECIFIED: Default, unset value.\n - FULFILMENT_METHOD_PICKUP: The customer collects the goods themselves.\n - FULFILMENT_METHOD_DELIVERY: The goods are delivered to the delivery address.\n - FULFILMENT_METHOD_INSTALL: The goods are installed at the install address."
    },
    "factory.api.v1.salesdocuments.KitComponent": {
      "type": "object",
      "properties": {
        "productType": {
          "$ref": "#/definitions/factory.common.v1.KitComponentType",
          "description": "The component's kind: catalogue product, on-the-fly, labour, or notes.\nRequired — it determines which of the fields below apply."
        },
        "productId": {
          "type": "string",
          "description": "The catalogue product id for this component. Optional."
        },
        "productRowId": {
          "type": "string",
          "description": "The catalogue product table-row (variant) id. Optional."
        },
        "kitProductId": {
          "type": "string",
          "description": "The catalogue kit-product-component id this component was instantiated from. Optional."
        },
        "productName": {
          "type": "string",
          "description": "Display name of the component. Set it on every write: component names\nare stored verbatim and are NEVER derived from `productId` or\n`productRowId` — a component created without a name is stored nameless\nand every read of the document returns it with an empty name. When\nbuilding this line from a catalogue kit template, echo the template\ncomponent's `productName` (returned by GetKit) into this field."
        },
        "productDescription": {
          "type": "string",
          "description": "Free-text description of the component."
        },
        "quantity": {
          "type": "string",
          "description": "Quantity for this component, as a decimal string."
        },
        "baseQuantity": {
          "type": "string",
          "description": "Base quantity before any per-unit multipliers, as a decimal string."
        },
        "unitPrice": {
          "$ref": "#/definitions/factory.common.v1.Money",
          "description": "Price per unit."
        },
        "totalPrice": {
          "$ref": "#/definitions/factory.common.v1.Money",
          "description": "Total price for this component. With derived pricing enabled every priced\ncomponent must carry it — a missing one is rejected rather than summed as\nzero — and it is checked against the component's own figures: unit price\nx quantity x (1 - discount / 100), with the measured amount replacing\nquantity on measurement-priced components (`quantity` is not a factor\nthere — see `measurements`)."
        },
        "customPrice": {
          "$ref": "#/definitions/factory.common.v1.Money",
          "description": "A manual override of the component's TOTAL price — not a unit price. When\nset, tax and margin are computed from this value in place of `totalPrice`,\nso a disagreeing pair stores an incoherent document. Send it only to\ndeliberately override the component's total, and never echo `unitPrice`\ninto it. With derived pricing enabled, a `customPrice` that disagrees with\n`totalPrice` is rejected (400)."
        },
        "customPricePerLength": {
          "$ref": "#/definitions/factory.common.v1.Money",
          "description": "A manual override of the price per unit of measured length (for example,\nper lineal metre), applied instead of the unit price on\nmeasurement-priced components. Send it only to deliberately override —\nomit it otherwise: an explicit $0.00 is stored as an override of $0.00,\nnot ignored."
        },
        "discount": {
          "type": "string",
          "description": "Discount applied to this component, as a decimal string."
        },
        "cost": {
          "$ref": "#/definitions/factory.common.v1.Money",
          "description": "Cost of the component."
        },
        "markup": {
          "type": "string",
          "description": "Markup applied to the component, as a decimal string. Interpreted as a\npercentage when `markupIsPercentage` is true."
        },
        "markupIsPercentage": {
          "type": "boolean",
          "description": "Whether markup is a percentage (true) or an absolute amount (false). Omit\nit to use the default, true; send false only for a fixed-amount markup.\nAlways present on reads."
        },
        "margin": {
          "$ref": "#/definitions/factory.common.v1.Money",
          "description": "Margin earned on the component."
        },
        "pricingStrategy": {
          "$ref": "#/definitions/factory.common.v1.PricingStrategy",
          "description": "How the component is priced."
        },
        "isTaxFree": {
          "type": "boolean",
          "description": "Whether this component is exempt from tax."
        },
        "attributes": {
          "type": "array",
          "items": {
            "type": "object"
          },
          "description": "Additional named attributes for the component, as a JSON array of\n{name, value} entries; for measurement-priced components they describe the\nmeasurement entries."
        },
        "notes": {
          "type": "string",
          "description": "Free-text notes — used when the component is a notes line."
        },
        "notesType": {
          "$ref": "#/definitions/factory.common.v1.NotesType",
          "description": "The kind of notes line, when this is a notes component."
        },
        "labourUserId": {
          "type": "string",
          "description": "The company user assigned, when this is a labour component."
        },
        "colour": {
          "type": "string",
          "description": "Colour of the component, populated for catalogue components from the\nproduct row."
        },
        "material": {
          "type": "string",
          "description": "Material of the component, populated for catalogue components from the\nproduct row."
        },
        "colourOptions": {
          "type": "array",
          "items": {
            "type": "object"
          },
          "description": "The selectable options for this component's variant attribute — most often\ncolour, but also other attributes such as size — as a JSON array of\n{label, value} entries, echoed from the catalogue. Display and read only."
        },
        "tax": {
          "$ref": "#/definitions/factory.common.v1.TaxDetail",
          "description": "The tax applied to this component: rate, code, and jurisdiction. Complements\nthe `isTaxFree` flag. In v1 the server applies your account's tax settings\nto taxable components; this object reserves the shape for richer tax later."
        },
        "measurements": {
          "type": "array",
          "items": {
            "type": "object",
            "$ref": "#/definitions/factory.api.v1.salesdocuments.MeasurementEntry"
          },
          "description": "The measurements that price this component, for the lineal and square\npricing strategies. Units follow the strategy: metres for *_METRES, feet\nfor *_FEET (see MeasurementEntry). Requires an explicit,\nmeasurement-priced `pricingStrategy` on this component. The measured\namount is the priced quantity — `quantity` is NOT a factor."
        },
        "asBuiltMeasurements": {
          "type": "array",
          "items": {
            "type": "object",
            "$ref": "#/definitions/factory.api.v1.salesdocuments.MeasurementEntry"
          },
          "description": "As-built measurements — what production actually cut, in the same shape\nand units as `measurements`. These drive cost, margin, and stock\nconsumption, never the component price. Optional: whenever omitted\n(create and update alike) they default to `measurements`. Send them only\nwhen your as-built figures differ from what was quoted."
        }
      },
      "description": "One component line inside a kit or sub-kit.\n\nA component is itself typed — it may be a catalogue-product, on-the-fly,\nnotes, labour, or kit-product line — and carries its own pricing and\nmeasurement details."
    },
    "factory.api.v1.salesdocuments.Label": {
      "type": "object",
      "properties": {
        "labelId": {
          "type": "string",
          "description": "The label's id.",
          "readOnly": true
        },
        "name": {
          "type": "string",
          "description": "Display name of the label. Unique within your company.",
          "readOnly": true
        },
        "colour": {
          "type": "string",
          "description": "Display colour of the label, as a hex string (for example \"#FF69B4\").",
          "readOnly": true
        }
      },
      "description": "A label: a small company-scoped tag that can be attached to any number of\nquotes and orders, with a display name and colour. Labels are managed in\nthe Factory app; renaming one there updates it everywhere it appears, and\nits id stays stable across the rename."
    },
    "factory.api.v1.salesdocuments.LabelPosition": {
      "type": "object",
      "properties": {
        "x": {
          "type": "number",
          "format": "double",
          "description": "The x coordinate of the label box, in canvas space."
        },
        "y": {
          "type": "number",
          "format": "double",
          "description": "The y coordinate of the label box, in canvas space."
        },
        "width": {
          "type": "number",
          "format": "double",
          "description": "The width of the label box, in canvas space."
        },
        "height": {
          "type": "number",
          "format": "double",
          "description": "The height of the label box, in canvas space."
        }
      },
      "description": "A label's bounding box in canvas (SVG) coordinate space. Used for line labels,\nangle labels, and finish labels."
    },
    "factory.api.v1.salesdocuments.LabourLine": {
      "type": "object",
      "properties": {
        "labourUserId": {
          "type": "string",
          "description": "The id of the worker who performed the labour. Required, and must be an\nactive user in the company that owns the order or quote."
        },
        "productName": {
          "type": "string",
          "description": "A short name for the labour."
        },
        "productDescription": {
          "type": "string",
          "description": "A longer description of the labour."
        },
        "pricing": {
          "$ref": "#/definitions/factory.api.v1.salesdocuments.LinePricing",
          "description": "Quantity and pricing for this line: the hours in `quantity`, the charge\nrate in `unitPrice` and the cost rate in `cost`. There are no rate\noverrides elsewhere on the line, and the worker's own rates are not\napplied: an omitted `cost` is stored as zero, not taken from the user's\n`hourlyRateCost`."
        }
      },
      "description": "A line item charging for labour performed by a team member.",
      "required": [
        "labourUserId"
      ]
    },
    "factory.api.v1.salesdocuments.LineCell": {
      "type": "object",
      "properties": {
        "size": {
          "type": "number",
          "format": "double",
          "description": "The size of the edge in millimetres. Omitted when a size has not yet been\nentered — but give every edge a size, or Factory counts the girth as 0."
        },
        "hidden": {
          "type": "boolean",
          "description": "Whether this edge's length label is hidden."
        }
      },
      "description": "The length entry for a single edge between two points."
    },
    "factory.api.v1.salesdocuments.LinePricing": {
      "type": "object",
      "properties": {
        "quantity": {
          "type": "string",
          "description": "Quantity for this line, as a decimal string (e.g. \"2\" or \"2.5\"). Supports up\nto 4 decimal places. It is the first factor of the line-total formula\ndocumented on `totalPrice` — EXCEPT on measurement-priced (lineal/square)\nlines, where the measured length or area alone sets the priced amount and\nthis factor is treated as 1 whenever the server derives or checks the\ntotal. Do not multiply a measurement-priced total by quantity. (Only on\nfree-text lines on accounts WITHOUT server-side derivation does the legacy\nformula still multiply by it — see `totalPrice`)."
        },
        "unitPrice": {
          "$ref": "#/definitions/factory.common.v1.Money",
          "description": "Per-unit price. Required on priced lines — except on catalogue lines with\nserver-side derivation enabled for your account, where you may omit it and\nsupply `cost` plus `markup` instead: the server computes it as\ncost x (1 + markup/100) (or cost + markup for a fixed markup). If you can\nsend the price directly, do; the derivation exists for callers whose\nsource system stores only cost and markup. The server rounds this to 4\ndecimal places (half up) before pricing, so unit-price precision finer\nthan that (micro amounts that are not a multiple of 100) is not preserved\nand can shift the computed line total."
        },
        "totalPrice": {
          "$ref": "#/definitions/factory.common.v1.Money",
          "description": "The line total, as a Money amount in micros.\n\nServer-side derivation is rolling out account by account. Once it is\nenabled for your account, this field is optional on most priced lines —\nomit it (leave the whole Money object out; an empty Money is a supplied\n$0.00) and the server computes it. Until then it is required on priced\nlines, exactly as before.\n\nThe formula is:\n\n  quantity * `unitPrice` * (1 - discount / 100)\n\nrounded to the nearest cent — 2 decimal places, half up (0.005 rounds up).\nFor lineal or square measurement pricing the result is then multiplied by\nthe measured length or area the server derives from the line's measurement\ndetails before rounding — and on measurement-priced lines the quantity\nfactor is pinned to 1 once derivation is enabled for your account, whether\nthe server derives the total or checks one you send: the measurements\nalone set the priced amount. (Without derivation, free-text lines still\nmultiply by `quantity` — match it when computing such a total yourself.)\nDeriving needs a non-zero quantity or measurement\nset (deriving with a zero quantity or an empty measurement set is rejected\nrather than stored as $0.00; a zero unit price — a free line — still\nderives). On a product\nkit priced as a standard kit, an omitted parent total is derived as the\nsum of its components and sub-kits (omitted sub-kit totals are likewise\nderived from their components) — priced component totals must accompany\nthe request; a missing one is rejected rather than summed as zero. A\nsupplied kit total, at any tier, is stored as sent.\n\nIf you DO send a value on a catalogue (once derivation is enabled for\nyour account), on-the-fly, or labour line, it is checked against the\nformula and a mismatch is rejected with HTTP 400.\nSend the amount as a whole number of cents (`amountMicros` a multiple of\n10000); a sub-cent amount is rounded to the nearest cent before the check.\n\nExceptions, where the total is never derived — send a correct value,\nbecause these lines are stored as sent:\n  - Flashing lines. Drawing pricing is never derived, and the value is\n    not checked either.\n  - Custom-formula lines, whose total also carries the formula's answer\n    as a factor. Required even with derivation enabled (an omitted total\n    is rejected); checked only when the discount is a percentage.\n  - Custom-priced kits (`customPricing` = true): the parent price is\n    yours, and an omitted total stores $0.00 — always send it.\n\nExample: quantity \"2.5\", `unitPrice` $10.00 (10000000 micros), discount\n\"10.00\" gives 2.5 * 10.00 * (1 - 0.10) = $22.50 — so `totalPrice` is\n22500000 micros."
        },
        "discount": {
          "type": "string",
          "description": "Discount applied to the line, as a decimal percentage string (e.g. \"10.00\"\nmeans 10%). Supports up to 2 decimal places. In the line-total formula on\n`totalPrice` it enters as the factor (1 - discount / 100)."
        },
        "cost": {
          "$ref": "#/definitions/factory.common.v1.Money",
          "description": "Unit cost for this line, used to compute margin — and, with derivation\nenabled, the base the server recomputes `markup` (and an omitted\n`unitPrice`) from. Rounded to 4 decimal places (half up) before use, so\nsub-4dp cost precision is not preserved."
        },
        "markup": {
          "type": "string",
          "description": "Markup applied to the line, as a decimal string — either a money amount or a\npercentage, depending on `markupIsPercentage`.\n\nWith server-side derivation enabled for your account: whenever `cost` is\nnon-zero, the value you send here is DISCARDED and recomputed from `cost`\nand the unit price (as a percentage — `markupIsPercentage` reads back\ntrue), so the stored markup always agrees with the stored prices. Do not\nexpect to read back the value you sent. With a zero or absent cost your\nvalue is kept as sent."
        },
        "markupIsPercentage": {
          "type": "boolean",
          "description": "Whether markup is a percentage (true) or a fixed amount (false). Omit it to\nuse the default, true; send false only for a fixed-amount markup. Always\npresent on reads, and reads back true whenever the server recomputed the\nmarkup (see `markup`)."
        },
        "margin": {
          "$ref": "#/definitions/factory.common.v1.Money",
          "description": "Margin for the line. When supplied, it is validated against cost and\nquantity — and with derivation enabled it is recomputed alongside the\ntotal whenever `cost` is present, so (like `markup`) the stored value may\ndiffer from what you sent. Simplest: omit it and let the server compute."
        },
        "pricingStrategy": {
          "$ref": "#/definitions/factory.common.v1.PricingStrategy",
          "description": "How this line is priced (per-unit quantity, lineal, or square measurement).\nDefaults to per-unit quantity pricing."
        },
        "priceLevel": {
          "type": "string",
          "description": "Optional named price level applied to this line."
        },
        "isTaxFree": {
          "type": "boolean",
          "description": "Whether this line is exempt from tax."
        },
        "tax": {
          "$ref": "#/definitions/factory.common.v1.TaxDetail",
          "description": "The tax applied to this line: rate, code, and jurisdiction. Complements the\n`isTaxFree` flag. In v1 the server applies your account's tax settings to\ntaxable lines; this object reserves the shape for richer per-line tax later."
        },
        "measurements": {
          "type": "array",
          "items": {
            "type": "object",
            "$ref": "#/definitions/factory.api.v1.salesdocuments.MeasurementEntry"
          },
          "description": "The measurements that price this line, for the lineal and square pricing\nstrategies. Units follow the strategy: metres for *_METRES, feet for\n*_FEET (see MeasurementEntry). Requires an explicit, measurement-priced\n`pricingStrategy` on this same object. On these strategies the measured\namount is the priced quantity — `quantity` is NOT a factor (on accounts\nwithout server-side derivation, free-text lines are the one legacy\nexception; see `quantity`); the server\nsums these pieces (applying your account's minimum-length setting on\nlineal strategies) to derive the amount that scales the line total."
        },
        "asBuiltMeasurements": {
          "type": "array",
          "items": {
            "type": "object",
            "$ref": "#/definitions/factory.api.v1.salesdocuments.MeasurementEntry"
          },
          "description": "As-built measurements — what production actually cut, in the same shape\nand units as `measurements`. These drive cost, margin, and stock\nconsumption, never the line price. Optional: whenever omitted (create and\nupdate alike) they default to `measurements`. Send them only when your\nas-built figures differ from what was quoted."
        }
      },
      "description": "Pricing for a single priced line: quantity, prices, discount, cost, markup,\nmargin, and pricing strategy. On priced lines the server validates — or,\nonce derivation is enabled for your account, derives — the line total from\nthe quantity, unit price, and discount; see `totalPrice` for the exact\nformula, the rounding rule, and what happens if it does not match."
    },
    "factory.api.v1.salesdocuments.LineSet": {
      "type": "object",
      "properties": {
        "values": {
          "type": "object",
          "additionalProperties": {
            "$ref": "#/definitions/factory.api.v1.salesdocuments.LineCell"
          },
          "description": "The size entry for each edge, keyed by the ids of the two points it\nconnects joined with a hyphen, source first: the edge from `p1` to `p2` is\n`\"p1-p2\"`."
        },
        "positions": {
          "type": "object",
          "additionalProperties": {
            "$ref": "#/definitions/factory.api.v1.salesdocuments.LabelPosition"
          },
          "description": "The label box for each edge's size label, keyed like `values`. Optional:\nFactory places any label without a box itself."
        }
      },
      "description": "The collection of edge lengths and their label positions for a drawing."
    },
    "factory.api.v1.salesdocuments.MeasurementEntry": {
      "type": "object",
      "properties": {
        "length": {
          "type": "string",
          "description": "Length of this piece, as a decimal string in the strategy's unit\n(e.g. \"2.4\" metres, or \"8\" feet)."
        },
        "width": {
          "type": "string",
          "description": "Width of this piece, as a decimal string in the strategy's unit.\nSquare strategies only — a width on a lineal entry is rejected, and a\nsquare entry without one is rejected."
        },
        "amount": {
          "type": "string",
          "description": "How many pieces of this size, as a decimal string. Defaults to 1."
        }
      },
      "description": "One measured piece (or panel) in a measurement-priced line or kit\ncomponent. Values are decimal strings in the line's strategy-native unit:\nmetres for the *_METRES pricing strategies, feet for *_FEET."
    },
    "factory.api.v1.salesdocuments.NotesLine": {
      "type": "object",
      "properties": {
        "notesType": {
          "$ref": "#/definitions/factory.common.v1.NotesType",
          "description": "The kind of note. Required."
        },
        "notes": {
          "type": "string",
          "description": "The note text."
        },
        "productName": {
          "type": "string",
          "description": "A short name for the note line."
        },
        "productDescription": {
          "type": "string",
          "description": "A longer description for the note line."
        }
      },
      "description": "A line item carrying a note rather than a priced product. Has no quantity or\nprice.",
      "required": [
        "notesType"
      ]
    },
    "factory.api.v1.salesdocuments.OnTheFlyLine": {
      "type": "object",
      "properties": {
        "productName": {
          "type": "string",
          "description": "Name of the item. Required."
        },
        "productDescription": {
          "type": "string",
          "description": "Optional longer description of the item."
        },
        "colour": {
          "type": "string",
          "description": "Optional colour for the item."
        },
        "pricing": {
          "$ref": "#/definitions/factory.api.v1.salesdocuments.LinePricing",
          "description": "Pricing for this line. Unit price is required; the line total is required\nuntil server-side derivation is enabled for your account (see\n`totalPrice` on LinePricing), except on custom-formula lines, where it is\nalways required."
        },
        "attributes": {
          "type": "array",
          "items": {
            "type": "object"
          },
          "description": "Additional named attributes for the line, as a JSON array of {name, value}\nentries (for example {name: \"Size\", value: \"76x38mm\"}). An entry may also\ncarry flags that hide it from specific documents or views, such as the\ninvoice PDF or the customer view."
        }
      },
      "description": "A free-text line: an ad-hoc line not tied to any catalogue product.",
      "required": [
        "productName"
      ]
    },
    "factory.api.v1.salesdocuments.Point": {
      "type": "object",
      "properties": {
        "id": {
          "type": "string",
          "description": "The unique id of this point within the drawing."
        },
        "x": {
          "type": "number",
          "format": "double",
          "description": "The x coordinate of the point, in canvas space."
        },
        "y": {
          "type": "number",
          "format": "double",
          "description": "The y coordinate of the point, in canvas space."
        },
        "vectors": {
          "type": "array",
          "items": {
            "type": "string"
          },
          "description": "The ids of points reached by edges leaving this point. Every edge must\nalso appear in the target point's `connect`."
        },
        "connect": {
          "type": "array",
          "items": {
            "type": "string"
          },
          "description": "The ids of points connected to this point by incoming edges. Every edge\nmust also appear in the source point's `vectors`."
        },
        "finish": {
          "$ref": "#/definitions/factory.api.v1.salesdocuments.Finish",
          "description": "Optional end treatment at this point."
        }
      },
      "description": "A vertex in the drawing. Points are connected by edges to form the flashing\nprofile."
    },
    "factory.api.v1.salesdocuments.ProductKitLine": {
      "type": "object",
      "properties": {
        "kitId": {
          "type": "string",
          "description": "The kit's id (`kitId` on Kit, from the catalogue read surface). Optional: omit\nit to define an on-the-fly kit not linked to any catalogue kit — the kit\nis described entirely by this line's name, pricing, components, and\nsub-kits."
        },
        "kitRowId": {
          "type": "string",
          "description": "The selected kit row/variant id. Optional."
        },
        "productName": {
          "type": "string",
          "description": "Display name of the kit."
        },
        "customPricing": {
          "type": "boolean",
          "description": "Whether the kit is custom-priced (true) rather than computed from its\ncomponents (false)."
        },
        "description": {
          "type": "string",
          "description": "Free-text description of the kit."
        },
        "displayKitItems": {
          "type": "array",
          "items": {
            "type": "object"
          },
          "description": "Which documents and views the kit's components are shown on, as a JSON\narray of display-target keys — for example \"displayOnWorkOrderPdf\",\n\"displayOnInvoicePdf\", or \"displayOnAllViews\". Optional."
        },
        "displaySubKits": {
          "type": "array",
          "items": {
            "type": "object"
          },
          "description": "Which documents and views the kit's sub-kits are shown on, as a JSON array\nof display-target keys, using the same values as `displayKitItems`.\nOptional."
        },
        "pricing": {
          "$ref": "#/definitions/factory.api.v1.salesdocuments.LinePricing",
          "description": "Overall pricing for the kit line."
        },
        "attributes": {
          "type": "array",
          "items": {
            "type": "object"
          },
          "description": "Additional named attributes for the kit line, as a JSON array of\n{name, value} entries; for measurement-priced kits they describe the\nmeasurement entries."
        },
        "components": {
          "type": "array",
          "items": {
            "type": "object",
            "$ref": "#/definitions/factory.api.v1.salesdocuments.KitComponent"
          },
          "description": "The kit's direct components."
        },
        "subKits": {
          "type": "array",
          "items": {
            "type": "object",
            "$ref": "#/definitions/factory.api.v1.salesdocuments.SubKit"
          },
          "description": "The kit's sub-assemblies, each with its own components."
        }
      },
      "description": "A configured kit line on an order or quote.\n\nA kit groups a set of components, optionally arranged into sub-kits. The\nparent line carries the kit's overall pricing; the component tree is one level\ndeep — a kit holds its direct components and sub-kits, and each sub-kit holds\nits own components."
    },
    "factory.api.v1.salesdocuments.SalesLine": {
      "type": "object",
      "properties": {
        "onTheFly": {
          "$ref": "#/definitions/factory.api.v1.salesdocuments.OnTheFlyLine",
          "description": "A free-form line not tied to a catalogue product. Set exactly one of the six\nline-type fields."
        },
        "catalogue": {
          "$ref": "#/definitions/factory.api.v1.salesdocuments.CatalogueLine",
          "description": "A line for a product from your catalogue. Set exactly one of the six\nline-type fields."
        },
        "labour": {
          "$ref": "#/definitions/factory.api.v1.salesdocuments.LabourLine",
          "description": "A line charging for labour. Set exactly one of the six line-type fields."
        },
        "notes": {
          "$ref": "#/definitions/factory.api.v1.salesdocuments.NotesLine",
          "description": "A note line, with no pricing. Set exactly one of the six line-type fields."
        },
        "flashing": {
          "$ref": "#/definitions/factory.api.v1.salesdocuments.FlashingLine",
          "description": "A flashing line, with its drawing and geometry. Set exactly one of the six\nline-type fields."
        },
        "productKit": {
          "$ref": "#/definitions/factory.api.v1.salesdocuments.ProductKitLine",
          "description": "A line for a predefined product kit. Set exactly one of the six line-type\nfields."
        },
        "id": {
          "type": "string",
          "description": "The server-assigned id of this line — unique and stable for the life of the\nline. Returned when a quote is read; pass it to update or remove the line.\nNot set when creating a line (the server assigns it). This id has a different\nform from the quote id.",
          "readOnly": true
        }
      },
      "description": "A single line item on an order or quote. Each line is exactly one of the\nsupported line types."
    },
    "factory.api.v1.salesdocuments.SquareAngle": {
      "type": "object",
      "properties": {
        "x": {
          "type": "number",
          "format": "double",
          "description": "The x coordinate of the marker, in canvas space. Omitted until placed."
        },
        "y": {
          "type": "number",
          "format": "double",
          "description": "The y coordinate of the marker, in canvas space. Omitted until placed."
        }
      },
      "description": "The right-angle (90°) marker placed on the drawing."
    },
    "factory.api.v1.salesdocuments.SubKit": {
      "type": "object",
      "properties": {
        "title": {
          "type": "string",
          "description": "Display title of the sub-assembly."
        },
        "quantity": {
          "type": "string",
          "description": "Quantity of this sub-assembly, as a decimal string."
        },
        "actualQuantity": {
          "type": "string",
          "description": "Actual measured quantity, as a decimal string."
        },
        "unitPrice": {
          "$ref": "#/definitions/factory.common.v1.Money",
          "description": "Price per unit of the sub-assembly."
        },
        "totalPrice": {
          "$ref": "#/definitions/factory.common.v1.Money",
          "description": "Total price of the sub-assembly."
        },
        "components": {
          "type": "array",
          "items": {
            "type": "object",
            "$ref": "#/definitions/factory.api.v1.salesdocuments.KitComponent"
          },
          "description": "The components that make up this sub-assembly."
        }
      },
      "description": "A sub-assembly within a kit.\n\nA sub-kit groups a set of components under its own title and quantity, letting\na kit be organized into named sub-assemblies."
    },
    "factory.api.v1.salesdocuments.TextBox": {
      "type": "object",
      "properties": {
        "x": {
          "type": "number",
          "format": "double",
          "description": "The x coordinate of the annotation, in canvas space. Omitted until placed."
        },
        "y": {
          "type": "number",
          "format": "double",
          "description": "The y coordinate of the annotation, in canvas space. Omitted until placed."
        },
        "width": {
          "type": "number",
          "format": "double",
          "description": "The width of the annotation box, in canvas space."
        },
        "height": {
          "type": "number",
          "format": "double",
          "description": "The height of the annotation box, in canvas space."
        },
        "value": {
          "type": "string",
          "description": "The annotation text as a flat string. May be present alongside the\nstructured `text` form."
        },
        "text": {
          "type": "array",
          "items": {
            "type": "object",
            "$ref": "#/definitions/factory.api.v1.salesdocuments.TextRow"
          },
          "description": "The annotation text in structured rows. May be present alongside `value`."
        },
        "rotateDeg": {
          "type": "number",
          "format": "double",
          "description": "Optional rotation of the annotation, in degrees."
        }
      },
      "description": "A free-text annotation placed on the drawing."
    },
    "factory.api.v1.salesdocuments.TextRow": {
      "type": "object",
      "properties": {
        "id": {
          "type": "integer",
          "format": "int32",
          "description": "The 0-based index of this row within the text annotation."
        },
        "indent": {
          "type": "integer",
          "format": "int32",
          "description": "The indentation level of this row."
        },
        "value": {
          "type": "array",
          "items": {
            "type": "string"
          },
          "description": "The text segments that make up this row, in order."
        }
      },
      "description": "One row of text within a text annotation."
    },
    "factory.api.v1.salesorders.AddOrderLineItemResponse": {
      "type": "object",
      "properties": {
        "order": {
          "$ref": "#/definitions/factory.api.v1.salesorders.models.Order",
          "description": "The order's id and recomputed totals (`subtotal`, `taxAmount`, `total`,\n`totalCost`, `margin`, `labourTotal`). No other field is populated —\n`lines` is empty and `isSubmitted` is absent regardless of the order's\nstate — so read the order with GetOrder for the new line's id and\nposition."
        }
      },
      "description": "Result of adding a line item to an order: the order's recomputed totals."
    },
    "factory.api.v1.salesorders.CreateOrderRequest": {
      "type": "object",
      "properties": {
        "customerId": {
          "type": "string",
          "description": "The customer's unique id, as returned by the Customers API. Exactly one of\n`customerId` or `companyName` must be set; in this version only\n`customerId` is accepted."
        },
        "companyName": {
          "type": "string",
          "description": "The customer's company name. Not yet supported: a request that sets\n`companyName` instead of `customerId` is rejected with HTTP 501\n(not_implemented). Resolve the name with ListCustomers and send\n`customerId`."
        },
        "lines": {
          "type": "array",
          "items": {
            "type": "object",
            "$ref": "#/definitions/factory.api.v1.salesdocuments.SalesLine"
          },
          "description": "The order's line items. Each line is one of the supported line types\n(on-the-fly, catalogue, labour, notes, flashing, or product kit)."
        },
        "adjustments": {
          "type": "array",
          "items": {
            "type": "object",
            "$ref": "#/definitions/factory.api.v1.salesdocuments.Adjustment"
          },
          "description": "Order-level fees, discounts, and markups applied across the whole order."
        },
        "fulfilmentMethod": {
          "$ref": "#/definitions/factory.api.v1.salesdocuments.FulfilmentMethod",
          "description": "How the customer receives the order: pickup, delivery, or installation.\nDetermines which address below applies."
        },
        "billingAddress": {
          "$ref": "#/definitions/factory.api.v1.Address",
          "description": "Billing address for the order."
        },
        "deliveryAddress": {
          "$ref": "#/definitions/factory.api.v1.Address",
          "description": "Delivery address. Used only when `fulfilmentMethod` is DELIVERY."
        },
        "installAddress": {
          "$ref": "#/definitions/factory.api.v1.Address",
          "description": "Installation address. Used only when `fulfilmentMethod` is INSTALL."
        },
        "isSubmitted": {
          "type": "boolean",
          "description": "Submit the order immediately (true) or save it as a draft (false).\nSubmitting assigns a status and finalizes the order."
        },
        "statusId": {
          "type": "string",
          "description": "Optional explicit workflow status to place the order in, by id. If omitted,\na default status is applied when the order is submitted. Discover your\naccount's statuses with GET /v1/company/order-statuses."
        },
        "reference": {
          "type": "string",
          "description": "Free-text external reference for this order — e.g. the order id or purchase\norder (PO) number from your own system. Shown as \"PO #\" in the Factory app\nand as \"PO\" on the order documents. Stored for lookup and audit; distinct\nfrom `requestId` (the retry key) and from the read-only `orderNumber`\n(Factory's own number for the order)."
        },
        "customFields": {
          "type": "object",
          "description": "Values for your account's custom-defined sales-document fields, keyed by\neach field's configured key. Unknown keys, keys of other modules, and\nvalues that don't match the field's type are rejected. Send number and\ncurrency values as numbers or decimal strings, checkboxes as booleans,\nmulti-selects as arrays of strings, and everything else as strings.\nSelect and multi-select values must be option keys from the field's\ndefinition, never display labels; discover your account's field keys and\noptions with GET /v1/company/custom-fields. Formula fields are computed\nand cannot be written."
        },
        "syncAccounting": {
          "type": "boolean",
          "description": "Whether to sync the order to your accounting system (Xero, MYOB, or QBO)\nwhen it is submitted. Defaults to false; leave it false for bulk imports to\navoid syncing every order."
        },
        "requiredAt": {
          "type": "string",
          "format": "date-time",
          "description": "The date the order is required or needed by."
        },
        "contact": {
          "$ref": "#/definitions/factory.api.v1.salesdocuments.Contact",
          "description": "The point-of-contact person for this order, distinct from the customer."
        },
        "notes": {
          "type": "string",
          "description": "Free-text notes about the order."
        },
        "pickupNotes": {
          "type": "string",
          "description": "Free-text notes shown when the order is picked up."
        },
        "requestId": {
          "type": "string",
          "description": "Optional idempotency key for this request: an opaque, client-generated\nUUID, scoped to the API key that sends it. Requests carrying the same key\nare executed at most once, so a retry can never create a second copy. Any\nrepeat is rejected with HTTP 409: if the original request completed, the\nbody's `resourceId` carries the id it created; if its outcome is\nstill unknown, or the key is reused with a materially different body,\nverify with a read before retrying with a fresh key. Keys are retained\nfor at least 24 hours. If the idempotency store is unavailable, requests\ncarrying a key are rejected with HTTP 503 (requests without a key are\nunaffected). Omit the key for no idempotency guarantee."
        },
        "tax": {
          "$ref": "#/definitions/factory.common.v1.TaxDetail",
          "description": "The tax to apply, as a rate/code/jurisdiction descriptor. Optional and\nadvisory in this version: the server applies your account's configured tax\nrate regardless of what you send here, and the created order echoes back the\nvalues actually applied (`tax` and `taxAmount` on the Order) — compare them\nto detect an override. Reserves the shape for future per-order tax support."
        },
        "labelIds": {
          "type": "array",
          "items": {
            "type": "string"
          },
          "description": "Labels to attach to the order as part of creating it, by id. Every id\nmust exist in your company's label set (discover them with\nGET /v1/company/labels): an unknown id fails the whole call with HTTP\n404 before the order is created. Labelling happens with the create but\nnot atomically inside it — in the rare case the order is created and\nlabelling then fails, the error names the created order id and the order\nexists WITHOUT labels; attach them with SetOrderLabels. The created\norder echoes the attached labels. Duplicate ids are rejected."
        },
        "fields": {
          "type": "string",
          "description": "Comma-separated response fields to include, using camelCase JSON names\n(e.g. `orderId,total.amountMicros`). Dot paths reach into nested objects\nand map transparently across arrays. Paths are relative to the resource,\nnot the response envelope; envelope keys are always preserved. Only 2xx\nJSON responses are filtered; error bodies pass through unmodified. Unknown\nnames are silently ignored. Takes precedence over `excludeFields` when\nboth are provided."
        },
        "excludeFields": {
          "type": "string",
          "description": "Comma-separated response fields to exclude, using camelCase JSON names.\nDot paths and array-transparency work the same as `fields`. Paths are\nrelative to the resource, not the response envelope. Only 2xx JSON\nresponses are filtered; error bodies pass through unmodified. Ignored\nwhen `fields` is also provided."
        }
      },
      "description": "Input for creating an order: the customer, line items, adjustments,\naddresses, and delivery/submission options. Order totals are derived by the\nserver, not supplied here."
    },
    "factory.api.v1.salesorders.CreateOrderResponse": {
      "type": "object",
      "properties": {
        "order": {
          "$ref": "#/definitions/factory.api.v1.salesorders.models.Order",
          "description": "The created order, including server-set fields and assigned identifiers."
        },
        "drawings": {
          "type": "array",
          "items": {
            "type": "object",
            "$ref": "#/definitions/factory.api.v1.salesdocuments.DrawingIdMapping"
          },
          "description": "The server identities of the flashing drawings created by this call, one\nentry per flashing line in the order you sent them, each pairing your\n`tempId` with the assigned `drawingId` — use them to address\nUploadDrawingSvg. Only flashing lines carry drawings; empty when the\norder had no flashing lines."
        }
      },
      "description": "Result of creating an order: the created order with all server-assigned fields\npopulated."
    },
    "factory.api.v1.salesorders.DeleteAttachmentResponse": {
      "type": "object",
      "description": "Result of removing an attachment. Empty on success."
    },
    "factory.api.v1.salesorders.DeleteOrderLineItemResponse": {
      "type": "object",
      "properties": {
        "order": {
          "$ref": "#/definitions/factory.api.v1.salesorders.models.Order",
          "description": "The updated order, with the line removed and totals recomputed."
        }
      },
      "description": "Result of removing a line item: the order with the line removed."
    },
    "factory.api.v1.salesorders.GetAttachmentResponse": {
      "type": "object",
      "properties": {
        "attachment": {
          "$ref": "#/definitions/factory.api.v1.collaborate.Attachment",
          "description": "The requested attachment, with a fresh presigned download URL."
        }
      },
      "description": "Result of retrieving a single attachment."
    },
    "factory.api.v1.salesorders.GetOrderResponse": {
      "type": "object",
      "properties": {
        "order": {
          "$ref": "#/definitions/factory.api.v1.salesorders.models.Order",
          "description": "The requested order."
        }
      },
      "description": "Result of retrieving a single order."
    },
    "factory.api.v1.salesorders.ListAttachmentsResponse": {
      "type": "object",
      "properties": {
        "attachments": {
          "type": "array",
          "items": {
            "type": "object",
            "$ref": "#/definitions/factory.api.v1.collaborate.Attachment"
          },
          "description": "The attachments in this page, oldest first."
        },
        "nextPageToken": {
          "type": "string",
          "description": "Token to pass as `pageToken` to fetch the next page; empty when there are\nno more results."
        }
      },
      "description": "A page of an order's attachments."
    },
    "factory.api.v1.salesorders.ListLabelsResponse": {
      "type": "object",
      "properties": {
        "labels": {
          "type": "array",
          "items": {
            "type": "object",
            "$ref": "#/definitions/factory.api.v1.salesdocuments.Label"
          },
          "description": "One page of labels, sorted by name."
        },
        "nextPageToken": {
          "type": "string",
          "description": "Token to pass as `pageToken` to fetch the next page; empty when there are\nno more results."
        }
      },
      "description": "Result of listing your company's labels."
    },
    "factory.api.v1.salesorders.ListMessagesResponse": {
      "type": "object",
      "properties": {
        "messages": {
          "type": "array",
          "items": {
            "type": "object",
            "$ref": "#/definitions/factory.api.v1.collaborate.Message"
          },
          "description": "The messages in this page, oldest first."
        },
        "nextPageToken": {
          "type": "string",
          "description": "Token to pass as `pageToken` to fetch the next page; empty when there are\nno more results."
        }
      },
      "description": "A page of an order's conversation."
    },
    "factory.api.v1.salesorders.ListOrderStatusesResponse": {
      "type": "object",
      "properties": {
        "statuses": {
          "type": "array",
          "items": {
            "type": "object",
            "$ref": "#/definitions/factory.api.v1.salesorders.OrderStatus"
          },
          "description": "The order statuses in this page, in display order."
        },
        "nextPageToken": {
          "type": "string",
          "description": "Token to pass as `pageToken` to fetch the next page; empty when there are no\nmore results."
        }
      },
      "description": "A page of order statuses."
    },
    "factory.api.v1.salesorders.ListOrdersResponse": {
      "type": "object",
      "properties": {
        "orders": {
          "type": "array",
          "items": {
            "type": "object",
            "$ref": "#/definitions/factory.api.v1.salesorders.models.Order"
          },
          "description": "The orders in this page."
        },
        "nextPageToken": {
          "type": "string",
          "description": "Token to pass as `pageToken` to fetch the next page; empty when there are no\nmore results."
        }
      },
      "description": "A page of orders."
    },
    "factory.api.v1.salesorders.OrderService.AddOrderLineItemBody": {
      "type": "object",
      "properties": {
        "line": {
          "$ref": "#/definitions/factory.api.v1.salesdocuments.SalesLine",
          "description": "The line item to add: one of the supported line types (on-the-fly,\ncatalogue, labour, notes, or product kit)."
        },
        "requestId": {
          "type": "string",
          "description": "Optional idempotency key for this request: an opaque, client-generated\nUUID, scoped to the API key that sends it. Requests carrying the same key\nare executed at most once, so a retry can never create a second copy. Any\nrepeat is rejected with HTTP 409: if the original request completed, the\nbody's `resourceId` carries the id it created; if its outcome is\nstill unknown, or the key is reused with a materially different body,\nverify with a read before retrying with a fresh key. Keys are retained\nfor at least 24 hours. If the idempotency store is unavailable, requests\ncarrying a key are rejected with HTTP 503 (requests without a key are\nunaffected). Omit the key for no idempotency guarantee."
        },
        "fields": {
          "type": "string",
          "description": "Comma-separated response fields to include, using camelCase JSON names\n(e.g. `orderId,total.amountMicros`). Dot paths reach into nested objects\nand map transparently across arrays. Paths are relative to the resource,\nnot the response envelope; envelope keys are always preserved. Only 2xx\nJSON responses are filtered; error bodies pass through unmodified. Unknown\nnames are silently ignored. Takes precedence over `excludeFields` when\nboth are provided."
        },
        "excludeFields": {
          "type": "string",
          "description": "Comma-separated response fields to exclude, using camelCase JSON names.\nDot paths and array-transparency work the same as `fields`. Paths are\nrelative to the resource, not the response envelope. Only 2xx JSON\nresponses are filtered; error bodies pass through unmodified. Ignored\nwhen `fields` is also provided."
        }
      },
      "description": "Input for adding a line item to an order.",
      "required": [
        "line"
      ]
    },
    "factory.api.v1.salesorders.OrderService.PostMessageBody": {
      "type": "object",
      "properties": {
        "text": {
          "type": "string",
          "description": "The message text, as plain text. Required."
        },
        "requestId": {
          "type": "string",
          "description": "Optional idempotency key for this request: an opaque, client-generated\nUUID, scoped to the API key that sends it. Requests carrying the same key\nare executed at most once, so a retry can never create a second copy. Any\nrepeat is rejected with HTTP 409: if the original request completed, the\nbody's `resourceId` carries the id it created; if its outcome is\nstill unknown, or the key is reused with a materially different body,\nverify with a read before retrying with a fresh key. Keys are retained\nfor at least 24 hours. If the idempotency store is unavailable, requests\ncarrying a key are rejected with HTTP 503 (requests without a key are\nunaffected). Omit the key for no idempotency guarantee."
        },
        "fields": {
          "type": "string",
          "description": "Comma-separated response fields to include, using camelCase JSON names\n(e.g. `orderId,total.amountMicros`). Dot paths reach into nested objects\nand map transparently across arrays. Paths are relative to the resource,\nnot the response envelope; envelope keys are always preserved. Only 2xx\nJSON responses are filtered; error bodies pass through unmodified. Unknown\nnames are silently ignored. Takes precedence over `excludeFields` when\nboth are provided."
        },
        "excludeFields": {
          "type": "string",
          "description": "Comma-separated response fields to exclude, using camelCase JSON names.\nDot paths and array-transparency work the same as `fields`. Paths are\nrelative to the resource, not the response envelope. Only 2xx JSON\nresponses are filtered; error bodies pass through unmodified. Ignored\nwhen `fields` is also provided."
        }
      },
      "description": "Input for posting a text message to an order's conversation.",
      "required": [
        "text"
      ]
    },
    "factory.api.v1.salesorders.OrderService.SetOrderLabelsBody": {
      "type": "object",
      "properties": {
        "labelIds": {
          "type": "array",
          "items": {
            "type": "string"
          },
          "description": "The order's desired complete label set. An empty list removes every\nlabel. Duplicate ids are rejected."
        },
        "fields": {
          "type": "string",
          "description": "Comma-separated response fields to include, using camelCase JSON names\n(e.g. `orderId,total.amountMicros`). Dot paths reach into nested objects\nand map transparently across arrays. Paths are relative to the resource,\nnot the response envelope; envelope keys are always preserved. Only 2xx\nJSON responses are filtered; error bodies pass through unmodified. Unknown\nnames are silently ignored. Takes precedence over `excludeFields` when\nboth are provided."
        },
        "excludeFields": {
          "type": "string",
          "description": "Comma-separated response fields to exclude, using camelCase JSON names.\nDot paths and array-transparency work the same as `fields`. Paths are\nrelative to the resource, not the response envelope. Only 2xx JSON\nresponses are filtered; error bodies pass through unmodified. Ignored\nwhen `fields` is also provided."
        }
      },
      "description": "Input for replacing the labels on an order."
    },
    "factory.api.v1.salesorders.OrderService.UploadAttachmentBody": {
      "type": "object",
      "properties": {
        "file": {
          "type": "string",
          "format": "byte",
          "description": "The file content, up to 20 MB. Required."
        },
        "filename": {
          "type": "string",
          "description": "The file's name, including its extension. Required."
        },
        "contentType": {
          "type": "string",
          "description": "The file's media type (for example `image/png`). Optional; derived from\nthe filename when omitted."
        },
        "caption": {
          "type": "string",
          "description": "Optional plain-text caption. The upload always creates one conversation\nmessage carrying the file; the caption becomes that message's text,\nstored in the same HTML-escaped, `\u003cp\u003e`-wrapped form as a posted message."
        },
        "requestId": {
          "type": "string",
          "description": "Optional idempotency key for this request: an opaque, client-generated\nUUID, scoped to the API key that sends it. Requests carrying the same key\nare executed at most once, so a retry can never create a second copy. Any\nrepeat is rejected with HTTP 409: if the original request completed, the\nbody's `resourceId` carries the id it created; if its outcome is\nstill unknown, or the key is reused with a materially different body,\nverify with a read before retrying with a fresh key. Keys are retained\nfor at least 24 hours. If the idempotency store is unavailable, requests\ncarrying a key are rejected with HTTP 503 (requests without a key are\nunaffected). Omit the key for no idempotency guarantee."
        },
        "fields": {
          "type": "string",
          "description": "Comma-separated response fields to include, using camelCase JSON names\n(e.g. `orderId,total.amountMicros`). Dot paths reach into nested objects\nand map transparently across arrays. Paths are relative to the resource,\nnot the response envelope; envelope keys are always preserved. Only 2xx\nJSON responses are filtered; error bodies pass through unmodified. Unknown\nnames are silently ignored. Takes precedence over `excludeFields` when\nboth are provided."
        },
        "excludeFields": {
          "type": "string",
          "description": "Comma-separated response fields to exclude, using camelCase JSON names.\nDot paths and array-transparency work the same as `fields`. Paths are\nrelative to the resource, not the response envelope. Only 2xx JSON\nresponses are filtered; error bodies pass through unmodified. Ignored\nwhen `fields` is also provided."
        }
      },
      "description": "Input for attaching one file to an order.",
      "required": [
        "file",
        "filename"
      ]
    },
    "factory.api.v1.salesorders.OrderService.UploadDrawingSvgBody": {
      "type": "object",
      "properties": {
        "svgs": {
          "type": "array",
          "items": {
            "type": "object",
            "$ref": "#/definitions/factory.api.v1.salesdocuments.DrawingSvg"
          },
          "description": "The flashing drawings and their rendered SVGs. At least one is required."
        },
        "fields": {
          "type": "string",
          "description": "Comma-separated response fields to include, using camelCase JSON names\n(e.g. `orderId,total.amountMicros`). Dot paths reach into nested objects\nand map transparently across arrays. Paths are relative to the resource,\nnot the response envelope; envelope keys are always preserved. Only 2xx\nJSON responses are filtered; error bodies pass through unmodified. Unknown\nnames are silently ignored. Takes precedence over `excludeFields` when\nboth are provided."
        },
        "excludeFields": {
          "type": "string",
          "description": "Comma-separated response fields to exclude, using camelCase JSON names.\nDot paths and array-transparency work the same as `fields`. Paths are\nrelative to the resource, not the response envelope. Only 2xx JSON\nresponses are filtered; error bodies pass through unmodified. Ignored\nwhen `fields` is also provided."
        }
      },
      "description": "Input for uploading rendered SVGs for an order's flashing drawings (the only\nline kind that carries drawings).",
      "required": [
        "svgs"
      ]
    },
    "factory.api.v1.salesorders.OrderStatus": {
      "type": "object",
      "properties": {
        "statusId": {
          "type": "string",
          "description": "The status's unique id. Pass this as `statusId` on CreateOrder."
        },
        "name": {
          "type": "string",
          "description": "The display name of the status (e.g. \"Submitted\")."
        },
        "index": {
          "type": "integer",
          "format": "int32",
          "description": "The status's position in the workflow, used for display ordering."
        },
        "colour": {
          "type": "string",
          "description": "The status's display colour."
        },
        "isDefault": {
          "type": "boolean",
          "description": "Whether this is the default status applied when an order is submitted\nwithout an explicit status."
        }
      },
      "description": "A single order status (a workflow column)."
    },
    "factory.api.v1.salesorders.PostMessageResponse": {
      "type": "object",
      "properties": {
        "message": {
          "$ref": "#/definitions/factory.api.v1.collaborate.Message",
          "description": "The created message."
        }
      },
      "description": "Result of posting a message: the created conversation entry."
    },
    "factory.api.v1.salesorders.QueryOrdersResponse": {
      "type": "object",
      "properties": {
        "orders": {
          "type": "array",
          "items": {
            "type": "object",
            "$ref": "#/definitions/factory.api.v1.salesorders.models.Order"
          },
          "description": "The orders in this page, oldest change first. Use the last order's update\ntime as the `updatedSince` for your next call."
        },
        "nextPageToken": {
          "type": "string",
          "description": "Token to pass as `pageToken` to fetch the next page; empty when there are no\nmore results."
        }
      },
      "description": "A page of orders from the change feed, ordered by when each order last changed\n(oldest first). The lower bound (`updatedSince`) is inclusive, so the order you\ncheckpoint on reappears as the first item of the next page — dedupe on `orderId`\n+ `lastUpdatedAt`, or skip ids you have already seen at the window boundary."
    },
    "factory.api.v1.salesorders.SetOrderLabelsResponse": {
      "type": "object",
      "properties": {
        "labels": {
          "type": "array",
          "items": {
            "type": "object",
            "$ref": "#/definitions/factory.api.v1.salesdocuments.Label"
          },
          "description": "The order's labels after the change, oldest attachment first."
        }
      },
      "description": "Result of replacing the labels on an order: the resulting label set."
    },
    "factory.api.v1.salesorders.UpdateOrderLineItemResponse": {
      "type": "object",
      "properties": {
        "order": {
          "$ref": "#/definitions/factory.api.v1.salesorders.models.Order",
          "description": "The updated order, including the recomputed totals."
        }
      },
      "description": "Result of replacing a line item: the order with recomputed totals."
    },
    "factory.api.v1.salesorders.UploadAttachmentResponse": {
      "type": "object",
      "properties": {
        "message": {
          "$ref": "#/definitions/factory.api.v1.collaborate.Message",
          "description": "The created message, carrying the uploaded file as its single\nattachment (with its id and a presigned download URL)."
        }
      },
      "description": "Result of attaching a file: the created conversation entry."
    },
    "factory.api.v1.salesorders.UploadDrawingSvgResponse": {
      "type": "object",
      "properties": {
        "results": {
          "type": "array",
          "items": {
            "type": "object",
            "$ref": "#/definitions/factory.api.v1.salesdocuments.DrawingSvgResult"
          },
          "description": "One result per submitted flashing drawing, in request order."
        }
      },
      "description": "Result of an SVG upload: one outcome per submitted flashing drawing."
    },
    "factory.api.v1.salesorders.models.Order": {
      "type": "object",
      "properties": {
        "orderId": {
          "type": "string",
          "description": "The order's unique id. Read-only.",
          "readOnly": true
        },
        "isSubmitted": {
          "type": "boolean",
          "description": "Whether the order has been submitted (true) or is still a draft (false)."
        },
        "customerId": {
          "type": "string",
          "description": "The id of the customer this order belongs to. Always present. Read-only.",
          "readOnly": true
        },
        "customerCompanyName": {
          "type": "string",
          "description": "The customer's company name. Read-only.",
          "readOnly": true
        },
        "statusId": {
          "type": "string",
          "description": "The id of the order's workflow status. Defaults to a \"Submitted\" status when\nthe order is submitted without one set."
        },
        "statusName": {
          "type": "string",
          "description": "The display name of the order's workflow status. Read-only.",
          "readOnly": true
        },
        "subtotal": {
          "$ref": "#/definitions/factory.common.v1.Money",
          "description": "Order subtotal — the sum of line totals before tax. Derived by the server\nfrom the line items; read-only.",
          "readOnly": true
        },
        "taxAmount": {
          "$ref": "#/definitions/factory.common.v1.Money",
          "description": "Total tax for the order, as an absolute money amount (the tax `amount`, not a\nrate). Derived by the server from your account's tax settings; read-only. See\n`tax` for the rate, code, and jurisdiction that produced it.",
          "readOnly": true
        },
        "total": {
          "$ref": "#/definitions/factory.common.v1.Money",
          "description": "Order grand total. Derived by the server; read-only.",
          "readOnly": true
        },
        "margin": {
          "$ref": "#/definitions/factory.common.v1.Money",
          "description": "Order margin, computed by the server from the line items. Read-only.",
          "readOnly": true
        },
        "totalCost": {
          "$ref": "#/definitions/factory.common.v1.Money",
          "description": "Total cost of the order, computed by the server from the line items. Read-only.",
          "readOnly": true
        },
        "lines": {
          "type": "array",
          "items": {
            "type": "object",
            "$ref": "#/definitions/factory.api.v1.salesdocuments.SalesLine"
          },
          "description": "The order's line items."
        },
        "adjustments": {
          "type": "array",
          "items": {
            "type": "object",
            "$ref": "#/definitions/factory.api.v1.salesdocuments.Adjustment"
          },
          "description": "Order-level fees, discounts, and markups. At most one fee per order is allowed."
        },
        "fulfilmentMethod": {
          "$ref": "#/definitions/factory.api.v1.salesdocuments.FulfilmentMethod",
          "description": "How the customer receives the order: pickup, delivery, or installation."
        },
        "billingAddress": {
          "$ref": "#/definitions/factory.api.v1.Address",
          "description": "Billing address for the order."
        },
        "deliveryAddress": {
          "$ref": "#/definitions/factory.api.v1.Address",
          "description": "Delivery address. Applies when `fulfilmentMethod` is DELIVERY."
        },
        "installAddress": {
          "$ref": "#/definitions/factory.api.v1.Address",
          "description": "Installation address. Applies when `fulfilmentMethod` is INSTALL."
        },
        "reference": {
          "type": "string",
          "description": "Free-text external reference, e.g. the order id or purchase order (PO)\nnumber from your own system. Shown as \"PO #\" in the Factory app and as \"PO\"\non the order documents."
        },
        "orderNumber": {
          "type": "string",
          "format": "int64",
          "description": "Factory's number for this order. Assigned by the server when the document\nwas created (a quote keeps its number when it becomes an order), sequential\nwithin your company, and never changed. Shown as \"Order #\" in the Factory\napp (order list, order page, workflow board) and on the order confirmation,\ninvoice, work order, and delivery docket, and used as the document\nreference in the accounting integrations. Read-only.",
          "readOnly": true
        },
        "receivedStatus": {
          "$ref": "#/definitions/factory.api.v1.salesorders.models.ReceivedStatus",
          "description": "Whether the order has been received. Read-only.",
          "readOnly": true
        },
        "paymentStatus": {
          "$ref": "#/definitions/factory.api.v1.salesorders.models.PaymentStatus",
          "description": "The order's payment status. Read-only.",
          "readOnly": true
        },
        "customFields": {
          "type": "object",
          "description": "The document's custom-field values, keyed by each field's configured key."
        },
        "createdAt": {
          "type": "string",
          "format": "date-time",
          "description": "When the order was created. Read-only.",
          "readOnly": true
        },
        "submittedAt": {
          "type": "string",
          "format": "date-time",
          "description": "When the order was submitted. Read-only.",
          "readOnly": true
        },
        "lastUpdatedAt": {
          "type": "string",
          "format": "date-time",
          "description": "When the order was last updated. Read-only.",
          "readOnly": true
        },
        "requiredAt": {
          "type": "string",
          "format": "date-time",
          "description": "The date the order is required or needed by."
        },
        "contact": {
          "$ref": "#/definitions/factory.api.v1.salesdocuments.Contact",
          "description": "The order's point-of-contact person."
        },
        "notes": {
          "type": "string",
          "description": "Free-text notes about the order."
        },
        "pickupNotes": {
          "type": "string",
          "description": "Free-text notes shown when the order is picked up."
        },
        "deliveryFee": {
          "$ref": "#/definitions/factory.common.v1.Money",
          "description": "The delivery fee charged on the order. Read-only.",
          "readOnly": true
        },
        "createdByUserId": {
          "type": "string",
          "description": "The id of the user who created the order, empty when unset. Resolve the user\nvia the Users API (which can return users who have since left). Read-only.",
          "readOnly": true
        },
        "submittedByUserId": {
          "type": "string",
          "description": "The id of the user who submitted the order, empty when unset. Resolve the user\nvia the Users API (which can return users who have since left). Read-only.",
          "readOnly": true
        },
        "discountAmount": {
          "$ref": "#/definitions/factory.common.v1.Money",
          "description": "The order-level discount total, rolled up from the per-line discounts and the\ndiscount entries in `adjustments`. Read-only.",
          "readOnly": true
        },
        "invoiceNumber": {
          "type": "string",
          "description": "The order's invoice number as displayed: the order's own number, or the\nlinked accounting system's number when synced. Empty until the order is\ninvoiced. Read-only.",
          "readOnly": true
        },
        "invoicedAt": {
          "type": "string",
          "format": "date-time",
          "description": "When the order was invoiced. Unset until the order is invoiced. Read-only.",
          "readOnly": true
        },
        "statusColour": {
          "type": "string",
          "description": "The colour of the order's workflow status, for display (defaults to\n\"transparent\"). The display companion to `statusName`. Read-only.",
          "readOnly": true
        },
        "tax": {
          "$ref": "#/definitions/factory.common.v1.TaxDetail",
          "description": "The tax applied to the order: rate, code, and jurisdiction. Reflects your\naccount's tax settings — the values actually applied — and is the companion\ndescriptor to the `taxAmount` amount. Read-only.",
          "readOnly": true
        },
        "labels": {
          "type": "array",
          "items": {
            "type": "object",
            "$ref": "#/definitions/factory.api.v1.salesdocuments.Label"
          },
          "description": "The labels attached to this order, oldest attachment first. Present on\ncreate responses when `labelIds` are supplied; absent from line-write\nresponses.",
          "readOnly": true
        },
        "isArchived": {
          "type": "boolean",
          "description": "Whether the order is archived. Archived orders are hidden from every read\nby default: reach them with GetOrder's `includeArchived`, ListOrders'\narchived, or QueryOrders' `includeArchived`. Always false on default\nreads. Read-only.",
          "readOnly": true
        },
        "labourTotal": {
          "$ref": "#/definitions/factory.common.v1.Money",
          "description": "The total charged for labour on the order: the sum of the labour line\ntotals, including labour components inside product kits. Derived by the\nserver from the line items on every write; read-only.",
          "readOnly": true
        }
      },
      "description": "A sales order, as returned by read endpoints. Includes the order\nheader, its line items, adjustments, addresses, contact, totals, and all\nserver-assigned fields. The same shape is returned whether the order was just\ncreated or fetched later."
    },
    "factory.api.v1.salesorders.models.PaymentStatus": {
      "type": "string",
      "enum": [
        "PAYMENT_STATUS_UNSPECIFIED",
        "PAYMENT_STATUS_UNPAID",
        "PAYMENT_STATUS_PARTIALLY_PAID",
        "PAYMENT_STATUS_PAID"
      ],
      "default": "PAYMENT_STATUS_UNSPECIFIED",
      "description": "Whether the order has been paid for. Read-only.\n\n - PAYMENT_STATUS_UNSPECIFIED: Default, unset value.\n - PAYMENT_STATUS_UNPAID: No payment has been received.\n - PAYMENT_STATUS_PARTIALLY_PAID: Part of the order total has been paid.\n - PAYMENT_STATUS_PAID: The order has been paid in full."
    },
    "factory.api.v1.salesorders.models.ReceivedStatus": {
      "type": "string",
      "enum": [
        "RECEIVED_STATUS_UNSPECIFIED",
        "RECEIVED_STATUS_DELIVERED",
        "RECEIVED_STATUS_PICKED_UP",
        "RECEIVED_STATUS_NOT_RECEIVED",
        "RECEIVED_STATUS_INSTALLED"
      ],
      "default": "RECEIVED_STATUS_UNSPECIFIED",
      "description": "Whether and how the order has reached the customer. Read-only.\n\n - RECEIVED_STATUS_UNSPECIFIED: Default, unset value.\n - RECEIVED_STATUS_DELIVERED: The order was delivered.\n - RECEIVED_STATUS_PICKED_UP: The order was picked up by the customer.\n - RECEIVED_STATUS_NOT_RECEIVED: The order has not yet reached the customer.\n - RECEIVED_STATUS_INSTALLED: The order was installed."
    },
    "factory.api.v1.search.CatalogueEntryType": {
      "type": "string",
      "enum": [
        "CATALOGUE_ENTRY_TYPE_UNSPECIFIED",
        "CATALOGUE_ENTRY_TYPE_PRODUCT",
        "CATALOGUE_ENTRY_TYPE_KIT",
        "CATALOGUE_ENTRY_TYPE_FLASHING"
      ],
      "default": "CATALOGUE_ENTRY_TYPE_UNSPECIFIED",
      "description": "The kind of catalogue entry a search hit refers to."
    },
    "factory.api.v1.search.CatalogueSearchHit": {
      "type": "object",
      "properties": {
        "id": {
          "type": "string",
          "description": "The entry's unique id. The prefix always agrees with `type`. A `prod_` id\ndrills in with GetProduct and a `kit_` id with GetKit. KNOWN LIMITATION:\na `flashing_` id is the flashing-family id and is NOT currently accepted\nby GetFlashing (which takes the fronting `flashtpl_` template id); flashing hits are\ntherefore not drillable yet. Treat a flashing hit as name-only for now."
        },
        "type": {
          "$ref": "#/definitions/factory.api.v1.search.CatalogueEntryType",
          "description": "What kind of catalogue entry this hit refers to."
        },
        "name": {
          "type": "string",
          "description": "The entry's display name."
        },
        "categoryId": {
          "type": "string",
          "description": "The id of the entry's category. Unset for flashings."
        },
        "categoryName": {
          "type": "string",
          "description": "The display name of the entry's category. Unset for flashings."
        }
      },
      "description": "One search hit: a minimal reference into the catalogue."
    },
    "factory.api.v1.search.SearchCatalogueResponse": {
      "type": "object",
      "properties": {
        "hits": {
          "type": "array",
          "items": {
            "type": "object",
            "$ref": "#/definitions/factory.api.v1.search.CatalogueSearchHit"
          },
          "description": "The hits in this page."
        },
        "nextPageToken": {
          "type": "string",
          "description": "Token to pass as `pageToken` to fetch the next page; empty when there are\nno more results."
        }
      },
      "description": "A page of search hits, most relevant first."
    },
    "factory.api.v1.suppliers.GetSupplierResponse": {
      "type": "object",
      "properties": {
        "supplier": {
          "$ref": "#/definitions/factory.api.v1.suppliers.Supplier",
          "description": "The requested supplier."
        }
      },
      "description": "Result of retrieving a single supplier."
    },
    "factory.api.v1.suppliers.ListSuppliersResponse": {
      "type": "object",
      "properties": {
        "suppliers": {
          "type": "array",
          "items": {
            "type": "object",
            "$ref": "#/definitions/factory.api.v1.suppliers.Supplier"
          },
          "description": "The suppliers in this page."
        },
        "nextPageToken": {
          "type": "string",
          "description": "Token to pass as `pageToken` to fetch the next page; empty when there are\nno more results."
        }
      },
      "description": "A page of suppliers."
    },
    "factory.api.v1.suppliers.Supplier": {
      "type": "object",
      "properties": {
        "supplierId": {
          "type": "string",
          "description": "The supplier's unique id.",
          "readOnly": true
        },
        "name": {
          "type": "string",
          "description": "The supplier's name.",
          "readOnly": true
        }
      },
      "description": "A supplier linked to your catalogue products."
    },
    "factory.api.v1.users.GetUserResponse": {
      "type": "object",
      "properties": {
        "user": {
          "$ref": "#/definitions/factory.api.v1.users.User",
          "description": "The requested user."
        }
      },
      "description": "Result of retrieving a single user."
    },
    "factory.api.v1.users.ListUsersResponse": {
      "type": "object",
      "properties": {
        "users": {
          "type": "array",
          "items": {
            "type": "object",
            "$ref": "#/definitions/factory.api.v1.users.User"
          },
          "description": "The users in this page."
        },
        "nextPageToken": {
          "type": "string",
          "description": "Token to pass as `pageToken` to fetch the next page; empty when there are no\nmore results."
        }
      },
      "description": "A page of users."
    },
    "factory.api.v1.users.User": {
      "type": "object",
      "properties": {
        "userId": {
          "type": "string",
          "description": "The user's unique id. This is the value to use as a labour line's user id\nwhen assigning the user to a labour line on an order."
        },
        "firstName": {
          "type": "string",
          "description": "The user's first name."
        },
        "lastName": {
          "type": "string",
          "description": "The user's last name."
        },
        "email": {
          "type": "string",
          "description": "The user's email address. In ListUsers responses this is populated only\nwhen the caller is a company administrator; GetUser always returns it."
        },
        "isActive": {
          "type": "boolean",
          "description": "Whether the user is active. Only active users can be assigned to a labour\nline."
        },
        "hourlyRateCharged": {
          "$ref": "#/definitions/factory.common.v1.Money",
          "description": "The default per-hour rate charged for this user's labour — what an hour\nof their work sells for. Zero when no rate has been set for the user."
        },
        "hourlyRateCost": {
          "$ref": "#/definitions/factory.common.v1.Money",
          "description": "The per-hour cost of this user's labour to your company. Zero when no\nrate has been set for the user."
        }
      },
      "description": "A user belonging to your company."
    },
    "factory.api.v1.webhooks.CreateWebhookRequest": {
      "type": "object",
      "properties": {
        "url": {
          "type": "string",
          "description": "The HTTPS endpoint to deliver events to. Required. Plain http URLs and\nendpoints resolving to private address space are rejected."
        },
        "eventTypes": {
          "type": "array",
          "items": {
            "type": "string"
          },
          "description": "The event types to deliver. Required, at least one; see\n`eventTypes` on Webhook for the valid values. Unknown values are rejected\nnaming the offender."
        },
        "description": {
          "type": "string",
          "description": "A free-form label for your own bookkeeping. Optional."
        }
      },
      "description": "Input for registering a webhook subscription."
    },
    "factory.api.v1.webhooks.CreateWebhookResponse": {
      "type": "object",
      "properties": {
        "webhook": {
          "$ref": "#/definitions/factory.api.v1.webhooks.models.Webhook",
          "description": "The created subscription. This response is the ONLY place secret is\never populated — store it now; it cannot be retrieved again."
        }
      },
      "description": "Result of registering a webhook subscription."
    },
    "factory.api.v1.webhooks.DeleteWebhookResponse": {
      "type": "object",
      "description": "Result of deleting a webhook subscription."
    },
    "factory.api.v1.webhooks.GetWebhookDeliveryResponse": {
      "type": "object",
      "properties": {
        "delivery": {
          "$ref": "#/definitions/factory.api.v1.webhooks.models.WebhookDelivery",
          "description": "The requested delivery record."
        }
      },
      "description": "Result of fetching one delivery record."
    },
    "factory.api.v1.webhooks.GetWebhookResponse": {
      "type": "object",
      "properties": {
        "webhook": {
          "$ref": "#/definitions/factory.api.v1.webhooks.models.Webhook",
          "description": "The requested subscription. secret is never populated here."
        }
      },
      "description": "Result of fetching one webhook subscription."
    },
    "factory.api.v1.webhooks.ListWebhookDeliveriesResponse": {
      "type": "object",
      "properties": {
        "deliveries": {
          "type": "array",
          "items": {
            "type": "object",
            "$ref": "#/definitions/factory.api.v1.webhooks.models.WebhookDelivery"
          },
          "description": "One page of delivery records, newest first."
        },
        "nextPageToken": {
          "type": "string",
          "description": "Token to pass as `pageToken` to fetch the next page; empty when there are\nno more results."
        }
      },
      "description": "Result of listing delivery records."
    },
    "factory.api.v1.webhooks.ListWebhooksResponse": {
      "type": "object",
      "properties": {
        "webhooks": {
          "type": "array",
          "items": {
            "type": "object",
            "$ref": "#/definitions/factory.api.v1.webhooks.models.Webhook"
          },
          "description": "One page of subscriptions, newest first. Secrets are never included."
        },
        "nextPageToken": {
          "type": "string",
          "description": "Token to pass as `pageToken` to fetch the next page; empty when there are\nno more results."
        }
      },
      "description": "Result of listing your company's webhook subscriptions."
    },
    "factory.api.v1.webhooks.TestWebhookResponse": {
      "type": "object",
      "properties": {
        "statusCode": {
          "type": "integer",
          "format": "int32",
          "description": "The HTTP status the endpoint returned; 0 when no response arrived\n(timeout or connection failure)."
        },
        "responseSnippet": {
          "type": "string",
          "description": "The start of the endpoint's response body, truncated to 1000 characters."
        },
        "durationMs": {
          "type": "string",
          "format": "int64",
          "description": "How long the round trip took."
        }
      },
      "description": "Result of the synchronous endpoint test."
    },
    "factory.api.v1.webhooks.UpdateWebhookResponse": {
      "type": "object",
      "properties": {
        "webhook": {
          "$ref": "#/definitions/factory.api.v1.webhooks.models.Webhook",
          "description": "The subscription after the change. secret is never populated here."
        }
      },
      "description": "Result of changing a webhook subscription."
    },
    "factory.api.v1.webhooks.WebhookService.TestWebhookBody": {
      "type": "object",
      "description": "Input for the synchronous endpoint test."
    },
    "factory.api.v1.webhooks.WebhookService.UpdateWebhookBody": {
      "type": "object",
      "properties": {
        "url": {
          "type": "string",
          "description": "A new HTTPS endpoint. Same validation as at create."
        },
        "eventTypes": {
          "type": "array",
          "items": {
            "type": "string"
          },
          "description": "A replacement event-type list (full replacement, not a merge). At least\none when provided."
        },
        "description": {
          "type": "string",
          "description": "A new description."
        },
        "status": {
          "$ref": "#/definitions/factory.api.v1.webhooks.models.WebhookStatus",
          "description": "Enable or disable deliveries. Re-enabling an auto-disabled subscription\nresumes deliveries of NEW events; exhausted deliveries are not revived."
        }
      },
      "description": "Input for changing a webhook subscription. Fields left unset keep their\ncurrent value."
    },
    "factory.api.v1.webhooks.models.DeliveryState": {
      "type": "string",
      "enum": [
        "DELIVERY_STATE_UNSPECIFIED",
        "DELIVERY_STATE_PENDING",
        "DELIVERY_STATE_SUCCEEDED",
        "DELIVERY_STATE_EXHAUSTED"
      ],
      "default": "DELIVERY_STATE_UNSPECIFIED",
      "description": "The state of one delivery (one event to one subscription).\n\n - DELIVERY_STATE_PENDING: Waiting for its first or next attempt (see `nextAttemptAt`).\n - DELIVERY_STATE_SUCCEEDED: The endpoint acknowledged the delivery with a 2xx response.\n - DELIVERY_STATE_EXHAUSTED: All retry attempts were used without a 2xx (about 10.6 hours across 6\nattempts). Exhausted deliveries are not retried again."
    },
    "factory.api.v1.webhooks.models.Webhook": {
      "type": "object",
      "properties": {
        "webhookId": {
          "type": "string",
          "description": "The subscription's id. Read-only.",
          "readOnly": true
        },
        "url": {
          "type": "string",
          "description": "The HTTPS endpoint deliveries are POSTed to. Plain http URLs and\nendpoints resolving to private address space are rejected at create and\nupdate time."
        },
        "eventTypes": {
          "type": "array",
          "items": {
            "type": "string"
          },
          "description": "The event types this subscription receives. At least one. Valid values\nare the v1 taxonomy: `order.created`, `order.updated`,\n`order.status_changed`, `order.archived`, `quote.created`,\n`quote.updated`, `quote.status_changed`, `quote.converted_to_order`.\nUnknown values are rejected naming the offender."
        },
        "status": {
          "$ref": "#/definitions/factory.api.v1.webhooks.models.WebhookStatus",
          "description": "Whether the subscription is receiving deliveries. New subscriptions are\nenabled; see WebhookStatus for how a subscription becomes disabled."
        },
        "description": {
          "type": "string",
          "description": "A free-form label for your own bookkeeping."
        },
        "secret": {
          "type": "string",
          "description": "The signing secret, in the form `whsec_\u003c43 base64url characters\u003e`.\nReturned ONLY in the CreateWebhook response — store it then; it cannot\nbe retrieved again (rotation is not available in v1). The FULL string,\nprefix included, is the HMAC-SHA256 key for verifying the\nX-Factory-Signature header on deliveries.",
          "readOnly": true
        },
        "createdAt": {
          "type": "string",
          "format": "date-time",
          "description": "When the subscription was created. Read-only.",
          "readOnly": true
        },
        "updatedAt": {
          "type": "string",
          "format": "date-time",
          "description": "When the subscription was last changed. Read-only.",
          "readOnly": true
        }
      },
      "description": "A webhook subscription: where to deliver events, and which ones."
    },
    "factory.api.v1.webhooks.models.WebhookDelivery": {
      "type": "object",
      "properties": {
        "deliveryId": {
          "type": "string",
          "description": "The delivery record's id. Read-only.",
          "readOnly": true
        },
        "webhookId": {
          "type": "string",
          "description": "The subscription this delivery targets. Read-only.",
          "readOnly": true
        },
        "eventId": {
          "type": "string",
          "description": "The event being delivered. Matches the `id` of the delivered payload —\nidentical across retries and duplicates; deduplicate on it. Read-only.",
          "readOnly": true
        },
        "eventType": {
          "type": "string",
          "description": "The event's type (see `eventTypes` on Webhook for the taxonomy). Read-only.",
          "readOnly": true
        },
        "state": {
          "$ref": "#/definitions/factory.api.v1.webhooks.models.DeliveryState",
          "description": "Where the delivery is in its lifecycle. Read-only.",
          "readOnly": true
        },
        "attemptCount": {
          "type": "integer",
          "format": "int32",
          "description": "Attempts made so far (0 while pending its first attempt). Read-only.",
          "readOnly": true
        },
        "nextAttemptAt": {
          "type": "string",
          "format": "date-time",
          "description": "When the next attempt is due; unset once succeeded or exhausted.\nRead-only.",
          "readOnly": true
        },
        "lastAttemptedAt": {
          "type": "string",
          "format": "date-time",
          "description": "When the most recent attempt ran; unset before the first attempt.\nRead-only.",
          "readOnly": true
        },
        "lastStatusCode": {
          "type": "integer",
          "format": "int32",
          "description": "The HTTP status the endpoint returned on the most recent attempt; 0 when\nthe attempt never got a response (timeout, connection failure, a URL\nrefused at send time, or the original event no longer being available).\nRead-only.",
          "readOnly": true
        },
        "lastResponseSnippet": {
          "type": "string",
          "description": "The start of the endpoint's response body on the most recent attempt,\ntruncated to 1000 characters. Read-only.",
          "readOnly": true
        },
        "createdAt": {
          "type": "string",
          "format": "date-time",
          "description": "When the delivery record was created (the event's intake time).\nRead-only.",
          "readOnly": true
        }
      },
      "description": "One event's delivery record for one subscription: current state plus\nlast-attempt details. Records are kept for 30 days."
    },
    "factory.api.v1.webhooks.models.WebhookStatus": {
      "type": "string",
      "enum": [
        "WEBHOOK_STATUS_UNSPECIFIED",
        "WEBHOOK_STATUS_ENABLED",
        "WEBHOOK_STATUS_DISABLED"
      ],
      "default": "WEBHOOK_STATUS_UNSPECIFIED",
      "description": "The lifecycle state of a webhook subscription.\n\n - WEBHOOK_STATUS_ENABLED: The subscription receives deliveries.\n - WEBHOOK_STATUS_DISABLED: The subscription receives no deliveries. Set manually via UpdateWebhook,\nor automatically when deliveries exhaust their retries and nothing has\nbeen delivered successfully for 7 days (auto-disable; re-enable via\nUpdateWebhook once the endpoint is healthy)."
    },
    "factory.common.v1.KitComponentType": {
      "type": "string",
      "enum": [
        "KIT_COMPONENT_TYPE_UNSPECIFIED",
        "KIT_COMPONENT_TYPE_CATALOGUE_PRODUCT",
        "KIT_COMPONENT_TYPE_ON_THE_FLY_PRODUCT",
        "KIT_COMPONENT_TYPE_NOTES",
        "KIT_COMPONENT_TYPE_LABOUR"
      ],
      "default": "KIT_COMPONENT_TYPE_UNSPECIFIED",
      "description": "The kind of a component within a product kit.\n\n - KIT_COMPONENT_TYPE_UNSPECIFIED: Default, unset value. Not a valid choice for a kit component.\n - KIT_COMPONENT_TYPE_CATALOGUE_PRODUCT: A component that is a product from your catalogue.\n - KIT_COMPONENT_TYPE_ON_THE_FLY_PRODUCT: A free-form component not tied to a catalogue product. Occurs on order and\nquote kits only — it is not a catalogue-template component kind.\n - KIT_COMPONENT_TYPE_NOTES: A note component: carries text rather than a priced product.\n - KIT_COMPONENT_TYPE_LABOUR: A labour component."
    },
    "factory.common.v1.Money": {
      "type": "object",
      "properties": {
        "amountMicros": {
          "type": "string",
          "format": "int64",
          "description": "Amount in micros — millionths of the currency's major unit. 1.00 =\n1_000_000 micros, so $12.34 is 12_340_000 — sent and returned over JSON as\nthe string \"12340000\" (64-bit integers serialize as JSON strings). A whole\nnumber of cents (or pence, etc.) is a multiple of 10_000 micros."
        },
        "currency": {
          "type": "string",
          "description": "ISO 4217 currency code (3 letters, e.g. \"AUD\")."
        }
      },
      "description": "Money — currency amount as an integer number of micros (no float).\n\nOne micro is a millionth of the currency's major unit: 1.00 = 1_000_000\nmicros, giving 6 decimal places of precision for exact line and order\namounts in any currency. Currency is ISO 4217, e.g. \"AUD\".\n\nTo leave an OPTIONAL money field unset, omit the whole object — an empty\nMoney (or one with amountMicros 0) is a real, supplied $0.00, not an\nomission. This matters wherever the server derives an omitted value."
    },
    "factory.common.v1.NotesType": {
      "type": "string",
      "enum": [
        "NOTES_TYPE_UNSPECIFIED",
        "NOTES_TYPE_INTERNAL",
        "NOTES_TYPE_EXTERNAL"
      ],
      "default": "NOTES_TYPE_UNSPECIFIED",
      "description": "Whether a notes line is internal-only or visible to the customer.\n\n - NOTES_TYPE_UNSPECIFIED: Default, unset value. Not a valid choice for a notes line.\n - NOTES_TYPE_INTERNAL: An internal note, not shown to the customer.\n - NOTES_TYPE_EXTERNAL: An external note, visible to the customer."
    },
    "factory.common.v1.PricingStrategy": {
      "type": "string",
      "enum": [
        "PRICING_STRATEGY_UNSPECIFIED",
        "PRICING_STRATEGY_BASIC_QUANTITIES",
        "PRICING_STRATEGY_LINEAL_METRES",
        "PRICING_STRATEGY_CUSTOM_FORMULA",
        "PRICING_STRATEGY_SQUARE_METRES",
        "PRICING_STRATEGY_LINEAL_FEET",
        "PRICING_STRATEGY_SQUARE_FEET"
      ],
      "default": "PRICING_STRATEGY_UNSPECIFIED",
      "description": "How a quantity is interpreted for pricing — whether it means a count of\neach, a length, or an area.\n\nOn a catalogue product this is the product's pricing basis. On an order or\nquote line, measurement-based strategies (lineal or square metres/feet)\nrequire the line's measurement details to be supplied.\n\n - PRICING_STRATEGY_UNSPECIFIED: Unset, or a legacy product with no pricing strategy recorded.\n - PRICING_STRATEGY_BASIC_QUANTITIES: Quantity is a simple per-each count.\n - PRICING_STRATEGY_LINEAL_METRES: Quantity is a length in lineal metres.\n - PRICING_STRATEGY_CUSTOM_FORMULA: Quantity is priced by a custom formula.\n - PRICING_STRATEGY_SQUARE_METRES: Quantity is an area in square metres.\n - PRICING_STRATEGY_LINEAL_FEET: Quantity is a length in lineal feet.\n - PRICING_STRATEGY_SQUARE_FEET: Quantity is an area in square feet."
    },
    "factory.common.v1.TaxDetail": {
      "type": "object",
      "properties": {
        "rate": {
          "type": "string",
          "description": "The tax rate applied, as a decimal fraction (e.g. \"0.10\" means 10%)."
        },
        "code": {
          "type": "string",
          "description": "The tax code, e.g. \"GST\", \"VAT\", or \"SALES_TAX\". Open-ended — not a fixed set."
        },
        "jurisdiction": {
          "type": "string",
          "description": "The tax jurisdiction, e.g. \"AU\", \"GB\", or \"US-CA-LOS_ANGELES\". Open-ended."
        }
      },
      "description": "The tax applied to an amount: its rate, code, and jurisdiction.\n\nA structured tax descriptor. Today a single rate, code, and jurisdiction apply\nacross your whole account; this object leaves room for richer\nper-jurisdiction tax later without changing shape."
    },
    "factory.common.v1.TaxIdentifier": {
      "type": "object",
      "properties": {
        "value": {
          "type": "string",
          "description": "The identifier value, e.g. an 11-digit ABN. Its format depends on `type` and\nis NOT validated in v1: an ABN is accepted as any non-empty string of up to\n15 characters. The length cap may widen when more schemes are supported."
        },
        "type": {
          "$ref": "#/definitions/factory.common.v1.TaxIdentifierType",
          "description": "Which tax / business registration scheme `value` belongs to. Must be a\nspecified scheme when a TaxIdentifier is present. v1 accepts only ABN."
        }
      },
      "description": "TaxIdentifier — a tax / business registration number paired with the scheme it\nbelongs to. This is a value object: `value` is meaningless without `type`.\n\nFuture-proofs the API for non-Australian customers. v1 accepts ONLY an ABN\n(TAX_IDENTIFIER_TYPE_ABN); the other schemes are listed but reserved for\nfuture releases — the v1 server rejects them with HTTP 400\n(validation_failure)."
    },
    "factory.common.v1.TaxIdentifierType": {
      "type": "string",
      "enum": [
        "TAX_IDENTIFIER_TYPE_UNSPECIFIED",
        "TAX_IDENTIFIER_TYPE_ABN",
        "TAX_IDENTIFIER_TYPE_EU_VAT",
        "TAX_IDENTIFIER_TYPE_US_EIN",
        "TAX_IDENTIFIER_TYPE_US_TIN",
        "TAX_IDENTIFIER_TYPE_US_SSN",
        "TAX_IDENTIFIER_TYPE_GB_VAT",
        "TAX_IDENTIFIER_TYPE_NZ_GST"
      ],
      "default": "TAX_IDENTIFIER_TYPE_UNSPECIFIED",
      "description": "The tax / business registration scheme a TaxIdentifier belongs to. v1 accepts\nonly ABN; the remaining values are reserved for future non-AU support.\n\n - TAX_IDENTIFIER_TYPE_UNSPECIFIED: Default, unset value. Not a valid scheme when a TaxIdentifier is supplied.\n - TAX_IDENTIFIER_TYPE_ABN: Australian Business Number. The only scheme accepted in v1.\n - TAX_IDENTIFIER_TYPE_EU_VAT: EU VAT number. Does not cover the United Kingdom — a UK VAT registration\nis TAX_IDENTIFIER_TYPE_GB_VAT. Reserved — not accepted in v1.\n - TAX_IDENTIFIER_TYPE_US_EIN: US Employer Identification Number. Reserved — not accepted in v1.\n - TAX_IDENTIFIER_TYPE_US_TIN: US Taxpayer Identification Number. Reserved — not accepted in v1.\n - TAX_IDENTIFIER_TYPE_US_SSN: US Social Security Number (used by sole traders). Reserved — not accepted in v1.\n - TAX_IDENTIFIER_TYPE_GB_VAT: United Kingdom VAT registration number. Reserved — not accepted in v1.\n - TAX_IDENTIFIER_TYPE_NZ_GST: New Zealand GST number (the IRD number of a GST-registered business).\nReserved — not accepted in v1."
    },
    "google.protobuf.NullValue": {
      "type": "string",
      "enum": [
        "NULL_VALUE"
      ],
      "default": "NULL_VALUE",
      "description": "`NullValue` is a singleton enumeration to represent the null value for the\n`Value` type union.\n\nThe JSON representation for `NullValue` is JSON `null`.\n\n - NULL_VALUE: Null value."
    }
  },
  "securityDefinitions": {
    "Bearer": {
      "type": "apiKey",
      "description": "Bearer token authentication. Send your token in the `Authorization` header of every request, in the form `Authorization: Bearer \u003ctoken\u003e`. Tokens are issued by Factory; contact your Factory representative to obtain one.",
      "name": "Authorization",
      "in": "header"
    }
  },
  "security": [
    {
      "Bearer": []
    }
  ]
}
