{
  "openapi": "3.0.1",
  "info": {
    "title": "Flex API",
    "description": "Flex's tenant API for integrations and AI agents. Every response is scoped to the organization that owns the presented key.\n\n**Authentication.** Send `Authorization: Bearer flx_live_…` on every request. Keys are created by an organization administrator inside Flex; the secret is shown once and stored hashed. A key belongs to exactly one organization.\n\n**Permissions.** A key is granted a subset of Flex permissions when it is created. Each endpoint states the permission it needs; a key without it receives 403. `GET /api/v1/modules` lists the permissions of the presented key.\n\n**Modules.** Endpoints that belong to an optional Flex module (scheduling, invoicing, …) answer 403 with a problem detail when the organization has not activated that module. `GET /api/v1/modules` shows which modules are active.\n\n**Rate limits.** Fixed one-minute windows, per key (default 120 requests/min, configurable per key) and per organization (600/min). Every response carries `X-RateLimit-Limit`, `X-RateLimit-Remaining` and `X-RateLimit-Reset` (seconds until the key's window resets) so a client can pace itself. Exceeding either limit returns 429 with a `Retry-After` header (seconds).\n\n**Pagination.** List endpoints return `{ \"items\": [...], \"nextCursor\": \"…\" }`. Pass `after=<nextCursor>` to fetch the next page and `limit` to size it (1–200, default 50). `nextCursor` is null on the last page; cursors are opaque and stable.\n\n**CSV.** Every list endpoint (`GET /api/v1/activity`, `/categories`, `/customers`, `/customers/{id}/locations`, `/jobs`, `/jobs/{id}/photos`, `/invoices`, `/items`, `/items/low-stock`, `/items/{id}/reorder-points`, `/items/{id}/units`, `/labor-rates`, `/locations`, `/purchase-orders`, `/reorder-points`, `/roles`, `/sales-orders`, `/schedule`, `/schedule-groups`, `/transfers`, `/users`, `/users/removal-requests`, `/vendors`) also answers in CSV: send `Accept: text/csv` or append `?format=csv`. Columns are the JSON property names in declaration order — a nested object is flattened to `parent.child` columns, a list of scalars is `;`-joined, a list of objects is compact JSON in the cell; values are written exactly as in the JSON (invariant decimals, ISO-8601 UTC dates, enum names, `true`/`false`, empty for null). The file is UTF-8 with BOM, CRLF rows, RFC 4180 quoting, and a text cell starting with `=`, `+`, `-`, `@`, TAB or CR is prefixed with an apostrophe so spreadsheets do not run it as a formula. Paging is identical to JSON; because the CSV body has no envelope the next page's cursor is returned in the `X-Next-Cursor` header together with `Link: <next page URL>; rel=\"next\"` (absent on the last page). Responses are attachments named `<resource>.csv`. `GET /api/v1/modules` and single-resource GETs are JSON only.\n\n**ETags.** Single-resource and list GETs return a weak `ETag`. Send it back as `If-None-Match` to receive 304 Not Modified with no body when nothing changed. A list's tag is per representation — the JSON and CSV tags of the same page differ — and list responses carry `Vary: Accept`.\n\n**Idempotency.** Send an `Idempotency-Key` header (any unique string, up to 128 characters) on POST, PUT, PATCH and DELETE. Replaying the same key with the same body returns the original response instead of repeating the write; the same key with a different body is rejected with 422. Keys are remembered for 24 hours per organization.\n\n**Errors.** Failures are RFC 7807 `application/problem+json`: 400 validation errors list per-field messages under `errors`; 401/403/404/409/422/429/500 carry `title`, `detail` and a `traceId` to quote in support requests.",
    "termsOfService": "https://flexonthejob.com/terms",
    "contact": {
      "name": "Flex developer support",
      "url": "https://flexonthejob.com/developers",
      "email": "developers@flexonthejob.com"
    },
    "license": {
      "name": "Proprietary — use is governed by the Flex Terms of Service",
      "url": "https://flexonthejob.com/terms"
    },
    "version": "v1",
    "x-changelog": "https://flexonthejob.com/changelog",
    "x-status-page": "https://status.flexonthejob.com/"
  },
  "servers": [
    {
      "url": "https://app.flexonthejob.com",
      "description": "Production — your organization's data, keys from Settings → Integrations → API Keys"
    },
    {
      "url": "https://sandbox.flexonthejob.com",
      "description": "Sandbox — demo organization, public read-only and write keys, reset nightly at 08:00 UTC. No real-world side effects: email, webhook delivery and automation actions don't run (automation rules match and are logged as skipped), and the AI assistant answers from a fixed demo script"
    }
  ],
  "paths": {
    "/api/v1/activity": {
      "get": {
        "tags": [
          "Activity"
        ],
        "summary": "Lists logged API and MCP requests, oldest first, one page at a time.",
        "description": "Requires `ManageOrganizations`. Rows are returned in the order they were written\n(ascending `id`), so a poller can pass the last `nextCursor` back as\n`after` to receive only newer rows. A row is written just after its response is\nsent, so a request appears a moment after it completes. Rows are kept 90 days.\nExample: `GET /api/v1/activity?since=2026-09-22T00:00:00Z&search=update_job_status`.\nAlso available as CSV (`Accept: text/csv` or `?format=csv`); paging cursor in `X-Next-Cursor`.",
        "parameters": [
          {
            "name": "since",
            "in": "query",
            "description": "Only requests that started at/after this instant (ISO-8601 UTC, e.g. `2026-09-22T00:00:00Z`).",
            "schema": {
              "type": "string",
              "format": "date-time"
            }
          },
          {
            "name": "until",
            "in": "query",
            "description": "Only requests that started before this instant (ISO-8601 UTC, exclusive).",
            "schema": {
              "type": "string",
              "format": "date-time"
            }
          },
          {
            "name": "apiKeyId",
            "in": "query",
            "description": "Only requests made with this key id. Example: `3`.",
            "schema": {
              "type": "integer",
              "format": "int32"
            }
          },
          {
            "name": "statusClass",
            "in": "query",
            "description": "Only responses of this HTTP status class, 1-5: e.g. `2` for 2xx, `4` for 4xx (1xx is accepted but never logged). Omit for all.",
            "schema": {
              "type": "integer",
              "format": "int32"
            }
          },
          {
            "name": "search",
            "in": "query",
            "description": "Case-insensitive match against the path, MCP method and MCP tool name. Example: `jobs`.",
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "after",
            "in": "query",
            "description": "Cursor from the previous page's `nextCursor` (a log row id). Omit for the first page.",
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "limit",
            "in": "query",
            "description": "Page size, 1..500 (default 50).",
            "schema": {
              "type": "integer",
              "format": "int32"
            }
          },
          {
            "name": "format",
            "in": "query",
            "description": "Alternative to the Accept header: `csv` returns text/csv.",
            "schema": {
              "enum": [
                "csv"
              ],
              "type": "string"
            }
          }
        ],
        "responses": {
          "401": {
            "description": "Unauthorized",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ProblemDetails"
                }
              }
            }
          },
          "403": {
            "description": "Forbidden",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ProblemDetails"
                }
              }
            }
          },
          "429": {
            "description": "Too Many Requests",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ProblemDetails"
                }
              }
            }
          },
          "200": {
            "description": "One page of log rows and the cursor for the next page (null on the last page).",
            "headers": {
              "X-Next-Cursor": {
                "description": "text/csv only: the page's nextCursor. Absent on the last page. Pass it as ?after= (or follow the Link header) to fetch the next page.",
                "schema": {
                  "type": "string"
                }
              },
              "Link": {
                "description": "text/csv only, alongside X-Next-Cursor: <absolute URL of the next page>; rel=\"next\".",
                "schema": {
                  "type": "string"
                }
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/PagedResponseOfApiActivityDto"
                }
              },
              "text/csv": {
                "schema": {
                  "type": "string",
                  "description": "CSV document: UTF-8 with BOM, CRLF rows, RFC 4180 quoting. The header row holds the JSON property names of one item (nested objects flattened as parent.child); one row per item of the page."
                }
              }
            }
          },
          "400": {
            "description": "Unusable `after` cursor or `statusClass`.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ValidationProblemDetails"
                }
              }
            }
          }
        }
      }
    },
    "/api/v1/categories": {
      "get": {
        "tags": [
          "Categories"
        ],
        "summary": "Lists the organization's item categories, one page at a time.",
        "description": "Requires `ViewInventory`. Categories are flat with a `parentCategoryId`; nest them\nclient-side. The built-in system category is not listed. Example:\n`GET /api/v1/categories?parentCategoryId=0` returns the top-level categories.\nAlso available as CSV (`Accept: text/csv` or `?format=csv`); paging cursor in `X-Next-Cursor`.",
        "parameters": [
          {
            "name": "search",
            "in": "query",
            "description": "Case-insensitive substring of the name, e.g. `lum`.",
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "parentCategoryId",
            "in": "query",
            "description": "Only direct children of this category; `0` for top-level categories only.",
            "schema": {
              "type": "integer",
              "format": "int32"
            }
          },
          {
            "name": "updatedSince",
            "in": "query",
            "description": "Only categories created or edited at/after this UTC instant (ISO-8601).",
            "schema": {
              "type": "string",
              "format": "date-time"
            }
          },
          {
            "name": "after",
            "in": "query",
            "description": "SyncId of the last category of the previous page; omit for the first page.",
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "limit",
            "in": "query",
            "description": "Page size, 1-200 (default 50).",
            "schema": {
              "type": "integer",
              "format": "int32"
            }
          },
          {
            "name": "format",
            "in": "query",
            "description": "Alternative to the Accept header: `csv` returns text/csv.",
            "schema": {
              "enum": [
                "csv"
              ],
              "type": "string"
            }
          }
        ],
        "responses": {
          "401": {
            "description": "Unauthorized",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ProblemDetails"
                }
              }
            }
          },
          "403": {
            "description": "Forbidden",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ProblemDetails"
                }
              }
            }
          },
          "429": {
            "description": "Too Many Requests",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ProblemDetails"
                }
              }
            }
          },
          "200": {
            "description": "A page of categories.",
            "headers": {
              "X-Next-Cursor": {
                "description": "text/csv only: the page's nextCursor. Absent on the last page. Pass it as ?after= (or follow the Link header) to fetch the next page.",
                "schema": {
                  "type": "string"
                }
              },
              "Link": {
                "description": "text/csv only, alongside X-Next-Cursor: <absolute URL of the next page>; rel=\"next\".",
                "schema": {
                  "type": "string"
                }
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/PagedResponseOfCategoryDto"
                }
              },
              "text/csv": {
                "schema": {
                  "type": "string",
                  "description": "CSV document: UTF-8 with BOM, CRLF rows, RFC 4180 quoting. The header row holds the JSON property names of one item (nested objects flattened as parent.child); one row per item of the page."
                }
              }
            }
          },
          "400": {
            "description": "Malformed `after` cursor.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ValidationProblemDetails"
                }
              }
            }
          }
        }
      },
      "post": {
        "tags": [
          "Categories"
        ],
        "summary": "Creates an item category.",
        "description": "Requires `ManageInventory` and an `Idempotency-Key` header. Same as \"New Category\" on\nthe Item Categories page, with stricter checks: the name must be unique among the categories\nwith the same parent, and the parent must be one of your categories that is neither the\nbuilt-in \"No Category\" nor Template Builder-generated. The new category has no properties.\nExample: `{ \"name\": \"Hardwood\", \"parentCategoryId\": 1 }`.",
        "requestBody": {
          "description": "Name plus optional description and parent.",
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/CreateCategoryApiRequest"
              }
            },
            "text/json": {
              "schema": {
                "$ref": "#/components/schemas/CreateCategoryApiRequest"
              }
            },
            "application/*+json": {
              "schema": {
                "$ref": "#/components/schemas/CreateCategoryApiRequest"
              }
            }
          }
        },
        "responses": {
          "401": {
            "description": "Unauthorized",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ProblemDetails"
                }
              }
            }
          },
          "403": {
            "description": "The key lacks `ManageInventory`.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ProblemDetails"
                }
              }
            }
          },
          "429": {
            "description": "Too Many Requests",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ProblemDetails"
                }
              }
            }
          },
          "201": {
            "description": "Created; body is the new category, `Location` points at it.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/CategoryDto"
                }
              }
            }
          },
          "400": {
            "description": "Malformed body, missing or blank `name`, a field over its limit, or missing `Idempotency-Key`.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ValidationProblemDetails"
                }
              }
            }
          },
          "409": {
            "description": "A category with this name already exists under the same parent, or the key's creator is unknown on this server.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ProblemDetails"
                }
              }
            }
          },
          "422": {
            "description": "No such (usable) parent category, including a negative `parentCategoryId` (the field is listed in `errors`); or `Idempotency-Key` reused with a different body.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ValidationProblemDetails"
                }
              }
            }
          }
        }
      }
    },
    "/api/v1/categories/{id}": {
      "delete": {
        "tags": [
          "Categories"
        ],
        "summary": "Deletes an item category.",
        "description": "Requires `ManageInventory` and an `Idempotency-Key` header. Same as \"Delete\" on the\nItem Categories page: only a category without child categories can be deleted (move or\ndelete them first). Its properties and its item assignments go with it; items whose primary\n`categoryId` it was move to the organization's default \"No Category\". Items keep the\nproperty values they already carry. Template Builder-generated categories\n(`isTemplateBuilderCategory` = true) are removed by deleting their Template Builder, not here.",
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "description": "Category id. Example: `4`.",
            "required": true,
            "schema": {
              "type": "integer",
              "format": "int32"
            }
          }
        ],
        "responses": {
          "401": {
            "description": "Unauthorized",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ProblemDetails"
                }
              }
            }
          },
          "403": {
            "description": "The key lacks `ManageInventory`.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ProblemDetails"
                }
              }
            }
          },
          "429": {
            "description": "Too Many Requests",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ProblemDetails"
                }
              }
            }
          },
          "200": {
            "description": "Deleted; body says what changed.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/CategoryDeletedResponse"
                }
              }
            }
          },
          "400": {
            "description": "Missing `Idempotency-Key`.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ValidationProblemDetails"
                }
              }
            }
          },
          "404": {
            "description": "No such category in this organization.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ProblemDetails"
                }
              }
            }
          },
          "409": {
            "description": "The category has child categories, is Template Builder-generated, or the key's creator is unknown on this server.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ProblemDetails"
                }
              }
            }
          }
        }
      },
      "get": {
        "tags": [
          "Categories"
        ],
        "summary": "Returns one item category.",
        "description": "Requires `ViewInventory`. Send the previous `ETag` as `If-None-Match` to receive `304 Not Modified` when unchanged.",
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "description": "Category id. Example: `4`.",
            "required": true,
            "schema": {
              "type": "integer",
              "format": "int32"
            }
          }
        ],
        "responses": {
          "401": {
            "description": "Unauthorized",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ProblemDetails"
                }
              }
            }
          },
          "403": {
            "description": "Forbidden",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ProblemDetails"
                }
              }
            }
          },
          "429": {
            "description": "Too Many Requests",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ProblemDetails"
                }
              }
            }
          },
          "200": {
            "description": "The category.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/CategoryDto"
                }
              }
            }
          },
          "304": {
            "description": "Unchanged since the supplied ETag."
          },
          "404": {
            "description": "No such category in this organization.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ProblemDetails"
                }
              }
            }
          }
        }
      },
      "patch": {
        "tags": [
          "Categories"
        ],
        "summary": "Renames, re-describes or moves an item category; only the fields sent change.",
        "description": "Requires `ManageInventory` and an `Idempotency-Key` header. Same as editing the\ncategory (or dragging it in the tree) on the Item Categories page. Moving a category\n(`parentCategoryId`; `0` = top level) is refused when it would create a cycle, and\nrecalculates every item's inherited parent categories. Template Builder-generated categories\nare managed by their Template Builder and cannot be changed here.\nExample: `{ \"name\": \"Softwood\", \"parentCategoryId\": 0 }`.",
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "description": "Category id. Example: `4`.",
            "required": true,
            "schema": {
              "type": "integer",
              "format": "int32"
            }
          }
        ],
        "requestBody": {
          "description": "The fields to change; at least one.",
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/UpdateCategoryApiRequest"
              }
            },
            "text/json": {
              "schema": {
                "$ref": "#/components/schemas/UpdateCategoryApiRequest"
              }
            },
            "application/*+json": {
              "schema": {
                "$ref": "#/components/schemas/UpdateCategoryApiRequest"
              }
            }
          }
        },
        "responses": {
          "401": {
            "description": "Unauthorized",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ProblemDetails"
                }
              }
            }
          },
          "403": {
            "description": "The key lacks `ManageInventory`.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ProblemDetails"
                }
              }
            }
          },
          "429": {
            "description": "Too Many Requests",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ProblemDetails"
                }
              }
            }
          },
          "200": {
            "description": "Changed; body is the category.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/CategoryDto"
                }
              }
            }
          },
          "400": {
            "description": "Empty body (no field to change), malformed body, a field over its limit, or missing `Idempotency-Key`.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ValidationProblemDetails"
                }
              }
            }
          },
          "404": {
            "description": "No such category in this organization.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ProblemDetails"
                }
              }
            }
          },
          "409": {
            "description": "The category is Template Builder-generated, another category under the (new) parent already has the name, or the key's creator is unknown on this server.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ProblemDetails"
                }
              }
            }
          },
          "422": {
            "description": "Blank name, no such (usable) parent, or the parent is the category itself or one of its descendants (the field is listed in `errors`); or `Idempotency-Key` reused with a different body.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ValidationProblemDetails"
                }
              }
            }
          }
        }
      }
    },
    "/api/v1/categories/{id}/properties": {
      "get": {
        "tags": [
          "Categories"
        ],
        "summary": "Lists a category's properties — its own and those inherited from parent categories.",
        "description": "Requires `ViewInventory`. Ordered by `displayOrder`; inherited properties carry\n`isInherited` and the defining `categoryId`. Supports `If-None-Match`.",
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "description": "Category id. Example: `4`.",
            "required": true,
            "schema": {
              "type": "integer",
              "format": "int32"
            }
          }
        ],
        "responses": {
          "401": {
            "description": "Unauthorized",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ProblemDetails"
                }
              }
            }
          },
          "403": {
            "description": "Forbidden",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ProblemDetails"
                }
              }
            }
          },
          "429": {
            "description": "Too Many Requests",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ProblemDetails"
                }
              }
            }
          },
          "200": {
            "description": "The category's properties.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/CategoryPropertiesResponse"
                }
              }
            }
          },
          "304": {
            "description": "Unchanged since the supplied ETag."
          },
          "404": {
            "description": "No such category in this organization.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ProblemDetails"
                }
              }
            }
          }
        }
      },
      "post": {
        "tags": [
          "Categories"
        ],
        "summary": "Adds a property to a category.",
        "description": "Requires `ManageInventory` and an `Idempotency-Key` header. Same as \"Add Property\"\non the category's Manage Properties page, with typed validation: the default value must fit\n`dataType` (and be among `options` for list types), options are only for\n`Dropdown` / `MultiSelect`. A property with a parent category's name overrides it\n(`overridesParent`). Existing items are not changed.\nExample: `{ \"name\": \"Species\", \"dataType\": \"Dropdown\", \"options\": [\"Pine\", \"Oak\"], \"defaultValue\": \"Pine\" }`.",
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "description": "Category id. Example: `4`.",
            "required": true,
            "schema": {
              "type": "integer",
              "format": "int32"
            }
          }
        ],
        "requestBody": {
          "description": "The property's definition; `name` is required.",
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/CreateCategoryPropertyApiRequest"
              }
            },
            "text/json": {
              "schema": {
                "$ref": "#/components/schemas/CreateCategoryPropertyApiRequest"
              }
            },
            "application/*+json": {
              "schema": {
                "$ref": "#/components/schemas/CreateCategoryPropertyApiRequest"
              }
            }
          }
        },
        "responses": {
          "401": {
            "description": "Unauthorized",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ProblemDetails"
                }
              }
            }
          },
          "403": {
            "description": "The key lacks `ManageInventory`.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ProblemDetails"
                }
              }
            }
          },
          "429": {
            "description": "Too Many Requests",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ProblemDetails"
                }
              }
            }
          },
          "201": {
            "description": "Created; body is the category's properties (the new one included). `Location` points at the list.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/CategoryPropertiesResponse"
                }
              }
            }
          },
          "400": {
            "description": "Malformed body, missing `name`, a field over its limit, or missing `Idempotency-Key`.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ValidationProblemDetails"
                }
              }
            }
          },
          "404": {
            "description": "No such category in this organization.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ProblemDetails"
                }
              }
            }
          },
          "409": {
            "description": "The category already has a property with this name, or the key's creator is unknown on this server.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ProblemDetails"
                }
              }
            }
          },
          "422": {
            "description": "Reserved name, a default value that does not fit the type or options, invalid options, or a negative `displayOrder` (the field is listed in `errors`); or `Idempotency-Key` reused with a different body.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ValidationProblemDetails"
                }
              }
            }
          }
        }
      }
    },
    "/api/v1/categories/{id}/properties/{propertyId}": {
      "delete": {
        "tags": [
          "Categories"
        ],
        "summary": "Deletes a category property.",
        "description": "Requires `ManageInventory` and an `Idempotency-Key` header. Same as \"Delete\" on the\nManage Properties page: the definition goes; items that already carry the property keep it.\nOnly a property defined on this category can be deleted.",
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "description": "Category id. Example: `4`.",
            "required": true,
            "schema": {
              "type": "integer",
              "format": "int32"
            }
          },
          {
            "name": "propertyId",
            "in": "path",
            "description": "Property id from GET (not inherited). Example: `57`.",
            "required": true,
            "schema": {
              "type": "integer",
              "format": "int32"
            }
          }
        ],
        "responses": {
          "401": {
            "description": "Unauthorized",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ProblemDetails"
                }
              }
            }
          },
          "403": {
            "description": "The key lacks `ManageInventory`.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ProblemDetails"
                }
              }
            }
          },
          "429": {
            "description": "Too Many Requests",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ProblemDetails"
                }
              }
            }
          },
          "200": {
            "description": "Deleted; body is the category's remaining properties.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/CategoryPropertiesResponse"
                }
              }
            }
          },
          "400": {
            "description": "Missing `Idempotency-Key`.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ValidationProblemDetails"
                }
              }
            }
          },
          "404": {
            "description": "No such category, or no such property defined on it.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ProblemDetails"
                }
              }
            }
          },
          "409": {
            "description": "The key's creator is unknown on this server.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ProblemDetails"
                }
              }
            }
          }
        }
      },
      "patch": {
        "tags": [
          "Categories"
        ],
        "summary": "Changes a category property; only the fields sent change.",
        "description": "Requires `ManageInventory` and an `Idempotency-Key` header. Same as editing the\nproperty on the Manage Properties page, as a partial update with the create rules. Only a\nproperty defined on this category can be changed (an inherited one is edited on its own\ncategory). Items that already carry the property keep their copy (name, type and value).\nExample: `{ \"defaultValue\": \"Oak\", \"displayOrder\": 2 }`.",
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "description": "Category id. Example: `4`.",
            "required": true,
            "schema": {
              "type": "integer",
              "format": "int32"
            }
          },
          {
            "name": "propertyId",
            "in": "path",
            "description": "Property id from GET (not inherited). Example: `57`.",
            "required": true,
            "schema": {
              "type": "integer",
              "format": "int32"
            }
          }
        ],
        "requestBody": {
          "description": "The fields to change; at least one.",
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/UpdateCategoryPropertyApiRequest"
              }
            },
            "text/json": {
              "schema": {
                "$ref": "#/components/schemas/UpdateCategoryPropertyApiRequest"
              }
            },
            "application/*+json": {
              "schema": {
                "$ref": "#/components/schemas/UpdateCategoryPropertyApiRequest"
              }
            }
          }
        },
        "responses": {
          "401": {
            "description": "Unauthorized",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ProblemDetails"
                }
              }
            }
          },
          "403": {
            "description": "The key lacks `ManageInventory`.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ProblemDetails"
                }
              }
            }
          },
          "429": {
            "description": "Too Many Requests",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ProblemDetails"
                }
              }
            }
          },
          "200": {
            "description": "Changed; body is the category's properties.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/CategoryPropertiesResponse"
                }
              }
            }
          },
          "400": {
            "description": "Empty body (no field to change), malformed body, a field over its limit, or missing `Idempotency-Key`.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ValidationProblemDetails"
                }
              }
            }
          },
          "404": {
            "description": "No such category, or no such property defined on it.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ProblemDetails"
                }
              }
            }
          },
          "409": {
            "description": "Another property of the category already has the name, or the key's creator is unknown on this server.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ProblemDetails"
                }
              }
            }
          },
          "422": {
            "description": "Blank or reserved name, a default value that does not fit the (new) type or options, invalid options, or a negative `displayOrder`; or `Idempotency-Key` reused with a different body.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ValidationProblemDetails"
                }
              }
            }
          }
        }
      }
    },
    "/api/v1/customers": {
      "get": {
        "tags": [
          "Customers"
        ],
        "summary": "Lists customers, oldest first, one page at a time.",
        "description": "Cursor pagination: pass the `nextCursor` of the previous response as\n`after`. Filters combine with AND. Example:\n`GET /api/v1/customers?isActive=true&updatedSince=2026-01-01T00:00:00Z&limit=100`.\nAlso available as CSV (`Accept: text/csv` or `?format=csv`); paging cursor in `X-Next-Cursor`.",
        "parameters": [
          {
            "name": "after",
            "in": "query",
            "description": "SyncId of the last item of the previous page; omit for the first page.",
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "limit",
            "in": "query",
            "description": "Page size, 1–200 (default 50).",
            "schema": {
              "type": "integer",
              "format": "int32"
            }
          },
          {
            "name": "updatedSince",
            "in": "query",
            "description": "Only customers created or edited at/after this UTC instant (ISO-8601, e.g. `2026-01-01T00:00:00Z`).",
            "schema": {
              "type": "string",
              "format": "date-time"
            }
          },
          {
            "name": "isActive",
            "in": "query",
            "description": "Only active (`true`) or deactivated (`false`) customers; omit for both.",
            "schema": {
              "type": "boolean"
            }
          },
          {
            "name": "customerType",
            "in": "query",
            "description": "Only this type: `EndCustomer`, `Installer`, `Distributor` or `Commercial`.",
            "schema": {
              "$ref": "#/components/schemas/CustomerType"
            }
          },
          {
            "name": "status",
            "in": "query",
            "description": "Only this status flag: `None`, `Preferred` or `Difficult`.",
            "schema": {
              "$ref": "#/components/schemas/CustomerStatus"
            }
          },
          {
            "name": "search",
            "in": "query",
            "description": "Case-insensitive substring matched against name, company name and e-mail.",
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "format",
            "in": "query",
            "description": "Alternative to the Accept header: `csv` returns text/csv.",
            "schema": {
              "enum": [
                "csv"
              ],
              "type": "string"
            }
          }
        ],
        "responses": {
          "401": {
            "description": "Unauthorized",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ProblemDetails"
                }
              }
            }
          },
          "403": {
            "description": "Forbidden",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ProblemDetails"
                }
              }
            }
          },
          "429": {
            "description": "Too Many Requests",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ProblemDetails"
                }
              }
            }
          },
          "200": {
            "description": "A page of customers.",
            "headers": {
              "X-Next-Cursor": {
                "description": "text/csv only: the page's nextCursor. Absent on the last page. Pass it as ?after= (or follow the Link header) to fetch the next page.",
                "schema": {
                  "type": "string"
                }
              },
              "Link": {
                "description": "text/csv only, alongside X-Next-Cursor: <absolute URL of the next page>; rel=\"next\".",
                "schema": {
                  "type": "string"
                }
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/PagedResponseOfCustomerDto"
                }
              },
              "text/csv": {
                "schema": {
                  "type": "string",
                  "description": "CSV document: UTF-8 with BOM, CRLF rows, RFC 4180 quoting. The header row holds the JSON property names of one item (nested objects flattened as parent.child); one row per item of the page."
                }
              }
            }
          },
          "400": {
            "description": "Malformed `after` cursor or filter value (validation problem listing the field).",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ValidationProblemDetails"
                }
              }
            }
          }
        }
      },
      "post": {
        "tags": [
          "Customers"
        ],
        "summary": "Creates a customer.",
        "description": "Requires `ManageCustomers` and an `Idempotency-Key` header (unique per attempt;\na retry with the same key and body replays the first response). The customer is created\nactive with a zero balance; `country` defaults to `\"USA\"`; when any of\n`address`/`city`/`state`/`postalCode` is given, a \"Primary Address\"\nmailing location is created too (see `/customers/{id}/locations`). The write is\nattributed to the user who created the API key.",
        "requestBody": {
          "description": "Customer fields; only `name` is required.",
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/CustomerWriteRequest"
              }
            },
            "text/json": {
              "schema": {
                "$ref": "#/components/schemas/CustomerWriteRequest"
              }
            },
            "application/*+json": {
              "schema": {
                "$ref": "#/components/schemas/CustomerWriteRequest"
              }
            }
          }
        },
        "responses": {
          "401": {
            "description": "Unauthorized",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ProblemDetails"
                }
              }
            }
          },
          "403": {
            "description": "Forbidden",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ProblemDetails"
                }
              }
            }
          },
          "429": {
            "description": "Too Many Requests",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ProblemDetails"
                }
              }
            }
          },
          "201": {
            "description": "The new customer; `Location` points at `/api/v1/customers/{id}`.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/CustomerDto"
                }
              }
            }
          },
          "400": {
            "description": "Malformed body, unknown enum value, or missing `Idempotency-Key`.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ValidationProblemDetails"
                }
              }
            }
          },
          "409": {
            "description": "The key's creator no longer exists on this server, so the write cannot be attributed.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ProblemDetails"
                }
              }
            }
          },
          "422": {
            "description": "Well-formed but invalid field values (problem lists the fields), or `Idempotency-Key` reused with a different body.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ValidationProblemDetails"
                }
              }
            }
          },
          "500": {
            "description": "The customer could not be saved.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ProblemDetails"
                }
              }
            }
          }
        }
      }
    },
    "/api/v1/customers/{id}": {
      "delete": {
        "tags": [
          "Customers"
        ],
        "summary": "Deletes a customer.",
        "description": "Requires `ManageCustomers` and an `Idempotency-Key` header. This is the same\nsoft delete as the Customers page: the customer disappears from every list and lookup\n(GETs answer 404) but keeps its history, and a user can restore it from\nCustomers → Deleted Customers in the app. The delete is refused with `409` while\nthe customer still has active jobs (not Completed/Cancelled), unpaid invoices\n(not Paid/Cancelled) or active sales orders — complete or cancel those first; the\nproblem `detail` names which. To merely hide a customer from pickers without\ndeleting it, `PUT` it with `isActive: false` instead.",
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "description": "Customer id.",
            "required": true,
            "schema": {
              "type": "integer",
              "format": "int32"
            }
          }
        ],
        "responses": {
          "401": {
            "description": "Unauthorized",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ProblemDetails"
                }
              }
            }
          },
          "403": {
            "description": "Forbidden",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ProblemDetails"
                }
              }
            }
          },
          "429": {
            "description": "Too Many Requests",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ProblemDetails"
                }
              }
            }
          },
          "200": {
            "description": "The customer was deleted; body carries its id and a confirmation message.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiOperationResponse"
                }
              }
            }
          },
          "400": {
            "description": "Missing `Idempotency-Key`.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ValidationProblemDetails"
                }
              }
            }
          },
          "404": {
            "description": "No such customer in this organization (also for already-deleted customers).",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ProblemDetails"
                }
              }
            }
          },
          "409": {
            "description": "Active jobs, unpaid invoices or active sales orders reference the customer, or the key's creator is unknown on this server.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ProblemDetails"
                }
              }
            }
          },
          "500": {
            "description": "The customer could not be saved.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ProblemDetails"
                }
              }
            }
          }
        }
      },
      "get": {
        "tags": [
          "Customers"
        ],
        "summary": "Returns one customer.",
        "description": "Send the previous `ETag` as `If-None-Match` to receive `304 Not Modified` when unchanged.",
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "description": "Customer id from a list response.",
            "required": true,
            "schema": {
              "type": "integer",
              "format": "int32"
            }
          }
        ],
        "responses": {
          "401": {
            "description": "Unauthorized",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ProblemDetails"
                }
              }
            }
          },
          "403": {
            "description": "Forbidden",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ProblemDetails"
                }
              }
            }
          },
          "429": {
            "description": "Too Many Requests",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ProblemDetails"
                }
              }
            }
          },
          "200": {
            "description": "The customer, with an `ETag` header.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/CustomerDto"
                }
              }
            }
          },
          "304": {
            "description": "Unchanged since the supplied `If-None-Match`."
          },
          "404": {
            "description": "No such customer in this organization (also for deleted customers).",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ProblemDetails"
                }
              }
            }
          }
        }
      },
      "put": {
        "tags": [
          "Customers"
        ],
        "summary": "Replaces the editable fields of one customer.",
        "description": "Requires `ManageCustomers` and an `Idempotency-Key` header. This is a full\nupdate: every editable field takes the value in the body (omitted optional fields\nbecome null / defaults), so GET the customer first and send it back modified.\nOptional optimistic concurrency: send the `ETag` from that GET as `If-Match`\nto get `412` instead of overwriting a change made in the meantime.\nBalances, deletion state and audit fields cannot be changed here.",
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "description": "Customer id.",
            "required": true,
            "schema": {
              "type": "integer",
              "format": "int32"
            }
          }
        ],
        "requestBody": {
          "description": "The complete set of editable fields.",
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/CustomerWriteRequest"
              }
            },
            "text/json": {
              "schema": {
                "$ref": "#/components/schemas/CustomerWriteRequest"
              }
            },
            "application/*+json": {
              "schema": {
                "$ref": "#/components/schemas/CustomerWriteRequest"
              }
            }
          }
        },
        "responses": {
          "401": {
            "description": "Unauthorized",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ProblemDetails"
                }
              }
            }
          },
          "403": {
            "description": "Forbidden",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ProblemDetails"
                }
              }
            }
          },
          "429": {
            "description": "Too Many Requests",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ProblemDetails"
                }
              }
            }
          },
          "200": {
            "description": "The updated customer.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/CustomerDto"
                }
              }
            }
          },
          "400": {
            "description": "Malformed body, unknown enum value, or missing `Idempotency-Key`.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ValidationProblemDetails"
                }
              }
            }
          },
          "404": {
            "description": "No such customer in this organization (also for deleted customers).",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ProblemDetails"
                }
              }
            }
          },
          "409": {
            "description": "Concurrent edit detected, or the key's creator no longer exists on this server.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ProblemDetails"
                }
              }
            }
          },
          "412": {
            "description": "`If-Match` does not match the customer's current `ETag`.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ProblemDetails"
                }
              }
            }
          },
          "422": {
            "description": "Well-formed but invalid field values (problem lists the fields), or `Idempotency-Key` reused with a different body.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ValidationProblemDetails"
                }
              }
            }
          },
          "500": {
            "description": "The customer could not be saved.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ProblemDetails"
                }
              }
            }
          }
        }
      }
    },
    "/api/v1/customers/{id}/locations": {
      "get": {
        "tags": [
          "Customers"
        ],
        "summary": "Lists the additional locations (job sites / addresses) of one customer.",
        "description": "The customer's primary address is on the customer resource itself; this collection\nholds the extra sites jobs can be booked at. Cursor-paginated like every list.\nAlso available as CSV (`Accept: text/csv` or `?format=csv`); paging cursor in `X-Next-Cursor`.",
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "description": "Customer id.",
            "required": true,
            "schema": {
              "type": "integer",
              "format": "int32"
            }
          },
          {
            "name": "after",
            "in": "query",
            "description": "SyncId of the last item of the previous page; omit for the first page.",
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "limit",
            "in": "query",
            "description": "Page size, 1–200 (default 50).",
            "schema": {
              "type": "integer",
              "format": "int32"
            }
          },
          {
            "name": "isActive",
            "in": "query",
            "description": "Only active (`true`) or retired (`false`) locations; omit for both.",
            "schema": {
              "type": "boolean"
            }
          },
          {
            "name": "format",
            "in": "query",
            "description": "Alternative to the Accept header: `csv` returns text/csv.",
            "schema": {
              "enum": [
                "csv"
              ],
              "type": "string"
            }
          }
        ],
        "responses": {
          "401": {
            "description": "Unauthorized",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ProblemDetails"
                }
              }
            }
          },
          "403": {
            "description": "Forbidden",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ProblemDetails"
                }
              }
            }
          },
          "429": {
            "description": "Too Many Requests",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ProblemDetails"
                }
              }
            }
          },
          "200": {
            "description": "A page of locations (may be empty).",
            "headers": {
              "X-Next-Cursor": {
                "description": "text/csv only: the page's nextCursor. Absent on the last page. Pass it as ?after= (or follow the Link header) to fetch the next page.",
                "schema": {
                  "type": "string"
                }
              },
              "Link": {
                "description": "text/csv only, alongside X-Next-Cursor: <absolute URL of the next page>; rel=\"next\".",
                "schema": {
                  "type": "string"
                }
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/PagedResponseOfCustomerLocationDto"
                }
              },
              "text/csv": {
                "schema": {
                  "type": "string",
                  "description": "CSV document: UTF-8 with BOM, CRLF rows, RFC 4180 quoting. The header row holds the JSON property names of one item (nested objects flattened as parent.child); one row per item of the page."
                }
              }
            }
          },
          "400": {
            "description": "Malformed `after` cursor.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ValidationProblemDetails"
                }
              }
            }
          },
          "404": {
            "description": "No such customer in this organization.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ProblemDetails"
                }
              }
            }
          }
        }
      },
      "post": {
        "tags": [
          "Customers"
        ],
        "summary": "Adds a location (job site / address) to a customer.",
        "description": "Requires `ManageCustomers` and an `Idempotency-Key` header. Same rules as\nCustomers → Addresses → Add in the app: `name`, `address`, `city`, `state`\nand `postalCode` are required; `isMailingAddress: true` moves the mailing flag here\nfrom the customer's other locations, and a customer's first location is always the mailing\naddress. Attributed to the user who created the API key.",
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "description": "Customer id.",
            "required": true,
            "schema": {
              "type": "integer",
              "format": "int32"
            }
          }
        ],
        "requestBody": {
          "description": "The location's fields.",
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/CustomerLocationWriteRequest"
              }
            },
            "text/json": {
              "schema": {
                "$ref": "#/components/schemas/CustomerLocationWriteRequest"
              }
            },
            "application/*+json": {
              "schema": {
                "$ref": "#/components/schemas/CustomerLocationWriteRequest"
              }
            }
          }
        },
        "responses": {
          "401": {
            "description": "Unauthorized",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ProblemDetails"
                }
              }
            }
          },
          "403": {
            "description": "Forbidden",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ProblemDetails"
                }
              }
            }
          },
          "429": {
            "description": "Too Many Requests",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ProblemDetails"
                }
              }
            }
          },
          "201": {
            "description": "The new location; `Location` points at `/api/v1/customers/{id}/locations/{locationId}`.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/CustomerLocationDto"
                }
              }
            }
          },
          "400": {
            "description": "Malformed body, a field over its length limit, or missing `Idempotency-Key`.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ValidationProblemDetails"
                }
              }
            }
          },
          "404": {
            "description": "No such customer in this organization (also for deleted customers).",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ProblemDetails"
                }
              }
            }
          },
          "409": {
            "description": "The key's creator is unknown on this server, so the write cannot be attributed.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ProblemDetails"
                }
              }
            }
          },
          "422": {
            "description": "A required field is missing; the fields are listed in `errors`.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ValidationProblemDetails"
                }
              }
            }
          }
        }
      }
    },
    "/api/v1/customers/{id}/locations/{locationId}": {
      "delete": {
        "tags": [
          "Customers"
        ],
        "summary": "Retires (deletes) a customer location.",
        "description": "Requires `ManageCustomers` and an `Idempotency-Key` header. The same delete as the\nCustomers → Addresses page: the location is deactivated (`isActive: false`) rather than\nerased, so it stays on history and disappears from pickers; restore it with a PATCH of\n`isActive: true`. Refused with `409` while any job (in any status) references it.",
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "description": "Customer id.",
            "required": true,
            "schema": {
              "type": "integer",
              "format": "int32"
            }
          },
          {
            "name": "locationId",
            "in": "path",
            "description": "Location id.",
            "required": true,
            "schema": {
              "type": "integer",
              "format": "int32"
            }
          }
        ],
        "responses": {
          "401": {
            "description": "Unauthorized",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ProblemDetails"
                }
              }
            }
          },
          "403": {
            "description": "Forbidden",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ProblemDetails"
                }
              }
            }
          },
          "429": {
            "description": "Too Many Requests",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ProblemDetails"
                }
              }
            }
          },
          "200": {
            "description": "The location was retired; body carries its id and a confirmation message.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiOperationResponse"
                }
              }
            }
          },
          "400": {
            "description": "Missing `Idempotency-Key`.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ValidationProblemDetails"
                }
              }
            }
          },
          "404": {
            "description": "No such customer in this organization, or the location belongs to another customer.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ProblemDetails"
                }
              }
            }
          },
          "409": {
            "description": "Jobs reference the location (the detail says how many), or the key's creator is unknown on this server.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ProblemDetails"
                }
              }
            }
          }
        }
      },
      "get": {
        "tags": [
          "Customers"
        ],
        "summary": "Returns one location of a customer.",
        "description": "Send the previous `ETag` as `If-None-Match` to receive `304 Not Modified` when unchanged; send it as `If-Match` on a PATCH to avoid overwriting a concurrent edit.",
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "description": "Customer id.",
            "required": true,
            "schema": {
              "type": "integer",
              "format": "int32"
            }
          },
          {
            "name": "locationId",
            "in": "path",
            "description": "Location id from the customer's location list.",
            "required": true,
            "schema": {
              "type": "integer",
              "format": "int32"
            }
          }
        ],
        "responses": {
          "401": {
            "description": "Unauthorized",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ProblemDetails"
                }
              }
            }
          },
          "403": {
            "description": "Forbidden",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ProblemDetails"
                }
              }
            }
          },
          "429": {
            "description": "Too Many Requests",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ProblemDetails"
                }
              }
            }
          },
          "200": {
            "description": "The location, with an `ETag` header.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/CustomerLocationDto"
                }
              }
            }
          },
          "304": {
            "description": "Unchanged since the supplied `If-None-Match`."
          },
          "404": {
            "description": "No such customer in this organization, or the location belongs to another customer.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ProblemDetails"
                }
              }
            }
          }
        }
      },
      "patch": {
        "tags": [
          "Customers"
        ],
        "summary": "Edits a customer location; only the fields sent change.",
        "description": "Requires `ManageCustomers` and an `Idempotency-Key` header. `\"\"` clears an optional\nfield; `name`, `address`, `city`, `state` and `postalCode` cannot be cleared.\n`isMailingAddress: true` unmarks the customer's other locations; `isActive` retires or\nrestores the location. Optional optimistic concurrency: send the `ETag` from\n`GET …/locations/{locationId}` as `If-Match` to get `412` instead of overwriting a\nchange made in the meantime. Example: `PATCH /api/v1/customers/5/locations/9 { \"notes\": \"Gate code 4411\" }`.",
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "description": "Customer id.",
            "required": true,
            "schema": {
              "type": "integer",
              "format": "int32"
            }
          },
          {
            "name": "locationId",
            "in": "path",
            "description": "Location id.",
            "required": true,
            "schema": {
              "type": "integer",
              "format": "int32"
            }
          }
        ],
        "requestBody": {
          "description": "The fields to change.",
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/CustomerLocationWriteRequest"
              }
            },
            "text/json": {
              "schema": {
                "$ref": "#/components/schemas/CustomerLocationWriteRequest"
              }
            },
            "application/*+json": {
              "schema": {
                "$ref": "#/components/schemas/CustomerLocationWriteRequest"
              }
            }
          }
        },
        "responses": {
          "401": {
            "description": "Unauthorized",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ProblemDetails"
                }
              }
            }
          },
          "403": {
            "description": "Forbidden",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ProblemDetails"
                }
              }
            }
          },
          "429": {
            "description": "Too Many Requests",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ProblemDetails"
                }
              }
            }
          },
          "200": {
            "description": "The location after the edit.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/CustomerLocationDto"
                }
              }
            }
          },
          "400": {
            "description": "Malformed body, a field over its length limit, or missing `Idempotency-Key`.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ValidationProblemDetails"
                }
              }
            }
          },
          "404": {
            "description": "No such customer in this organization, or the location belongs to another customer.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ProblemDetails"
                }
              }
            }
          },
          "409": {
            "description": "Concurrent edit detected, or the key's creator is unknown on this server.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ProblemDetails"
                }
              }
            }
          },
          "412": {
            "description": "`If-Match` does not match the location's current `ETag`.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ProblemDetails"
                }
              }
            }
          },
          "422": {
            "description": "A required field was cleared; the fields are listed in `errors`.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ValidationProblemDetails"
                }
              }
            }
          }
        }
      }
    },
    "/api/v1/invoices": {
      "get": {
        "tags": [
          "Invoices"
        ],
        "summary": "Lists invoice headers.",
        "description": "Requires `ViewInvoices`. Cursor-paginated: pass the last invoice's `syncId`\nas `after`.\nAlso available as CSV (`Accept: text/csv` or `?format=csv`); paging cursor in `X-Next-Cursor`.",
        "parameters": [
          {
            "name": "status",
            "in": "query",
            "description": "Only invoices in this status (name such as `Sent` or numeric value).",
            "schema": {
              "$ref": "#/components/schemas/InvoiceStatus"
            }
          },
          {
            "name": "jobId",
            "in": "query",
            "description": "Only invoices billing this job.",
            "schema": {
              "type": "integer",
              "format": "int32"
            }
          },
          {
            "name": "customerId",
            "in": "query",
            "description": "Only invoices billed to this customer.",
            "schema": {
              "type": "integer",
              "format": "int32"
            }
          },
          {
            "name": "updatedSince",
            "in": "query",
            "description": "Only invoices created or modified at/after this UTC instant.",
            "schema": {
              "type": "string",
              "format": "date-time"
            }
          },
          {
            "name": "after",
            "in": "query",
            "description": "SyncId of the last invoice of the previous page.",
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "limit",
            "in": "query",
            "description": "Page size, 1..200 (default 50).",
            "schema": {
              "type": "integer",
              "format": "int32"
            }
          },
          {
            "name": "format",
            "in": "query",
            "description": "Alternative to the Accept header: `csv` returns text/csv.",
            "schema": {
              "enum": [
                "csv"
              ],
              "type": "string"
            }
          }
        ],
        "responses": {
          "401": {
            "description": "Unauthorized",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ProblemDetails"
                }
              }
            }
          },
          "403": {
            "description": "Forbidden",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ProblemDetails"
                }
              }
            }
          },
          "429": {
            "description": "Too Many Requests",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ProblemDetails"
                }
              }
            }
          },
          "200": {
            "description": "One page of invoice headers.",
            "headers": {
              "X-Next-Cursor": {
                "description": "text/csv only: the page's nextCursor. Absent on the last page. Pass it as ?after= (or follow the Link header) to fetch the next page.",
                "schema": {
                  "type": "string"
                }
              },
              "Link": {
                "description": "text/csv only, alongside X-Next-Cursor: <absolute URL of the next page>; rel=\"next\".",
                "schema": {
                  "type": "string"
                }
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/PagedResponseOfInvoiceSummary"
                }
              },
              "text/csv": {
                "schema": {
                  "type": "string",
                  "description": "CSV document: UTF-8 with BOM, CRLF rows, RFC 4180 quoting. The header row holds the JSON property names of one item (nested objects flattened as parent.child); one row per item of the page."
                }
              }
            }
          },
          "400": {
            "description": "Unknown `after` cursor or unrecognized `status`.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ValidationProblemDetails"
                }
              }
            }
          }
        }
      },
      "post": {
        "tags": [
          "Invoices"
        ],
        "summary": "Creates a standalone invoice (not tied to a job) from explicit lines.",
        "description": "Requires `ManageInvoices` and an `Idempotency-Key` header. The subtotal is\nthe sum of the lines; tax applies to all of it at `taxRate` (0 for a tax-exempt\ncustomer). Every omitted field defaults exactly as the New Invoice page does (next\n`INV-SA-…` number, today, the organization's default payment terms, tax rate and\npayment instructions). Like that page, the invoice is created already finalized\n(status `Sent`) — there is no draft step — so a payment can be recorded at once\nwith `POST /api/v1/invoices/{id}/payments`. To bill a job instead, use\n`POST /api/v1/invoices/from-job`.",
        "requestBody": {
          "description": "Customer, lines and, optionally, how to bill.",
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/CreateStandaloneInvoiceApiRequest"
              }
            },
            "text/json": {
              "schema": {
                "$ref": "#/components/schemas/CreateStandaloneInvoiceApiRequest"
              }
            },
            "application/*+json": {
              "schema": {
                "$ref": "#/components/schemas/CreateStandaloneInvoiceApiRequest"
              }
            }
          }
        },
        "responses": {
          "401": {
            "description": "Unauthorized",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ProblemDetails"
                }
              }
            }
          },
          "403": {
            "description": "Forbidden",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ProblemDetails"
                }
              }
            }
          },
          "429": {
            "description": "Too Many Requests",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ProblemDetails"
                }
              }
            }
          },
          "201": {
            "description": "The new invoice; `Location` points at it.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/InvoiceSummary"
                }
              }
            }
          },
          "400": {
            "description": "Malformed body, no lines, or an out-of-range value.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ValidationProblemDetails"
                }
              }
            }
          },
          "409": {
            "description": "The invoice number is already used, or the key's creator is unknown on this server.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ProblemDetails"
                }
              }
            }
          },
          "422": {
            "description": "Unknown `customerId` or `customerAddressId`, or `dueDate` precedes `invoiceDate`.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ValidationProblemDetails"
                }
              }
            }
          },
          "500": {
            "description": "The invoice could not be saved.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ProblemDetails"
                }
              }
            }
          }
        }
      }
    },
    "/api/v1/invoices/{id}": {
      "delete": {
        "tags": [
          "Invoices"
        ],
        "summary": "Deletes a draft invoice.",
        "description": "Requires `ManageInvoices` and an `Idempotency-Key` header; no body. The API twin of\nthe draft invoice page's \"Delete Draft\" button. Only a `Draft` can be deleted — a\nfinalized invoice is voided instead (`POST /api/v1/invoices/{id}/void`). A consolidated\ndraft, or a draft carrying a payment other than the job's transferred down payment, is refused.\nMoney goes back where it came from: the job's down payment returns to the job (the next\ninvoice re-applies it), applied store credit returns to the customer, and the job's or sales\norder's payment status is recalculated. A job moved from `Completed` to `Invoiced` by\ngenerating this draft goes back to `Completed` when no other non-voided invoice remains\n(counter-sales organizations keep `Invoiced`); its stock movements stay, and the job can\nsimply be invoiced again (`POST /api/v1/invoices/from-job`).\nDeletion is permanent — there is no restore, and the invoice, its lines, payments and notes\ndisappear from every synced device. Fires `invoice.deleted`. Repeating the request with the\nsame `Idempotency-Key` replays the 204; a new key for an already-deleted id gets 404.\nThe deletion is recorded on the job's history, attributed to the user who created the key.\nExample: `DELETE /api/v1/invoices/57`.",
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "description": "Numeric invoice id, e.g. `57`.",
            "required": true,
            "schema": {
              "type": "integer",
              "format": "int32"
            }
          }
        ],
        "responses": {
          "401": {
            "description": "Unauthorized",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ProblemDetails"
                }
              }
            }
          },
          "403": {
            "description": "Forbidden",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ProblemDetails"
                }
              }
            }
          },
          "429": {
            "description": "Too Many Requests",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ProblemDetails"
                }
              }
            }
          },
          "204": {
            "description": "The draft invoice was deleted; no body."
          },
          "400": {
            "description": "Missing `Idempotency-Key`.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ValidationProblemDetails"
                }
              }
            }
          },
          "404": {
            "description": "No such invoice in the calling organization (or it was already deleted).",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ProblemDetails"
                }
              }
            }
          },
          "409": {
            "description": "The invoice is not a draft (void it instead), is a consolidated invoice, has recorded payments, or the key's creator is unknown on this server — the `detail` says which.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ProblemDetails"
                }
              }
            }
          },
          "500": {
            "description": "The deletion could not be saved.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ProblemDetails"
                }
              }
            }
          }
        }
      },
      "get": {
        "tags": [
          "Invoices"
        ],
        "summary": "Returns one invoice with its lines and payments.",
        "description": "Requires `ViewInvoices`. `id` may be the numeric id or the `syncId`.\nSends a weak `ETag`; repeat with `If-None-Match` to get 304 when unchanged.",
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "description": "Numeric id or SyncId.",
            "required": true,
            "schema": {
              "type": "string"
            }
          }
        ],
        "responses": {
          "401": {
            "description": "Unauthorized",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ProblemDetails"
                }
              }
            }
          },
          "403": {
            "description": "Forbidden",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ProblemDetails"
                }
              }
            }
          },
          "429": {
            "description": "Too Many Requests",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ProblemDetails"
                }
              }
            }
          },
          "200": {
            "description": "The invoice.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/InvoiceDetail"
                }
              }
            }
          },
          "304": {
            "description": "Unchanged since the supplied ETag."
          },
          "404": {
            "description": "No such invoice in the calling organization.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ProblemDetails"
                }
              }
            }
          }
        }
      },
      "patch": {
        "tags": [
          "Invoices"
        ],
        "summary": "Edits a draft invoice's header (number, dates, terms, notes, payment instructions, tax rate).",
        "description": "Requires `ManageInvoices` and an `Idempotency-Key` header. PATCH semantics: only the\nfields you send change. Only a `Draft` invoice can be edited (a finalized invoice is\nimmutable — void it and issue a new one). Totals are not editable here: the subtotal comes\nfrom the job, so recompute it with `POST /api/v1/invoices/{id}/refresh-from-job` after the\njob's lines change. The write is attributed to the user who created the key.",
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "description": "Numeric invoice id.",
            "required": true,
            "schema": {
              "type": "integer",
              "format": "int32"
            }
          }
        ],
        "requestBody": {
          "description": "The header fields to change.",
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/UpdateInvoiceDraftApiRequest"
              }
            },
            "text/json": {
              "schema": {
                "$ref": "#/components/schemas/UpdateInvoiceDraftApiRequest"
              }
            },
            "application/*+json": {
              "schema": {
                "$ref": "#/components/schemas/UpdateInvoiceDraftApiRequest"
              }
            }
          }
        },
        "responses": {
          "401": {
            "description": "Unauthorized",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ProblemDetails"
                }
              }
            }
          },
          "403": {
            "description": "Forbidden",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ProblemDetails"
                }
              }
            }
          },
          "429": {
            "description": "Too Many Requests",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ProblemDetails"
                }
              }
            }
          },
          "200": {
            "description": "The updated invoice.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/InvoiceSummary"
                }
              }
            }
          },
          "400": {
            "description": "Malformed body, missing `Idempotency-Key`, or a tax rate out of 0–100.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ValidationProblemDetails"
                }
              }
            }
          },
          "404": {
            "description": "No such invoice in the calling organization.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ProblemDetails"
                }
              }
            }
          },
          "409": {
            "description": "The invoice is not a draft, or the key's creator is unknown on this server.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ProblemDetails"
                }
              }
            }
          },
          "422": {
            "description": "The invoice number was sent blank.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ValidationProblemDetails"
                }
              }
            }
          }
        }
      }
    },
    "/api/v1/invoices/{id}/mark-sent": {
      "post": {
        "tags": [
          "Invoices"
        ],
        "summary": "Finalizes a draft invoice (marks it sent).",
        "description": "Requires `ManageInvoices` and an `Idempotency-Key` header. Freezes the\ndraft's lines, records the send and moves it to `Sent` — the same step as the\n\"Finalize\" button, minus the customer email (send that through your own channel using\n`GET /api/v1/invoices/{id}`). Calling it on an already-finalized invoice only\nrefreshes its audit fields and returns it unchanged.",
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "description": "Numeric invoice id.",
            "required": true,
            "schema": {
              "type": "integer",
              "format": "int32"
            }
          }
        ],
        "responses": {
          "401": {
            "description": "Unauthorized",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ProblemDetails"
                }
              }
            }
          },
          "403": {
            "description": "Forbidden",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ProblemDetails"
                }
              }
            }
          },
          "429": {
            "description": "Too Many Requests",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ProblemDetails"
                }
              }
            }
          },
          "200": {
            "description": "The invoice after finalization.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/InvoiceSummary"
                }
              }
            }
          },
          "404": {
            "description": "No such invoice in the calling organization.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ProblemDetails"
                }
              }
            }
          },
          "409": {
            "description": "The invoice is voided or was consolidated into another invoice, or the key's creator is unknown on this server.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ProblemDetails"
                }
              }
            }
          }
        }
      }
    },
    "/api/v1/invoices/{id}/payments": {
      "post": {
        "tags": [
          "Invoices"
        ],
        "summary": "Records a payment and/or applies store credit to an invoice.",
        "description": "Requires `ManageInvoices` and an `Idempotency-Key` header. Same rules as the\nRecord Payment dialog: at least one of `amount` or store credit is required, a\npayment needs a `paymentMethod`, and the total may not exceed the balance due\n(extra money belongs in store credit, not on the invoice). Only a finalized invoice\ntakes payments — a draft must be marked sent first; a consolidated member is paid\nthrough the consolidated invoice. The invoice moves to `PartiallyPaid` or\n`Paid` (which fires `invoice.paid`), and the job's or sales order's payment\nstatus follows. Returns the updated invoice; the payment itself appears in\n`GET /api/v1/invoices/{id}`.",
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "description": "Numeric invoice id.",
            "required": true,
            "schema": {
              "type": "integer",
              "format": "int32"
            }
          }
        ],
        "requestBody": {
          "description": "Amount, method and reference, and/or store credit to apply.",
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/RecordPaymentApiRequest"
              }
            },
            "text/json": {
              "schema": {
                "$ref": "#/components/schemas/RecordPaymentApiRequest"
              }
            },
            "application/*+json": {
              "schema": {
                "$ref": "#/components/schemas/RecordPaymentApiRequest"
              }
            }
          }
        },
        "responses": {
          "401": {
            "description": "Unauthorized",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ProblemDetails"
                }
              }
            }
          },
          "403": {
            "description": "Forbidden",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ProblemDetails"
                }
              }
            }
          },
          "429": {
            "description": "Too Many Requests",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ProblemDetails"
                }
              }
            }
          },
          "200": {
            "description": "The invoice after the payment (status, `amountPaid`, `balanceDue`).",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/InvoiceSummary"
                }
              }
            }
          },
          "400": {
            "description": "Malformed body or out-of-range value.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ValidationProblemDetails"
                }
              }
            }
          },
          "404": {
            "description": "No such invoice in the calling organization.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ProblemDetails"
                }
              }
            }
          },
          "409": {
            "description": "The invoice is a draft or was consolidated into another invoice, or the key's creator is unknown on this server.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ProblemDetails"
                }
              }
            }
          },
          "422": {
            "description": "Nothing to apply, a payment without a method, or the payment exceeds the balance due.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ValidationProblemDetails"
                }
              }
            }
          },
          "500": {
            "description": "The payment could not be saved.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ProblemDetails"
                }
              }
            }
          }
        }
      }
    },
    "/api/v1/invoices/{id}/refresh-from-job": {
      "post": {
        "tags": [
          "Invoices"
        ],
        "summary": "Recomputes a draft invoice's total from its job's current unbilled amount.",
        "description": "Requires `ManageInvoices` and an `Idempotency-Key` header; no body. Same as the\napp's \"Update draft from job\": after you change the job's lines (materials, labor, charges),\nthis refreshes the draft's subtotal to the job's remaining unbilled amount (including any\nretroactive tier credits). Only a `Draft` invoice that bills a job. The write is\nattributed to the user who created the key.",
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "description": "Numeric invoice id.",
            "required": true,
            "schema": {
              "type": "integer",
              "format": "int32"
            }
          }
        ],
        "responses": {
          "401": {
            "description": "Unauthorized",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ProblemDetails"
                }
              }
            }
          },
          "403": {
            "description": "Forbidden",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ProblemDetails"
                }
              }
            }
          },
          "429": {
            "description": "Too Many Requests",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ProblemDetails"
                }
              }
            }
          },
          "200": {
            "description": "The refreshed invoice with its updated subtotal and total.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/InvoiceSummary"
                }
              }
            }
          },
          "400": {
            "description": "Missing `Idempotency-Key`.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ValidationProblemDetails"
                }
              }
            }
          },
          "404": {
            "description": "No such invoice in the calling organization.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ProblemDetails"
                }
              }
            }
          },
          "409": {
            "description": "The invoice is not a draft or is not linked to a job, or the key's creator is unknown on this server.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ProblemDetails"
                }
              }
            }
          }
        }
      }
    },
    "/api/v1/invoices/{id}/void": {
      "post": {
        "tags": [
          "Invoices"
        ],
        "summary": "Voids an invoice.",
        "description": "Requires `ManageInvoices` and an `Idempotency-Key` header. Same rules as the\nVoid button: only a finalized invoice with no recorded payments can be voided (delete\nor refund its payments first); a draft is not voided but deleted\n(`DELETE /api/v1/invoices/{id}`); a consolidated invoice is voided as a whole, not\nthrough its members. Applied store credit is handed back to the\ncustomer and the job's payment status is recalculated. Voiding is permanent — the\ninvoice stays visible with status `Cancelled`. The body is optional.",
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "description": "Numeric invoice id.",
            "required": true,
            "schema": {
              "type": "integer",
              "format": "int32"
            }
          }
        ],
        "requestBody": {
          "description": "Optional reason, recorded on the job.",
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/VoidInvoiceRequest"
              }
            },
            "text/json": {
              "schema": {
                "$ref": "#/components/schemas/VoidInvoiceRequest"
              }
            },
            "application/*+json": {
              "schema": {
                "$ref": "#/components/schemas/VoidInvoiceRequest"
              }
            }
          }
        },
        "responses": {
          "401": {
            "description": "Unauthorized",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ProblemDetails"
                }
              }
            }
          },
          "403": {
            "description": "Forbidden",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ProblemDetails"
                }
              }
            }
          },
          "429": {
            "description": "Too Many Requests",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ProblemDetails"
                }
              }
            }
          },
          "200": {
            "description": "The voided invoice (status `Cancelled`).",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/InvoiceSummary"
                }
              }
            }
          },
          "400": {
            "description": "Malformed body or missing `Idempotency-Key`.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ValidationProblemDetails"
                }
              }
            }
          },
          "404": {
            "description": "No such invoice in the calling organization.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ProblemDetails"
                }
              }
            }
          },
          "409": {
            "description": "Already voided, a draft (delete it with `DELETE /api/v1/invoices/{id}` instead), consolidated, has recorded payments, or the key's creator is unknown on this server — the `detail` says which.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ProblemDetails"
                }
              }
            }
          }
        }
      }
    },
    "/api/v1/invoices/from-job": {
      "post": {
        "tags": [
          "Invoices"
        ],
        "summary": "Creates a draft invoice for a job.",
        "description": "Requires `ManageInvoices` and an `Idempotency-Key` header. Bills the job's\nfull price — or, once a finalized invoice exists, only its unbilled remainder (a\nsupplemental invoice). The job's down payment transfers onto the invoice; store credit\nis applied when requested. Every omitted field defaults exactly as the Generate Invoice\npage does. The result is a `Draft`: finalize it with\n`POST /api/v1/invoices/{id}/mark-sent`, or discard it with\n`DELETE /api/v1/invoices/{id}` (a pending draft blocks a second one).",
        "requestBody": {
          "description": "What to bill and, optionally, how.",
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/CreateInvoiceFromJobRequest"
              }
            },
            "text/json": {
              "schema": {
                "$ref": "#/components/schemas/CreateInvoiceFromJobRequest"
              }
            },
            "application/*+json": {
              "schema": {
                "$ref": "#/components/schemas/CreateInvoiceFromJobRequest"
              }
            }
          }
        },
        "responses": {
          "401": {
            "description": "Unauthorized",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ProblemDetails"
                }
              }
            }
          },
          "403": {
            "description": "Forbidden",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ProblemDetails"
                }
              }
            }
          },
          "429": {
            "description": "Too Many Requests",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ProblemDetails"
                }
              }
            }
          },
          "201": {
            "description": "The new draft invoice; `Location` points at it.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/InvoiceSummary"
                }
              }
            }
          },
          "400": {
            "description": "Malformed body or out-of-range value.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ValidationProblemDetails"
                }
              }
            }
          },
          "404": {
            "description": "No such job in the calling organization.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ProblemDetails"
                }
              }
            }
          },
          "409": {
            "description": "The job cannot be invoiced (cancelled, estimate option not chosen), a draft is already pending, the invoice number is taken, or the key's creator is unknown on this server.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ProblemDetails"
                }
              }
            }
          },
          "422": {
            "description": "Nothing left to bill, or `dueDate` precedes `invoiceDate`.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ValidationProblemDetails"
                }
              }
            }
          },
          "500": {
            "description": "The invoice could not be saved.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ProblemDetails"
                }
              }
            }
          }
        }
      }
    },
    "/api/v1/items": {
      "get": {
        "tags": [
          "Items"
        ],
        "summary": "Lists items (templates) with pricing and total on-hand.",
        "description": "Requires `ViewInventory`. Draft and deleted templates are excluded.\nCursor-paginated: pass the last item's `syncId` as `after`.\nAlso available as CSV (`Accept: text/csv` or `?format=csv`); paging cursor in `X-Next-Cursor`.",
        "parameters": [
          {
            "name": "search",
            "in": "query",
            "description": "Case-insensitive match against name and description.",
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "categoryId",
            "in": "query",
            "description": "Only items in this category (primary or assigned).",
            "schema": {
              "type": "integer",
              "format": "int32"
            }
          },
          {
            "name": "updatedSince",
            "in": "query",
            "description": "Only items created or updated at/after this UTC instant.",
            "schema": {
              "type": "string",
              "format": "date-time"
            }
          },
          {
            "name": "after",
            "in": "query",
            "description": "SyncId of the last item of the previous page.",
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "limit",
            "in": "query",
            "description": "Page size, 1..200 (default 50).",
            "schema": {
              "type": "integer",
              "format": "int32"
            }
          },
          {
            "name": "format",
            "in": "query",
            "description": "Alternative to the Accept header: `csv` returns text/csv.",
            "schema": {
              "enum": [
                "csv"
              ],
              "type": "string"
            }
          }
        ],
        "responses": {
          "401": {
            "description": "Unauthorized",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ProblemDetails"
                }
              }
            }
          },
          "403": {
            "description": "Forbidden",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ProblemDetails"
                }
              }
            }
          },
          "429": {
            "description": "Too Many Requests",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ProblemDetails"
                }
              }
            }
          },
          "200": {
            "description": "One page of items.",
            "headers": {
              "X-Next-Cursor": {
                "description": "text/csv only: the page's nextCursor. Absent on the last page. Pass it as ?after= (or follow the Link header) to fetch the next page.",
                "schema": {
                  "type": "string"
                }
              },
              "Link": {
                "description": "text/csv only, alongside X-Next-Cursor: <absolute URL of the next page>; rel=\"next\".",
                "schema": {
                  "type": "string"
                }
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/PagedResponseOfItemSummary"
                }
              },
              "text/csv": {
                "schema": {
                  "type": "string",
                  "description": "CSV document: UTF-8 with BOM, CRLF rows, RFC 4180 quoting. The header row holds the JSON property names of one item (nested objects flattened as parent.child); one row per item of the page."
                }
              }
            }
          },
          "400": {
            "description": "Unknown `after` cursor.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ValidationProblemDetails"
                }
              }
            }
          }
        }
      },
      "post": {
        "tags": [
          "Items"
        ],
        "summary": "Creates a catalog item.",
        "description": "Requires `ManageInventory` and an `Idempotency-Key` header. Same as adding an\nitem in the app: the item gets the organization's default \"No Category\" when\n`categoryId` is omitted, its `Cost` and `Unit Price` properties are\ncreated (0 unless `cost` / `unitPrice` are supplied, stored to 2 decimals) and\nthe category's properties are copied onto it. An active item with the same name is\nrefused with `409` (the rule the app's template builder applies). Creates no stock —\nfollow up with `POST /api/v1/items/{id}/stock/adjust`. No webhook fires for a\ncreate. Permissions mirror the app exactly: the Items page creates items, sets price/cost\nand category under `ManageInventory`, so that is all this endpoint needs — except\n`description`, which in the app is edited only on the Item Templates page and so\nadditionally requires `ManageTemplates` (`403` otherwise; omit the field or\ngrant the permission to the key).\nExample: `{ \"name\": \"1/2\\\" PEX Coupling\", \"categoryId\": 4, \"unitPrice\": 1.25, \"cost\": 0.40 }`.",
        "requestBody": {
          "description": "Name plus optional description, category, starting price and cost.",
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/CreateItemApiRequest"
              }
            },
            "text/json": {
              "schema": {
                "$ref": "#/components/schemas/CreateItemApiRequest"
              }
            },
            "application/*+json": {
              "schema": {
                "$ref": "#/components/schemas/CreateItemApiRequest"
              }
            }
          }
        },
        "responses": {
          "401": {
            "description": "Unauthorized",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ProblemDetails"
                }
              }
            }
          },
          "403": {
            "description": "The key lacks `ManageInventory`, or it sent `description` without holding `ManageTemplates`.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ProblemDetails"
                }
              }
            }
          },
          "429": {
            "description": "Too Many Requests",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ProblemDetails"
                }
              }
            }
          },
          "201": {
            "description": "Created; body is the item exactly as `GET /api/v1/items/{id}` returns it, `Location` points at it.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ItemDetail"
                }
              }
            }
          },
          "400": {
            "description": "Malformed body, missing `name`, out-of-range value, or missing `Idempotency-Key`.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ValidationProblemDetails"
                }
              }
            }
          },
          "409": {
            "description": "An active item with this name already exists, or the key's creator is unknown on this server.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ProblemDetails"
                }
              }
            }
          },
          "422": {
            "description": "No such category in this organization (`categoryId`), or `Idempotency-Key` reused with a different body.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ValidationProblemDetails"
                }
              }
            }
          },
          "500": {
            "description": "The item could not be saved; nothing was created.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ProblemDetails"
                }
              }
            }
          }
        }
      }
    },
    "/api/v1/items/{id}": {
      "delete": {
        "tags": [
          "Items"
        ],
        "summary": "Deactivates (soft-deletes) an item.",
        "description": "Requires `ManageInventory` and an `Idempotency-Key` header. Same as\n\"Deactivate\" on the Items page: the item leaves the catalogue and this API (GETs answer\n404), its physical inventory rows and history are untouched, and a user can restore it\nfrom Items → Deactivated Items in the app. A parent group that still contains active\nitems is refused with `409`; repeat with `?cascadeChildren=true` to deactivate\nthe group and its items together (each restorable individually).",
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "description": "Numeric item id.",
            "required": true,
            "schema": {
              "type": "integer",
              "format": "int32"
            }
          },
          {
            "name": "cascadeChildren",
            "in": "query",
            "description": "For a parent group: also deactivate its active child items instead of refusing.",
            "schema": {
              "type": "boolean"
            }
          }
        ],
        "responses": {
          "401": {
            "description": "Unauthorized",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ProblemDetails"
                }
              }
            }
          },
          "403": {
            "description": "Forbidden",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ProblemDetails"
                }
              }
            }
          },
          "429": {
            "description": "Too Many Requests",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ProblemDetails"
                }
              }
            }
          },
          "200": {
            "description": "The item was deactivated; body carries its id and a confirmation (mentions cascaded children when any).",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiOperationResponse"
                }
              }
            }
          },
          "400": {
            "description": "Missing `Idempotency-Key`.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ValidationProblemDetails"
                }
              }
            }
          },
          "404": {
            "description": "No such active item in this organization.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ProblemDetails"
                }
              }
            }
          },
          "409": {
            "description": "The group still contains active items and `cascadeChildren` was not set, or the key's creator is unknown on this server.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ProblemDetails"
                }
              }
            }
          }
        }
      },
      "get": {
        "tags": [
          "Items"
        ],
        "summary": "Returns one item with its properties, price tiers, barcodes and per-location stock.",
        "description": "Requires `ViewInventory`. `id` may be the numeric id or the `syncId`.\nSends a weak `ETag`; repeat with `If-None-Match` to get 304 when unchanged.",
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "description": "Numeric id or SyncId.",
            "required": true,
            "schema": {
              "type": "string"
            }
          }
        ],
        "responses": {
          "401": {
            "description": "Unauthorized",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ProblemDetails"
                }
              }
            }
          },
          "403": {
            "description": "Forbidden",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ProblemDetails"
                }
              }
            }
          },
          "429": {
            "description": "Too Many Requests",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ProblemDetails"
                }
              }
            }
          },
          "200": {
            "description": "The item.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ItemDetail"
                }
              }
            }
          },
          "304": {
            "description": "Unchanged since the supplied ETag."
          },
          "404": {
            "description": "No such item in the calling organization.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ProblemDetails"
                }
              }
            }
          }
        }
      },
      "patch": {
        "tags": [
          "Items"
        ],
        "summary": "Changes some of an item's name, description, category, price, cost, preferred vendor or vendor part number.",
        "description": "Requires `ManageInventory` and an `Idempotency-Key` header. Partial update:\nonly the fields present in the body change; omitted (or `null`) fields are left\nalone, and `\"description\": \"\"` (or whitespace only) clears the description. The same rules as the Items\npage apply — renaming also renames every inventory unit of the item; a new\n`unitPrice` / `cost` is written to the item and its future stock only\n(existing units keep their own pricing, as with \"push to existing inventory\" left off);\nchanging `categoryId` copies that category's properties onto the item. A rename to\nan active item's name is refused with `409`. Optional optimistic concurrency: send\nthe `ETag` from `GET /api/v1/items/{id}` as `If-Match` to get `412`\ninstead of overwriting a change made in the meantime (the tag covers stock too, so a\nconcurrent stock move also yields 412 — GET again and retry). Fires the\n`item.updated` webhook naming the changed fields. Permissions mirror the app\nexactly: name, price/cost and category are edited on the Items page under\n`ManageInventory`; `description` is edited only on the Item Templates page, so\nsending it (including `\"\"` to clear) additionally requires `ManageTemplates`\n(`403` otherwise, nothing changed). The same applies to `preferredVendorId`\n(an active vendor of the organization; `0` removes the link) and\n`vendorPartNumber` (`\"\"` clears it), which the app edits on Item Templates → Edit;\nan unknown or inactive vendor is `422`.\nExample: `{ \"unitPrice\": 1.35 }` or `{ \"preferredVendorId\": 3, \"vendorPartNumber\": \"PX-12C\" }`.",
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "description": "Numeric item id.",
            "required": true,
            "schema": {
              "type": "integer",
              "format": "int32"
            }
          }
        ],
        "requestBody": {
          "description": "The fields to change; at least one.",
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/UpdateItemApiRequest"
              }
            },
            "text/json": {
              "schema": {
                "$ref": "#/components/schemas/UpdateItemApiRequest"
              }
            },
            "application/*+json": {
              "schema": {
                "$ref": "#/components/schemas/UpdateItemApiRequest"
              }
            }
          }
        },
        "responses": {
          "401": {
            "description": "Unauthorized",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ProblemDetails"
                }
              }
            }
          },
          "403": {
            "description": "The key lacks `ManageInventory`, or it sent `description`, `preferredVendorId` or `vendorPartNumber` without holding `ManageTemplates`.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ProblemDetails"
                }
              }
            }
          },
          "429": {
            "description": "Too Many Requests",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ProblemDetails"
                }
              }
            }
          },
          "200": {
            "description": "Updated; body is the item exactly as `GET /api/v1/items/{id}` returns it.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ItemDetail"
                }
              }
            }
          },
          "400": {
            "description": "Empty body (no field to change), malformed body, out-of-range value, or missing `Idempotency-Key`.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ValidationProblemDetails"
                }
              }
            }
          },
          "404": {
            "description": "No such active item in this organization.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ProblemDetails"
                }
              }
            }
          },
          "409": {
            "description": "An active item with the new name already exists, or the key's creator is unknown on this server.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ProblemDetails"
                }
              }
            }
          },
          "412": {
            "description": "`If-Match` does not match the item's current `ETag`.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ProblemDetails"
                }
              }
            }
          },
          "422": {
            "description": "Blank `name`, no such category in this organization (`categoryId`), no such active vendor (`preferredVendorId`), or `Idempotency-Key` reused with a different body.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ValidationProblemDetails"
                }
              }
            }
          },
          "500": {
            "description": "The item could not be saved; nothing changed.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ProblemDetails"
                }
              }
            }
          }
        }
      }
    },
    "/api/v1/items/{id}/categories": {
      "get": {
        "tags": [
          "Categories"
        ],
        "summary": "Lists the categories an item is in, and which of them are assigned to it by hand.",
        "description": "Requires `ViewInventory`. An item is in the categories assigned to it by hand\n(`assignedCategoryIds`), their parent categories, and those its Template Builder gives\nit; its primary `categoryId` is shown separately. Supports `If-None-Match`.",
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "description": "Item id. Example: `12`.",
            "required": true,
            "schema": {
              "type": "integer",
              "format": "int32"
            }
          }
        ],
        "responses": {
          "401": {
            "description": "Unauthorized",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ProblemDetails"
                }
              }
            }
          },
          "403": {
            "description": "Forbidden",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ProblemDetails"
                }
              }
            }
          },
          "429": {
            "description": "Too Many Requests",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ProblemDetails"
                }
              }
            }
          },
          "200": {
            "description": "The item's categories.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ItemCategoriesResponse"
                }
              }
            }
          },
          "304": {
            "description": "Unchanged since the supplied ETag."
          },
          "404": {
            "description": "No such item in this organization.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ProblemDetails"
                }
              }
            }
          }
        }
      }
    },
    "/api/v1/items/{id}/categories/{categoryId}": {
      "delete": {
        "tags": [
          "Categories"
        ],
        "summary": "Removes a hand-assigned category from an item.",
        "description": "Requires `ManageInventory` and an `Idempotency-Key` header. Same as unticking the\ncategory on the item's page; the item's categories are recalculated, so a parent category\nstays while another assigned child needs it, and Template Builder categories always stay.\nRemoving a category that is not assigned by hand changes nothing.",
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "description": "Item id. Example: `12`.",
            "required": true,
            "schema": {
              "type": "integer",
              "format": "int32"
            }
          },
          {
            "name": "categoryId",
            "in": "path",
            "description": "Category id from `assignedCategoryIds`. Example: `4`.",
            "required": true,
            "schema": {
              "type": "integer",
              "format": "int32"
            }
          }
        ],
        "responses": {
          "401": {
            "description": "Unauthorized",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ProblemDetails"
                }
              }
            }
          },
          "403": {
            "description": "The key lacks `ManageInventory`.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ProblemDetails"
                }
              }
            }
          },
          "429": {
            "description": "Too Many Requests",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ProblemDetails"
                }
              }
            }
          },
          "200": {
            "description": "Removed; body is the item's categories.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ItemCategoriesResponse"
                }
              }
            }
          },
          "400": {
            "description": "Missing `Idempotency-Key`.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ValidationProblemDetails"
                }
              }
            }
          },
          "404": {
            "description": "No such item, or no such category, in this organization.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ProblemDetails"
                }
              }
            }
          },
          "409": {
            "description": "The key's creator is unknown on this server.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ProblemDetails"
                }
              }
            }
          }
        }
      },
      "post": {
        "tags": [
          "Categories"
        ],
        "summary": "Assigns a category to an item.",
        "description": "Requires `ManageInventory` and an `Idempotency-Key` header. Same as ticking the\ncategory under Categories on the item's page: the item (and its inventory units) also get the\ncategory's parent categories. Assigning an already assigned category changes nothing. The\nbuilt-in \"No Category\" and Template Builder-generated categories cannot be assigned by hand.\nThe item's primary `categoryId` is not changed (use `PATCH /api/v1/items/{id}`).",
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "description": "Item id. Example: `12`.",
            "required": true,
            "schema": {
              "type": "integer",
              "format": "int32"
            }
          },
          {
            "name": "categoryId",
            "in": "path",
            "description": "Category id from `GET /api/v1/categories`. Example: `4`.",
            "required": true,
            "schema": {
              "type": "integer",
              "format": "int32"
            }
          }
        ],
        "responses": {
          "401": {
            "description": "Unauthorized",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ProblemDetails"
                }
              }
            }
          },
          "403": {
            "description": "The key lacks `ManageInventory`.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ProblemDetails"
                }
              }
            }
          },
          "429": {
            "description": "Too Many Requests",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ProblemDetails"
                }
              }
            }
          },
          "200": {
            "description": "Assigned; body is the item's categories.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ItemCategoriesResponse"
                }
              }
            }
          },
          "400": {
            "description": "Missing `Idempotency-Key`.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ValidationProblemDetails"
                }
              }
            }
          },
          "404": {
            "description": "No such item, or no such category, in this organization.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ProblemDetails"
                }
              }
            }
          },
          "409": {
            "description": "The key's creator is unknown on this server.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ProblemDetails"
                }
              }
            }
          },
          "422": {
            "description": "The category is the built-in \"No Category\" or Template Builder-generated; or `Idempotency-Key` reused with a different body.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ValidationProblemDetails"
                }
              }
            }
          }
        }
      }
    },
    "/api/v1/items/{id}/properties": {
      "get": {
        "tags": [
          "ItemProperties"
        ],
        "summary": "Lists an item's custom fields (properties) with definitions and current values.",
        "description": "Requires `ViewInventory`. Returns every property of the item — custom fields plus the\npricing properties `Cost` and `Unit Price` (flagged `isPricing`) — ordered by\nname, with its data type, options, whether it is calculated (read-only), item-level or\ncustomized. Values are the item's (template's) values; each inventory unit carries a copy\nthat follows the item unless it was individually customized in the app.\nSupports `If-None-Match`.",
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "description": "Numeric item id.",
            "required": true,
            "schema": {
              "type": "integer",
              "format": "int32"
            }
          }
        ],
        "responses": {
          "401": {
            "description": "Unauthorized",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ProblemDetails"
                }
              }
            }
          },
          "403": {
            "description": "Forbidden",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ProblemDetails"
                }
              }
            }
          },
          "429": {
            "description": "Too Many Requests",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ProblemDetails"
                }
              }
            }
          },
          "200": {
            "description": "The item's properties.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ItemPropertiesResponse"
                }
              }
            }
          },
          "304": {
            "description": "Unchanged since the supplied ETag."
          },
          "404": {
            "description": "No such active item in this organization.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ProblemDetails"
                }
              }
            }
          }
        }
      },
      "patch": {
        "tags": [
          "ItemProperties"
        ],
        "summary": "Sets several custom-field values of an item at once, by property name.",
        "description": "Requires `ManageInventory` and an `Idempotency-Key` header. Same as editing values\nin the Items page's item modal: a property that came from the Item Template becomes\n\"Customized\" (`isCustomized`) and stops following the template, and each new value\nflows to every inventory unit whose copy was not individually customized. Values are\nchecked against the property's type and options (Number: decimal like `\"12.5\"`;\nBoolean: `\"true\"`/`\"false\"`; Date: `yyyy-MM-dd`; Dropdown: one option;\nMultiSelect: comma-separated options; `\"\"` clears). All or nothing: one unknown name,\ninvalid value, calculated or pricing property refuses the whole request. Calculated values\nthat depend on a changed property are recomputed in the same write, on the item and on its\ninventory units (as in the modal). Fires `item.updated` (`changedFields``properties`, names in `changedProperties`).\nExample: `{ \"values\": { \"Color\": \"Red\", \"Length\": \"12\" } }`.",
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "description": "Numeric item id.",
            "required": true,
            "schema": {
              "type": "integer",
              "format": "int32"
            }
          }
        ],
        "requestBody": {
          "description": "Property name → new value.",
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/SetItemPropertyValuesApiRequest"
              }
            },
            "text/json": {
              "schema": {
                "$ref": "#/components/schemas/SetItemPropertyValuesApiRequest"
              }
            },
            "application/*+json": {
              "schema": {
                "$ref": "#/components/schemas/SetItemPropertyValuesApiRequest"
              }
            }
          }
        },
        "responses": {
          "401": {
            "description": "Unauthorized",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ProblemDetails"
                }
              }
            }
          },
          "403": {
            "description": "Forbidden",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ProblemDetails"
                }
              }
            }
          },
          "429": {
            "description": "Too Many Requests",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ProblemDetails"
                }
              }
            }
          },
          "200": {
            "description": "Values saved; body is the item's properties as GET returns them, `propagated` = inventory unit copies updated.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ItemPropertiesResponse"
                }
              }
            }
          },
          "400": {
            "description": "Malformed body, no values, more than 100 values, or missing `Idempotency-Key`.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ValidationProblemDetails"
                }
              }
            }
          },
          "404": {
            "description": "No such active item in this organization.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ProblemDetails"
                }
              }
            }
          },
          "409": {
            "description": "A named property is calculated or a pricing property (`Cost` / `Unit Price`), or the key's creator is unknown on this server.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ProblemDetails"
                }
              }
            }
          },
          "422": {
            "description": "A name matches no property of the item, a name is listed twice, or a value does not fit its property (the error is keyed `values[name]`); or `Idempotency-Key` reused with a different body.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ValidationProblemDetails"
                }
              }
            }
          }
        }
      },
      "post": {
        "tags": [
          "ItemProperties"
        ],
        "summary": "Adds an item-level custom field to an item.",
        "description": "Requires `ManageInventory` and an `Idempotency-Key` header. Same as \"Add property\"\nin the Items page's item modal: the property belongs to this item only (`isItemLevel`;\nthe Item Template never changes it) and is also added, with the same value, to every\nexisting inventory unit that lacks it (new stock copies it automatically). The value is\nchecked against `dataType`. List types start without options; set them with\n`PATCH .../properties/{propertyId}` (`ManageTemplates`).\nExample: `{ \"name\": \"Warranty (years)\", \"dataType\": \"Number\", \"value\": \"2\" }`.",
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "description": "Numeric item id.",
            "required": true,
            "schema": {
              "type": "integer",
              "format": "int32"
            }
          }
        ],
        "requestBody": {
          "description": "Name, data type and optional starting value.",
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/CreateItemPropertyApiRequest"
              }
            },
            "text/json": {
              "schema": {
                "$ref": "#/components/schemas/CreateItemPropertyApiRequest"
              }
            },
            "application/*+json": {
              "schema": {
                "$ref": "#/components/schemas/CreateItemPropertyApiRequest"
              }
            }
          }
        },
        "responses": {
          "401": {
            "description": "Unauthorized",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ProblemDetails"
                }
              }
            }
          },
          "403": {
            "description": "Forbidden",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ProblemDetails"
                }
              }
            }
          },
          "429": {
            "description": "Too Many Requests",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ProblemDetails"
                }
              }
            }
          },
          "201": {
            "description": "Created; body is the item's properties (the new one included), `propagated` = inventory units it was added to. `Location` points at the list.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ItemPropertiesResponse"
                }
              }
            }
          },
          "400": {
            "description": "Malformed body, missing `name`, or missing `Idempotency-Key`.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ValidationProblemDetails"
                }
              }
            }
          },
          "404": {
            "description": "No such active item in this organization.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ProblemDetails"
                }
              }
            }
          },
          "409": {
            "description": "The item already has a property with this name, or the key's creator is unknown on this server.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ProblemDetails"
                }
              }
            }
          },
          "422": {
            "description": "Reserved name (`Cost` / `Unit Price`), or the value does not fit `dataType`; or `Idempotency-Key` reused with a different body.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ValidationProblemDetails"
                }
              }
            }
          }
        }
      }
    },
    "/api/v1/items/{id}/properties/{propertyId}": {
      "delete": {
        "tags": [
          "ItemProperties"
        ],
        "summary": "Deletes a property from an item.",
        "description": "Requires an `Idempotency-Key` header and the permission of the app page that deletes that\nkind of property. An item-level property (`isItemLevel`) is deleted as in the Items\npage's item modal (`ManageInventory`): the copies on inventory units that were not\nindividually customized are removed with it. Any other property is deleted as on the Item\nTemplates page (`ManageTemplates`): the item's property goes, inventory units keep\ntheir copies. Either way a property referenced by another property's calculation is refused\nand the property's own calculation is removed; `Cost` and `Unit Price` can never be deleted.",
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "description": "Numeric item id.",
            "required": true,
            "schema": {
              "type": "integer",
              "format": "int32"
            }
          },
          {
            "name": "propertyId",
            "in": "path",
            "description": "Property id from GET.",
            "required": true,
            "schema": {
              "type": "integer",
              "format": "int32"
            }
          }
        ],
        "responses": {
          "401": {
            "description": "Unauthorized",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ProblemDetails"
                }
              }
            }
          },
          "403": {
            "description": "The key lacks `ManageInventory` (item-level property) or `ManageTemplates` (any other property).",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ProblemDetails"
                }
              }
            }
          },
          "429": {
            "description": "Too Many Requests",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ProblemDetails"
                }
              }
            }
          },
          "200": {
            "description": "Deleted; body is the item's remaining properties, `propagated` = inventory unit copies removed.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ItemPropertiesResponse"
                }
              }
            }
          },
          "400": {
            "description": "Missing `Idempotency-Key`.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ValidationProblemDetails"
                }
              }
            }
          },
          "404": {
            "description": "No such property on an active item in this organization.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ProblemDetails"
                }
              }
            }
          },
          "409": {
            "description": "The property is `Cost` / `Unit Price` or is referenced by another property's calculation, or the key's creator is unknown on this server.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ProblemDetails"
                }
              }
            }
          }
        }
      },
      "patch": {
        "tags": [
          "ItemProperties"
        ],
        "summary": "Changes a property's name, data type or options.",
        "description": "Requires `ManageTemplates` and an `Idempotency-Key` header — in the app a property's\ndefinition is edited on the Item Templates page. Only the fields sent change, all in one\nsave (every field is validated first). Rules as on that page: names are unique on the item;\nchanging `dataType` clears the value unless the new type is `Text` and is refused\nwhile the property has an enabled calculation. Additionally: options are allowed only for\n`Dropdown` / `MultiSelect` and dropped when the type leaves them; `Cost` /\n`Unit Price` cannot be changed and are reserved names. Inventory units keep their copies\nunder the old name / type (as with the page).\nExample: `{ \"dataType\": \"Dropdown\", \"options\": [\"Red\", \"Blue\"] }`.",
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "description": "Numeric item id.",
            "required": true,
            "schema": {
              "type": "integer",
              "format": "int32"
            }
          },
          {
            "name": "propertyId",
            "in": "path",
            "description": "Property id from GET.",
            "required": true,
            "schema": {
              "type": "integer",
              "format": "int32"
            }
          }
        ],
        "requestBody": {
          "description": "The fields to change; at least one.",
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/UpdateItemPropertyApiRequest"
              }
            },
            "text/json": {
              "schema": {
                "$ref": "#/components/schemas/UpdateItemPropertyApiRequest"
              }
            },
            "application/*+json": {
              "schema": {
                "$ref": "#/components/schemas/UpdateItemPropertyApiRequest"
              }
            }
          }
        },
        "responses": {
          "401": {
            "description": "Unauthorized",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ProblemDetails"
                }
              }
            }
          },
          "403": {
            "description": "The key lacks `ManageTemplates`.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ProblemDetails"
                }
              }
            }
          },
          "429": {
            "description": "Too Many Requests",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ProblemDetails"
                }
              }
            }
          },
          "200": {
            "description": "Changed; body is the item's properties.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ItemPropertiesResponse"
                }
              }
            }
          },
          "400": {
            "description": "Empty body (no field to change), malformed body, or missing `Idempotency-Key`.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ValidationProblemDetails"
                }
              }
            }
          },
          "404": {
            "description": "No such property on an active item in this organization.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ProblemDetails"
                }
              }
            }
          },
          "409": {
            "description": "Another property already has the name, the type change is blocked by an enabled calculation, the property is `Cost` / `Unit Price`, or the key's creator is unknown on this server.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ProblemDetails"
                }
              }
            }
          },
          "422": {
            "description": "Blank or reserved name, options on a non-list type, or an invalid option (blank, comma, duplicate); or `Idempotency-Key` reused with a different body.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ValidationProblemDetails"
                }
              }
            }
          }
        }
      }
    },
    "/api/v1/items/{id}/properties/{propertyId}/revert": {
      "post": {
        "tags": [
          "ItemProperties"
        ],
        "summary": "Reverts a customized property to its Item Template default.",
        "description": "Requires `ManageInventory` and an `Idempotency-Key` header. Same as \"Revert\" in the\nItems page's item modal: the value goes back to the Item Template's default (or its first\nvisible option, or empty), `isCustomized` clears so the property follows the template\nagain, and the value flows to inventory units like an edit. Only for properties that came\nfrom the item's Item Template.",
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "description": "Numeric item id.",
            "required": true,
            "schema": {
              "type": "integer",
              "format": "int32"
            }
          },
          {
            "name": "propertyId",
            "in": "path",
            "description": "Property id from GET.",
            "required": true,
            "schema": {
              "type": "integer",
              "format": "int32"
            }
          }
        ],
        "responses": {
          "401": {
            "description": "Unauthorized",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ProblemDetails"
                }
              }
            }
          },
          "403": {
            "description": "Forbidden",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ProblemDetails"
                }
              }
            }
          },
          "429": {
            "description": "Too Many Requests",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ProblemDetails"
                }
              }
            }
          },
          "200": {
            "description": "Reverted; body is the item's properties.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ItemPropertiesResponse"
                }
              }
            }
          },
          "400": {
            "description": "Missing `Idempotency-Key`.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ValidationProblemDetails"
                }
              }
            }
          },
          "404": {
            "description": "No such property on an active item in this organization.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ProblemDetails"
                }
              }
            }
          },
          "409": {
            "description": "The property is item-level, the item is not assigned to an Item Template, the template no longer has the property, or the key's creator is unknown on this server.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ProblemDetails"
                }
              }
            }
          }
        }
      }
    },
    "/api/v1/items/{id}/properties/{propertyId}/value": {
      "put": {
        "tags": [
          "ItemProperties"
        ],
        "summary": "Replaces one custom-field value.",
        "description": "Requires `ManageInventory` and an `Idempotency-Key` header. Single-property form of\n`PATCH /api/v1/items/{id}/properties` with the same rules (Customized flag, flow to\ninventory units, dependent calculated values recomputed, typed validation, calculated and\npricing properties refused).\nExample: `{ \"value\": \"Red\" }`.",
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "description": "Numeric item id.",
            "required": true,
            "schema": {
              "type": "integer",
              "format": "int32"
            }
          },
          {
            "name": "propertyId",
            "in": "path",
            "description": "Property id from GET.",
            "required": true,
            "schema": {
              "type": "integer",
              "format": "int32"
            }
          }
        ],
        "requestBody": {
          "description": "The new value.",
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/SetItemPropertyValueApiRequest"
              }
            },
            "text/json": {
              "schema": {
                "$ref": "#/components/schemas/SetItemPropertyValueApiRequest"
              }
            },
            "application/*+json": {
              "schema": {
                "$ref": "#/components/schemas/SetItemPropertyValueApiRequest"
              }
            }
          }
        },
        "responses": {
          "401": {
            "description": "Unauthorized",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ProblemDetails"
                }
              }
            }
          },
          "403": {
            "description": "Forbidden",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ProblemDetails"
                }
              }
            }
          },
          "429": {
            "description": "Too Many Requests",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ProblemDetails"
                }
              }
            }
          },
          "200": {
            "description": "Value saved; body is the item's properties, `propagated` = inventory unit copies updated.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ItemPropertiesResponse"
                }
              }
            }
          },
          "400": {
            "description": "Malformed body or missing `Idempotency-Key`.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ValidationProblemDetails"
                }
              }
            }
          },
          "404": {
            "description": "No such property on an active item in this organization.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ProblemDetails"
                }
              }
            }
          },
          "409": {
            "description": "The property is calculated or a pricing property, or the key's creator is unknown on this server.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ProblemDetails"
                }
              }
            }
          },
          "422": {
            "description": "The value does not fit the property's type or options, or `Idempotency-Key` reused with a different body.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ValidationProblemDetails"
                }
              }
            }
          }
        }
      }
    },
    "/api/v1/items/{id}/reorder-points": {
      "get": {
        "tags": [
          "Reorder"
        ],
        "summary": "Lists an item's reorder points.",
        "description": "Requires `ViewInventory`. Every reorder point of the item, switched on or off: the\nall-locations one first, then by location name. Not cursor-paginated: `nextCursor` is\nalways `null`. Example: `GET /api/v1/items/42/reorder-points`.\nAlso available as CSV (`Accept: text/csv` or `?format=csv`); paging cursor in `X-Next-Cursor`.",
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "description": "Numeric item id.",
            "required": true,
            "schema": {
              "type": "integer",
              "format": "int32"
            }
          },
          {
            "name": "format",
            "in": "query",
            "description": "Alternative to the Accept header: `csv` returns text/csv.",
            "schema": {
              "enum": [
                "csv"
              ],
              "type": "string"
            }
          }
        ],
        "responses": {
          "401": {
            "description": "Unauthorized",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ProblemDetails"
                }
              }
            }
          },
          "403": {
            "description": "Forbidden",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ProblemDetails"
                }
              }
            }
          },
          "429": {
            "description": "Too Many Requests",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ProblemDetails"
                }
              }
            }
          },
          "200": {
            "description": "The item's reorder points (possibly none).",
            "headers": {
              "X-Next-Cursor": {
                "description": "text/csv only: the page's nextCursor. Absent on the last page. Pass it as ?after= (or follow the Link header) to fetch the next page.",
                "schema": {
                  "type": "string"
                }
              },
              "Link": {
                "description": "text/csv only, alongside X-Next-Cursor: <absolute URL of the next page>; rel=\"next\".",
                "schema": {
                  "type": "string"
                }
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/PagedResponseOfReorderPointDto"
                }
              },
              "text/csv": {
                "schema": {
                  "type": "string",
                  "description": "CSV document: UTF-8 with BOM, CRLF rows, RFC 4180 quoting. The header row holds the JSON property names of one item (nested objects flattened as parent.child); one row per item of the page."
                }
              }
            }
          },
          "304": {
            "description": "Unchanged since the supplied `If-None-Match`."
          },
          "404": {
            "description": "No such active item in this organization.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ProblemDetails"
                }
              }
            }
          }
        }
      },
      "put": {
        "tags": [
          "Reorder"
        ],
        "summary": "Creates or updates an item's reorder point for one location.",
        "description": "Requires `ManageInventory` and an `Idempotency-Key` header. The reorder point for\n(item, `locationId`) is created, or updated when one exists — the oldest, if the\nThresholds page has made duplicates. Omit `locationId` for the rule on the item's\nstock at all locations together. Same values and ranges as Reorder → Thresholds in the\napp; send `\"isActive\": false` to switch it off. Takes effect at once for\n`GET /api/v1/items/low-stock`. Example:\n`{ \"locationId\": 3, \"minimumStock\": 10, \"reorderQuantity\": 24 }`.",
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "description": "Numeric item id.",
            "required": true,
            "schema": {
              "type": "integer",
              "format": "int32"
            }
          }
        ],
        "requestBody": {
          "description": "Location, minimum stock, optional reorder quantity and on/off.",
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/SetReorderPointApiRequest"
              }
            },
            "text/json": {
              "schema": {
                "$ref": "#/components/schemas/SetReorderPointApiRequest"
              }
            },
            "application/*+json": {
              "schema": {
                "$ref": "#/components/schemas/SetReorderPointApiRequest"
              }
            }
          }
        },
        "responses": {
          "401": {
            "description": "Unauthorized",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ProblemDetails"
                }
              }
            }
          },
          "403": {
            "description": "Forbidden",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ProblemDetails"
                }
              }
            }
          },
          "429": {
            "description": "Too Many Requests",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ProblemDetails"
                }
              }
            }
          },
          "200": {
            "description": "Updated an existing reorder point; body is it.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ReorderPointDto"
                }
              }
            }
          },
          "201": {
            "description": "Created a reorder point; body is it.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ReorderPointDto"
                }
              }
            }
          },
          "400": {
            "description": "Malformed body, missing or negative `minimumStock`, negative `reorderQuantity`, or missing `Idempotency-Key`.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ValidationProblemDetails"
                }
              }
            }
          },
          "404": {
            "description": "No such active item in this organization.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ProblemDetails"
                }
              }
            }
          },
          "409": {
            "description": "The key's creator is unknown on this server.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ProblemDetails"
                }
              }
            }
          },
          "422": {
            "description": "No such location in this organization (`locationId`), or `Idempotency-Key` reused with a different body.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ValidationProblemDetails"
                }
              }
            }
          }
        }
      }
    },
    "/api/v1/items/{id}/stock/adjust": {
      "post": {
        "tags": [
          "Items"
        ],
        "summary": "Adds or removes stock for an item at one location.",
        "description": "Requires `ManageInventory` and an `Idempotency-Key` header. A positive\n`delta` creates that many stock units at the location exactly like \"Add inventory\"\non the Items page (template properties, cost, barcodes copied); a negative `delta`\nwrites off the oldest available units the way \"Delete\" on the Inventory page does\n(soft delete - restorable in the app, recorded in each unit's change history with\n`reason`). Stock never goes negative here: removing more than is on hand answers\n`409` and changes nothing. Units in transit on a transfer are never touched.\nOmit `locationId` for the organization's \"No Location\" bucket. Misc items, parent\ngroups and sub-unit items hold no stock (`409`); job sites are refused (`422`).\nExample: `{ \"locationId\": 3, \"delta\": -2, \"reason\": \"Damaged in van\" }`.",
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "description": "Numeric item id.",
            "required": true,
            "schema": {
              "type": "integer",
              "format": "int32"
            }
          }
        ],
        "requestBody": {
          "description": "Location, signed unit count and optional reason.",
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/AdjustStockRequest"
              }
            },
            "text/json": {
              "schema": {
                "$ref": "#/components/schemas/AdjustStockRequest"
              }
            },
            "application/*+json": {
              "schema": {
                "$ref": "#/components/schemas/AdjustStockRequest"
              }
            }
          }
        },
        "responses": {
          "401": {
            "description": "Unauthorized",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ProblemDetails"
                }
              }
            }
          },
          "403": {
            "description": "Forbidden",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ProblemDetails"
                }
              }
            }
          },
          "429": {
            "description": "Too Many Requests",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ProblemDetails"
                }
              }
            }
          },
          "200": {
            "description": "Stock changed; body carries the confirmation and the item's updated per-location stock.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/StockChangeResponse"
                }
              }
            }
          },
          "400": {
            "description": "Malformed body, `delta` of zero, or missing `Idempotency-Key`.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ValidationProblemDetails"
                }
              }
            }
          },
          "404": {
            "description": "No such active item in this organization.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ProblemDetails"
                }
              }
            }
          },
          "409": {
            "description": "Not enough units at the location to remove, the item cannot hold stock, or the key's creator is unknown on this server.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ProblemDetails"
                }
              }
            }
          },
          "422": {
            "description": "Unknown location, or the location is a job site.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ProblemDetails"
                }
              }
            }
          }
        }
      }
    },
    "/api/v1/items/{id}/stock/move": {
      "post": {
        "tags": [
          "Items"
        ],
        "summary": "Moves stock for an item from one location to another.",
        "description": "Requires `ManageInventory` and an `Idempotency-Key` header. Re-points\n`quantity` of the oldest available units at `fromLocationId` to\n`toLocationId` - the same thing as \"Quick move\" on the Inventory page, recorded in\neach unit's change history. Nothing is created or destroyed; the item's total on hand is\nunchanged. Fewer available units than requested answers `409` and moves nothing.\nOmit `toLocationId` for the organization's \"No Location\" bucket. Job sites are refused\nat either end (`422`) - stock reaches a job site through a job or a transfer.\nExample: `{ \"fromLocationId\": 1, \"toLocationId\": 4, \"quantity\": 3 }`.",
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "description": "Numeric item id.",
            "required": true,
            "schema": {
              "type": "integer",
              "format": "int32"
            }
          }
        ],
        "requestBody": {
          "description": "Source, destination, unit count and optional note.",
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/MoveStockRequest"
              }
            },
            "text/json": {
              "schema": {
                "$ref": "#/components/schemas/MoveStockRequest"
              }
            },
            "application/*+json": {
              "schema": {
                "$ref": "#/components/schemas/MoveStockRequest"
              }
            }
          }
        },
        "responses": {
          "401": {
            "description": "Unauthorized",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ProblemDetails"
                }
              }
            }
          },
          "403": {
            "description": "Forbidden",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ProblemDetails"
                }
              }
            }
          },
          "429": {
            "description": "Too Many Requests",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ProblemDetails"
                }
              }
            }
          },
          "200": {
            "description": "Stock moved; body carries the confirmation and the item's updated per-location stock.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/StockChangeResponse"
                }
              }
            }
          },
          "400": {
            "description": "Malformed body or missing `Idempotency-Key`.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ValidationProblemDetails"
                }
              }
            }
          },
          "404": {
            "description": "No such active item in this organization.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ProblemDetails"
                }
              }
            }
          },
          "409": {
            "description": "Not enough units at the source, the item cannot hold stock, or the key's creator is unknown on this server.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ProblemDetails"
                }
              }
            }
          },
          "422": {
            "description": "Unknown location, a job site at either end, or source and destination are the same.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ProblemDetails"
                }
              }
            }
          }
        }
      }
    },
    "/api/v1/items/{id}/units": {
      "get": {
        "tags": [
          "ItemUnits"
        ],
        "summary": "Lists an item's inventory units with their per-unit property values, one page at a time.",
        "description": "Requires `ViewInventory`. Every active unit is listed, including units in transit, at\njob sites and in bundles (see `status`). Example:\n`GET /api/v1/items/12/units?locationId=3` returns the units held at location 3.\nAlso available as CSV (`Accept: text/csv` or `?format=csv`; `properties` is\none JSON cell); paging cursor in `X-Next-Cursor`.",
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "description": "Numeric item id. Example: `12`.",
            "required": true,
            "schema": {
              "type": "integer",
              "format": "int32"
            }
          },
          {
            "name": "locationId",
            "in": "query",
            "description": "Only units held at this location.",
            "schema": {
              "type": "integer",
              "format": "int32"
            }
          },
          {
            "name": "updatedSince",
            "in": "query",
            "description": "Only units created or changed (including a custom-field value change) at/after this UTC instant (ISO-8601).",
            "schema": {
              "type": "string",
              "format": "date-time"
            }
          },
          {
            "name": "after",
            "in": "query",
            "description": "SyncId of the last unit of the previous page; omit for the first page.",
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "limit",
            "in": "query",
            "description": "Page size, 1-200 (default 50).",
            "schema": {
              "type": "integer",
              "format": "int32"
            }
          },
          {
            "name": "format",
            "in": "query",
            "description": "Alternative to the Accept header: `csv` returns text/csv.",
            "schema": {
              "enum": [
                "csv"
              ],
              "type": "string"
            }
          }
        ],
        "responses": {
          "401": {
            "description": "Unauthorized",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ProblemDetails"
                }
              }
            }
          },
          "403": {
            "description": "Forbidden",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ProblemDetails"
                }
              }
            }
          },
          "429": {
            "description": "Too Many Requests",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ProblemDetails"
                }
              }
            }
          },
          "200": {
            "description": "A page of units.",
            "headers": {
              "X-Next-Cursor": {
                "description": "text/csv only: the page's nextCursor. Absent on the last page. Pass it as ?after= (or follow the Link header) to fetch the next page.",
                "schema": {
                  "type": "string"
                }
              },
              "Link": {
                "description": "text/csv only, alongside X-Next-Cursor: <absolute URL of the next page>; rel=\"next\".",
                "schema": {
                  "type": "string"
                }
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/PagedResponseOfItemUnitDto"
                }
              },
              "text/csv": {
                "schema": {
                  "type": "string",
                  "description": "CSV document: UTF-8 with BOM, CRLF rows, RFC 4180 quoting. The header row holds the JSON property names of one item (nested objects flattened as parent.child); one row per item of the page."
                }
              }
            }
          },
          "400": {
            "description": "Malformed `after` cursor.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ValidationProblemDetails"
                }
              }
            }
          },
          "404": {
            "description": "No such active item in this organization.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ProblemDetails"
                }
              }
            }
          }
        }
      }
    },
    "/api/v1/items/{id}/units/{unitId}": {
      "get": {
        "tags": [
          "ItemUnits"
        ],
        "summary": "Returns one inventory unit of an item with its property values.",
        "description": "Requires `ViewInventory`. Supports `If-None-Match`.",
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "description": "Numeric item id. Example: `12`.",
            "required": true,
            "schema": {
              "type": "integer",
              "format": "int32"
            }
          },
          {
            "name": "unitId",
            "in": "path",
            "description": "Unit id from the list. Example: `5012`.",
            "required": true,
            "schema": {
              "type": "integer",
              "format": "int32"
            }
          }
        ],
        "responses": {
          "401": {
            "description": "Unauthorized",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ProblemDetails"
                }
              }
            }
          },
          "403": {
            "description": "Forbidden",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ProblemDetails"
                }
              }
            }
          },
          "429": {
            "description": "Too Many Requests",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ProblemDetails"
                }
              }
            }
          },
          "200": {
            "description": "The unit.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ItemUnitDto"
                }
              }
            }
          },
          "304": {
            "description": "Unchanged since the supplied ETag."
          },
          "404": {
            "description": "No such active item, or no such active unit of it.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ProblemDetails"
                }
              }
            }
          }
        }
      }
    },
    "/api/v1/items/{id}/units/{unitId}/properties/{name}": {
      "put": {
        "tags": [
          "ItemUnits"
        ],
        "summary": "Sets one custom-field value on one inventory unit.",
        "description": "Requires `ManageInventory` and an `Idempotency-Key` header. Same as editing the\nvalue on the unit's Inventory edit page: the unit's copy becomes customized\n(`isCustomized`) and stops following the item's value, and the unit's calculated values\nthat read it are recomputed in the same write. Only this unit changes — to change the item\nand every uncustomized unit use `PATCH /api/v1/items/{id}/properties`. The value is\nchecked against the property's type and options; calculated values and `Cost` /\n`Unit Price` are refused. `{name}` is the property name, URL-encoded,\ncase-insensitive. Fires `item.updated` (`changedFields``unitProperties`).\nExample: `PUT /api/v1/items/12/units/5012/properties/Color` with `{ \"value\": \"Blue\" }`.",
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "description": "Numeric item id. Example: `12`.",
            "required": true,
            "schema": {
              "type": "integer",
              "format": "int32"
            }
          },
          {
            "name": "unitId",
            "in": "path",
            "description": "Unit id from the list. Example: `5012`.",
            "required": true,
            "schema": {
              "type": "integer",
              "format": "int32"
            }
          },
          {
            "name": "name",
            "in": "path",
            "description": "Property name on the unit, e.g. `Color`.",
            "required": true,
            "schema": {
              "type": "string"
            }
          }
        ],
        "requestBody": {
          "description": "The new value.",
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/SetItemUnitPropertyApiRequest"
              }
            },
            "text/json": {
              "schema": {
                "$ref": "#/components/schemas/SetItemUnitPropertyApiRequest"
              }
            },
            "application/*+json": {
              "schema": {
                "$ref": "#/components/schemas/SetItemUnitPropertyApiRequest"
              }
            }
          }
        },
        "responses": {
          "401": {
            "description": "Unauthorized",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ProblemDetails"
                }
              }
            }
          },
          "403": {
            "description": "The key lacks `ManageInventory`.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ProblemDetails"
                }
              }
            }
          },
          "429": {
            "description": "Too Many Requests",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ProblemDetails"
                }
              }
            }
          },
          "200": {
            "description": "Saved; body is the unit as GET returns it.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ItemUnitDto"
                }
              }
            }
          },
          "400": {
            "description": "Malformed body, a value over 4000 characters, or missing `Idempotency-Key`.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ValidationProblemDetails"
                }
              }
            }
          },
          "404": {
            "description": "No such active item, unit, or property on the unit.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ProblemDetails"
                }
              }
            }
          },
          "409": {
            "description": "The property is calculated on this unit or is `Cost` / `Unit Price`, or the key's creator is unknown on this server.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ProblemDetails"
                }
              }
            }
          },
          "422": {
            "description": "The value does not fit the property's type or options (keyed `value`); or `Idempotency-Key` reused with a different body.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ValidationProblemDetails"
                }
              }
            }
          }
        }
      }
    },
    "/api/v1/items/low-stock": {
      "get": {
        "tags": [
          "Reorder"
        ],
        "summary": "Lists items below one of their reorder points, with on-hand, reorder quantity and vendor.",
        "description": "Requires `ViewInventory`. One line per active reorder point whose watched stock is\nstrictly below its `minimumStock` — an item with reorder points at two locations can\nappear twice. Stock is the count of non-deleted units at the reorder point's location, or\neverywhere when it has none (the Reorder page's rule). `vendorId` is the item's\npreferred vendor (set on Item Templates → Edit or `PATCH /api/v1/items/{id}`), else\nthe vendor of the CSV import that created it. Computed on request and read-only: unlike\nopening the Reorder page it raises no alert and fires no `inventory.low_stock`\nwebhook. Ordered by item name then location. Not cursor-paginated (the list is bounded by\nthe reorder points): everything comes back with `nextCursor` always `null`.\nExample: `GET /api/v1/items/low-stock?vendorId=3`.\nAlso available as CSV (`Accept: text/csv` or `?format=csv`); paging cursor in `X-Next-Cursor`.",
        "parameters": [
          {
            "name": "vendorId",
            "in": "query",
            "description": "Only lines whose resolved `vendorId` is this vendor; `0` = only lines with no vendor. Example: `3`.",
            "schema": {
              "type": "integer",
              "format": "int32"
            }
          },
          {
            "name": "locationId",
            "in": "query",
            "description": "Only reorder points watching this location. Example: `5`.",
            "schema": {
              "type": "integer",
              "format": "int32"
            }
          },
          {
            "name": "format",
            "in": "query",
            "description": "Alternative to the Accept header: `csv` returns text/csv.",
            "schema": {
              "enum": [
                "csv"
              ],
              "type": "string"
            }
          }
        ],
        "responses": {
          "401": {
            "description": "Unauthorized",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ProblemDetails"
                }
              }
            }
          },
          "403": {
            "description": "Forbidden",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ProblemDetails"
                }
              }
            }
          },
          "429": {
            "description": "Too Many Requests",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ProblemDetails"
                }
              }
            }
          },
          "200": {
            "description": "Every low-stock line (possibly none).",
            "headers": {
              "X-Next-Cursor": {
                "description": "text/csv only: the page's nextCursor. Absent on the last page. Pass it as ?after= (or follow the Link header) to fetch the next page.",
                "schema": {
                  "type": "string"
                }
              },
              "Link": {
                "description": "text/csv only, alongside X-Next-Cursor: <absolute URL of the next page>; rel=\"next\".",
                "schema": {
                  "type": "string"
                }
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/PagedResponseOfLowStockItemDto"
                }
              },
              "text/csv": {
                "schema": {
                  "type": "string",
                  "description": "CSV document: UTF-8 with BOM, CRLF rows, RFC 4180 quoting. The header row holds the JSON property names of one item (nested objects flattened as parent.child); one row per item of the page."
                }
              }
            }
          },
          "304": {
            "description": "Unchanged since the supplied `If-None-Match`."
          }
        }
      }
    },
    "/api/v1/items/stock/batch": {
      "post": {
        "tags": [
          "Items"
        ],
        "summary": "Applies several stock adjustments and moves in one call, all or nothing.",
        "description": "Requires `ManageInventory` and an `Idempotency-Key` header (one key covers the\nwhole batch). Each operation follows the rules of `POST /api/v1/items/{id}/stock/adjust`\n(`\"action\": \"Adjust\"`) or `/stock/move` (`\"action\": \"Move\"`). Operations apply\nin order, so a later removal can use units an earlier one added. Every operation is checked\nbefore anything is written, and the writes share one transaction: if any operation fails,\nnothing changes and the problem's `detail` starts with `operations[i]` naming the first\nfailing one (also in the `failedIndex` extension). 1..100 operations, at most 1000 units in total.\nExample: <c>{ \"operations\": [ { \"action\": \"Adjust\", \"itemId\": 12, \"locationId\": 3, \"delta\": 10 },\n{ \"action\": \"Move\", \"itemId\": 12, \"fromLocationId\": 3, \"toLocationId\": 4, \"quantity\": 2 } ] }</c>.",
        "requestBody": {
          "description": "The operations, in order.",
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/StockBatchRequest"
              }
            },
            "text/json": {
              "schema": {
                "$ref": "#/components/schemas/StockBatchRequest"
              }
            },
            "application/*+json": {
              "schema": {
                "$ref": "#/components/schemas/StockBatchRequest"
              }
            }
          }
        },
        "responses": {
          "401": {
            "description": "Unauthorized",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ProblemDetails"
                }
              }
            }
          },
          "403": {
            "description": "Forbidden",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ProblemDetails"
                }
              }
            }
          },
          "429": {
            "description": "Too Many Requests",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ProblemDetails"
                }
              }
            }
          },
          "200": {
            "description": "Every operation applied; one result per operation with the item's stock after the batch.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/StockBatchResponse"
                }
              }
            }
          },
          "400": {
            "description": "Malformed body, empty or oversized batch, an operation missing its `delta`/`quantity`, or missing `Idempotency-Key`.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ValidationProblemDetails"
                }
              }
            }
          },
          "404": {
            "description": "An operation names no active item in this organization.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ProblemDetails"
                }
              }
            }
          },
          "409": {
            "description": "Not enough units for an operation, an item that cannot hold stock, or the key's creator is unknown on this server.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ProblemDetails"
                }
              }
            }
          },
          "422": {
            "description": "Unknown location, a job site, source equal to destination, or too many units in total.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ProblemDetails"
                }
              }
            }
          }
        }
      }
    },
    "/api/v1/jobs": {
      "get": {
        "tags": [
          "Jobs"
        ],
        "summary": "Lists the organization's jobs, oldest first, one page at a time.",
        "description": "Pagination: pass the previous response's `nextCursor` as `after`; `limit`\ndefaults to 50 and is capped at 200. All filters combine with AND. Response carries a\nweak `ETag`; resend it as `If-None-Match` to get 304 when nothing changed.\nAlso available as CSV (`Accept: text/csv` or `?format=csv`); paging cursor in `X-Next-Cursor`.",
        "parameters": [
          {
            "name": "status",
            "in": "query",
            "description": "Workflow status name to match exactly (case-insensitive), e.g. `Scheduled`, `InProgress`, `Completed`.",
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "customerId",
            "in": "query",
            "description": "Only jobs for this customer id.",
            "schema": {
              "type": "integer",
              "format": "int32"
            }
          },
          {
            "name": "updatedSince",
            "in": "query",
            "description": "Only jobs created or modified at or after this instant (ISO-8601 UTC, e.g. `2026-09-01T00:00:00Z`). Use it to poll for changes.",
            "schema": {
              "type": "string",
              "format": "date-time"
            }
          },
          {
            "name": "scheduledFrom",
            "in": "query",
            "description": "Only jobs whose scheduled start is at or after this local wall-clock time (`yyyy-MM-ddTHH:mm:ss`, no zone suffix).",
            "schema": {
              "type": "string",
              "format": "date-time"
            }
          },
          {
            "name": "scheduledTo",
            "in": "query",
            "description": "Only jobs whose scheduled start is at or before this local wall-clock time.",
            "schema": {
              "type": "string",
              "format": "date-time"
            }
          },
          {
            "name": "after",
            "in": "query",
            "description": "Cursor from the previous page's `nextCursor` (a job SyncId). Omit for the first page.",
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "limit",
            "in": "query",
            "description": "Page size, 1–200 (default 50).",
            "schema": {
              "type": "integer",
              "format": "int32"
            }
          },
          {
            "name": "format",
            "in": "query",
            "description": "Alternative to the Accept header: `csv` returns text/csv.",
            "schema": {
              "enum": [
                "csv"
              ],
              "type": "string"
            }
          }
        ],
        "responses": {
          "401": {
            "description": "Unauthorized",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ProblemDetails"
                }
              }
            }
          },
          "403": {
            "description": "Forbidden",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ProblemDetails"
                }
              }
            }
          },
          "429": {
            "description": "Too Many Requests",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ProblemDetails"
                }
              }
            }
          },
          "200": {
            "description": "A page of jobs and the cursor for the next page (null on the last page).",
            "headers": {
              "X-Next-Cursor": {
                "description": "text/csv only: the page's nextCursor. Absent on the last page. Pass it as ?after= (or follow the Link header) to fetch the next page.",
                "schema": {
                  "type": "string"
                }
              },
              "Link": {
                "description": "text/csv only, alongside X-Next-Cursor: <absolute URL of the next page>; rel=\"next\".",
                "schema": {
                  "type": "string"
                }
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/PagedResponseOfJobSummaryDto"
                }
              },
              "text/csv": {
                "schema": {
                  "type": "string",
                  "description": "CSV document: UTF-8 with BOM, CRLF rows, RFC 4180 quoting. The header row holds the JSON property names of one item (nested objects flattened as parent.child); one row per item of the page."
                }
              }
            }
          },
          "304": {
            "description": "Unchanged since the `If-None-Match` ETag."
          },
          "400": {
            "description": "Unknown `status` or unusable `after` cursor (field listed in `errors`).",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ValidationProblemDetails"
                }
              }
            }
          }
        }
      },
      "post": {
        "tags": [
          "Jobs"
        ],
        "summary": "Creates a job (estimate or draft) for a customer, optionally with material, labor and custom-charge lines.",
        "description": "Same code path as the MVC \"Create job\" page. Requires an `Idempotency-Key`\nheader. Line totals are computed on the server from the lines you send. The job\nnumber is generated when omitted. Initial status: `Estimated`, or `Draft`\nwhen `isDraft` is true, unless `statusId` / `statusKey` names one of\nthe organization's statuses. `publishImmediately: true` is the \"Create & Publish\"\nbutton: the job is created as a `Draft` and published in the same call under the\nrules of `POST /api/v1/jobs/{id}/finalize`; when it cannot be published yet it is\nstill created as a `Draft` and the response's `warning` says why (never a 4xx\nfor an unpublishable-but-created job). The write is attributed to the user who created the key.",
        "requestBody": {
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/CreateJobApiRequest"
              }
            },
            "text/json": {
              "schema": {
                "$ref": "#/components/schemas/CreateJobApiRequest"
              }
            },
            "application/*+json": {
              "schema": {
                "$ref": "#/components/schemas/CreateJobApiRequest"
              }
            }
          }
        },
        "responses": {
          "401": {
            "description": "Unauthorized",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ProblemDetails"
                }
              }
            }
          },
          "403": {
            "description": "Forbidden",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ProblemDetails"
                }
              }
            }
          },
          "429": {
            "description": "Too Many Requests",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ProblemDetails"
                }
              }
            }
          },
          "201": {
            "description": "The created job (same shape as `GET /api/v1/jobs/{id}`, plus `warning` when a requested publish did not happen); `Location` points at it.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/JobDetailDto"
                }
              }
            }
          },
          "400": {
            "description": "Malformed body, missing `Idempotency-Key`, both `statusId` and `statusKey` given, or `publishImmediately` with a non-Draft status.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ValidationProblemDetails"
                }
              }
            }
          },
          "409": {
            "description": "The key's creator is unknown on this server, or the service refused the create.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ProblemDetails"
                }
              }
            }
          },
          "422": {
            "description": "Unknown customer, site address, status, item template, labor rate or task template for this organization (field listed in `errors`).",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ValidationProblemDetails"
                }
              }
            }
          }
        }
      }
    },
    "/api/v1/jobs/{id}": {
      "get": {
        "tags": [
          "Jobs"
        ],
        "summary": "Returns one job with its site address, pricing, assignees, material and labor lines and note timeline.",
        "description": "Removed (soft-deleted) lines and assignments are excluded. Response carries a weak\n`ETag`; resend it as `If-None-Match` to get 304 when nothing changed.",
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "description": "Job id from the list endpoint.",
            "required": true,
            "schema": {
              "type": "integer",
              "format": "int32"
            }
          }
        ],
        "responses": {
          "401": {
            "description": "Unauthorized",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ProblemDetails"
                }
              }
            }
          },
          "403": {
            "description": "Forbidden",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ProblemDetails"
                }
              }
            }
          },
          "429": {
            "description": "Too Many Requests",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ProblemDetails"
                }
              }
            }
          },
          "200": {
            "description": "The job.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/JobDetailDto"
                }
              }
            }
          },
          "304": {
            "description": "Unchanged since the `If-None-Match` ETag."
          },
          "404": {
            "description": "No such job in this organization.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ProblemDetails"
                }
              }
            }
          }
        }
      },
      "patch": {
        "tags": [
          "Jobs"
        ],
        "summary": "Edits a draft/estimate job's core fields (type, description, notes, schedule, customer, address). Status changes go through `POST /api/v1/jobs/{id}/status`.",
        "description": "PATCH semantics: only the fields you send change; omitted fields are left as they are.\nSame rules as the web Edit page: only jobs in an editable status (Draft, Estimated,\nInspection in progress, Awaiting option selection) can be edited, and the customer,\naddress must belong to your organization. Status changes use `POST /api/v1/jobs/{id}/status`. Line items, labor and custom\ncharges are edited through their own endpoints, not here. Totals are never taken from the\ncaller — the job is repriced after the change, so a new customer's tax is applied.\nRequires an `Idempotency-Key` header; the write is attributed to the user who created the key.",
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "description": "Job id from `GET /api/v1/jobs`.",
            "required": true,
            "schema": {
              "type": "integer",
              "format": "int32"
            }
          }
        ],
        "requestBody": {
          "description": "The fields to change.",
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/UpdateJobApiRequest"
              }
            },
            "text/json": {
              "schema": {
                "$ref": "#/components/schemas/UpdateJobApiRequest"
              }
            },
            "application/*+json": {
              "schema": {
                "$ref": "#/components/schemas/UpdateJobApiRequest"
              }
            }
          }
        },
        "responses": {
          "401": {
            "description": "Unauthorized",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ProblemDetails"
                }
              }
            }
          },
          "403": {
            "description": "Forbidden",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ProblemDetails"
                }
              }
            }
          },
          "429": {
            "description": "Too Many Requests",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ProblemDetails"
                }
              }
            }
          },
          "200": {
            "description": "The updated job (same shape as `GET /api/v1/jobs/{id}`).",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/JobDetailDto"
                }
              }
            }
          },
          "404": {
            "description": "No job with this id in your organization.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ProblemDetails"
                }
              }
            }
          },
          "422": {
            "description": "The job is not in an editable status, or the customer, address or status does not belong to your organization (the detail says which).",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ProblemDetails"
                }
              }
            }
          },
          "400": {
            "description": "Malformed body or missing `Idempotency-Key`."
          },
          "409": {
            "description": "The key's creator is unknown on this server."
          }
        }
      }
    },
    "/api/v1/jobs/{id}/assignees": {
      "post": {
        "tags": [
          "Jobs"
        ],
        "summary": "Assigns people to a job — additively by default, or as the complete new crew with `replace: true`.",
        "description": "Requires the Scheduling module. User ids come from `GET /api/v1/users` and must be\nactive members of the organization. People already on the job keep their crew\n(schedule group) context; newly added people are assigned as individuals. With\n`replace: true` anyone not listed is unassigned (an empty list clears the job).\nRequires an `Idempotency-Key` header.",
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "description": "Job id from a list response.",
            "required": true,
            "schema": {
              "type": "integer",
              "format": "int32"
            }
          }
        ],
        "requestBody": {
          "description": "User ids to assign and whether they replace the current set.",
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/AssignJobRequest"
              }
            },
            "text/json": {
              "schema": {
                "$ref": "#/components/schemas/AssignJobRequest"
              }
            },
            "application/*+json": {
              "schema": {
                "$ref": "#/components/schemas/AssignJobRequest"
              }
            }
          }
        },
        "responses": {
          "401": {
            "description": "Unauthorized",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ProblemDetails"
                }
              }
            }
          },
          "403": {
            "description": "Scheduling module not active for this organization.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ProblemDetails"
                }
              }
            }
          },
          "429": {
            "description": "Too Many Requests",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ProblemDetails"
                }
              }
            }
          },
          "200": {
            "description": "Job id and a message listing who was assigned / unassigned (or \"Assignments unchanged.\").",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiOperationResponse"
                }
              }
            }
          },
          "400": {
            "description": "`userIds` missing, or missing `Idempotency-Key`.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ValidationProblemDetails"
                }
              }
            }
          },
          "404": {
            "description": "No such job in this organization.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ProblemDetails"
                }
              }
            }
          },
          "409": {
            "description": "The key's creator is unknown on this server.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ProblemDetails"
                }
              }
            }
          },
          "422": {
            "description": "One or more `userIds` are not active members of this organization (listed in the message).",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ValidationProblemDetails"
                }
              }
            }
          }
        }
      }
    },
    "/api/v1/jobs/{id}/cancel": {
      "post": {
        "tags": [
          "Jobs"
        ],
        "summary": "Cancels a job, returning its materials to stock.",
        "description": "Same rules as the UI: a job that has received payment, or has sent/paid invoices,\ncannot be cancelled until the money is refunded / invoices voided (409). A job with\nan uncollected down payment can be cancelled with `issueStoreCredit: true`\nwhen the key holds `ManageStoreCredit`. Cancelling an already-cancelled job\nanswers 200 with a `warning`. Requires an `Idempotency-Key` header.",
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "description": "Job id from a list response.",
            "required": true,
            "schema": {
              "type": "integer",
              "format": "int32"
            }
          }
        ],
        "requestBody": {
          "description": "Optional reason and the store-credit choice.",
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/CancelJobRequest"
              }
            },
            "text/json": {
              "schema": {
                "$ref": "#/components/schemas/CancelJobRequest"
              }
            },
            "application/*+json": {
              "schema": {
                "$ref": "#/components/schemas/CancelJobRequest"
              }
            }
          }
        },
        "responses": {
          "401": {
            "description": "Unauthorized",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ProblemDetails"
                }
              }
            }
          },
          "403": {
            "description": "Forbidden",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ProblemDetails"
                }
              }
            }
          },
          "429": {
            "description": "Too Many Requests",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ProblemDetails"
                }
              }
            }
          },
          "200": {
            "description": "Job id, confirmation message and an optional warning.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiOperationResponse"
                }
              }
            }
          },
          "400": {
            "description": "Malformed body or missing `Idempotency-Key`.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ValidationProblemDetails"
                }
              }
            }
          },
          "404": {
            "description": "No such job in this organization.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ProblemDetails"
                }
              }
            }
          },
          "409": {
            "description": "Payments or invoices block the cancellation, or the key's creator is unknown on this server.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ProblemDetails"
                }
              }
            }
          }
        }
      }
    },
    "/api/v1/jobs/{id}/charges": {
      "post": {
        "tags": [
          "Jobs"
        ],
        "summary": "Adds a custom charge to a job (fees, disposal, permits …).",
        "description": "Requires `ManageJobs` and an `Idempotency-Key` header. A cancelled job is\nread-only. The write is attributed to the key's creator.",
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "description": "Job id.",
            "required": true,
            "schema": {
              "type": "integer",
              "format": "int32"
            }
          }
        ],
        "requestBody": {
          "description": "The charge to add.",
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/AddJobChargeApiRequest"
              }
            },
            "text/json": {
              "schema": {
                "$ref": "#/components/schemas/AddJobChargeApiRequest"
              }
            },
            "application/*+json": {
              "schema": {
                "$ref": "#/components/schemas/AddJobChargeApiRequest"
              }
            }
          }
        },
        "responses": {
          "401": {
            "description": "Unauthorized",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ProblemDetails"
                }
              }
            }
          },
          "403": {
            "description": "Forbidden",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ProblemDetails"
                }
              }
            }
          },
          "429": {
            "description": "Too Many Requests",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ProblemDetails"
                }
              }
            }
          },
          "200": {
            "description": "The updated job.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/JobDetailDto"
                }
              }
            }
          },
          "404": {
            "description": "No job with this id in your organization.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ProblemDetails"
                }
              }
            }
          },
          "422": {
            "description": "The job is cancelled or the charge is invalid (the detail says which).",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ProblemDetails"
                }
              }
            }
          },
          "400": {
            "description": "Malformed body or missing `Idempotency-Key`."
          }
        }
      }
    },
    "/api/v1/jobs/{id}/charges/{chargeId}": {
      "delete": {
        "tags": [
          "Jobs"
        ],
        "summary": "Removes a custom charge from a job (soft delete).",
        "description": "Requires `ManageJobs` and an `Idempotency-Key` header; no body. A charge on a\nfinalized invoice can't be deleted (use Return Items); a cancelled job is read-only.",
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "description": "Job id.",
            "required": true,
            "schema": {
              "type": "integer",
              "format": "int32"
            }
          },
          {
            "name": "chargeId",
            "in": "path",
            "description": "Custom-charge id.",
            "required": true,
            "schema": {
              "type": "integer",
              "format": "int32"
            }
          }
        ],
        "responses": {
          "401": {
            "description": "Unauthorized",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ProblemDetails"
                }
              }
            }
          },
          "403": {
            "description": "Forbidden",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ProblemDetails"
                }
              }
            }
          },
          "429": {
            "description": "Too Many Requests",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ProblemDetails"
                }
              }
            }
          },
          "200": {
            "description": "The updated job with the charge removed.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/JobDetailDto"
                }
              }
            }
          },
          "404": {
            "description": "No such job in your organization, or the charge is not one of its live charges.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ProblemDetails"
                }
              }
            }
          },
          "422": {
            "description": "The job is cancelled or the charge is billed (the detail says which).",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ProblemDetails"
                }
              }
            }
          },
          "400": {
            "description": "Missing `Idempotency-Key`."
          }
        }
      },
      "patch": {
        "tags": [
          "Jobs"
        ],
        "summary": "Edits a custom charge's name, price and/or description.",
        "description": "Requires `ManageJobs` and an `Idempotency-Key` header. PATCH semantics. A charge\non a finalized invoice changes only through Return Items; a cancelled job is read-only.",
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "description": "Job id.",
            "required": true,
            "schema": {
              "type": "integer",
              "format": "int32"
            }
          },
          {
            "name": "chargeId",
            "in": "path",
            "description": "Custom-charge id (`customCharges[].id` of the job).",
            "required": true,
            "schema": {
              "type": "integer",
              "format": "int32"
            }
          }
        ],
        "requestBody": {
          "description": "The fields to change.",
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/UpdateJobChargeApiRequest"
              }
            },
            "text/json": {
              "schema": {
                "$ref": "#/components/schemas/UpdateJobChargeApiRequest"
              }
            },
            "application/*+json": {
              "schema": {
                "$ref": "#/components/schemas/UpdateJobChargeApiRequest"
              }
            }
          }
        },
        "responses": {
          "401": {
            "description": "Unauthorized",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ProblemDetails"
                }
              }
            }
          },
          "403": {
            "description": "Forbidden",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ProblemDetails"
                }
              }
            }
          },
          "429": {
            "description": "Too Many Requests",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ProblemDetails"
                }
              }
            }
          },
          "200": {
            "description": "The updated job.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/JobDetailDto"
                }
              }
            }
          },
          "404": {
            "description": "No such job in your organization, or the charge is not one of its live charges.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ProblemDetails"
                }
              }
            }
          },
          "422": {
            "description": "The job is cancelled or the charge is billed (the detail says which).",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ProblemDetails"
                }
              }
            }
          },
          "400": {
            "description": "Malformed body or missing `Idempotency-Key`."
          }
        }
      }
    },
    "/api/v1/jobs/{id}/finalize": {
      "post": {
        "tags": [
          "Jobs"
        ],
        "summary": "Publishes a draft estimate.",
        "description": "Requires `ManageJobs` and an `Idempotency-Key` header. Same rules as the\n\"Publish Estimate\" button on the job page: only a `Draft` can be published, it\nneeds at least one charge (labor, material or custom) and every material line must\nhave a unit price. The job lands on `Estimated`, on `Scheduled` when a\nscheduled date was already set while drafting, or on `AwaitingOptionSelection`\nfor a multi-option estimate; a history entry records the change. No body is required.\nTo create and publish in one call use `publishImmediately` on `POST /api/v1/jobs`.",
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "description": "Job id from a list response.",
            "required": true,
            "schema": {
              "type": "integer",
              "format": "int32"
            }
          }
        ],
        "responses": {
          "401": {
            "description": "Unauthorized",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ProblemDetails"
                }
              }
            }
          },
          "403": {
            "description": "Forbidden",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ProblemDetails"
                }
              }
            }
          },
          "429": {
            "description": "Too Many Requests",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ProblemDetails"
                }
              }
            }
          },
          "200": {
            "description": "Job id and confirmation message (e.g. `Estimate published successfully! You can now send it to the customer.`).",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiOperationResponse"
                }
              }
            }
          },
          "400": {
            "description": "Missing `Idempotency-Key`.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ValidationProblemDetails"
                }
              }
            }
          },
          "404": {
            "description": "No such job in this organization.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ProblemDetails"
                }
              }
            }
          },
          "409": {
            "description": "The job is not a draft, it has no charges / an unpriced material line, or the key's creator is unknown on this server.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ProblemDetails"
                }
              }
            }
          },
          "500": {
            "description": "The change could not be saved; nothing was changed. Retry with the same `Idempotency-Key`.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ProblemDetails"
                }
              }
            }
          }
        }
      }
    },
    "/api/v1/jobs/{id}/items": {
      "post": {
        "tags": [
          "Jobs"
        ],
        "summary": "Adds a material line to a job (the app's \"Add material\" button).",
        "description": "Requires `ManageJobs` and an `Idempotency-Key` header. Same rules as the web\napp: a cancelled job is read-only, parent / sell-by-unit / add-on items are added through\nthe app instead, and a matching plain planned line is merged rather than duplicated. The\nline price is resolved by the pricing engine (tiers) unless you send a `unitPrice`\nthat differs from the base price, which is kept as a manual override. In counter-sales mode\nstock is consumed immediately. The write is attributed to the user who created the key.",
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "description": "Job id from `GET /api/v1/jobs`.",
            "required": true,
            "schema": {
              "type": "integer",
              "format": "int32"
            }
          }
        ],
        "requestBody": {
          "description": "The material line to add.",
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/AddJobItemApiRequest"
              }
            },
            "text/json": {
              "schema": {
                "$ref": "#/components/schemas/AddJobItemApiRequest"
              }
            },
            "application/*+json": {
              "schema": {
                "$ref": "#/components/schemas/AddJobItemApiRequest"
              }
            }
          }
        },
        "responses": {
          "401": {
            "description": "Unauthorized",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ProblemDetails"
                }
              }
            }
          },
          "403": {
            "description": "Forbidden",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ProblemDetails"
                }
              }
            }
          },
          "429": {
            "description": "Too Many Requests",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ProblemDetails"
                }
              }
            }
          },
          "200": {
            "description": "The updated job (same shape as `GET /api/v1/jobs/{id}`).",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/JobDetailDto"
                }
              }
            }
          },
          "404": {
            "description": "No job with this id in your organization.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ProblemDetails"
                }
              }
            }
          },
          "422": {
            "description": "The item template is unknown, not addable this way, or the job is cancelled (the detail says which).",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ProblemDetails"
                }
              }
            }
          },
          "400": {
            "description": "Malformed body or missing `Idempotency-Key`."
          }
        }
      }
    },
    "/api/v1/jobs/{id}/items/{itemId}": {
      "delete": {
        "tags": [
          "Jobs"
        ],
        "summary": "Removes a material line from a job (soft delete).",
        "description": "Requires `ManageJobs` and an `Idempotency-Key` header; no body. Only Planned\nlines (or counter-sales lines whose stock can be restored) can go; a line on an active\ntransfer or a finalized invoice can't — use Return Items for delivered goods. Totals are\nrecomputed. The write is attributed to the user who created the key.",
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "description": "Job id.",
            "required": true,
            "schema": {
              "type": "integer",
              "format": "int32"
            }
          },
          {
            "name": "itemId",
            "in": "path",
            "description": "Material line id.",
            "required": true,
            "schema": {
              "type": "integer",
              "format": "int32"
            }
          }
        ],
        "responses": {
          "401": {
            "description": "Unauthorized",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ProblemDetails"
                }
              }
            }
          },
          "403": {
            "description": "Forbidden",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ProblemDetails"
                }
              }
            }
          },
          "429": {
            "description": "Too Many Requests",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ProblemDetails"
                }
              }
            }
          },
          "200": {
            "description": "The updated job with the line removed.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/JobDetailDto"
                }
              }
            }
          },
          "404": {
            "description": "No such job in your organization, or the line is not one of its live material lines.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ProblemDetails"
                }
              }
            }
          },
          "422": {
            "description": "The job is cancelled, the line is billed, on an active transfer, or not deletable (the detail says which).",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ProblemDetails"
                }
              }
            }
          },
          "400": {
            "description": "Missing `Idempotency-Key`."
          }
        }
      },
      "patch": {
        "tags": [
          "Jobs"
        ],
        "summary": "Edits a material line's quantity, price and/or note.",
        "description": "Requires `ManageJobs` and an `Idempotency-Key` header. PATCH semantics: only the\nfields you send change. Quantity changes only apply while the line is Planned (or a\ncounter-sales line) and can't drop below the delivered amount; price edits are kept as a\nmanual override unless they equal the resolved price (send `clearPriceOverride` to\nrevert). A configured (add-on) line's price is changed through the app. Lines on a\nfinalized invoice change only through Return Items. Notes may be edited even on billed lines.",
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "description": "Job id from `GET /api/v1/jobs`.",
            "required": true,
            "schema": {
              "type": "integer",
              "format": "int32"
            }
          },
          {
            "name": "itemId",
            "in": "path",
            "description": "Material line id (`items[].id` of the job).",
            "required": true,
            "schema": {
              "type": "integer",
              "format": "int32"
            }
          }
        ],
        "requestBody": {
          "description": "The fields to change.",
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/UpdateJobItemApiRequest"
              }
            },
            "text/json": {
              "schema": {
                "$ref": "#/components/schemas/UpdateJobItemApiRequest"
              }
            },
            "application/*+json": {
              "schema": {
                "$ref": "#/components/schemas/UpdateJobItemApiRequest"
              }
            }
          }
        },
        "responses": {
          "401": {
            "description": "Unauthorized",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ProblemDetails"
                }
              }
            }
          },
          "403": {
            "description": "Forbidden",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ProblemDetails"
                }
              }
            }
          },
          "429": {
            "description": "Too Many Requests",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ProblemDetails"
                }
              }
            }
          },
          "200": {
            "description": "The updated job.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/JobDetailDto"
                }
              }
            }
          },
          "404": {
            "description": "No such job in your organization, or the line is not one of its live material lines.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ProblemDetails"
                }
              }
            }
          },
          "422": {
            "description": "The job is cancelled, the line is billed or configured, or the quantity/price is not allowed (the detail says which).",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ProblemDetails"
                }
              }
            }
          },
          "400": {
            "description": "Malformed body or missing `Idempotency-Key`."
          }
        }
      }
    },
    "/api/v1/jobs/{id}/labor": {
      "post": {
        "tags": [
          "Jobs"
        ],
        "summary": "Adds a labor line to a job.",
        "description": "Requires `ManageJobs` and an `Idempotency-Key` header. The customer rate is the\nlabor rate's hourly figure × `multiplier`; send `customerPrice` for $0-rate\nvariable-price labor. A cancelled job is read-only. The write is attributed to the key's creator.",
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "description": "Job id.",
            "required": true,
            "schema": {
              "type": "integer",
              "format": "int32"
            }
          }
        ],
        "requestBody": {
          "description": "The labor line to add.",
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/AddJobLaborApiRequest"
              }
            },
            "text/json": {
              "schema": {
                "$ref": "#/components/schemas/AddJobLaborApiRequest"
              }
            },
            "application/*+json": {
              "schema": {
                "$ref": "#/components/schemas/AddJobLaborApiRequest"
              }
            }
          }
        },
        "responses": {
          "401": {
            "description": "Unauthorized",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ProblemDetails"
                }
              }
            }
          },
          "403": {
            "description": "Forbidden",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ProblemDetails"
                }
              }
            }
          },
          "429": {
            "description": "Too Many Requests",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ProblemDetails"
                }
              }
            }
          },
          "200": {
            "description": "The updated job.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/JobDetailDto"
                }
              }
            }
          },
          "404": {
            "description": "No job with this id in your organization.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ProblemDetails"
                }
              }
            }
          },
          "422": {
            "description": "The labor rate or task template is unknown, or the job is cancelled (the detail says which).",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ProblemDetails"
                }
              }
            }
          },
          "400": {
            "description": "Malformed body or missing `Idempotency-Key`."
          }
        }
      }
    },
    "/api/v1/jobs/{id}/labor/{laborId}": {
      "delete": {
        "tags": [
          "Jobs"
        ],
        "summary": "Removes a labor line from a job (soft delete).",
        "description": "Requires `ManageJobs` and an `Idempotency-Key` header; no body. A line on a\nfinalized invoice can't be deleted (use Return Items); a cancelled job is read-only.\nTotals are recomputed. The write is attributed to the key's creator.",
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "description": "Job id.",
            "required": true,
            "schema": {
              "type": "integer",
              "format": "int32"
            }
          },
          {
            "name": "laborId",
            "in": "path",
            "description": "Labor line id.",
            "required": true,
            "schema": {
              "type": "integer",
              "format": "int32"
            }
          }
        ],
        "responses": {
          "401": {
            "description": "Unauthorized",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ProblemDetails"
                }
              }
            }
          },
          "403": {
            "description": "Forbidden",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ProblemDetails"
                }
              }
            }
          },
          "429": {
            "description": "Too Many Requests",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ProblemDetails"
                }
              }
            }
          },
          "200": {
            "description": "The updated job with the labor line removed.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/JobDetailDto"
                }
              }
            }
          },
          "404": {
            "description": "No such job in your organization, or the line is not one of its live labor lines.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ProblemDetails"
                }
              }
            }
          },
          "422": {
            "description": "The job is cancelled or the line is billed (the detail says which).",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ProblemDetails"
                }
              }
            }
          },
          "400": {
            "description": "Missing `Idempotency-Key`."
          }
        }
      },
      "patch": {
        "tags": [
          "Jobs"
        ],
        "summary": "Edits a labor line's rate, hours and/or note.",
        "description": "Requires `ManageJobs` and an `Idempotency-Key` header. PATCH semantics. Changing\nthe rate resets the customer rate and clears any per-job price; recording actual hours\n(> 0) marks the line completed. Lines on a finalized invoice change only through Return\nItems (notes may still be edited). A cancelled job is read-only.",
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "description": "Job id.",
            "required": true,
            "schema": {
              "type": "integer",
              "format": "int32"
            }
          },
          {
            "name": "laborId",
            "in": "path",
            "description": "Labor line id (`labor[].id` of the job).",
            "required": true,
            "schema": {
              "type": "integer",
              "format": "int32"
            }
          }
        ],
        "requestBody": {
          "description": "The fields to change.",
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/UpdateJobLaborApiRequest"
              }
            },
            "text/json": {
              "schema": {
                "$ref": "#/components/schemas/UpdateJobLaborApiRequest"
              }
            },
            "application/*+json": {
              "schema": {
                "$ref": "#/components/schemas/UpdateJobLaborApiRequest"
              }
            }
          }
        },
        "responses": {
          "401": {
            "description": "Unauthorized",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ProblemDetails"
                }
              }
            }
          },
          "403": {
            "description": "Forbidden",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ProblemDetails"
                }
              }
            }
          },
          "429": {
            "description": "Too Many Requests",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ProblemDetails"
                }
              }
            }
          },
          "200": {
            "description": "The updated job.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/JobDetailDto"
                }
              }
            }
          },
          "404": {
            "description": "No such job in your organization, or the line is not one of its live labor lines.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ProblemDetails"
                }
              }
            }
          },
          "422": {
            "description": "The job is cancelled, the line is billed, or the labor rate is unknown (the detail says which).",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ProblemDetails"
                }
              }
            }
          },
          "400": {
            "description": "Malformed body or missing `Idempotency-Key`."
          }
        }
      }
    },
    "/api/v1/jobs/{id}/photos": {
      "get": {
        "tags": [
          "Jobs"
        ],
        "summary": "Lists a job's photos, newest first.",
        "description": "Returns every `Photo` entry on the job's timeline — photos taken with the Flex field app's\ncamera and photos added in the web app or with `POST /jobs/{id}/photos`. Signatures are not\nphotos and are not listed (they stay in the job's `updates`). All photos come back in one\npage (`nextCursor` is always null). Also available as CSV (`Accept: text/csv` or\n`?format=csv`). Needs `ViewJobs`. Example:\n`curl https://app.flexonthejob.com/api/v1/jobs/42/photos -H \"Authorization: Bearer flx_live_…\"`.",
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "description": "Job id from a list response, e.g. `42`.",
            "required": true,
            "schema": {
              "type": "integer",
              "format": "int32"
            }
          },
          {
            "name": "format",
            "in": "query",
            "description": "Alternative to the Accept header: `csv` returns text/csv.",
            "schema": {
              "enum": [
                "csv"
              ],
              "type": "string"
            }
          }
        ],
        "responses": {
          "401": {
            "description": "Unauthorized",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ProblemDetails"
                }
              }
            }
          },
          "403": {
            "description": "Forbidden",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ProblemDetails"
                }
              }
            }
          },
          "429": {
            "description": "Too Many Requests",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ProblemDetails"
                }
              }
            }
          },
          "200": {
            "description": "The job's photos, newest first.",
            "headers": {
              "X-Next-Cursor": {
                "description": "text/csv only: the page's nextCursor. Absent on the last page. Pass it as ?after= (or follow the Link header) to fetch the next page.",
                "schema": {
                  "type": "string"
                }
              },
              "Link": {
                "description": "text/csv only, alongside X-Next-Cursor: <absolute URL of the next page>; rel=\"next\".",
                "schema": {
                  "type": "string"
                }
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/PagedResponseOfJobPhotoDto"
                }
              },
              "text/csv": {
                "schema": {
                  "type": "string",
                  "description": "CSV document: UTF-8 with BOM, CRLF rows, RFC 4180 quoting. The header row holds the JSON property names of one item (nested objects flattened as parent.child); one row per item of the page."
                }
              }
            }
          },
          "304": {
            "description": "Unchanged since the `If-None-Match` ETag."
          },
          "404": {
            "description": "No such job in this organization.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ProblemDetails"
                }
              }
            }
          }
        }
      },
      "post": {
        "tags": [
          "Jobs"
        ],
        "summary": "Attaches a photo to a job; it appears on the job's timeline as a `Photo` entry.",
        "description": "Send `multipart/form-data` with the image in a part named `file` and an optional\n`caption` part, e.g.\n`curl -X POST https://app.flexonthejob.com/api/v1/jobs/42/photos -H \"Authorization: Bearer flx_live_…\" -H \"Idempotency-Key: 5f0c…\" -F \"file=@roof.jpg\" -F \"caption=Rust on the bottom panel\"`.\nJPEG and PNG only (detected from the file's bytes), up to 10 MB. The entry is attributed to the\nkey's creator and is the same kind of entry the Flex field app creates from the phone camera:\nit shows as a thumbnail on the job's page in the web app, syncs to every device, and its image\nis included in the organization data export. Needs `ManageJobs` — the same permission as\nadding a job update in the app — and an `Idempotency-Key` header (a retry with the same\nkey and the same file replays the first response instead of adding a second photo).",
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "description": "Job id from a list response, e.g. `42`.",
            "required": true,
            "schema": {
              "type": "integer",
              "format": "int32"
            }
          }
        ],
        "requestBody": {
          "content": {
            "multipart/form-data": {
              "schema": {
                "type": "object",
                "properties": {
                  "file": {
                    "type": "string",
                    "description": "The photo, as a multipart file part named `file`. JPEG or PNG only — the type is read from\nthe file's leading bytes, not its name or declared content type. 1 byte to 10 MB (10,485,760 bytes).\nExample: `-F \"file=@roof.jpg\"`.",
                    "format": "binary"
                  },
                  "caption": {
                    "type": "string",
                    "description": "Optional caption shown on the job's timeline, up to 2000 characters. Omitted or blank →\n`Photo`. Example: `Rust on the bottom panel`."
                  }
                }
              },
              "encoding": {
                "file": {
                  "style": "form"
                },
                "caption": {
                  "style": "form"
                }
              }
            }
          }
        },
        "responses": {
          "401": {
            "description": "Unauthorized",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ProblemDetails"
                }
              }
            }
          },
          "403": {
            "description": "Forbidden",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ProblemDetails"
                }
              }
            }
          },
          "429": {
            "description": "Too Many Requests",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ProblemDetails"
                }
              }
            }
          },
          "201": {
            "description": "The created timeline entry (`type` = `Photo`, `attachmentName` set). `Location` points at the job.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/JobNoteDto"
                }
              }
            }
          },
          "400": {
            "description": "No `file` part, an empty file, a caption over 2000 characters, or a missing `Idempotency-Key`.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ValidationProblemDetails"
                }
              }
            }
          },
          "404": {
            "description": "No such job in this organization.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ProblemDetails"
                }
              }
            }
          },
          "409": {
            "description": "The key's creator is unknown on this server or deactivated, or this server does not store photos (the demo sandbox).",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ProblemDetails"
                }
              }
            }
          },
          "413": {
            "description": "The file is larger than 10 MB.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ProblemDetails"
                }
              }
            }
          },
          "415": {
            "description": "The file is not a JPEG or PNG image.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ProblemDetails"
                }
              }
            }
          }
        }
      }
    },
    "/api/v1/jobs/{id}/photos/{photoId}": {
      "delete": {
        "tags": [
          "Jobs"
        ],
        "summary": "Deletes a photo from a job.",
        "description": "Removes a `Photo` entry from the job's timeline and deletes its image. `photoId` is the\n`id` from `GET /jobs/{id}/photos`, the `id` of a `Photo` entry in the job's\n`updates`, or the `id` returned by `POST /jobs/{id}/photos`. Only photos can be\ndeleted: signatures, notes and status changes are part of the job's record and answer 404. The\nremoval syncs to every device running a current Flex version (older installs keep the entry\nuntil they update), and an internal note \"Photo removed: …\" attributed to\nthe key's creator is added to the timeline. Needs `ManageJobs`. Not idempotent-keyed: deleting\nthe same photo again answers 404. Example:\n`curl -X DELETE https://app.flexonthejob.com/api/v1/jobs/42/photos/77 -H \"Authorization: Bearer flx_live_…\"`.",
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "description": "Job id, e.g. `42`.",
            "required": true,
            "schema": {
              "type": "integer",
              "format": "int32"
            }
          },
          {
            "name": "photoId",
            "in": "path",
            "description": "Photo id, e.g. `77`.",
            "required": true,
            "schema": {
              "type": "integer",
              "format": "int32"
            }
          }
        ],
        "responses": {
          "401": {
            "description": "Unauthorized",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ProblemDetails"
                }
              }
            }
          },
          "403": {
            "description": "Forbidden",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ProblemDetails"
                }
              }
            }
          },
          "429": {
            "description": "Too Many Requests",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ProblemDetails"
                }
              }
            }
          },
          "204": {
            "description": "Deleted."
          },
          "404": {
            "description": "No such photo on this job in this organization (including signatures and other non-photo entries, and a photo already deleted).",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ProblemDetails"
                }
              }
            }
          },
          "409": {
            "description": "The key's creator is unknown on this server or deactivated.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ProblemDetails"
                }
              }
            }
          }
        }
      }
    },
    "/api/v1/jobs/{id}/photos/{photoId}/content": {
      "get": {
        "tags": [
          "Jobs"
        ],
        "summary": "Downloads a job photo's image.",
        "description": "Streams the image bytes with `Content-Type``image/jpeg` or `image/png` (up to 10 MB).\n`photoId` is the `id` from `GET /jobs/{id}/photos` (or of a `Photo` entry in the\njob's `updates`). A photo taken on a phone becomes downloadable once that phone has synced\nits upload; until then this answers 503. Needs `ViewJobs`. Example:\n`curl https://app.flexonthejob.com/api/v1/jobs/42/photos/77/content -H \"Authorization: Bearer flx_live_…\" -o roof.jpg`.",
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "description": "Job id, e.g. `42`.",
            "required": true,
            "schema": {
              "type": "integer",
              "format": "int32"
            }
          },
          {
            "name": "photoId",
            "in": "path",
            "description": "Photo id from `GET /jobs/{id}/photos`, e.g. `77`.",
            "required": true,
            "schema": {
              "type": "integer",
              "format": "int32"
            }
          }
        ],
        "responses": {
          "401": {
            "description": "Unauthorized",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ProblemDetails"
                }
              }
            }
          },
          "403": {
            "description": "Forbidden",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ProblemDetails"
                }
              }
            }
          },
          "429": {
            "description": "Too Many Requests",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ProblemDetails"
                }
              }
            }
          },
          "200": {
            "description": "The image bytes.",
            "content": {
              "image/jpeg": {
                "schema": {
                  "type": "string",
                  "format": "binary"
                }
              },
              "image/png": {
                "schema": {
                  "type": "string",
                  "format": "binary"
                }
              }
            }
          },
          "404": {
            "description": "No such photo on this job in this organization.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ProblemDetails"
                }
              }
            }
          },
          "503": {
            "description": "The image has not reached the server yet (a phone that has not synced) or storage is unreachable; retry later.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ProblemDetails"
                }
              }
            }
          }
        }
      }
    },
    "/api/v1/jobs/{id}/reschedule": {
      "post": {
        "tags": [
          "Jobs"
        ],
        "summary": "Moves a scheduled job to a new start time (and optionally a new end).",
        "description": "Requires the Scheduling module. Only jobs in `Scheduled` status — or draft /\nestimated jobs that already carry a scheduled date — can be rescheduled; use\n`POST /jobs/{id}/status` to schedule an unscheduled job first. Times are the\norganization's local wall clock (`yyyy-MM-ddTHH:mm:ss`); a trailing `Z`\nis converted to local time. Requires an `Idempotency-Key` header.",
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "description": "Job id from a list response.",
            "required": true,
            "schema": {
              "type": "integer",
              "format": "int32"
            }
          }
        ],
        "requestBody": {
          "description": "New start, optional end and note.",
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/RescheduleJobRequest"
              }
            },
            "text/json": {
              "schema": {
                "$ref": "#/components/schemas/RescheduleJobRequest"
              }
            },
            "application/*+json": {
              "schema": {
                "$ref": "#/components/schemas/RescheduleJobRequest"
              }
            }
          }
        },
        "responses": {
          "401": {
            "description": "Unauthorized",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ProblemDetails"
                }
              }
            }
          },
          "403": {
            "description": "Scheduling module not active for this organization.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ProblemDetails"
                }
              }
            }
          },
          "429": {
            "description": "Too Many Requests",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ProblemDetails"
                }
              }
            }
          },
          "200": {
            "description": "Job id and confirmation message.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiOperationResponse"
                }
              }
            }
          },
          "400": {
            "description": "`start` missing, `end` not after `start`, or missing `Idempotency-Key`.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ValidationProblemDetails"
                }
              }
            }
          },
          "404": {
            "description": "No such job in this organization.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ProblemDetails"
                }
              }
            }
          },
          "409": {
            "description": "The job's status does not allow rescheduling, or the key's creator is unknown on this server.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ProblemDetails"
                }
              }
            }
          }
        }
      }
    },
    "/api/v1/jobs/{id}/status": {
      "post": {
        "tags": [
          "Jobs"
        ],
        "summary": "Moves a job to another status.",
        "description": "Name the target with `statusId` (one of the organization's status definitions)\nor `statusKey` (a status definition name, or a system key such as\n`InProgress` / `Completed`), not both. No transition rules are enforced —\nany status may follow any other, exactly as in the UI. Requires an\n`Idempotency-Key` header.",
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "description": "Job id from a list response.",
            "required": true,
            "schema": {
              "type": "integer",
              "format": "int32"
            }
          }
        ],
        "requestBody": {
          "description": "Target status and optional note.",
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/SetJobStatusRequest"
              }
            },
            "text/json": {
              "schema": {
                "$ref": "#/components/schemas/SetJobStatusRequest"
              }
            },
            "application/*+json": {
              "schema": {
                "$ref": "#/components/schemas/SetJobStatusRequest"
              }
            }
          }
        },
        "responses": {
          "401": {
            "description": "Unauthorized",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ProblemDetails"
                }
              }
            }
          },
          "403": {
            "description": "Forbidden",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ProblemDetails"
                }
              }
            }
          },
          "429": {
            "description": "Too Many Requests",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ProblemDetails"
                }
              }
            }
          },
          "200": {
            "description": "Job id and confirmation message.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiOperationResponse"
                }
              }
            }
          },
          "400": {
            "description": "Neither or both of `statusId` / `statusKey`, or missing `Idempotency-Key`.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ValidationProblemDetails"
                }
              }
            }
          },
          "404": {
            "description": "No such job in this organization.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ProblemDetails"
                }
              }
            }
          },
          "409": {
            "description": "The key's creator is unknown on this server, or the service refused the change.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ProblemDetails"
                }
              }
            }
          },
          "422": {
            "description": "Unknown status for this organization.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ValidationProblemDetails"
                }
              }
            }
          }
        }
      }
    },
    "/api/v1/jobs/{id}/transfer": {
      "post": {
        "tags": [
          "Transfers"
        ],
        "summary": "Creates the job-site transfer for a job (the Job Details \"Create Transfer\" button).",
        "description": "Requires `ManageTransfers` and an `Idempotency-Key` header; no body. Same rules as\nthe button, which is only offered when the organization is not in counter-sales mode and the\njob is `Estimated`, `Scheduled` or `InProgress`: stock is auto-selected for every `Planned` material line that still needs\ndelivery, the transfer is created `Pending` with the job's site location as its\ndestination, the job lines become `Reserved` and a \"Transfer created\" entry is added\nto the job history. When the organization allows negative inventory a shortfall becomes a\nstock-less line instead of an error. Returns the new transfer with its lines; its\n`Location` header is `/api/v1/transfers/{transferId}`.",
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "description": "Job id from `/api/v1/jobs`.",
            "required": true,
            "schema": {
              "type": "integer",
              "format": "int32"
            }
          }
        ],
        "responses": {
          "401": {
            "description": "Unauthorized",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ProblemDetails"
                }
              }
            }
          },
          "403": {
            "description": "Forbidden",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ProblemDetails"
                }
              }
            }
          },
          "429": {
            "description": "Too Many Requests",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ProblemDetails"
                }
              }
            }
          },
          "201": {
            "description": "The created transfer.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/TransferDetailDto"
                }
              }
            }
          },
          "400": {
            "description": "Missing `Idempotency-Key`.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ValidationProblemDetails"
                }
              }
            }
          },
          "404": {
            "description": "No such job in this organization.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ProblemDetails"
                }
              }
            }
          },
          "409": {
            "description": "The organization is in counter-sales mode, the job is not `Estimated`/`Scheduled`/`InProgress`, the job has no job site location, no line needs transferring, no stock is available, or the key's creator is unknown on this server (detail says which).",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ProblemDetails"
                }
              }
            }
          },
          "500": {
            "description": "The transfer could not be saved. Retry with the same `Idempotency-Key`.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ProblemDetails"
                }
              }
            }
          }
        }
      }
    },
    "/api/v1/labor-rates": {
      "get": {
        "tags": [
          "LaborRates"
        ],
        "summary": "Lists the organization's labor rates, one page at a time.",
        "description": "Requires `ViewJobs`. Retired rates are excluded unless `includeInactive=true`.\nCursor-paginated: pass the last rate's `syncId` as `after`. Example:\n`GET /api/v1/labor-rates?search=electrician`.\nAlso available as CSV (`Accept: text/csv` or `?format=csv`); paging cursor in `X-Next-Cursor`.",
        "parameters": [
          {
            "name": "search",
            "in": "query",
            "description": "Case-insensitive match against name and description. Example: `electrician`.",
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "includeInactive",
            "in": "query",
            "description": "Also return retired (inactive) rates (default `false`).",
            "schema": {
              "type": "boolean"
            }
          },
          {
            "name": "updatedSince",
            "in": "query",
            "description": "Only rates created or updated at/after this instant (ISO-8601 UTC, e.g. `2026-09-01T00:00:00Z`).",
            "schema": {
              "type": "string",
              "format": "date-time"
            }
          },
          {
            "name": "after",
            "in": "query",
            "description": "Cursor from the previous page's `nextCursor` (a labor-rate SyncId). Omit for the first page.",
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "limit",
            "in": "query",
            "description": "Page size, 1..200 (default 50).",
            "schema": {
              "type": "integer",
              "format": "int32"
            }
          },
          {
            "name": "format",
            "in": "query",
            "description": "Alternative to the Accept header: `csv` returns text/csv.",
            "schema": {
              "enum": [
                "csv"
              ],
              "type": "string"
            }
          }
        ],
        "responses": {
          "401": {
            "description": "Unauthorized",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ProblemDetails"
                }
              }
            }
          },
          "403": {
            "description": "Forbidden",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ProblemDetails"
                }
              }
            }
          },
          "429": {
            "description": "Too Many Requests",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ProblemDetails"
                }
              }
            }
          },
          "200": {
            "description": "One page of labor rates and the cursor for the next page (null on the last page).",
            "headers": {
              "X-Next-Cursor": {
                "description": "text/csv only: the page's nextCursor. Absent on the last page. Pass it as ?after= (or follow the Link header) to fetch the next page.",
                "schema": {
                  "type": "string"
                }
              },
              "Link": {
                "description": "text/csv only, alongside X-Next-Cursor: <absolute URL of the next page>; rel=\"next\".",
                "schema": {
                  "type": "string"
                }
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/PagedResponseOfLaborRateDto"
                }
              },
              "text/csv": {
                "schema": {
                  "type": "string",
                  "description": "CSV document: UTF-8 with BOM, CRLF rows, RFC 4180 quoting. The header row holds the JSON property names of one item (nested objects flattened as parent.child); one row per item of the page."
                }
              }
            }
          },
          "400": {
            "description": "Unusable `after` cursor.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ValidationProblemDetails"
                }
              }
            }
          }
        }
      }
    },
    "/api/v1/labor-rates/{id}": {
      "get": {
        "tags": [
          "LaborRates"
        ],
        "summary": "Returns one labor rate.",
        "description": "Requires `ViewJobs`. `id` may be the numeric id or the `syncId`. Sends a weak `ETag`.",
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "description": "Numeric id or SyncId. Example: `4`.",
            "required": true,
            "schema": {
              "type": "string"
            }
          }
        ],
        "responses": {
          "401": {
            "description": "Unauthorized",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ProblemDetails"
                }
              }
            }
          },
          "403": {
            "description": "Forbidden",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ProblemDetails"
                }
              }
            }
          },
          "429": {
            "description": "Too Many Requests",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ProblemDetails"
                }
              }
            }
          },
          "200": {
            "description": "The labor rate.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/LaborRateDto"
                }
              }
            }
          },
          "304": {
            "description": "Unchanged since the supplied ETag."
          },
          "404": {
            "description": "No such labor rate in this organization.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ProblemDetails"
                }
              }
            }
          }
        }
      }
    },
    "/api/v1/locations": {
      "get": {
        "tags": [
          "Locations"
        ],
        "summary": "Lists stock locations (warehouses, shelves, vehicles; job sites on request).",
        "description": "Requires `ViewInventory`. Job sites are excluded by default because one exists per\njob and stock there is never \"on hand\"; pass `includeJobSites=true` to see them.\nCursor-paginated: pass the last location's `syncId` as `after`. Example:\n`GET /api/v1/locations?search=van`.\nAlso available as CSV (`Accept: text/csv` or `?format=csv`); paging cursor in `X-Next-Cursor`.",
        "parameters": [
          {
            "name": "search",
            "in": "query",
            "description": "Case-insensitive match against name and address.",
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "includeJobSites",
            "in": "query",
            "description": "Also return job-site locations (default `false`).",
            "schema": {
              "type": "boolean"
            }
          },
          {
            "name": "includeInactive",
            "in": "query",
            "description": "Also return locations whose status is not `Active` (default `false`).",
            "schema": {
              "type": "boolean"
            }
          },
          {
            "name": "parentLocationId",
            "in": "query",
            "description": "Only direct children of this location; `0` for top-level locations only.",
            "schema": {
              "type": "integer",
              "format": "int32"
            }
          },
          {
            "name": "updatedSince",
            "in": "query",
            "description": "Only locations created or updated at/after this UTC instant.",
            "schema": {
              "type": "string",
              "format": "date-time"
            }
          },
          {
            "name": "after",
            "in": "query",
            "description": "SyncId of the last location of the previous page.",
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "limit",
            "in": "query",
            "description": "Page size, 1..200 (default 50).",
            "schema": {
              "type": "integer",
              "format": "int32"
            }
          },
          {
            "name": "format",
            "in": "query",
            "description": "Alternative to the Accept header: `csv` returns text/csv.",
            "schema": {
              "enum": [
                "csv"
              ],
              "type": "string"
            }
          }
        ],
        "responses": {
          "401": {
            "description": "Unauthorized",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ProblemDetails"
                }
              }
            }
          },
          "403": {
            "description": "Forbidden",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ProblemDetails"
                }
              }
            }
          },
          "429": {
            "description": "Too Many Requests",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ProblemDetails"
                }
              }
            }
          },
          "200": {
            "description": "One page of locations.",
            "headers": {
              "X-Next-Cursor": {
                "description": "text/csv only: the page's nextCursor. Absent on the last page. Pass it as ?after= (or follow the Link header) to fetch the next page.",
                "schema": {
                  "type": "string"
                }
              },
              "Link": {
                "description": "text/csv only, alongside X-Next-Cursor: <absolute URL of the next page>; rel=\"next\".",
                "schema": {
                  "type": "string"
                }
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/PagedResponseOfLocationSummary"
                }
              },
              "text/csv": {
                "schema": {
                  "type": "string",
                  "description": "CSV document: UTF-8 with BOM, CRLF rows, RFC 4180 quoting. The header row holds the JSON property names of one item (nested objects flattened as parent.child); one row per item of the page."
                }
              }
            }
          },
          "400": {
            "description": "Unknown `after` cursor.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ValidationProblemDetails"
                }
              }
            }
          }
        }
      }
    },
    "/api/v1/locations/{id}": {
      "get": {
        "tags": [
          "Locations"
        ],
        "summary": "Returns one stock location.",
        "description": "Requires `ViewInventory`. `id` may be the numeric id or the `syncId`. Sends a weak `ETag`.",
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "description": "Numeric id or SyncId.",
            "required": true,
            "schema": {
              "type": "string"
            }
          }
        ],
        "responses": {
          "401": {
            "description": "Unauthorized",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ProblemDetails"
                }
              }
            }
          },
          "403": {
            "description": "Forbidden",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ProblemDetails"
                }
              }
            }
          },
          "429": {
            "description": "Too Many Requests",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ProblemDetails"
                }
              }
            }
          },
          "200": {
            "description": "The location.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/LocationSummary"
                }
              }
            }
          },
          "304": {
            "description": "Unchanged since the supplied ETag."
          },
          "404": {
            "description": "No such location in the calling organization.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ProblemDetails"
                }
              }
            }
          }
        }
      }
    },
    "/api/v1/me": {
      "get": {
        "tags": [
          "Me"
        ],
        "summary": "Returns the key's organization and the permissions it currently holds.",
        "description": "Requires only a valid API key (any permission). Integration platforms call this to validate a\nconnection and label it. Example: `GET /api/v1/me`.",
        "responses": {
          "401": {
            "description": "Unauthorized",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ProblemDetails"
                }
              }
            }
          },
          "403": {
            "description": "Forbidden",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ProblemDetails"
                }
              }
            }
          },
          "429": {
            "description": "Too Many Requests",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ProblemDetails"
                }
              }
            }
          },
          "200": {
            "description": "The authenticated key's identity.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/MeDto"
                }
              }
            }
          }
        }
      }
    },
    "/api/v1/modules": {
      "get": {
        "tags": [
          "Modules"
        ],
        "summary": "Lists the calling organization's modules and the presented key's permissions.",
        "description": "Requires a valid API key but no specific permission. Module gating on other\nendpoints follows the `active` flag returned here.",
        "responses": {
          "401": {
            "description": "Unauthorized",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ProblemDetails"
                }
              }
            }
          },
          "403": {
            "description": "Forbidden",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ProblemDetails"
                }
              }
            }
          },
          "429": {
            "description": "Too Many Requests",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ProblemDetails"
                }
              }
            }
          },
          "200": {
            "description": "Modules with activation state, plus the key's permissions.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ModulesResponse"
                }
              }
            }
          }
        }
      }
    },
    "/api/v1/purchase-orders": {
      "get": {
        "tags": [
          "PurchaseOrders"
        ],
        "summary": "Lists the organization's purchase orders, oldest first, one page at a time.",
        "description": "Requires `ViewPurchaseOrders`. Deleted orders are never returned. Pagination: pass the\nprevious response's `nextCursor` as `after`; `limit` defaults to 50 and is\ncapped at 200. All filters combine with AND. Example:\n`GET /api/v1/purchase-orders?status=Delivered&vendorId=7` lists what is waiting to be\nreceived from vendor 7. Response carries a weak `ETag`; resend it as\n`If-None-Match` to get 304 when nothing changed.\nAlso available as CSV (`Accept: text/csv` or `?format=csv`); paging cursor in `X-Next-Cursor`.",
        "parameters": [
          {
            "name": "status",
            "in": "query",
            "description": "Order status name to match exactly (case-insensitive): `Draft`, `Ordered`, `Shipped`, `PartialShipped`, `Delivered`, `PartialReceived`, `Received` or `Cancelled`. Example: `status=PartialReceived`.",
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "vendorId",
            "in": "query",
            "description": "Only orders placed with this vendor id. Example: `vendorId=7`.",
            "schema": {
              "type": "integer",
              "format": "int32"
            }
          },
          {
            "name": "updatedSince",
            "in": "query",
            "description": "Only orders created or modified (a receipt counts) at or after this instant (ISO-8601 UTC, e.g. `2026-09-01T00:00:00Z`). Use it to poll for changes.",
            "schema": {
              "type": "string",
              "format": "date-time"
            }
          },
          {
            "name": "after",
            "in": "query",
            "description": "Cursor from the previous page's `nextCursor` (a purchase-order SyncId). Omit for the first page.",
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "limit",
            "in": "query",
            "description": "Page size, 1–200 (default 50).",
            "schema": {
              "type": "integer",
              "format": "int32"
            }
          },
          {
            "name": "format",
            "in": "query",
            "description": "Alternative to the Accept header: `csv` returns text/csv.",
            "schema": {
              "enum": [
                "csv"
              ],
              "type": "string"
            }
          }
        ],
        "responses": {
          "401": {
            "description": "Unauthorized",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ProblemDetails"
                }
              }
            }
          },
          "403": {
            "description": "Forbidden",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ProblemDetails"
                }
              }
            }
          },
          "429": {
            "description": "Too Many Requests",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ProblemDetails"
                }
              }
            }
          },
          "200": {
            "description": "A page of purchase orders and the cursor for the next page (null on the last page).",
            "headers": {
              "X-Next-Cursor": {
                "description": "text/csv only: the page's nextCursor. Absent on the last page. Pass it as ?after= (or follow the Link header) to fetch the next page.",
                "schema": {
                  "type": "string"
                }
              },
              "Link": {
                "description": "text/csv only, alongside X-Next-Cursor: <absolute URL of the next page>; rel=\"next\".",
                "schema": {
                  "type": "string"
                }
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/PagedResponseOfPurchaseOrderSummaryDto"
                }
              },
              "text/csv": {
                "schema": {
                  "type": "string",
                  "description": "CSV document: UTF-8 with BOM, CRLF rows, RFC 4180 quoting. The header row holds the JSON property names of one item (nested objects flattened as parent.child); one row per item of the page."
                }
              }
            }
          },
          "304": {
            "description": "Unchanged since the `If-None-Match` ETag."
          },
          "400": {
            "description": "Unknown `status` or unusable `after` cursor (field listed in `errors`).",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ValidationProblemDetails"
                }
              }
            }
          }
        }
      },
      "post": {
        "tags": [
          "PurchaseOrders"
        ],
        "summary": "Creates a purchase order in `Draft`, optionally with its lines.",
        "description": "Requires `ManagePurchaseOrders` and an `Idempotency-Key` header. Same as \"New Purchase\nOrder\" in the app: the vendor must be active in your organization (or omit `vendorId` and\nname a one-off vendor), the receiving location must be an active top-level location that is not\na job site, and each line names one of your items or bundles. The order and its lines are\nwritten together or not at all. A new order is a `Draft`: move it along with\n`/mark-ordered`, `/mark-shipped` and `/mark-delivered`, then `/receive` it.\nAttributed to the key's creator. Example:\n`{ \"vendorId\": 7, \"receivingLocationId\": 1, \"lines\": [ { \"itemId\": 12, \"quantityOrdered\": 24, \"unitPrice\": 3.10 } ] }`.",
        "requestBody": {
          "description": "The order and its lines.",
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/CreatePurchaseOrderApiRequest"
              }
            },
            "text/json": {
              "schema": {
                "$ref": "#/components/schemas/CreatePurchaseOrderApiRequest"
              }
            },
            "application/*+json": {
              "schema": {
                "$ref": "#/components/schemas/CreatePurchaseOrderApiRequest"
              }
            }
          }
        },
        "responses": {
          "401": {
            "description": "Unauthorized",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ProblemDetails"
                }
              }
            }
          },
          "403": {
            "description": "Forbidden",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ProblemDetails"
                }
              }
            }
          },
          "429": {
            "description": "Too Many Requests",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ProblemDetails"
                }
              }
            }
          },
          "201": {
            "description": "Created; `Location` points at the order, body as `GET /api/v1/purchase-orders/{id}`.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/PurchaseOrderDetailDto"
                }
              }
            }
          },
          "400": {
            "description": "Malformed body or missing `Idempotency-Key`.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ValidationProblemDetails"
                }
              }
            }
          },
          "409": {
            "description": "The key's creator is unknown on this server.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ProblemDetails"
                }
              }
            }
          },
          "422": {
            "description": "Unknown or inactive vendor, unusable receiving location, or a bad line (e.g. `lines[0].itemId`: no active item with this id); the field is listed in `errors`. Nothing was written.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ValidationProblemDetails"
                }
              }
            }
          }
        }
      }
    },
    "/api/v1/purchase-orders/{id}": {
      "get": {
        "tags": [
          "PurchaseOrders"
        ],
        "summary": "Returns one purchase order with its lines and their ordered, shipped and received quantities.",
        "description": "Requires `ViewPurchaseOrders`. Each line's `id` is the `lineId` to pass to\n`POST /api/v1/purchase-orders/{id}/receive` and its `quantityRemaining` is the most\nthat call accepts. Response carries a weak `ETag`; resend it as\n`If-None-Match` to get 304 when nothing changed.",
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "description": "Purchase-order id from the list endpoint. Example: `412`.",
            "required": true,
            "schema": {
              "type": "integer",
              "format": "int32"
            }
          }
        ],
        "responses": {
          "401": {
            "description": "Unauthorized",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ProblemDetails"
                }
              }
            }
          },
          "403": {
            "description": "Forbidden",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ProblemDetails"
                }
              }
            }
          },
          "429": {
            "description": "Too Many Requests",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ProblemDetails"
                }
              }
            }
          },
          "200": {
            "description": "The purchase order.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/PurchaseOrderDetailDto"
                }
              }
            }
          },
          "304": {
            "description": "Unchanged since the `If-None-Match` ETag."
          },
          "404": {
            "description": "No such purchase order in this organization (or it is deleted).",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ProblemDetails"
                }
              }
            }
          }
        }
      }
    },
    "/api/v1/purchase-orders/{id}/cancel": {
      "post": {
        "tags": [
          "PurchaseOrders"
        ],
        "summary": "Cancels a purchase order and its shipments.",
        "description": "Requires `ManagePurchaseOrders` and an `Idempotency-Key`. The app's \"Cancel Order\": refused once anything has been received.",
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "description": "Purchase-order id. Example: `412`.",
            "required": true,
            "schema": {
              "type": "integer",
              "format": "int32"
            }
          }
        ],
        "responses": {
          "401": {
            "description": "Unauthorized",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ProblemDetails"
                }
              }
            }
          },
          "403": {
            "description": "Forbidden",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ProblemDetails"
                }
              }
            }
          },
          "429": {
            "description": "Too Many Requests",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ProblemDetails"
                }
              }
            }
          },
          "200": {
            "description": "The order, now `Cancelled`.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/PurchaseOrderDetailDto"
                }
              }
            }
          },
          "404": {
            "description": "No such purchase order in this organization.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ProblemDetails"
                }
              }
            }
          },
          "409": {
            "description": "Already received (in part or full) or already cancelled, or the key's creator is unknown on this server.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ProblemDetails"
                }
              }
            }
          }
        }
      }
    },
    "/api/v1/purchase-orders/{id}/lines": {
      "post": {
        "tags": [
          "PurchaseOrders"
        ],
        "summary": "Adds a line to a purchase order.",
        "description": "Requires `ManagePurchaseOrders` and an `Idempotency-Key` header. Allowed until the order\nis fully received or cancelled (the app allows the same). Example:\n`POST /api/v1/purchase-orders/412/lines { \"itemId\": 12, \"quantityOrdered\": 6 }`.",
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "description": "Purchase-order id. Example: `412`.",
            "required": true,
            "schema": {
              "type": "integer",
              "format": "int32"
            }
          }
        ],
        "requestBody": {
          "description": "The line.",
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/PurchaseOrderLineApiRequest"
              }
            },
            "text/json": {
              "schema": {
                "$ref": "#/components/schemas/PurchaseOrderLineApiRequest"
              }
            },
            "application/*+json": {
              "schema": {
                "$ref": "#/components/schemas/PurchaseOrderLineApiRequest"
              }
            }
          }
        },
        "responses": {
          "401": {
            "description": "Unauthorized",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ProblemDetails"
                }
              }
            }
          },
          "403": {
            "description": "Forbidden",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ProblemDetails"
                }
              }
            }
          },
          "429": {
            "description": "Too Many Requests",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ProblemDetails"
                }
              }
            }
          },
          "200": {
            "description": "The purchase order with the new line.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/PurchaseOrderDetailDto"
                }
              }
            }
          },
          "400": {
            "description": "Malformed body or missing `Idempotency-Key`.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ValidationProblemDetails"
                }
              }
            }
          },
          "404": {
            "description": "No such purchase order in this organization.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ProblemDetails"
                }
              }
            }
          },
          "409": {
            "description": "The order is `Received` or `Cancelled` (detail: `Cannot add items to this purchase order in its current status`), or the key's creator is unknown on this server.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ProblemDetails"
                }
              }
            }
          },
          "422": {
            "description": "The line is unacceptable; the field is listed in `errors`.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ValidationProblemDetails"
                }
              }
            }
          }
        }
      }
    },
    "/api/v1/purchase-orders/{id}/mark-delivered": {
      "post": {
        "tags": [
          "PurchaseOrders"
        ],
        "summary": "Marks a shipped purchase order as delivered to the receiving location (`Delivered`).",
        "description": "Requires `ManagePurchaseOrders` and an `Idempotency-Key`. The app's \"Mark All Delivered\": every shipped shipment is delivered. The order can then be received with `POST /api/v1/purchase-orders/{id}/receive`.",
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "description": "Purchase-order id. Example: `412`.",
            "required": true,
            "schema": {
              "type": "integer",
              "format": "int32"
            }
          }
        ],
        "responses": {
          "401": {
            "description": "Unauthorized",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ProblemDetails"
                }
              }
            }
          },
          "403": {
            "description": "Forbidden",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ProblemDetails"
                }
              }
            }
          },
          "429": {
            "description": "Too Many Requests",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ProblemDetails"
                }
              }
            }
          },
          "200": {
            "description": "The order, now `Delivered`.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/PurchaseOrderDetailDto"
                }
              }
            }
          },
          "404": {
            "description": "No such purchase order in this organization.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ProblemDetails"
                }
              }
            }
          },
          "409": {
            "description": "The order is not `Shipped` or `PartialShipped`, or the key's creator is unknown on this server.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ProblemDetails"
                }
              }
            }
          }
        }
      }
    },
    "/api/v1/purchase-orders/{id}/mark-ordered": {
      "post": {
        "tags": [
          "PurchaseOrders"
        ],
        "summary": "Marks a `Draft` purchase order as placed with the vendor (`Ordered`).",
        "description": "Requires `ManagePurchaseOrders` and an `Idempotency-Key`. The app's \"Mark as Ordered\": the order must be a `Draft` with at least one line.",
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "description": "Purchase-order id. Example: `412`.",
            "required": true,
            "schema": {
              "type": "integer",
              "format": "int32"
            }
          }
        ],
        "responses": {
          "401": {
            "description": "Unauthorized",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ProblemDetails"
                }
              }
            }
          },
          "403": {
            "description": "Forbidden",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ProblemDetails"
                }
              }
            }
          },
          "429": {
            "description": "Too Many Requests",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ProblemDetails"
                }
              }
            }
          },
          "200": {
            "description": "The order, now `Ordered`.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/PurchaseOrderDetailDto"
                }
              }
            }
          },
          "404": {
            "description": "No such purchase order in this organization.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ProblemDetails"
                }
              }
            }
          },
          "409": {
            "description": "Not a `Draft`, or it has no lines (detail says which), or the key's creator is unknown on this server.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ProblemDetails"
                }
              }
            }
          }
        }
      }
    },
    "/api/v1/purchase-orders/{id}/mark-shipped": {
      "post": {
        "tags": [
          "PurchaseOrders"
        ],
        "summary": "Marks an `Ordered` purchase order as shipped by the vendor (`Shipped`).",
        "description": "Requires `ManagePurchaseOrders` and an `Idempotency-Key`. The app's \"Mark All Shipped\": one shipment is created holding every line not yet on a shipment, with every unit shipped.",
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "description": "Purchase-order id. Example: `412`.",
            "required": true,
            "schema": {
              "type": "integer",
              "format": "int32"
            }
          }
        ],
        "responses": {
          "401": {
            "description": "Unauthorized",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ProblemDetails"
                }
              }
            }
          },
          "403": {
            "description": "Forbidden",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ProblemDetails"
                }
              }
            }
          },
          "429": {
            "description": "Too Many Requests",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ProblemDetails"
                }
              }
            }
          },
          "200": {
            "description": "The order, now `Shipped`.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/PurchaseOrderDetailDto"
                }
              }
            }
          },
          "404": {
            "description": "No such purchase order in this organization.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ProblemDetails"
                }
              }
            }
          },
          "409": {
            "description": "The order is not `Ordered`, or the key's creator is unknown on this server.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ProblemDetails"
                }
              }
            }
          }
        }
      }
    },
    "/api/v1/purchase-orders/{id}/receive": {
      "post": {
        "tags": [
          "PurchaseOrders"
        ],
        "summary": "Receives purchase-order lines into stock (the app's receiving panel).",
        "description": "Requires `ManagePurchaseOrders` and an `Idempotency-Key` header. Body:\n`{ \"lines\": [ { \"lineId\": 1207, \"quantity\": 4 }, { \"lineId\": 1208, \"quantity\": 1, \"locationId\": 11 } ] }`.\nSame code path and rules as receiving in the app: only an order whose `status` is\n`Delivered` or `PartialReceived` can be received (409 otherwise, e.g. for a\n`Cancelled`, `Ordered` or fully `Received` order); every line is checked before anything\nis written (one bad line and nothing is received); a quantity above the line's\n`quantityRemaining` is refused (over-receipt, also when a concurrent receipt of the\nsame line got there first); `locationId` defaults to the order's\n`receivingLocationId`. An item line creates one stock item per unit at that location,\nwith the template's properties, barcodes and category, a `Cost` property from the\nline's `unitPrice`, and `Shipping Cost` / `Total Cost` properties when the\nline is on a shipment with a shipping cost; a bundle line creates one bundle per unit with\nits items. The order `status` is then recomputed from its shipments: it becomes\n`Received` when every unit is received and every shipment is delivered, otherwise\n`PartialReceived`. The receipt is attributed to the key's\ncreator. Returns the order as `GET /api/v1/purchase-orders/{id}` does.",
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "description": "Purchase-order id from `/api/v1/purchase-orders`. Example: `412`.",
            "required": true,
            "schema": {
              "type": "integer",
              "format": "int32"
            }
          }
        ],
        "requestBody": {
          "description": "The lines to receive.",
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/ReceivePurchaseOrderApiRequest"
              }
            },
            "text/json": {
              "schema": {
                "$ref": "#/components/schemas/ReceivePurchaseOrderApiRequest"
              }
            },
            "application/*+json": {
              "schema": {
                "$ref": "#/components/schemas/ReceivePurchaseOrderApiRequest"
              }
            }
          }
        },
        "responses": {
          "401": {
            "description": "Unauthorized",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ProblemDetails"
                }
              }
            }
          },
          "403": {
            "description": "Forbidden",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ProblemDetails"
                }
              }
            }
          },
          "429": {
            "description": "Too Many Requests",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ProblemDetails"
                }
              }
            }
          },
          "200": {
            "description": "The purchase order after the receipt.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/PurchaseOrderDetailDto"
                }
              }
            }
          },
          "400": {
            "description": "Missing `Idempotency-Key`, or a malformed body (no lines, `quantity` below 1; fields listed in `errors`).",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ValidationProblemDetails"
                }
              }
            }
          },
          "404": {
            "description": "No such purchase order in this organization (or it is deleted).",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ProblemDetails"
                }
              }
            }
          },
          "409": {
            "description": "The order is not `Delivered` or `PartialReceived` (detail: `Purchase order is Cancelled; only Delivered or PartialReceived orders can be received`), or the key's creator is unknown on this server. Nothing was received.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ProblemDetails"
                }
              }
            }
          },
          "422": {
            "description": "A line cannot be received: `Item not found in purchase order` (unknown `lineId`), `Cannot receive more than N remaining units` (over-receipt), or `Invalid destination location`; the offending field (e.g. `lines[0].quantity`) is listed in `errors`. Nothing was received.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ValidationProblemDetails"
                }
              }
            }
          }
        }
      }
    },
    "/api/v1/reorder-points": {
      "get": {
        "tags": [
          "ReorderPoints"
        ],
        "summary": "Lists the organization's reorder points, oldest first, one page at a time.",
        "description": "Requires `ViewInventory`. Every reorder point (switched on or off) of an active item —\nthe rows of Reorder → Thresholds in the app, minus those of deleted or draft items.\nCursor pagination: pass the `nextCursor` of the previous response as `after`.\nFilters combine with AND. Example:\n`GET /api/v1/reorder-points?locationId=3&isActive=true&limit=100`.\nAlso available as CSV (`Accept: text/csv` or `?format=csv`); paging cursor in `X-Next-Cursor`.",
        "parameters": [
          {
            "name": "after",
            "in": "query",
            "description": "Cursor: the `nextCursor` of the previous page; omit for the first page.",
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "limit",
            "in": "query",
            "description": "Page size, 1–200 (default 50).",
            "schema": {
              "type": "integer",
              "format": "int32"
            }
          },
          {
            "name": "itemId",
            "in": "query",
            "description": "Only this item's reorder points. Example: `42`.",
            "schema": {
              "type": "integer",
              "format": "int32"
            }
          },
          {
            "name": "locationId",
            "in": "query",
            "description": "Only reorder points watching this location; `0` = only the all-locations ones. Example: `3`.",
            "schema": {
              "type": "integer",
              "format": "int32"
            }
          },
          {
            "name": "isActive",
            "in": "query",
            "description": "Only switched-on (`true`) or switched-off (`false`) reorder points; omit for both.",
            "schema": {
              "type": "boolean"
            }
          },
          {
            "name": "updatedSince",
            "in": "query",
            "description": "Only reorder points created or edited at/after this UTC instant (ISO-8601, e.g. `2026-01-01T00:00:00Z`).",
            "schema": {
              "type": "string",
              "format": "date-time"
            }
          },
          {
            "name": "format",
            "in": "query",
            "description": "Alternative to the Accept header: `csv` returns text/csv.",
            "schema": {
              "enum": [
                "csv"
              ],
              "type": "string"
            }
          }
        ],
        "responses": {
          "401": {
            "description": "Unauthorized",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ProblemDetails"
                }
              }
            }
          },
          "403": {
            "description": "Forbidden",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ProblemDetails"
                }
              }
            }
          },
          "429": {
            "description": "Too Many Requests",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ProblemDetails"
                }
              }
            }
          },
          "200": {
            "description": "A page of reorder points.",
            "headers": {
              "X-Next-Cursor": {
                "description": "text/csv only: the page's nextCursor. Absent on the last page. Pass it as ?after= (or follow the Link header) to fetch the next page.",
                "schema": {
                  "type": "string"
                }
              },
              "Link": {
                "description": "text/csv only, alongside X-Next-Cursor: <absolute URL of the next page>; rel=\"next\".",
                "schema": {
                  "type": "string"
                }
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/PagedResponseOfReorderPointDto"
                }
              },
              "text/csv": {
                "schema": {
                  "type": "string",
                  "description": "CSV document: UTF-8 with BOM, CRLF rows, RFC 4180 quoting. The header row holds the JSON property names of one item (nested objects flattened as parent.child); one row per item of the page."
                }
              }
            }
          },
          "304": {
            "description": "Unchanged since the supplied `If-None-Match`."
          },
          "400": {
            "description": "Malformed `after` cursor or filter value.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ValidationProblemDetails"
                }
              }
            }
          }
        }
      }
    },
    "/api/v1/reorder-points/{id}": {
      "delete": {
        "tags": [
          "ReorderPoints"
        ],
        "summary": "Deletes one reorder point.",
        "description": "Requires `ManageInventory` and an `Idempotency-Key` header. Removes the reorder point\n(the Delete button on Reorder → Thresholds): the item stops being listed by\n`GET /api/v1/items/low-stock` for it at once, and synced devices drop it on their next pull.\nTo keep the rule but stop it, send `\"isActive\": false` to\n`PUT /api/v1/items/{id}/reorder-points` instead. Find ids with\n`GET /api/v1/reorder-points` or `GET /api/v1/items/{id}/reorder-points`.\nExample: `DELETE /api/v1/reorder-points/7`.",
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "description": "Reorder point id. Example: `7`.",
            "required": true,
            "schema": {
              "type": "integer",
              "format": "int32"
            }
          }
        ],
        "responses": {
          "401": {
            "description": "Unauthorized",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ProblemDetails"
                }
              }
            }
          },
          "403": {
            "description": "Forbidden",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ProblemDetails"
                }
              }
            }
          },
          "429": {
            "description": "Too Many Requests",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ProblemDetails"
                }
              }
            }
          },
          "200": {
            "description": "Deleted; body is the reorder point as it was.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ReorderPointDto"
                }
              }
            }
          },
          "400": {
            "description": "Missing `Idempotency-Key`.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ValidationProblemDetails"
                }
              }
            }
          },
          "404": {
            "description": "No such reorder point in this organization.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ProblemDetails"
                }
              }
            }
          },
          "409": {
            "description": "The key's creator is unknown on this server.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ProblemDetails"
                }
              }
            }
          }
        }
      }
    },
    "/api/v1/roles": {
      "get": {
        "tags": [
          "Roles"
        ],
        "summary": "Lists the roles accounts can hold: built-in first (highest rank first), then custom roles by name.",
        "description": "Requires `ManageUsers`. Roles are few, so this collection is <b>not</b> cursor-paginated:\neverything comes back in one response with `nextCursor` always `null`. Sends a weak\n`ETag`. Example: `GET /api/v1/roles`.\nAlso available as CSV (`Accept: text/csv` or `?format=csv`); paging cursor in `X-Next-Cursor`.",
        "parameters": [
          {
            "name": "format",
            "in": "query",
            "description": "Alternative to the Accept header: `csv` returns text/csv.",
            "schema": {
              "enum": [
                "csv"
              ],
              "type": "string"
            }
          }
        ],
        "responses": {
          "401": {
            "description": "Unauthorized",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ProblemDetails"
                }
              }
            }
          },
          "403": {
            "description": "Forbidden",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ProblemDetails"
                }
              }
            }
          },
          "429": {
            "description": "Too Many Requests",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ProblemDetails"
                }
              }
            }
          },
          "200": {
            "description": "All roles.",
            "headers": {
              "X-Next-Cursor": {
                "description": "text/csv only: the page's nextCursor. Absent on the last page. Pass it as ?after= (or follow the Link header) to fetch the next page.",
                "schema": {
                  "type": "string"
                }
              },
              "Link": {
                "description": "text/csv only, alongside X-Next-Cursor: <absolute URL of the next page>; rel=\"next\".",
                "schema": {
                  "type": "string"
                }
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/PagedResponseOfRoleDto"
                }
              },
              "text/csv": {
                "schema": {
                  "type": "string",
                  "description": "CSV document: UTF-8 with BOM, CRLF rows, RFC 4180 quoting. The header row holds the JSON property names of one item (nested objects flattened as parent.child); one row per item of the page."
                }
              }
            }
          },
          "304": {
            "description": "Unchanged since the supplied `If-None-Match`."
          }
        }
      },
      "post": {
        "tags": [
          "Roles"
        ],
        "summary": "Creates a custom role, like Roles → Create Role in the app.",
        "description": "Requires `ManageRoles` and an `Idempotency-Key`. The name must be unique in the\norganization (case-insensitive) and not a built-in role name; every permission must be a\nFlexPermissions name held by both the API key and its creator. Assign it to accounts with\n`customRoleId` / `customRoleIds` on `/api/v1/users`. Example:\n`{ \"name\": \"Dispatcher\", \"description\": \"Schedules crews\", \"permissions\": [\"ViewJobs\", \"ManageJobs\"] }`.",
        "requestBody": {
          "description": "The role; `name` is required.",
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/CustomRoleWriteApiRequest"
              }
            },
            "text/json": {
              "schema": {
                "$ref": "#/components/schemas/CustomRoleWriteApiRequest"
              }
            },
            "application/*+json": {
              "schema": {
                "$ref": "#/components/schemas/CustomRoleWriteApiRequest"
              }
            }
          }
        },
        "responses": {
          "401": {
            "description": "Unauthorized",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ProblemDetails"
                }
              }
            }
          },
          "403": {
            "description": "A permission is not held by the key or its creator (listed in `errors`).",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ValidationProblemDetails"
                }
              }
            }
          },
          "429": {
            "description": "Too Many Requests",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ProblemDetails"
                }
              }
            }
          },
          "201": {
            "description": "Created; `Location` points at the new role.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/RoleDto"
                }
              }
            }
          },
          "400": {
            "description": "Malformed body or missing `Idempotency-Key`.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ValidationProblemDetails"
                }
              }
            }
          },
          "409": {
            "description": "A role with this name already exists, or the key's creator is unknown on this server.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ProblemDetails"
                }
              }
            }
          },
          "422": {
            "description": "A field is unacceptable (blank or long name, unknown permission); listed in `errors`.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ValidationProblemDetails"
                }
              }
            }
          }
        }
      }
    },
    "/api/v1/roles/{id}": {
      "delete": {
        "tags": [
          "Roles"
        ],
        "summary": "Deletes a custom role, like Roles → Delete in the app.",
        "description": "Requires `ManageRoles` and an `Idempotency-Key` header. The role and its permissions are\nremoved for good on the hosted web app and API (there is no restore); installed desktop and\nback-office apps that sync the organization may keep listing it until they are reset. Built-in roles (`OrganizationAdmin`, `Manager`,\n`User`) have no id and cannot be deleted. While any account (active or deactivated) still holds\nthe role the delete is refused with `409`, whose `detail` says how many; either remove it\nfrom them first (`PATCH /api/v1/users/{id}` with the remaining `customRoleIds`) or repeat\nwith `?unassignUsers=true` to take it away from every account in the same call. Those accounts\nare refused its permission-gated actions at once; menus in a signed-in browser session catch up\nwithin about 5 minutes, and API keys minted by them lose the permissions within 60 seconds. A\n`409` is stored against its `Idempotency-Key` for 24 hours like any other result, so\nevery retry after a `409` (whether you unassigned the holders first or add\n`?unassignUsers=true`) needs a new `Idempotency-Key`; resending the old one replays the\n`409`.\nExample: `DELETE /api/v1/roles/4?unassignUsers=true`.",
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "description": "Custom role id (from `GET /api/v1/roles`). Example: `4`.",
            "required": true,
            "schema": {
              "type": "integer",
              "format": "int32"
            }
          },
          {
            "name": "unassignUsers",
            "in": "query",
            "description": "When `true`, remove the role from every account holding it and then delete it; when `false` (default) the delete is refused with `409` while any account holds it. Example: `true`.",
            "schema": {
              "type": "boolean"
            }
          }
        ],
        "responses": {
          "401": {
            "description": "Unauthorized",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ProblemDetails"
                }
              }
            }
          },
          "403": {
            "description": "Forbidden",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ProblemDetails"
                }
              }
            }
          },
          "429": {
            "description": "Too Many Requests",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ProblemDetails"
                }
              }
            }
          },
          "200": {
            "description": "Deleted; `id` is the deleted role and `message` names how many accounts lost it.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiOperationResponse"
                }
              }
            }
          },
          "400": {
            "description": "Missing `Idempotency-Key`.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ValidationProblemDetails"
                }
              }
            }
          },
          "404": {
            "description": "No such custom role in this organization (also built-in and already-deleted roles).",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ProblemDetails"
                }
              }
            }
          },
          "409": {
            "description": "Accounts still hold the role and `unassignUsers` was not set, or the key's creator is unknown on this server.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ProblemDetails"
                }
              }
            }
          },
          "500": {
            "description": "The role could not be removed from an account or deleted; nothing after the failing step ran — retry.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ProblemDetails"
                }
              }
            }
          }
        }
      },
      "get": {
        "tags": [
          "Roles"
        ],
        "summary": "Returns one custom role.",
        "description": "Requires `ManageUsers`. Built-in roles have no id; read them from the list. Example: `GET /api/v1/roles/4`.",
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "description": "Custom role id. Example: `4`.",
            "required": true,
            "schema": {
              "type": "integer",
              "format": "int32"
            }
          }
        ],
        "responses": {
          "401": {
            "description": "Unauthorized",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ProblemDetails"
                }
              }
            }
          },
          "403": {
            "description": "Forbidden",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ProblemDetails"
                }
              }
            }
          },
          "429": {
            "description": "Too Many Requests",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ProblemDetails"
                }
              }
            }
          },
          "200": {
            "description": "The role.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/RoleDto"
                }
              }
            }
          },
          "304": {
            "description": "Unchanged since the supplied ETag."
          },
          "404": {
            "description": "No such custom role in this organization.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ProblemDetails"
                }
              }
            }
          }
        }
      },
      "patch": {
        "tags": [
          "Roles"
        ],
        "summary": "Edits a custom role; only the fields sent change.",
        "description": "Requires `ManageRoles` and an `Idempotency-Key`. `permissions` replaces the whole\nset and takes effect for every account holding the role; each permission sent must be held by\nboth the API key and its creator. Built-in roles cannot be edited. Example:\n`PATCH /api/v1/roles/4 { \"permissions\": [\"ViewJobs\"] }`.",
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "description": "Custom role id. Example: `4`.",
            "required": true,
            "schema": {
              "type": "integer",
              "format": "int32"
            }
          }
        ],
        "requestBody": {
          "description": "The fields to change.",
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/CustomRoleWriteApiRequest"
              }
            },
            "text/json": {
              "schema": {
                "$ref": "#/components/schemas/CustomRoleWriteApiRequest"
              }
            },
            "application/*+json": {
              "schema": {
                "$ref": "#/components/schemas/CustomRoleWriteApiRequest"
              }
            }
          }
        },
        "responses": {
          "401": {
            "description": "Unauthorized",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ProblemDetails"
                }
              }
            }
          },
          "403": {
            "description": "A permission is not held by the key or its creator (listed in `errors`).",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ValidationProblemDetails"
                }
              }
            }
          },
          "429": {
            "description": "Too Many Requests",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ProblemDetails"
                }
              }
            }
          },
          "200": {
            "description": "The role after the edit.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/RoleDto"
                }
              }
            }
          },
          "400": {
            "description": "Malformed body or missing `Idempotency-Key`.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ValidationProblemDetails"
                }
              }
            }
          },
          "404": {
            "description": "No such custom role in this organization.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ProblemDetails"
                }
              }
            }
          },
          "409": {
            "description": "Another role already has this name, or the key's creator is unknown on this server.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ProblemDetails"
                }
              }
            }
          },
          "422": {
            "description": "A field is unacceptable; listed in `errors`.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ValidationProblemDetails"
                }
              }
            }
          }
        }
      }
    },
    "/api/v1/sales-orders": {
      "get": {
        "tags": [
          "SalesOrders"
        ],
        "summary": "Lists the organization's sales orders, oldest first, one page at a time.",
        "description": "Requires `ViewSalesOrders`. Pagination: pass the previous response's\n`nextCursor` as `after`; `limit` defaults to 50 and is capped at 200. All\nfilters combine with AND. Example: `GET /api/v1/sales-orders?status=Pending&customerId=42`\nlists customer 42's orders that are waiting to be fulfilled. Response carries a weak\n`ETag`; resend it as `If-None-Match` to get 304 when nothing changed.\nAlso available as CSV (`Accept: text/csv` or `?format=csv`); paging cursor in `X-Next-Cursor`.",
        "parameters": [
          {
            "name": "status",
            "in": "query",
            "description": "Order status to match exactly (case-insensitive): `Pending`, `Picking`, `Packed`, `Shipped`, `Delivered`, `Complete`, `Cancelled` (or the deprecated `Confirmed` of legacy rows). Example: `status=Pending`.",
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "customerId",
            "in": "query",
            "description": "Only orders for this customer id. Example: `customerId=42`.",
            "schema": {
              "type": "integer",
              "format": "int32"
            }
          },
          {
            "name": "updatedSince",
            "in": "query",
            "description": "Only orders created or modified (header or line edits, lines added or removed, status change, fulfilment and pick progress, `fulfillmentStatus` change) at or after this instant (ISO-8601 UTC, e.g. `2026-09-01T00:00:00Z`). Use it to poll for changes. Payments do not bump `updatedAt`: `paymentStatus`, `amountPaid` and `balanceDue` can change without it, so poll `/api/v1/invoices` (or re-read the order with `If-None-Match`) to follow payments.",
            "schema": {
              "type": "string",
              "format": "date-time"
            }
          },
          {
            "name": "after",
            "in": "query",
            "description": "Cursor from the previous page's `nextCursor` (a sales-order SyncId). Omit for the first page.",
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "limit",
            "in": "query",
            "description": "Page size, 1–200 (default 50).",
            "schema": {
              "type": "integer",
              "format": "int32"
            }
          },
          {
            "name": "format",
            "in": "query",
            "description": "Alternative to the Accept header: `csv` returns text/csv.",
            "schema": {
              "enum": [
                "csv"
              ],
              "type": "string"
            }
          }
        ],
        "responses": {
          "401": {
            "description": "Unauthorized",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ProblemDetails"
                }
              }
            }
          },
          "403": {
            "description": "Forbidden",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ProblemDetails"
                }
              }
            }
          },
          "429": {
            "description": "Too Many Requests",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ProblemDetails"
                }
              }
            }
          },
          "200": {
            "description": "A page of sales orders and the cursor for the next page (null on the last page).",
            "headers": {
              "X-Next-Cursor": {
                "description": "text/csv only: the page's nextCursor. Absent on the last page. Pass it as ?after= (or follow the Link header) to fetch the next page.",
                "schema": {
                  "type": "string"
                }
              },
              "Link": {
                "description": "text/csv only, alongside X-Next-Cursor: <absolute URL of the next page>; rel=\"next\".",
                "schema": {
                  "type": "string"
                }
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/PagedResponseOfSalesOrderSummaryDto"
                }
              },
              "text/csv": {
                "schema": {
                  "type": "string",
                  "description": "CSV document: UTF-8 with BOM, CRLF rows, RFC 4180 quoting. The header row holds the JSON property names of one item (nested objects flattened as parent.child); one row per item of the page."
                }
              }
            }
          },
          "304": {
            "description": "Unchanged since the `If-None-Match` ETag."
          },
          "400": {
            "description": "Unknown `status` or unusable `after` cursor (field listed in `errors`).",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ValidationProblemDetails"
                }
              }
            }
          }
        }
      },
      "post": {
        "tags": [
          "SalesOrders"
        ],
        "summary": "Creates a sales order, optionally with its lines (the app's New Sales Order page plus its item modal).",
        "description": "Requires `ManageSalesOrders` and an `Idempotency-Key` header. Body:\n`{ \"customerId\": 42, \"customerPONumber\": \"PO-7781\", \"lines\": [ { \"itemId\": 5, \"quantity\": 10, \"unitPrice\": 125.00 } ] }`.\nSame code path and defaults as the app: the order is created `Pending` with the next\n`SO{yy}{MM}{nnnn}` number; `taxRate` omitted or 0 → the organization's default\nrate, and 0 for a tax-exempt customer; `paymentTerms` omitted → the organization's\ndefault terms; `orderDate` omitted → now. The customer must exist in this organization\nand not be marked Difficult, `deliveryAddressId` must be one of its addresses and each\nline's `itemId` a non-deleted catalog item — everything is checked before anything is\nwritten. Lines are added as the app adds them: two lines for the same item become one line\n(quantities add up; a later non-zero price or cost wins), line total = quantity × unit price,\nthen `subTotal`, `taxAmount` and `totalAmount` are computed. The order's\nhistory records \"Sales order created (via API key '…')\". Creating reserves no stock; call\n`POST /api/v1/sales-orders/{id}/fulfill` to start fulfilment.",
        "requestBody": {
          "description": "The order to create.",
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/CreateSalesOrderApiRequest"
              }
            },
            "text/json": {
              "schema": {
                "$ref": "#/components/schemas/CreateSalesOrderApiRequest"
              }
            },
            "application/*+json": {
              "schema": {
                "$ref": "#/components/schemas/CreateSalesOrderApiRequest"
              }
            }
          }
        },
        "responses": {
          "401": {
            "description": "Unauthorized",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ProblemDetails"
                }
              }
            }
          },
          "403": {
            "description": "Forbidden",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ProblemDetails"
                }
              }
            }
          },
          "429": {
            "description": "Too Many Requests",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ProblemDetails"
                }
              }
            }
          },
          "201": {
            "description": "Created; body is the order as `GET /api/v1/sales-orders/{id}` returns it, `Location` points at it.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/SalesOrderDetailDto"
                }
              }
            }
          },
          "400": {
            "description": "Missing `Idempotency-Key`, or a malformed body (`quantity` below 1, negative price, tax rate outside 0–100, more than 200 lines; fields listed in `errors`).",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ValidationProblemDetails"
                }
              }
            }
          },
          "409": {
            "description": "The key's creator is unknown on this server or belongs to another organization. Nothing was created.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ProblemDetails"
                }
              }
            }
          },
          "422": {
            "description": "Unknown or Difficult customer (`customerId`), an address that is not the customer's (`deliveryAddressId`) or an unknown item (`lines[i].itemId`). Nothing was created.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ValidationProblemDetails"
                }
              }
            }
          }
        }
      }
    },
    "/api/v1/sales-orders/{id}": {
      "get": {
        "tags": [
          "SalesOrders"
        ],
        "summary": "Returns one sales order with its lines.",
        "description": "Requires `ViewSalesOrders`. Removed lines are not returned. Response carries a weak\n`ETag`; resend it as `If-None-Match` to get 304 when nothing changed.",
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "description": "Sales-order id from the list endpoint. Example: `318`.",
            "required": true,
            "schema": {
              "type": "integer",
              "format": "int32"
            }
          }
        ],
        "responses": {
          "401": {
            "description": "Unauthorized",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ProblemDetails"
                }
              }
            }
          },
          "403": {
            "description": "Forbidden",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ProblemDetails"
                }
              }
            }
          },
          "429": {
            "description": "Too Many Requests",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ProblemDetails"
                }
              }
            }
          },
          "200": {
            "description": "The sales order.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/SalesOrderDetailDto"
                }
              }
            }
          },
          "304": {
            "description": "Unchanged since the `If-None-Match` ETag."
          },
          "404": {
            "description": "No such sales order in this organization.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ProblemDetails"
                }
              }
            }
          }
        }
      },
      "patch": {
        "tags": [
          "SalesOrders"
        ],
        "summary": "Edits a sales order's header — the app's inline edits on the Details page (order number, PO, terms, dates, notes, tax rate, shipping cost).",
        "description": "Requires `ManageSalesOrders` and an `Idempotency-Key` header. Partial update:\nomitted members are unchanged, `\"\"` clears an optional text field,\n`clearRequestedDeliveryDate: true` clears the date. Example:\n`{ \"shippingCost\": 25.00, \"taxRate\": 8.25 }`. Same code path and rules as the Details\npage: every field is checked before anything is written; a tax-rate or shipping change\nrecomputes `taxAmount` / `totalAmount` and the payment status and updates the\norder's draft invoices (their subtotal, tax rate and \"Shipping & Handling\" line).\n`updatedAt` moves; no history entry is written (as in the app). Complete and\nCancelled orders cannot be edited.",
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "description": "Sales-order id. Example: `318`.",
            "required": true,
            "schema": {
              "type": "integer",
              "format": "int32"
            }
          }
        ],
        "requestBody": {
          "description": "The fields to change.",
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/UpdateSalesOrderApiRequest"
              }
            },
            "text/json": {
              "schema": {
                "$ref": "#/components/schemas/UpdateSalesOrderApiRequest"
              }
            },
            "application/*+json": {
              "schema": {
                "$ref": "#/components/schemas/UpdateSalesOrderApiRequest"
              }
            }
          }
        },
        "responses": {
          "401": {
            "description": "Unauthorized",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ProblemDetails"
                }
              }
            }
          },
          "403": {
            "description": "Forbidden",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ProblemDetails"
                }
              }
            }
          },
          "429": {
            "description": "Too Many Requests",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ProblemDetails"
                }
              }
            }
          },
          "200": {
            "description": "Updated; body is the order as `GET /api/v1/sales-orders/{id}` returns it.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/SalesOrderDetailDto"
                }
              }
            }
          },
          "400": {
            "description": "Missing `Idempotency-Key`, no field to change, `requestedDeliveryDate` sent with `clearRequestedDeliveryDate`, or a value out of range (fields listed in `errors`).",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ValidationProblemDetails"
                }
              }
            }
          },
          "404": {
            "description": "No such sales order in this organization.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ProblemDetails"
                }
              }
            }
          },
          "409": {
            "description": "The order is `Complete` or `Cancelled` (detail: `Cannot edit a cancelled or completed order`), or the key's creator is unknown on this server.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ProblemDetails"
                }
              }
            }
          },
          "422": {
            "description": "Blank `orderNumber`, unknown `paymentTerms`, or a non-zero `taxRate` for a tax-exempt customer (field listed in `errors`). Nothing changed.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ValidationProblemDetails"
                }
              }
            }
          }
        }
      }
    },
    "/api/v1/sales-orders/{id}/cancel": {
      "post": {
        "tags": [
          "SalesOrders"
        ],
        "summary": "Cancels a sales order (the app's Cancel page): delivered goods come back, transfers are cancelled.",
        "description": "Requires `ManageSalesOrders` and an `Idempotency-Key` header. Body optional:\n`{ \"reason\": \"Customer changed their mind\" }`. Same code path as the app's Cancel\npage: units already delivered by completed transfers are returned to inventory, live\ntransfers are cancelled and their reserved stock released, the order's lines are removed,\nand the order and its payment status become `Cancelled` with the history entry\n\"Order cancelled by … (was …) (via API key '…')\". `Cancelled` is terminal;\n`Complete` and `Cancelled` orders are refused.",
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "description": "Sales-order id. Example: `318`.",
            "required": true,
            "schema": {
              "type": "integer",
              "format": "int32"
            }
          }
        ],
        "requestBody": {
          "description": "Optional reason.",
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/CancelSalesOrderApiRequest"
              }
            },
            "text/json": {
              "schema": {
                "$ref": "#/components/schemas/CancelSalesOrderApiRequest"
              }
            },
            "application/*+json": {
              "schema": {
                "$ref": "#/components/schemas/CancelSalesOrderApiRequest"
              }
            }
          }
        },
        "responses": {
          "401": {
            "description": "Unauthorized",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ProblemDetails"
                }
              }
            }
          },
          "403": {
            "description": "Forbidden",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ProblemDetails"
                }
              }
            }
          },
          "429": {
            "description": "Too Many Requests",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ProblemDetails"
                }
              }
            }
          },
          "200": {
            "description": "Cancelled; `itemsReturned` and `transfersCancelled` say what was undone.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/SalesOrderStepResponse"
                }
              }
            }
          },
          "400": {
            "description": "Missing `Idempotency-Key` or a reason over 500 characters.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ValidationProblemDetails"
                }
              }
            }
          },
          "404": {
            "description": "No such sales order in this organization.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ProblemDetails"
                }
              }
            }
          },
          "409": {
            "description": "The order is already `Complete` or `Cancelled` (detail: `This order has already been completed or cancelled.`), or the key's creator is unknown.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ProblemDetails"
                }
              }
            }
          },
          "500": {
            "description": "Returning the stock or cancelling the transfers failed; everything was rolled back and the order is unchanged. Retry with the same `Idempotency-Key`.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ProblemDetails"
                }
              }
            }
          }
        }
      }
    },
    "/api/v1/sales-orders/{id}/complete": {
      "post": {
        "tags": [
          "SalesOrders"
        ],
        "summary": "Marks a delivered sales order complete (the app's \"Mark Complete\").",
        "description": "Requires `ManageSalesOrders` and an `Idempotency-Key` header; no body. Only a\n`Delivered` order can be completed; the history records \"Order marked as complete\n(via API key '…')\". `Complete` is terminal. An order whose invoices are fully paid\nmay already have become `Complete` on delivery.",
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "description": "Sales-order id. Example: `318`.",
            "required": true,
            "schema": {
              "type": "integer",
              "format": "int32"
            }
          }
        ],
        "responses": {
          "401": {
            "description": "Unauthorized",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ProblemDetails"
                }
              }
            }
          },
          "403": {
            "description": "Forbidden",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ProblemDetails"
                }
              }
            }
          },
          "429": {
            "description": "Too Many Requests",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ProblemDetails"
                }
              }
            }
          },
          "200": {
            "description": "Completed.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/SalesOrderStepResponse"
                }
              }
            }
          },
          "400": {
            "description": "Missing `Idempotency-Key`.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ValidationProblemDetails"
                }
              }
            }
          },
          "404": {
            "description": "No such sales order in this organization.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ProblemDetails"
                }
              }
            }
          },
          "409": {
            "description": "The order is not `Delivered` (detail: `Cannot transition from Shipped to Complete`), or the key's creator is unknown.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ProblemDetails"
                }
              }
            }
          }
        }
      }
    },
    "/api/v1/sales-orders/{id}/complete-pick": {
      "post": {
        "tags": [
          "SalesOrders"
        ],
        "summary": "Finishes picking a transfer (the scan dialog's \"Complete\"): the transfer leaves the warehouse (In Transit).",
        "description": "Requires `ManageSalesOrders` and an `Idempotency-Key` header. Body optional:\n`{ \"transferId\": 904 }`; omitted = the order's single `Pending` transfer. Every\nline of the transfer must be fully picked (`/pick`). The transfer becomes\n`InTransit`, the history records \"Transfer dispatched (via API key '…')\" and the order's\n`status` is recalculated — `Packed` once every line is picked and covered. Stock\nstays reserved to the transfer until `/deliver`.",
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "description": "Sales-order id. Example: `318`.",
            "required": true,
            "schema": {
              "type": "integer",
              "format": "int32"
            }
          }
        ],
        "requestBody": {
          "description": "Optional transfer to act on.",
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/SalesOrderTransferStepApiRequest"
              }
            },
            "text/json": {
              "schema": {
                "$ref": "#/components/schemas/SalesOrderTransferStepApiRequest"
              }
            },
            "application/*+json": {
              "schema": {
                "$ref": "#/components/schemas/SalesOrderTransferStepApiRequest"
              }
            }
          }
        },
        "responses": {
          "401": {
            "description": "Unauthorized",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ProblemDetails"
                }
              }
            }
          },
          "403": {
            "description": "Forbidden",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ProblemDetails"
                }
              }
            }
          },
          "429": {
            "description": "Too Many Requests",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ProblemDetails"
                }
              }
            }
          },
          "200": {
            "description": "Transfer in transit; the recalculated order status.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/SalesOrderStepResponse"
                }
              }
            }
          },
          "400": {
            "description": "Missing `Idempotency-Key`.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ValidationProblemDetails"
                }
              }
            }
          },
          "404": {
            "description": "No such sales order in this organization, or `transferId` is not one of its transfers.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ProblemDetails"
                }
              }
            }
          },
          "409": {
            "description": "The transfer is not `Pending` (detail: `Transfer is already in InTransit status`), some lines are not fully picked (the detail names them), the order has no `Pending` transfer, or the key's creator is unknown.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ProblemDetails"
                }
              }
            }
          },
          "422": {
            "description": "Several `Pending` transfers and no `transferId` (`errors.transferId` lists them).",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ValidationProblemDetails"
                }
              }
            }
          }
        }
      }
    },
    "/api/v1/sales-orders/{id}/deliver": {
      "post": {
        "tags": [
          "SalesOrders"
        ],
        "summary": "Marks a shipped transfer delivered (the app's \"Mark as Delivered\"): its stock moves to the customer.",
        "description": "Requires `ManageSalesOrders` and an `Idempotency-Key` header. Body optional:\n`{ \"transferId\": 904 }`; omitted = the order's single in-transit transfer. The transfer\nmust be in transit (at its last stop, if it has several) and have shipping info\n(`/ship`). Same code path as the app: the transfer becomes `Completed`, each remaining\nline's `quantityDelivered` is recorded, its stock units move to the delivery location and are\nreleased — and consumed (removed from inventory) when that location's type consumes\ninventory or is a job site, as customer delivery locations usually are. Lines marked\n`Failed` in transit are not delivered: their units stay at their current location, are\nreleased from the transfer, and the line ends `Returned` with no `quantityDelivered`\nand a \"[RETURNED] …\" note on the item; lines already `Returned` are skipped. The history records\n\"Transfer completed — items delivered (via API key '…')\" and the order's `status` is\nrecalculated: `Delivered` when every transfer is complete, or straight to\n`Complete` when its invoices already cover the order and are paid.",
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "description": "Sales-order id. Example: `318`.",
            "required": true,
            "schema": {
              "type": "integer",
              "format": "int32"
            }
          }
        ],
        "requestBody": {
          "description": "Optional transfer to act on.",
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/SalesOrderTransferStepApiRequest"
              }
            },
            "text/json": {
              "schema": {
                "$ref": "#/components/schemas/SalesOrderTransferStepApiRequest"
              }
            },
            "application/*+json": {
              "schema": {
                "$ref": "#/components/schemas/SalesOrderTransferStepApiRequest"
              }
            }
          }
        },
        "responses": {
          "401": {
            "description": "Unauthorized",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ProblemDetails"
                }
              }
            }
          },
          "403": {
            "description": "Forbidden",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ProblemDetails"
                }
              }
            }
          },
          "429": {
            "description": "Too Many Requests",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ProblemDetails"
                }
              }
            }
          },
          "200": {
            "description": "Delivered; the recalculated order status.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/SalesOrderStepResponse"
                }
              }
            }
          },
          "400": {
            "description": "Missing `Idempotency-Key`.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ValidationProblemDetails"
                }
              }
            }
          },
          "404": {
            "description": "No such sales order in this organization, or `transferId` is not one of its transfers.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ProblemDetails"
                }
              }
            }
          },
          "409": {
            "description": "The transfer is not in transit (detail: `Transfer must be In Transit to complete. Current status: …`), has waypoints left, has no shipping info yet, the order has no transfer in transit, or the key's creator is unknown.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ProblemDetails"
                }
              }
            }
          },
          "422": {
            "description": "Several transfers in transit and no `transferId` (`errors.transferId` lists them).",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ValidationProblemDetails"
                }
              }
            }
          }
        }
      }
    },
    "/api/v1/sales-orders/{id}/fulfill": {
      "post": {
        "tags": [
          "SalesOrders"
        ],
        "summary": "Starts fulfilment of a sales order (the app's Begin Fulfillment button): creates the pick transfer.",
        "description": "Requires `ManageSalesOrders` and an `Idempotency-Key` header; no body. Same code\npath and rules as the Begin Fulfillment button: the order must not be `Complete` or\n`Cancelled`; a pick transfer is created from the order's source locations (else the\norganization's warehouse locations) to its delivery destination for every line not yet covered, using\navailable stock; the history records \"Fulfillment started (via API key '…')\"; the order's\n`status` is then recalculated — `Picking` when the transfers cover every line,\notherwise it stays `Pending` (call again once stock arrives). The response always\ncarries the real status; never assume `Picking`. Replaying the same\n`Idempotency-Key` returns the first response. A call with a new key creates another\ntransfer only for the quantity not yet covered by a live (not Cancelled/Failed) transfer —\na partly covered line is topped up, never claimed twice — and is refused with 409\n`Every line is already covered by a transfer` when nothing is left to cover, the same\nrule that shows the Begin Fulfillment button in the app. Next steps: `/pick`,\n`/complete-pick`, `/ship`, `/deliver`, `/complete`.",
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "description": "Sales-order id from `/api/v1/sales-orders`. Example: `318`.",
            "required": true,
            "schema": {
              "type": "integer",
              "format": "int32"
            }
          }
        ],
        "responses": {
          "401": {
            "description": "Unauthorized",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ProblemDetails"
                }
              }
            }
          },
          "403": {
            "description": "Forbidden",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ProblemDetails"
                }
              }
            }
          },
          "429": {
            "description": "Too Many Requests",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ProblemDetails"
                }
              }
            }
          },
          "200": {
            "description": "Fulfilment started; the resulting status and the pick transfer id.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/SalesOrderFulfillmentResponse"
                }
              }
            }
          },
          "400": {
            "description": "Missing `Idempotency-Key`.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ValidationProblemDetails"
                }
              }
            }
          },
          "404": {
            "description": "No such sales order in this organization.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ProblemDetails"
                }
              }
            }
          },
          "409": {
            "description": "The order is `Complete` or `Cancelled` (detail: `Cannot transition from Complete to Picking`), every line is already covered by a transfer (detail: `Every line is already covered by a transfer`) or the order has no lines (`The order has no lines to fulfil`), the pick transfer could not be created (detail: `Error creating transfer: No source locations found for sales order`, or no available stock for any line), or the key's creator is unknown on this server. Nothing changed.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ProblemDetails"
                }
              }
            }
          }
        }
      }
    },
    "/api/v1/sales-orders/{id}/lines": {
      "post": {
        "tags": [
          "SalesOrders"
        ],
        "summary": "Adds lines to an existing sales order (the app's item picker on the Details page).",
        "description": "Requires `ManageSalesOrders` and an `Idempotency-Key` header. Body:\n`{ \"lines\": [ { \"itemId\": 5, \"quantity\": 2, \"unitPrice\": 125.00 } ] }`. Every line is\nchecked as on create (a non-deleted catalog item, quantity 1–1,000,000, price and cost\n0–1,000,000,000) before anything is written; then the lines are added exactly as in the\napp: a line for an item already on the order and not in a live transfer is merged into\nit (quantities add up, a non-zero price or cost replaces the old one), totals and draft\ninvoices are recomputed and the history records \"Added n item(s) (via API key '…')\".\nAdding lines reserves no stock; `/fulfill` (or `/transfers`) covers them.",
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "description": "Sales-order id. Example: `318`.",
            "required": true,
            "schema": {
              "type": "integer",
              "format": "int32"
            }
          }
        ],
        "requestBody": {
          "description": "The lines.",
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/AddSalesOrderLinesApiRequest"
              }
            },
            "text/json": {
              "schema": {
                "$ref": "#/components/schemas/AddSalesOrderLinesApiRequest"
              }
            },
            "application/*+json": {
              "schema": {
                "$ref": "#/components/schemas/AddSalesOrderLinesApiRequest"
              }
            }
          }
        },
        "responses": {
          "401": {
            "description": "Unauthorized",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ProblemDetails"
                }
              }
            }
          },
          "403": {
            "description": "Forbidden",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ProblemDetails"
                }
              }
            }
          },
          "429": {
            "description": "Too Many Requests",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ProblemDetails"
                }
              }
            }
          },
          "200": {
            "description": "Added; body is the order with its lines.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/SalesOrderDetailDto"
                }
              }
            }
          },
          "400": {
            "description": "Missing `Idempotency-Key` or a malformed body (no lines, more than 200, values out of range).",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ValidationProblemDetails"
                }
              }
            }
          },
          "404": {
            "description": "No such sales order in this organization.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ProblemDetails"
                }
              }
            }
          },
          "409": {
            "description": "The order is `Complete` or `Cancelled`, or the key's creator is unknown on this server.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ProblemDetails"
                }
              }
            }
          },
          "422": {
            "description": "An unknown, deleted or draft item (`lines[i].itemId`) or lines totalling more than 1,000,000,000,000. Nothing was added.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ValidationProblemDetails"
                }
              }
            }
          }
        }
      }
    },
    "/api/v1/sales-orders/{id}/lines/{lineId}": {
      "delete": {
        "tags": [
          "SalesOrders"
        ],
        "summary": "Removes a line from a sales order (the app's line remove button).",
        "description": "Requires `ManageSalesOrders` and an `Idempotency-Key` header; no body. Only a\nline that is on no live transfer can go — not even a completed one (\"Cannot delete: item\nis part of a transfer. Cancel the transfer first.\"); use `/returns` for delivered\ngoods. The line is soft-deleted (it disappears from `lines`), totals, payment status\nand draft invoices are recomputed and the history records \"Removed item '…'\n(via API key '…')\".",
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "description": "Sales-order id. Example: `318`.",
            "required": true,
            "schema": {
              "type": "integer",
              "format": "int32"
            }
          },
          {
            "name": "lineId",
            "in": "path",
            "description": "Line id. Example: `1502`.",
            "required": true,
            "schema": {
              "type": "integer",
              "format": "int32"
            }
          }
        ],
        "responses": {
          "401": {
            "description": "Unauthorized",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ProblemDetails"
                }
              }
            }
          },
          "403": {
            "description": "Forbidden",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ProblemDetails"
                }
              }
            }
          },
          "429": {
            "description": "Too Many Requests",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ProblemDetails"
                }
              }
            }
          },
          "200": {
            "description": "Removed; body is the order with its remaining lines.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/SalesOrderDetailDto"
                }
              }
            }
          },
          "400": {
            "description": "Missing `Idempotency-Key`.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ValidationProblemDetails"
                }
              }
            }
          },
          "404": {
            "description": "No such sales order in this organization, or the line is not one of its live lines (already removed).",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ProblemDetails"
                }
              }
            }
          },
          "409": {
            "description": "The line is on a transfer, the order is `Complete` or `Cancelled`, or the key's creator is unknown on this server.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ProblemDetails"
                }
              }
            }
          }
        }
      },
      "patch": {
        "tags": [
          "SalesOrders"
        ],
        "summary": "Changes a line's unit price and/or unit cost (the app's inline price edit).",
        "description": "Requires `ManageSalesOrders` and an `Idempotency-Key` header. Body:\n`{ \"unitPrice\": 119.50 }`, `{ \"unitCost\": 80.00 }` or both. Same code path as the\nDetails page: the line total (quantity × price, less the line's discounts), the order's\n`subTotal` / `taxAmount` / `totalAmount` and payment status are recomputed,\ndraft invoices are updated and the history records \"Updated unit price on item '…'\n(via API key '…')\". Quantities cannot be changed (remove the line and add a new one).",
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "description": "Sales-order id. Example: `318`.",
            "required": true,
            "schema": {
              "type": "integer",
              "format": "int32"
            }
          },
          {
            "name": "lineId",
            "in": "path",
            "description": "Line id (`lines[].id` of the order). Example: `1502`.",
            "required": true,
            "schema": {
              "type": "integer",
              "format": "int32"
            }
          }
        ],
        "requestBody": {
          "description": "The new price and/or cost.",
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/UpdateSalesOrderLineApiRequest"
              }
            },
            "text/json": {
              "schema": {
                "$ref": "#/components/schemas/UpdateSalesOrderLineApiRequest"
              }
            },
            "application/*+json": {
              "schema": {
                "$ref": "#/components/schemas/UpdateSalesOrderLineApiRequest"
              }
            }
          }
        },
        "responses": {
          "401": {
            "description": "Unauthorized",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ProblemDetails"
                }
              }
            }
          },
          "403": {
            "description": "Forbidden",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ProblemDetails"
                }
              }
            }
          },
          "429": {
            "description": "Too Many Requests",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ProblemDetails"
                }
              }
            }
          },
          "200": {
            "description": "Updated; body is the order with its lines.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/SalesOrderDetailDto"
                }
              }
            }
          },
          "400": {
            "description": "Missing `Idempotency-Key`, neither field sent, or a value out of range.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ValidationProblemDetails"
                }
              }
            }
          },
          "404": {
            "description": "No such sales order in this organization, or the line is not one of its (live) lines.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ProblemDetails"
                }
              }
            }
          },
          "409": {
            "description": "The order is `Complete` or `Cancelled`, or the key's creator is unknown on this server.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ProblemDetails"
                }
              }
            }
          }
        }
      }
    },
    "/api/v1/sales-orders/{id}/lines/{lineId}/pick": {
      "post": {
        "tags": [
          "SalesOrders"
        ],
        "summary": "Picks one more unit of a line (the Fulfill page's per-line \"Pick\").",
        "description": "Requires `ManageSalesOrders` and an `Idempotency-Key` header; no body. Adds 1 to\nthe line's `quantityPicked` — a line-level count only: no stock unit or transfer\nchanges (the transfer scan flow is `/pick`). Refused once the line is picked in full.\n`orderComplete` says whether every line is now picked.",
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "description": "Sales-order id. Example: `318`.",
            "required": true,
            "schema": {
              "type": "integer",
              "format": "int32"
            }
          },
          {
            "name": "lineId",
            "in": "path",
            "description": "Line id. Example: `1502`.",
            "required": true,
            "schema": {
              "type": "integer",
              "format": "int32"
            }
          }
        ],
        "responses": {
          "401": {
            "description": "Unauthorized",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ProblemDetails"
                }
              }
            }
          },
          "403": {
            "description": "Forbidden",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ProblemDetails"
                }
              }
            }
          },
          "429": {
            "description": "Too Many Requests",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ProblemDetails"
                }
              }
            }
          },
          "200": {
            "description": "Picked; the line's quantities.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/SalesOrderLinePickResponse"
                }
              }
            }
          },
          "400": {
            "description": "Missing `Idempotency-Key`.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ValidationProblemDetails"
                }
              }
            }
          },
          "404": {
            "description": "No such sales order in this organization, or the line is not one of its live lines.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ProblemDetails"
                }
              }
            }
          },
          "409": {
            "description": "The order is `Complete` or `Cancelled` (detail: `Sales order is Cancelled; picking is closed.`), the line is already fully picked (detail: `Item '…' is already fully picked (10/10)`), or the key's creator is unknown.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ProblemDetails"
                }
              }
            }
          }
        }
      }
    },
    "/api/v1/sales-orders/{id}/lines/{lineId}/picked-quantity": {
      "put": {
        "tags": [
          "SalesOrders"
        ],
        "summary": "Sets a line's picked quantity (the Details page's picked-quantity editor); lowering it unpicks and releases stock.",
        "description": "Requires `ManageSalesOrders` and an `Idempotency-Key` header. Body:\n`{ \"quantityPicked\": 3 }`. Only while the order is `Picking`. Same code path as\nthe app: raising the quantity claims that many more available units (the primary source\nlocation and already-opened bundles first) onto the order's live transfer — or creates a\ntransfer for the uncovered remainder when there is none; lowering it takes units off the\nlive transfers (secondary locations first), releases them to stock and cancels a transfer\nleft empty. Over-picking is allowed. The order's `updatedAt` moves and a \"Picking\nprogress — X of Y unit(s) picked\" entry is written; the status does not change. This is\nthe line-level count; the transfer scan flow is `/pick`.",
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "description": "Sales-order id. Example: `318`.",
            "required": true,
            "schema": {
              "type": "integer",
              "format": "int32"
            }
          },
          {
            "name": "lineId",
            "in": "path",
            "description": "Line id. Example: `1502`.",
            "required": true,
            "schema": {
              "type": "integer",
              "format": "int32"
            }
          }
        ],
        "requestBody": {
          "description": "The new picked quantity.",
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/SetSalesOrderLinePickedApiRequest"
              }
            },
            "text/json": {
              "schema": {
                "$ref": "#/components/schemas/SetSalesOrderLinePickedApiRequest"
              }
            },
            "application/*+json": {
              "schema": {
                "$ref": "#/components/schemas/SetSalesOrderLinePickedApiRequest"
              }
            }
          }
        },
        "responses": {
          "401": {
            "description": "Unauthorized",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ProblemDetails"
                }
              }
            }
          },
          "403": {
            "description": "Forbidden",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ProblemDetails"
                }
              }
            }
          },
          "429": {
            "description": "Too Many Requests",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ProblemDetails"
                }
              }
            }
          },
          "200": {
            "description": "Saved; the line's quantities.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/SalesOrderLinePickResponse"
                }
              }
            }
          },
          "400": {
            "description": "Missing `Idempotency-Key` or `quantityPicked` missing / out of range.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ValidationProblemDetails"
                }
              }
            }
          },
          "404": {
            "description": "No such sales order in this organization, or the line is not one of its live lines.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ProblemDetails"
                }
              }
            }
          },
          "409": {
            "description": "The order is not `Picking` (detail: `Can only save picking progress when order is in Picking status`), a needed transfer could not be created (`Error creating transfer: …`), or the key's creator is unknown.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ProblemDetails"
                }
              }
            }
          }
        }
      }
    },
    "/api/v1/sales-orders/{id}/lines/pick-all": {
      "post": {
        "tags": [
          "SalesOrders"
        ],
        "summary": "Marks every line of a sales order picked in full (the Fulfill page's \"Pick All\").",
        "description": "Requires `ManageSalesOrders` and an `Idempotency-Key` header; no body. Each line's\n`quantityPicked` is raised to its ordered quantity — a line-level count only, no stock\nunit or transfer changes. `linesUpdated` is 0 when everything was already picked\n(message `All items were already fully picked`).",
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "description": "Sales-order id. Example: `318`.",
            "required": true,
            "schema": {
              "type": "integer",
              "format": "int32"
            }
          }
        ],
        "responses": {
          "401": {
            "description": "Unauthorized",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ProblemDetails"
                }
              }
            }
          },
          "403": {
            "description": "Forbidden",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ProblemDetails"
                }
              }
            }
          },
          "429": {
            "description": "Too Many Requests",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ProblemDetails"
                }
              }
            }
          },
          "200": {
            "description": "Picked; `linesUpdated` and `unitsAdded` say how much changed.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/SalesOrderLinePickResponse"
                }
              }
            }
          },
          "400": {
            "description": "Missing `Idempotency-Key`.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ValidationProblemDetails"
                }
              }
            }
          },
          "404": {
            "description": "No such sales order in this organization.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ProblemDetails"
                }
              }
            }
          },
          "409": {
            "description": "The order is `Complete` or `Cancelled` (detail: `Sales order is Complete; picking is closed.`), the order has no lines (detail: `No items found in this sales order`), or the key's creator is unknown.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ProblemDetails"
                }
              }
            }
          }
        }
      }
    },
    "/api/v1/sales-orders/{id}/pick": {
      "post": {
        "tags": [
          "SalesOrders"
        ],
        "summary": "Picks a sales order's pick transfer — all of it (the app's \"Pick All\") or given units per line (the scan dialog).",
        "description": "Requires `ManageSalesOrders` and an `Idempotency-Key` header. Body optional:\n`{}` or no body picks every line of the order's single `Pending` transfer in\nfull; `{ \"transferId\": 904, \"lines\": [ { \"transferItemId\": 5120, \"quantity\": 2 } ] }`\nadds 2 to that transfer line's picked count (line ids: `lines[].id` of\n`GET /api/v1/transfers/{transferId}`). Same code path as the app's transfer scan\ndialog: the transfer line is stamped scanned by the key's creator and the order line's\n`quantityPicked` rises by the same units. Every line is checked before anything is\nwritten. Picking moves no stock and does not change the order's `status`; call\n`/complete-pick` once every line is picked. Repeating a full pick is harmless; a\npartial pick sent again with a new `Idempotency-Key` picks those units again.",
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "description": "Sales-order id from `/api/v1/sales-orders`. Example: `318`.",
            "required": true,
            "schema": {
              "type": "integer",
              "format": "int32"
            }
          }
        ],
        "requestBody": {
          "description": "Which transfer and, optionally, which lines.",
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/PickSalesOrderApiRequest"
              }
            },
            "text/json": {
              "schema": {
                "$ref": "#/components/schemas/PickSalesOrderApiRequest"
              }
            },
            "application/*+json": {
              "schema": {
                "$ref": "#/components/schemas/PickSalesOrderApiRequest"
              }
            }
          }
        },
        "responses": {
          "401": {
            "description": "Unauthorized",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ProblemDetails"
                }
              }
            }
          },
          "403": {
            "description": "Forbidden",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ProblemDetails"
                }
              }
            }
          },
          "429": {
            "description": "Too Many Requests",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ProblemDetails"
                }
              }
            }
          },
          "200": {
            "description": "Picked; `unitsPicked` says how many units this call picked.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/SalesOrderStepResponse"
                }
              }
            }
          },
          "400": {
            "description": "Missing `Idempotency-Key` or a malformed body.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ValidationProblemDetails"
                }
              }
            }
          },
          "404": {
            "description": "No such sales order in this organization, or `transferId` is not one of its transfers.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ProblemDetails"
                }
              }
            }
          },
          "409": {
            "description": "The transfer is not `Pending`, the order has no `Pending` transfer, or the key's creator is unknown on this server.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ProblemDetails"
                }
              }
            }
          },
          "422": {
            "description": "The order has several `Pending` transfers and `transferId` was omitted, or a line is not on the transfer, repeated, Returned/Failed, or asks for more units than are left to pick (field listed in `errors`). Nothing was picked.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ValidationProblemDetails"
                }
              }
            }
          }
        }
      }
    },
    "/api/v1/sales-orders/{id}/returnable-items": {
      "get": {
        "tags": [
          "SalesOrders"
        ],
        "summary": "Lists what can still be returned from a sales order (the app's return modal).",
        "description": "Requires `ViewSalesOrders` and `ManageReturns`. The order's completed transfers,\neach with the stock units still at the customer (`items[].stockUnitId`) and the\nuntracked lines still returnable for value (`untrackedLines[]`). When a source\nlocation was deleted, `needsReturnLocation` is true and `returnLocations` lists\nwhere the units can go instead. Feed the ids to `POST …/returns`. Response carries a\nweak `ETag`.",
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "description": "Sales-order id. Example: `318`.",
            "required": true,
            "schema": {
              "type": "integer",
              "format": "int32"
            }
          }
        ],
        "responses": {
          "401": {
            "description": "Unauthorized",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ProblemDetails"
                }
              }
            }
          },
          "403": {
            "description": "Forbidden",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ProblemDetails"
                }
              }
            }
          },
          "429": {
            "description": "Too Many Requests",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ProblemDetails"
                }
              }
            }
          },
          "200": {
            "description": "What is returnable (empty lists when nothing was delivered).",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/SalesOrderReturnableItemsDto"
                }
              }
            }
          },
          "304": {
            "description": "Unchanged since the `If-None-Match` ETag."
          },
          "404": {
            "description": "No such sales order in this organization.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ProblemDetails"
                }
              }
            }
          }
        }
      }
    },
    "/api/v1/sales-orders/{id}/returns": {
      "post": {
        "tags": [
          "SalesOrders"
        ],
        "summary": "Returns delivered goods of a sales order (the app's return modal): selected units, one transfer, or everything.",
        "description": "Requires `ManageSalesOrders`, `ManageReturns` and an `Idempotency-Key`\nheader. Exactly one of: `{ \"reason\": \"…\", \"stockUnitIds\": [4410], \"untrackedLines\": [ { \"transferItemId\": 5121, \"quantity\": 1 } ] }`\n(selective), `{ \"reason\": \"…\", \"transferId\": 904 }` (one completed transfer, which is\nthen cancelled) or `{ \"reason\": \"…\", \"all\": true }` (every completed transfer). Ids\ncome from `GET …/returnable-items`. Same code path as the app: returned units are\nrestored to stock at their source location (or `overrideLocationId` when it was\ndeleted), untracked lines are credited for value only, the order's lines and totals are\nreduced, finalized invoice lines shrink, any overpayment becomes store credit on the\ncustomer (`storeCreditIssued`), fully returned transfers are cancelled and the return\nis logged with the reason and \"(via API key '…')\". The whole return is one transaction.\nA unit already returned is skipped, so replaying with a new key returns nothing more\n(409 `These items have already been returned.`). The order's `status` is not changed.",
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "description": "Sales-order id. Example: `318`.",
            "required": true,
            "schema": {
              "type": "integer",
              "format": "int32"
            }
          }
        ],
        "requestBody": {
          "description": "What to return and why.",
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/ReturnSalesOrderItemsApiRequest"
              }
            },
            "text/json": {
              "schema": {
                "$ref": "#/components/schemas/ReturnSalesOrderItemsApiRequest"
              }
            },
            "application/*+json": {
              "schema": {
                "$ref": "#/components/schemas/ReturnSalesOrderItemsApiRequest"
              }
            }
          }
        },
        "responses": {
          "401": {
            "description": "Unauthorized",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ProblemDetails"
                }
              }
            }
          },
          "403": {
            "description": "Forbidden",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ProblemDetails"
                }
              }
            }
          },
          "429": {
            "description": "Too Many Requests",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ProblemDetails"
                }
              }
            }
          },
          "200": {
            "description": "Returned; counts, store credit and the order's status.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/SalesOrderReturnResponse"
                }
              }
            }
          },
          "400": {
            "description": "Missing `Idempotency-Key`, no reason, or not exactly one of `all` / `transferId` / `stockUnitIds`+`untrackedLines`.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ValidationProblemDetails"
                }
              }
            }
          },
          "404": {
            "description": "No such sales order in this organization, or `transferId` is not one of its transfers.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ProblemDetails"
                }
              }
            }
          },
          "409": {
            "description": "Nothing to return (`No items found to return.`, `These items have already been returned.`, `No completed transfers found.`, `Transfer not found or is not in Completed status.`), or the key's creator is unknown.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ProblemDetails"
                }
              }
            }
          },
          "422": {
            "description": "An `overrideLocationId` that is not one of `returnLocations` (`errors.overrideLocationId`).",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ValidationProblemDetails"
                }
              }
            }
          },
          "500": {
            "description": "The return failed part-way; everything was rolled back. Retry with the same `Idempotency-Key`.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ProblemDetails"
                }
              }
            }
          }
        }
      }
    },
    "/api/v1/sales-orders/{id}/scan": {
      "post": {
        "tags": [
          "SalesOrders"
        ],
        "summary": "Picks one unit by barcode (the Fulfill page's scanner).",
        "description": "Requires `ManageSalesOrders` and an `Idempotency-Key` header. Body:\n`{ \"barcode\": \"0012345678905\" }`. A stock unit's barcode picks the line pinned to that\nunit; otherwise a catalog item's barcode picks the order's line for that item. The line's\n`quantityPicked` rises by 1 — a line-level count only, no stock or transfer changes.",
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "description": "Sales-order id. Example: `318`.",
            "required": true,
            "schema": {
              "type": "integer",
              "format": "int32"
            }
          }
        ],
        "requestBody": {
          "description": "The scanned barcode.",
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/ScanSalesOrderBarcodeApiRequest"
              }
            },
            "text/json": {
              "schema": {
                "$ref": "#/components/schemas/ScanSalesOrderBarcodeApiRequest"
              }
            },
            "application/*+json": {
              "schema": {
                "$ref": "#/components/schemas/ScanSalesOrderBarcodeApiRequest"
              }
            }
          }
        },
        "responses": {
          "401": {
            "description": "Unauthorized",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ProblemDetails"
                }
              }
            }
          },
          "403": {
            "description": "Forbidden",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ProblemDetails"
                }
              }
            }
          },
          "429": {
            "description": "Too Many Requests",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ProblemDetails"
                }
              }
            }
          },
          "200": {
            "description": "Picked; the matched line's quantities and name.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/SalesOrderLinePickResponse"
                }
              }
            }
          },
          "400": {
            "description": "Missing `Idempotency-Key` or `barcode`.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ValidationProblemDetails"
                }
              }
            }
          },
          "404": {
            "description": "No such sales order in this organization.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ProblemDetails"
                }
              }
            }
          },
          "409": {
            "description": "The item is not on the order (detail: `Item '…' is not part of this sales order`), its line is already fully picked, the order is `Complete` or `Cancelled` (`… picking is closed.`), or the key's creator is unknown.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ProblemDetails"
                }
              }
            }
          },
          "422": {
            "description": "No stock unit or catalog item has this barcode (`errors.barcode`).",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ValidationProblemDetails"
                }
              }
            }
          }
        }
      }
    },
    "/api/v1/sales-orders/{id}/ship": {
      "post": {
        "tags": [
          "SalesOrders"
        ],
        "summary": "Records the shipment of an in-transit transfer (the app's Ship dialog): carrier and tracking, or \"ship without tracking\".",
        "description": "Requires `ManageSalesOrders` and an `Idempotency-Key` header. Body:\n`{ \"transferId\": 904, \"carrier\": \"UPS\", \"trackingNumber\": \"1Z999AA10123456784\" }`, or\n`{ \"transferId\": 904, \"withoutTracking\": true }`. `transferId` omitted = the\norder's single in-transit transfer. The transfer must be in transit (`/complete-pick`\nfirst). Same code path as the app: the transfer's shipment is added — or replaced when it\nalready has one (every field is overwritten; `shipDate` omitted = now) — the history\nrecords \"Shipment added — UPS tracking … (via API key '…')\" and the order's `status`\nis recalculated: `Shipped` once every live transfer has shipping info. Without\ntracking only flags the transfer shipped (\"Shipped without tracking\").",
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "description": "Sales-order id. Example: `318`.",
            "required": true,
            "schema": {
              "type": "integer",
              "format": "int32"
            }
          }
        ],
        "requestBody": {
          "description": "The shipment.",
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/ShipSalesOrderApiRequest"
              }
            },
            "text/json": {
              "schema": {
                "$ref": "#/components/schemas/ShipSalesOrderApiRequest"
              }
            },
            "application/*+json": {
              "schema": {
                "$ref": "#/components/schemas/ShipSalesOrderApiRequest"
              }
            }
          }
        },
        "responses": {
          "401": {
            "description": "Unauthorized",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ProblemDetails"
                }
              }
            }
          },
          "403": {
            "description": "Forbidden",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ProblemDetails"
                }
              }
            }
          },
          "429": {
            "description": "Too Many Requests",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ProblemDetails"
                }
              }
            }
          },
          "200": {
            "description": "Shipment saved; the recalculated order status.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/SalesOrderStepResponse"
                }
              }
            }
          },
          "400": {
            "description": "Missing `Idempotency-Key`, a malformed body (a field over its length), or `withoutTracking` sent with shipment fields.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ValidationProblemDetails"
                }
              }
            }
          },
          "404": {
            "description": "No such sales order in this organization, or `transferId` is not one of its transfers.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ProblemDetails"
                }
              }
            }
          },
          "409": {
            "description": "The transfer is not in transit (detail: `Shipping information can only be added while the transfer is In Transit. Current status: Pending`), the order has no transfer in transit, or the key's creator is unknown.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ProblemDetails"
                }
              }
            }
          },
          "422": {
            "description": "Several transfers in transit and no `transferId` (`errors.transferId` lists them).",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ValidationProblemDetails"
                }
              }
            }
          }
        }
      }
    },
    "/api/v1/sales-orders/{id}/transfers": {
      "post": {
        "tags": [
          "SalesOrders"
        ],
        "summary": "Creates another pick transfer for whatever lines are not yet covered (the app's \"Create transfer\").",
        "description": "Requires `ManageSalesOrders` and an `Idempotency-Key` header; no body. Unlike\n`/fulfill` it writes no status entry and works in any non-terminal status: the\ntransfer covers only the quantity of live lines not already on a live transfer, from the\norder's source locations (else the warehouses), using available stock; the history\nrecords \"Transfer created (via API key '…')\" and the order's `status` is recalculated.\nRefused (409 `Error creating transfer: …`) when there is no source location or\nnothing left to cover / no available stock. Next: `/pick`.",
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "description": "Sales-order id. Example: `318`.",
            "required": true,
            "schema": {
              "type": "integer",
              "format": "int32"
            }
          }
        ],
        "responses": {
          "401": {
            "description": "Unauthorized",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ProblemDetails"
                }
              }
            }
          },
          "403": {
            "description": "Forbidden",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ProblemDetails"
                }
              }
            }
          },
          "429": {
            "description": "Too Many Requests",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ProblemDetails"
                }
              }
            }
          },
          "200": {
            "description": "Created; `transferId` is the new transfer, `status` the recalculated order status.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/SalesOrderStepResponse"
                }
              }
            }
          },
          "400": {
            "description": "Missing `Idempotency-Key`.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ValidationProblemDetails"
                }
              }
            }
          },
          "404": {
            "description": "No such sales order in this organization.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ProblemDetails"
                }
              }
            }
          },
          "409": {
            "description": "The order is `Complete` or `Cancelled` (detail: `Cannot create transfer for order with status: Complete`), no transfer could be created, or the key's creator is unknown.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ProblemDetails"
                }
              }
            }
          }
        }
      }
    },
    "/api/v1/schedule": {
      "get": {
        "tags": [
          "Schedule"
        ],
        "summary": "Lists the jobs scheduled to start within a date range as calendar events.",
        "description": "Times are the organization's local wall clock with no zone suffix\n(`2026-09-08T00:00:00`); a trailing `Z` is converted to local time first.\nThe window may span at most 92 days. Events are not paginated — a quarter of jobs\nis returned in one array. Response carries a weak `ETag`; resend it as\n`If-None-Match` to get 304 when nothing changed.\nAlso available as CSV (`Accept: text/csv` or `?format=csv`).",
        "parameters": [
          {
            "name": "from",
            "in": "query",
            "description": "Inclusive start of the window (required). Example: `2026-09-01T00:00:00`.",
            "schema": {
              "type": "string",
              "format": "date-time"
            }
          },
          {
            "name": "to",
            "in": "query",
            "description": "Inclusive end of the window (required), at most 92 days after from. Example: `2026-09-30T23:59:59`.",
            "schema": {
              "type": "string",
              "format": "date-time"
            }
          },
          {
            "name": "format",
            "in": "query",
            "description": "Alternative to the Accept header: `csv` returns text/csv.",
            "schema": {
              "enum": [
                "csv"
              ],
              "type": "string"
            }
          }
        ],
        "responses": {
          "401": {
            "description": "Unauthorized",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ProblemDetails"
                }
              }
            }
          },
          "403": {
            "description": "Key lacks `ViewJobs` or the Scheduling module is not active.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ProblemDetails"
                }
              }
            }
          },
          "429": {
            "description": "Too Many Requests",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ProblemDetails"
                }
              }
            }
          },
          "200": {
            "description": "Calendar events ordered as the service returns them (by job).",
            "headers": {
              "X-Next-Cursor": {
                "description": "text/csv only: the page's nextCursor. Absent on the last page. Pass it as ?after= (or follow the Link header) to fetch the next page.",
                "schema": {
                  "type": "string"
                }
              },
              "Link": {
                "description": "text/csv only, alongside X-Next-Cursor: <absolute URL of the next page>; rel=\"next\".",
                "schema": {
                  "type": "string"
                }
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "type": "array",
                  "items": {
                    "$ref": "#/components/schemas/ScheduleEventDto"
                  }
                }
              },
              "text/csv": {
                "schema": {
                  "type": "string",
                  "description": "CSV document: UTF-8 with BOM, CRLF rows, RFC 4180 quoting. The header row holds the JSON property names of one item (nested objects flattened as parent.child); one row per item of the page."
                }
              }
            }
          },
          "304": {
            "description": "Unchanged since the `If-None-Match` ETag."
          },
          "400": {
            "description": "Missing parameter, `to` before `from`, or a window longer than 92 days (field listed in `errors`).",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ValidationProblemDetails"
                }
              }
            }
          }
        }
      }
    },
    "/api/v1/schedule-groups": {
      "get": {
        "tags": [
          "ScheduleGroups"
        ],
        "summary": "Lists schedule groups with their active members, one page at a time.",
        "description": "Groups are flat with a `parentGroupId`; nest them client-side. Example:\n`GET /api/v1/schedule-groups?parentGroupId=3` returns the crews of division 3.\nAlso available as CSV (`Accept: text/csv` or `?format=csv`); paging cursor in `X-Next-Cursor`.",
        "parameters": [
          {
            "name": "after",
            "in": "query",
            "description": "SyncId of the last item of the previous page; omit for the first page.",
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "limit",
            "in": "query",
            "description": "Page size, 1–200 (default 50).",
            "schema": {
              "type": "integer",
              "format": "int32"
            }
          },
          {
            "name": "updatedSince",
            "in": "query",
            "description": "Only groups created or edited at/after this UTC instant (ISO-8601). Adding or removing a member counts as an edit.",
            "schema": {
              "type": "string",
              "format": "date-time"
            }
          },
          {
            "name": "parentGroupId",
            "in": "query",
            "description": "Only direct children of this group; pass `0` for top-level groups only.",
            "schema": {
              "type": "integer",
              "format": "int32"
            }
          },
          {
            "name": "format",
            "in": "query",
            "description": "Alternative to the Accept header: `csv` returns text/csv.",
            "schema": {
              "enum": [
                "csv"
              ],
              "type": "string"
            }
          }
        ],
        "responses": {
          "401": {
            "description": "Unauthorized",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ProblemDetails"
                }
              }
            }
          },
          "403": {
            "description": "The `scheduling` module is not active for this organization.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ProblemDetails"
                }
              }
            }
          },
          "429": {
            "description": "Too Many Requests",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ProblemDetails"
                }
              }
            }
          },
          "200": {
            "description": "A page of groups.",
            "headers": {
              "X-Next-Cursor": {
                "description": "text/csv only: the page's nextCursor. Absent on the last page. Pass it as ?after= (or follow the Link header) to fetch the next page.",
                "schema": {
                  "type": "string"
                }
              },
              "Link": {
                "description": "text/csv only, alongside X-Next-Cursor: <absolute URL of the next page>; rel=\"next\".",
                "schema": {
                  "type": "string"
                }
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/PagedResponseOfScheduleGroupDto"
                }
              },
              "text/csv": {
                "schema": {
                  "type": "string",
                  "description": "CSV document: UTF-8 with BOM, CRLF rows, RFC 4180 quoting. The header row holds the JSON property names of one item (nested objects flattened as parent.child); one row per item of the page."
                }
              }
            }
          },
          "400": {
            "description": "Malformed `after` cursor.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ValidationProblemDetails"
                }
              }
            }
          }
        }
      },
      "post": {
        "tags": [
          "ScheduleGroups"
        ],
        "summary": "Creates a schedule group (a division, crew or team).",
        "description": "Requires `ManageJobs` and an `Idempotency-Key` header. Same as \"New group\" on the\nSchedule Groups page: the group is placed last among its siblings. Add people with\n`PUT /api/v1/schedule-groups/{id}/members/{userId}`.\nExample: `{ \"name\": \"Install Crew A\", \"colorHex\": \"#2e7d32\", \"parentGroupId\": 3 }`.",
        "requestBody": {
          "description": "The group's fields; `name` is required.",
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/ScheduleGroupWriteApiRequest"
              }
            },
            "text/json": {
              "schema": {
                "$ref": "#/components/schemas/ScheduleGroupWriteApiRequest"
              }
            },
            "application/*+json": {
              "schema": {
                "$ref": "#/components/schemas/ScheduleGroupWriteApiRequest"
              }
            }
          }
        },
        "responses": {
          "401": {
            "description": "Unauthorized",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ProblemDetails"
                }
              }
            }
          },
          "403": {
            "description": "The `scheduling` module is not active for this organization.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ProblemDetails"
                }
              }
            }
          },
          "429": {
            "description": "Too Many Requests",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ProblemDetails"
                }
              }
            }
          },
          "201": {
            "description": "The new group; `Location` points at `/api/v1/schedule-groups/{id}`.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ScheduleGroupDto"
                }
              }
            }
          },
          "400": {
            "description": "Malformed body, a field over its length limit, or missing `Idempotency-Key`.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ValidationProblemDetails"
                }
              }
            }
          },
          "409": {
            "description": "The key's creator is unknown on this server, so the write cannot be attributed.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ProblemDetails"
                }
              }
            }
          },
          "422": {
            "description": "Blank name, a colour that is not 6-digit hex, or an unknown `parentGroupId`; the field is listed in `errors`.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ValidationProblemDetails"
                }
              }
            }
          }
        }
      }
    },
    "/api/v1/schedule-groups/{id}": {
      "delete": {
        "tags": [
          "ScheduleGroups"
        ],
        "summary": "Deletes a schedule group.",
        "description": "Requires `ManageJobs` and an `Idempotency-Key` header. The same soft delete as the\nSchedule Groups page: the group and its memberships disappear, and jobs assigned under it keep\ntheir assignees (only the group context on those assignments is cleared). Refused with\n`409` while the group still has child groups — delete or move those first.",
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "description": "Schedule group id. Example: `7`.",
            "required": true,
            "schema": {
              "type": "integer",
              "format": "int32"
            }
          }
        ],
        "responses": {
          "401": {
            "description": "Unauthorized",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ProblemDetails"
                }
              }
            }
          },
          "403": {
            "description": "The `scheduling` module is not active for this organization.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ProblemDetails"
                }
              }
            }
          },
          "429": {
            "description": "Too Many Requests",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ProblemDetails"
                }
              }
            }
          },
          "200": {
            "description": "The group was deleted; body carries its id and a confirmation message.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiOperationResponse"
                }
              }
            }
          },
          "400": {
            "description": "Missing `Idempotency-Key`.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ValidationProblemDetails"
                }
              }
            }
          },
          "404": {
            "description": "No such group in this organization (also for already-deleted groups).",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ProblemDetails"
                }
              }
            }
          },
          "409": {
            "description": "The group still has child groups, or the key's creator is unknown on this server.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ProblemDetails"
                }
              }
            }
          }
        }
      },
      "get": {
        "tags": [
          "ScheduleGroups"
        ],
        "summary": "Returns one schedule group with its active members.",
        "description": "Send the previous `ETag` as `If-None-Match` to receive `304 Not Modified` when unchanged.",
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "description": "Schedule group id. Example: `3`.",
            "required": true,
            "schema": {
              "type": "integer",
              "format": "int32"
            }
          }
        ],
        "responses": {
          "401": {
            "description": "Unauthorized",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ProblemDetails"
                }
              }
            }
          },
          "403": {
            "description": "The `scheduling` module is not active for this organization.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ProblemDetails"
                }
              }
            }
          },
          "429": {
            "description": "Too Many Requests",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ProblemDetails"
                }
              }
            }
          },
          "200": {
            "description": "The group, with an `ETag` header.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ScheduleGroupDto"
                }
              }
            }
          },
          "304": {
            "description": "Unchanged since the supplied `If-None-Match`."
          },
          "404": {
            "description": "No such group in this organization (also for deleted groups).",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ProblemDetails"
                }
              }
            }
          }
        }
      },
      "patch": {
        "tags": [
          "ScheduleGroups"
        ],
        "summary": "Edits a schedule group; only the fields sent change.",
        "description": "Requires `ManageJobs` and an `Idempotency-Key` header. `\"\"` clears\n`description` / `colorHex`; `\"parentGroupId\": 0` moves the group to the top level.\nMoving a group under itself or one of its own crews is refused.\nExample: `PATCH /api/v1/schedule-groups/7 { \"name\": \"Crew A (North)\" }`.",
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "description": "Schedule group id. Example: `7`.",
            "required": true,
            "schema": {
              "type": "integer",
              "format": "int32"
            }
          }
        ],
        "requestBody": {
          "description": "The fields to change.",
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/ScheduleGroupWriteApiRequest"
              }
            },
            "text/json": {
              "schema": {
                "$ref": "#/components/schemas/ScheduleGroupWriteApiRequest"
              }
            },
            "application/*+json": {
              "schema": {
                "$ref": "#/components/schemas/ScheduleGroupWriteApiRequest"
              }
            }
          }
        },
        "responses": {
          "401": {
            "description": "Unauthorized",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ProblemDetails"
                }
              }
            }
          },
          "403": {
            "description": "The `scheduling` module is not active for this organization.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ProblemDetails"
                }
              }
            }
          },
          "429": {
            "description": "Too Many Requests",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ProblemDetails"
                }
              }
            }
          },
          "200": {
            "description": "The group after the edit.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ScheduleGroupDto"
                }
              }
            }
          },
          "400": {
            "description": "Malformed body, a field over its length limit, or missing `Idempotency-Key`.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ValidationProblemDetails"
                }
              }
            }
          },
          "404": {
            "description": "No such group in this organization.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ProblemDetails"
                }
              }
            }
          },
          "409": {
            "description": "The key's creator is unknown on this server.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ProblemDetails"
                }
              }
            }
          },
          "422": {
            "description": "Blank name, bad colour, unknown `parentGroupId`, or a move that would create a cycle; the field is listed in `errors`.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ValidationProblemDetails"
                }
              }
            }
          }
        }
      }
    },
    "/api/v1/schedule-groups/{id}/members": {
      "put": {
        "tags": [
          "ScheduleGroups"
        ],
        "summary": "Replaces a schedule group's complete membership.",
        "description": "Requires `ManageJobs` and an `Idempotency-Key` header. Same as saving the member\npicker on the Schedule Groups page: listed users become (or stay) members, everyone else is\nremoved, and `isLead` is set exactly for `leadIds`. All or nothing: an id that is\nnot an active user of this organization, or a lead missing from `userIds`, changes\nnothing. Jobs already assigned are not changed. For single changes use\n`PUT` / `DELETE .../members/{userId}`.\nExample: `{ \"userIds\": [\"3f2a…\", \"9b1c…\"], \"leadIds\": [\"3f2a…\"] }`.",
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "description": "Schedule group id. Example: `7`.",
            "required": true,
            "schema": {
              "type": "integer",
              "format": "int32"
            }
          }
        ],
        "requestBody": {
          "description": "The full member list and the leads among them.",
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/ScheduleGroupMembersApiRequest"
              }
            },
            "text/json": {
              "schema": {
                "$ref": "#/components/schemas/ScheduleGroupMembersApiRequest"
              }
            },
            "application/*+json": {
              "schema": {
                "$ref": "#/components/schemas/ScheduleGroupMembersApiRequest"
              }
            }
          }
        },
        "responses": {
          "401": {
            "description": "Unauthorized",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ProblemDetails"
                }
              }
            }
          },
          "403": {
            "description": "The `scheduling` module is not active for this organization.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ProblemDetails"
                }
              }
            }
          },
          "429": {
            "description": "Too Many Requests",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ProblemDetails"
                }
              }
            }
          },
          "200": {
            "description": "The group with its members after the change.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ScheduleGroupDto"
                }
              }
            }
          },
          "400": {
            "description": "Malformed body, more than 500 ids, or missing `Idempotency-Key`.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ValidationProblemDetails"
                }
              }
            }
          },
          "404": {
            "description": "No such group in this organization.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ProblemDetails"
                }
              }
            }
          },
          "409": {
            "description": "The key's creator is unknown on this server.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ProblemDetails"
                }
              }
            }
          },
          "422": {
            "description": "An unknown or inactive user id (`userIds`), or a lead not listed in `userIds` (`leadIds`).",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ValidationProblemDetails"
                }
              }
            }
          }
        }
      }
    },
    "/api/v1/schedule-groups/{id}/members/{userId}": {
      "delete": {
        "tags": [
          "ScheduleGroups"
        ],
        "summary": "Removes a user from a schedule group.",
        "description": "Requires `ManageJobs` and an `Idempotency-Key` header. Jobs the user is already\nassigned to are not changed.",
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "description": "Schedule group id. Example: `7`.",
            "required": true,
            "schema": {
              "type": "integer",
              "format": "int32"
            }
          },
          {
            "name": "userId",
            "in": "path",
            "description": "User id (string GUID) to remove.",
            "required": true,
            "schema": {
              "type": "string"
            }
          }
        ],
        "responses": {
          "401": {
            "description": "Unauthorized",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ProblemDetails"
                }
              }
            }
          },
          "403": {
            "description": "The `scheduling` module is not active for this organization.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ProblemDetails"
                }
              }
            }
          },
          "429": {
            "description": "Too Many Requests",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ProblemDetails"
                }
              }
            }
          },
          "200": {
            "description": "The group with its members after the change.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ScheduleGroupDto"
                }
              }
            }
          },
          "400": {
            "description": "Missing `Idempotency-Key`.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ValidationProblemDetails"
                }
              }
            }
          },
          "404": {
            "description": "No such group in this organization, or the user is not a member of it.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ProblemDetails"
                }
              }
            }
          },
          "409": {
            "description": "The key's creator is unknown on this server.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ProblemDetails"
                }
              }
            }
          }
        }
      },
      "put": {
        "tags": [
          "ScheduleGroups"
        ],
        "summary": "Adds a user to a schedule group, or sets their lead flag when already a member.",
        "description": "Requires `ManageJobs` and an `Idempotency-Key` header. The user must be an active\nuser of this organization (ids from `GET /api/v1/users`). A user may belong to several\ngroups. Example: `PUT /api/v1/schedule-groups/7/members/3f2a…` with `{ \"isLead\": true }`.",
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "description": "Schedule group id. Example: `7`.",
            "required": true,
            "schema": {
              "type": "integer",
              "format": "int32"
            }
          },
          {
            "name": "userId",
            "in": "path",
            "description": "User id (string GUID) to add.",
            "required": true,
            "schema": {
              "type": "string"
            }
          }
        ],
        "requestBody": {
          "description": "Optional body; `isLead` sets the crew-lead flag.",
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/ScheduleGroupMemberWriteApiRequest"
              }
            },
            "text/json": {
              "schema": {
                "$ref": "#/components/schemas/ScheduleGroupMemberWriteApiRequest"
              }
            },
            "application/*+json": {
              "schema": {
                "$ref": "#/components/schemas/ScheduleGroupMemberWriteApiRequest"
              }
            }
          }
        },
        "responses": {
          "401": {
            "description": "Unauthorized",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ProblemDetails"
                }
              }
            }
          },
          "403": {
            "description": "The `scheduling` module is not active for this organization.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ProblemDetails"
                }
              }
            }
          },
          "429": {
            "description": "Too Many Requests",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ProblemDetails"
                }
              }
            }
          },
          "200": {
            "description": "The group with its members after the change.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ScheduleGroupDto"
                }
              }
            }
          },
          "400": {
            "description": "Malformed body or missing `Idempotency-Key`.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ValidationProblemDetails"
                }
              }
            }
          },
          "404": {
            "description": "No such group in this organization.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ProblemDetails"
                }
              }
            }
          },
          "409": {
            "description": "The key's creator is unknown on this server.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ProblemDetails"
                }
              }
            }
          },
          "422": {
            "description": "No active user with this id exists in the organization.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ValidationProblemDetails"
                }
              }
            }
          }
        }
      }
    },
    "/api/v1/schedule-groups/reorder": {
      "post": {
        "tags": [
          "ScheduleGroups"
        ],
        "summary": "Reorders the groups under one parent (sibling display order).",
        "description": "Requires `ManageJobs` and an `Idempotency-Key` header. Same as dragging groups on the\nSchedule Groups page: `displayOrder` becomes 0, 1, 2, … in the order given. List every\ngroup under the parent exactly once — a partial list is refused rather than half-applied.\nExample: `{ \"parentGroupId\": 3, \"ids\": [9, 7, 8] }`.",
        "requestBody": {
          "description": "The parent (omit or `0` for top level) and the complete ordered id list.",
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/ScheduleGroupReorderApiRequest"
              }
            },
            "text/json": {
              "schema": {
                "$ref": "#/components/schemas/ScheduleGroupReorderApiRequest"
              }
            },
            "application/*+json": {
              "schema": {
                "$ref": "#/components/schemas/ScheduleGroupReorderApiRequest"
              }
            }
          }
        },
        "responses": {
          "401": {
            "description": "Unauthorized",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ProblemDetails"
                }
              }
            }
          },
          "403": {
            "description": "The `scheduling` module is not active for this organization.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ProblemDetails"
                }
              }
            }
          },
          "429": {
            "description": "Too Many Requests",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ProblemDetails"
                }
              }
            }
          },
          "200": {
            "description": "The siblings in their new order.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ScheduleGroupOrderResponse"
                }
              }
            }
          },
          "400": {
            "description": "Malformed body, more than 500 ids, or missing `Idempotency-Key`.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ValidationProblemDetails"
                }
              }
            }
          },
          "404": {
            "description": "No such parent group in this organization.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ProblemDetails"
                }
              }
            }
          },
          "409": {
            "description": "The key's creator is unknown on this server.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ProblemDetails"
                }
              }
            }
          },
          "422": {
            "description": "`ids` misses a group under the parent, names one that is not under it, or repeats one.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ValidationProblemDetails"
                }
              }
            }
          }
        }
      }
    },
    "/api/v1/transfers": {
      "get": {
        "tags": [
          "Transfers"
        ],
        "summary": "Lists the organization's inventory transfers, oldest first, one page at a time.",
        "description": "Requires `ManageTransfers` (the Transfers pages need it for reads too). Pagination:\npass the previous response's `nextCursor` as `after`; `limit` defaults to 50\nand is capped at 200. All filters combine with AND. Example:\n`GET /api/v1/transfers?jobId=1042&status=Pending`. The approval queue (what the\nPending Approvals page lists, and what `approve` / `reject` accept) is\n`GET /api/v1/transfers?approvalStatus=Pending&status=Pending`. Response carries a weak\n`ETag`; resend it as `If-None-Match` to get 304 when nothing changed.\nAlso available as CSV (`Accept: text/csv` or `?format=csv`); paging cursor in `X-Next-Cursor`.",
        "parameters": [
          {
            "name": "jobId",
            "in": "query",
            "description": "Only transfers for this job id (see `/api/v1/jobs`).",
            "schema": {
              "type": "integer",
              "format": "int32"
            }
          },
          {
            "name": "status",
            "in": "query",
            "description": "Transfer status name to match exactly (case-insensitive), e.g. `Pending`, `InTransit`, `Completed`, `Cancelled`.",
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "approvalStatus",
            "in": "query",
            "description": "Approval state to match exactly (case-insensitive): `Pending` (awaiting a decision), `Approved` or `Rejected`. Transfers that never needed approval are excluded whenever this is set. Example: `approvalStatus=Pending`.",
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "updatedSince",
            "in": "query",
            "description": "Only transfers created or modified at or after this instant (ISO-8601 UTC, e.g. `2026-09-01T00:00:00Z`). Use it to poll for changes.",
            "schema": {
              "type": "string",
              "format": "date-time"
            }
          },
          {
            "name": "after",
            "in": "query",
            "description": "Cursor from the previous page's `nextCursor` (a transfer SyncId). Omit for the first page.",
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "limit",
            "in": "query",
            "description": "Page size, 1–200 (default 50).",
            "schema": {
              "type": "integer",
              "format": "int32"
            }
          },
          {
            "name": "format",
            "in": "query",
            "description": "Alternative to the Accept header: `csv` returns text/csv.",
            "schema": {
              "enum": [
                "csv"
              ],
              "type": "string"
            }
          }
        ],
        "responses": {
          "401": {
            "description": "Unauthorized",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ProblemDetails"
                }
              }
            }
          },
          "403": {
            "description": "Forbidden",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ProblemDetails"
                }
              }
            }
          },
          "429": {
            "description": "Too Many Requests",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ProblemDetails"
                }
              }
            }
          },
          "200": {
            "description": "A page of transfers and the cursor for the next page (null on the last page).",
            "headers": {
              "X-Next-Cursor": {
                "description": "text/csv only: the page's nextCursor. Absent on the last page. Pass it as ?after= (or follow the Link header) to fetch the next page.",
                "schema": {
                  "type": "string"
                }
              },
              "Link": {
                "description": "text/csv only, alongside X-Next-Cursor: <absolute URL of the next page>; rel=\"next\".",
                "schema": {
                  "type": "string"
                }
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/PagedResponseOfTransferSummaryDto"
                }
              },
              "text/csv": {
                "schema": {
                  "type": "string",
                  "description": "CSV document: UTF-8 with BOM, CRLF rows, RFC 4180 quoting. The header row holds the JSON property names of one item (nested objects flattened as parent.child); one row per item of the page."
                }
              }
            }
          },
          "304": {
            "description": "Unchanged since the `If-None-Match` ETag."
          },
          "400": {
            "description": "Unknown `status` or `approvalStatus`, or unusable `after` cursor (field listed in `errors`).",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ValidationProblemDetails"
                }
              }
            }
          }
        }
      },
      "post": {
        "tags": [
          "Transfers"
        ],
        "summary": "Creates a standalone transfer of stock from one location to another (Transfers → Create).",
        "description": "Requires `ManageTransfers` and an `Idempotency-Key` header. Body:\n`{ \"sourceLocationId\": 1, \"destinationLocationId\": 4, \"notes\": \"Restock van 2\", \"lines\": [ { \"itemId\": 12, \"quantity\": 3 } ] }`.\nFor each line the server picks the concrete units the way stock moves do: units of that item\nat exactly `sourceLocationId` (not its child locations) that are not in transit, not in\na bundle and not already on an open transfer — fewest units consumed, then oldest first.\nThe units are grouped into transfer lines and the transfer is created `Pending`. When its\nvalue reaches the organization's approval threshold it also gets an approval request\n(`approvalStatus``Pending`, managers are emailed) and must be approved\n(`POST /api/v1/transfers/{id}/approve`, which moves it straight to `InTransit`) or\nrejected before anything else can happen. Otherwise the lifecycle is\n`POST …/pick` → `POST …/start` → `POST …/complete` (or `POST …/cancel`\nwhile `Pending`). Nothing is written unless every line can be filled. Returns the new\ntransfer; its `Location` header is `/api/v1/transfers/{id}`.",
        "requestBody": {
          "description": "Source, destination, optional notes and the lines to move.",
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/CreateTransferRequest"
              }
            },
            "text/json": {
              "schema": {
                "$ref": "#/components/schemas/CreateTransferRequest"
              }
            },
            "application/*+json": {
              "schema": {
                "$ref": "#/components/schemas/CreateTransferRequest"
              }
            }
          }
        },
        "responses": {
          "401": {
            "description": "Unauthorized",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ProblemDetails"
                }
              }
            }
          },
          "403": {
            "description": "Forbidden",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ProblemDetails"
                }
              }
            }
          },
          "429": {
            "description": "Too Many Requests",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ProblemDetails"
                }
              }
            }
          },
          "201": {
            "description": "The created transfer with its lines.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/TransferDetailDto"
                }
              }
            }
          },
          "400": {
            "description": "Malformed body (missing locations, no lines, more than 100 lines, quantity outside 1–1000) or missing `Idempotency-Key`.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ValidationProblemDetails"
                }
              }
            }
          },
          "409": {
            "description": "Fewer available units at the source than a line asks for (detail names the item), or the key's creator is unknown on this server.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ProblemDetails"
                }
              }
            }
          },
          "422": {
            "description": "Unknown, foreign or inactive location, source equal to destination, or unknown / deleted item (field listed in `errors`, e.g. `lines[0].itemId`).",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ValidationProblemDetails"
                }
              }
            }
          },
          "500": {
            "description": "The transfer could not be saved. Retry with the same `Idempotency-Key`.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ProblemDetails"
                }
              }
            }
          }
        }
      }
    },
    "/api/v1/transfers/{id}": {
      "get": {
        "tags": [
          "Transfers"
        ],
        "summary": "Returns one transfer with its item lines and approval state.",
        "description": "Requires `ManageTransfers`. Response carries a weak `ETag`; resend it as\n`If-None-Match` to get 304 when nothing changed.",
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "description": "Transfer id from the list endpoint.",
            "required": true,
            "schema": {
              "type": "integer",
              "format": "int32"
            }
          }
        ],
        "responses": {
          "401": {
            "description": "Unauthorized",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ProblemDetails"
                }
              }
            }
          },
          "403": {
            "description": "Forbidden",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ProblemDetails"
                }
              }
            }
          },
          "429": {
            "description": "Too Many Requests",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ProblemDetails"
                }
              }
            }
          },
          "200": {
            "description": "The transfer.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/TransferDetailDto"
                }
              }
            }
          },
          "304": {
            "description": "Unchanged since the `If-None-Match` ETag."
          },
          "404": {
            "description": "No such transfer in this organization.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ProblemDetails"
                }
              }
            }
          }
        }
      }
    },
    "/api/v1/transfers/{id}/approve": {
      "post": {
        "tags": [
          "Transfers"
        ],
        "summary": "Approves a transfer that is awaiting approval (Pending Approvals → Approve).",
        "description": "Requires `ManageTransfers` and an `Idempotency-Key` header; no body. Only a\ntransfer whose `approvalStatus` is `Pending` and whose `status` is still\n`Pending` or `Draft` can be approved (list them with\n`?approvalStatus=Pending&status=Pending`) — standalone\ntransfers whose value exceeded the organization's approval threshold; job-site transfers\nnever need approval. The transfer moves to `InTransit` and the requester is emailed.",
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "description": "Transfer id from `/api/v1/transfers`.",
            "required": true,
            "schema": {
              "type": "integer",
              "format": "int32"
            }
          }
        ],
        "responses": {
          "401": {
            "description": "Unauthorized",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ProblemDetails"
                }
              }
            }
          },
          "403": {
            "description": "Forbidden",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ProblemDetails"
                }
              }
            }
          },
          "429": {
            "description": "Too Many Requests",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ProblemDetails"
                }
              }
            }
          },
          "200": {
            "description": "Transfer id and confirmation message (`Transfer approved.`).",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiOperationResponse"
                }
              }
            }
          },
          "400": {
            "description": "Missing `Idempotency-Key`.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ValidationProblemDetails"
                }
              }
            }
          },
          "404": {
            "description": "No such transfer in this organization.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ProblemDetails"
                }
              }
            }
          },
          "409": {
            "description": "The transfer has no pending approval (none requested, already decided, or the transfer was since cancelled, completed or moved), or the key's creator is unknown on this server.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ProblemDetails"
                }
              }
            }
          },
          "500": {
            "description": "The approval could not be saved. Retry with the same `Idempotency-Key`.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ProblemDetails"
                }
              }
            }
          }
        }
      }
    },
    "/api/v1/transfers/{id}/cancel": {
      "post": {
        "tags": [
          "Transfers"
        ],
        "summary": "Cancels a job-site or standalone transfer and releases its stock.",
        "description": "Requires `ManageTransfers` and an `Idempotency-Key` header; no body. A job-site\ntransfer (`isJobTransfer` true) is cancelled like the Job Details \"Cancel\" button: the\nstock becomes available again, the job lines return to `Planned` and a \"Transfer\ncancelled\" entry is added to the job history. A standalone transfer is cancelled like the\nTransfers page's \"Cancel Transfer\" button, which is offered only while it is `Draft`, or\n`Pending` and not awaiting approval (reject it instead). Sales-order transfers are\ncancelled in the app.",
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "description": "Transfer id from `/api/v1/transfers`.",
            "required": true,
            "schema": {
              "type": "integer",
              "format": "int32"
            }
          }
        ],
        "responses": {
          "401": {
            "description": "Unauthorized",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ProblemDetails"
                }
              }
            }
          },
          "403": {
            "description": "Forbidden",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ProblemDetails"
                }
              }
            }
          },
          "429": {
            "description": "Too Many Requests",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ProblemDetails"
                }
              }
            }
          },
          "200": {
            "description": "Transfer id and confirmation message (e.g. `Transfer cancelled. Items are now available again.`).",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiOperationResponse"
                }
              }
            }
          },
          "400": {
            "description": "Missing `Idempotency-Key`.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ValidationProblemDetails"
                }
              }
            }
          },
          "404": {
            "description": "No such transfer in this organization.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ProblemDetails"
                }
              }
            }
          },
          "409": {
            "description": "The transfer is completed or already cancelled, a standalone transfer that is in transit or awaiting approval, a sales-order transfer, or the key's creator is unknown on this server.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ProblemDetails"
                }
              }
            }
          },
          "500": {
            "description": "The change could not be saved. Retry with the same `Idempotency-Key`.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ProblemDetails"
                }
              }
            }
          }
        }
      }
    },
    "/api/v1/transfers/{id}/complete": {
      "post": {
        "tags": [
          "Transfers"
        ],
        "summary": "Completes (receives) an in-transit standalone transfer (\"Complete Transfer\"), optionally with lines that did not arrive.",
        "description": "Requires `ManageTransfers` and an `Idempotency-Key` header. Body optional:\n`{ \"failedLines\": [ { \"lineId\": 911, \"reason\": \"Damaged in transit\" } ] }` marks those\nlines failed first (the app's per-line \"Mark as Failed\"); they stay where they are and end\n`Returned`. Every other unit moves to its line's destination, its in-transit flag is\ncleared, `quantityDelivered` is recorded and the transfer becomes `Completed`\n(the `transfer.completed` webhook fires). Only standalone transfers in an in-transit\nstatus (`InTransit`, `InTransitToWaypoint`, `AtWaypoint`,\n`InTransitToDestination`) can be completed; job-site and sales-order transfers are\ncompleted from their job or order.",
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "description": "Transfer id from `/api/v1/transfers`.",
            "required": true,
            "schema": {
              "type": "integer",
              "format": "int32"
            }
          }
        ],
        "requestBody": {
          "description": "Optional lines that did not arrive.",
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/CompleteTransferRequest"
              }
            },
            "text/json": {
              "schema": {
                "$ref": "#/components/schemas/CompleteTransferRequest"
              }
            },
            "application/*+json": {
              "schema": {
                "$ref": "#/components/schemas/CompleteTransferRequest"
              }
            }
          }
        },
        "responses": {
          "401": {
            "description": "Unauthorized",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ProblemDetails"
                }
              }
            }
          },
          "403": {
            "description": "Forbidden",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ProblemDetails"
                }
              }
            }
          },
          "429": {
            "description": "Too Many Requests",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ProblemDetails"
                }
              }
            }
          },
          "200": {
            "description": "Transfer id and confirmation message (e.g. `Transfer completed.`).",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiOperationResponse"
                }
              }
            }
          },
          "400": {
            "description": "Malformed body or missing `Idempotency-Key`.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ValidationProblemDetails"
                }
              }
            }
          },
          "404": {
            "description": "No such transfer in this organization.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ProblemDetails"
                }
              }
            }
          },
          "409": {
            "description": "A job-site or sales-order transfer, not in transit (start it first), a failed line that is not `InTransit`, or the key's creator is unknown on this server.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ProblemDetails"
                }
              }
            }
          },
          "422": {
            "description": "A `failedLines` entry names a line that is not on this transfer, or has a blank reason (field listed in `errors`).",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ValidationProblemDetails"
                }
              }
            }
          },
          "500": {
            "description": "The change could not be saved. Retry with the same `Idempotency-Key`.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ProblemDetails"
                }
              }
            }
          }
        }
      }
    },
    "/api/v1/transfers/{id}/fail": {
      "post": {
        "tags": [
          "Transfers"
        ],
        "summary": "Marks an in-transit standalone transfer failed (the Details page's \"Mark as Failed\").",
        "description": "Requires `ManageTransfers` and an `Idempotency-Key` header; no body. Only a\nstandalone transfer in `InTransit`. Its units keep their current location and their\nin-transit flags are cleared; the transfer becomes `Failed`. To record that only some\nlines failed, complete the transfer with `failedLines` instead.",
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "description": "Transfer id from `/api/v1/transfers`.",
            "required": true,
            "schema": {
              "type": "integer",
              "format": "int32"
            }
          }
        ],
        "responses": {
          "401": {
            "description": "Unauthorized",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ProblemDetails"
                }
              }
            }
          },
          "403": {
            "description": "Forbidden",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ProblemDetails"
                }
              }
            }
          },
          "429": {
            "description": "Too Many Requests",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ProblemDetails"
                }
              }
            }
          },
          "200": {
            "description": "Transfer id and confirmation message.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiOperationResponse"
                }
              }
            }
          },
          "400": {
            "description": "Missing `Idempotency-Key`.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ValidationProblemDetails"
                }
              }
            }
          },
          "404": {
            "description": "No such transfer in this organization.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ProblemDetails"
                }
              }
            }
          },
          "409": {
            "description": "A job-site or sales-order transfer, not `InTransit`, or the key's creator is unknown on this server.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ProblemDetails"
                }
              }
            }
          },
          "500": {
            "description": "The change could not be saved. Retry with the same `Idempotency-Key`.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ProblemDetails"
                }
              }
            }
          }
        }
      }
    },
    "/api/v1/transfers/{id}/lines/{lineId}": {
      "patch": {
        "tags": [
          "Transfers"
        ],
        "summary": "Reduces the number of units one line moves.",
        "description": "Requires `ManageTransfers` and an `Idempotency-Key` header. Body:\n`{ \"quantity\": 2 }`. A line can only shrink (1 up to its current `quantity`): it keeps\nits first units, the others are released and become available again, and\n`quantityPicked` is capped at the new quantity. To move more, create another transfer —\ngrowing a line could cross the approval threshold. Only on a standalone transfer that is\n`Pending` and not awaiting approval, and only for a line that carries stock units.",
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "description": "Transfer id from `/api/v1/transfers`.",
            "required": true,
            "schema": {
              "type": "integer",
              "format": "int32"
            }
          },
          {
            "name": "lineId",
            "in": "path",
            "description": "Line id (`lines[].id`).",
            "required": true,
            "schema": {
              "type": "integer",
              "format": "int32"
            }
          }
        ],
        "requestBody": {
          "description": "The new quantity.",
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/UpdateTransferLineRequest"
              }
            },
            "text/json": {
              "schema": {
                "$ref": "#/components/schemas/UpdateTransferLineRequest"
              }
            },
            "application/*+json": {
              "schema": {
                "$ref": "#/components/schemas/UpdateTransferLineRequest"
              }
            }
          }
        },
        "responses": {
          "401": {
            "description": "Unauthorized",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ProblemDetails"
                }
              }
            }
          },
          "403": {
            "description": "Forbidden",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ProblemDetails"
                }
              }
            }
          },
          "429": {
            "description": "Too Many Requests",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ProblemDetails"
                }
              }
            }
          },
          "200": {
            "description": "Transfer id and confirmation (e.g. `Line quantity reduced from 3 to 2; 1 unit(s) released.`).",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiOperationResponse"
                }
              }
            }
          },
          "400": {
            "description": "Malformed body, missing `Idempotency-Key`, or `quantity` outside 1–1000 (listed in `errors.quantity`).",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ValidationProblemDetails"
                }
              }
            }
          },
          "404": {
            "description": "No such transfer in this organization, or no such line on it.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ProblemDetails"
                }
              }
            }
          },
          "409": {
            "description": "A larger quantity than the line has, a line without stock units, not `Pending`, awaiting approval, a job-site or sales-order transfer, or the key's creator is unknown on this server.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ProblemDetails"
                }
              }
            }
          },
          "500": {
            "description": "The change could not be saved. Retry with the same `Idempotency-Key`.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ProblemDetails"
                }
              }
            }
          }
        }
      }
    },
    "/api/v1/transfers/{id}/lines/{lineId}/pick": {
      "post": {
        "tags": [
          "Transfers"
        ],
        "summary": "Picks units of one line (the scan dialog's per-line pick).",
        "description": "Requires `ManageTransfers` and an `Idempotency-Key` header. Body optional:\n`{ \"quantity\": 2 }` picks two more units; omit it to pick the rest of the line. Only on a\nstandalone transfer that is `Pending` and not awaiting approval. The line's\n`quantityPicked` rises (never above `quantity`); once every line is fully picked the\ntransfer can be started (`POST …/start`). `POST …/pick` picks every line at once.",
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "description": "Transfer id from `/api/v1/transfers`.",
            "required": true,
            "schema": {
              "type": "integer",
              "format": "int32"
            }
          },
          {
            "name": "lineId",
            "in": "path",
            "description": "Line id (`lines[].id` of `GET /api/v1/transfers/{id}`).",
            "required": true,
            "schema": {
              "type": "integer",
              "format": "int32"
            }
          }
        ],
        "requestBody": {
          "description": "Optional number of units.",
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/TransferLinePickRequest"
              }
            },
            "text/json": {
              "schema": {
                "$ref": "#/components/schemas/TransferLinePickRequest"
              }
            },
            "application/*+json": {
              "schema": {
                "$ref": "#/components/schemas/TransferLinePickRequest"
              }
            }
          }
        },
        "responses": {
          "401": {
            "description": "Unauthorized",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ProblemDetails"
                }
              }
            }
          },
          "403": {
            "description": "Forbidden",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ProblemDetails"
                }
              }
            }
          },
          "429": {
            "description": "Too Many Requests",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ProblemDetails"
                }
              }
            }
          },
          "200": {
            "description": "Transfer id and the line's progress (e.g. `Line 1207: 2 of 3 picked.`).",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiOperationResponse"
                }
              }
            }
          },
          "400": {
            "description": "Malformed body, missing `Idempotency-Key`, or `quantity` outside 1–1000 (listed in `errors.quantity`).",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ValidationProblemDetails"
                }
              }
            }
          },
          "404": {
            "description": "No such transfer in this organization, or no such line on it.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ProblemDetails"
                }
              }
            }
          },
          "409": {
            "description": "Already fully picked or more units than are left, not `Pending`, awaiting approval, a job-site or sales-order transfer, or the key's creator is unknown on this server.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ProblemDetails"
                }
              }
            }
          },
          "500": {
            "description": "The change could not be saved. Retry with the same `Idempotency-Key`.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ProblemDetails"
                }
              }
            }
          }
        }
      }
    },
    "/api/v1/transfers/{id}/lines/{lineId}/redirect": {
      "post": {
        "tags": [
          "Transfers"
        ],
        "summary": "Redirects one line to another waypoint, the final destination or another location.",
        "description": "Requires `ManageTransfers` and an `Idempotency-Key` header. Body:\n`{ \"destinationType\": \"Location\", \"destinationId\": 4, \"reason\": \"Side entrance\" }`. A\n`Waypoint` target must be a `Pending` waypoint of this transfer (the line is then\ndropped off there on arrival); `Destination` sends it to\nthe final destination; `Location` (an active location) makes that the line's own\ndestination when the transfer completes. The reason is noted on the line. The transfer must be a\nstandalone transfer that is `Pending` (not awaiting approval) or in transit, and the line\n`Pending` or `InTransit` and not already dropped off at an `Arrived` /\n`Departed` waypoint.",
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "description": "Transfer id from `/api/v1/transfers`.",
            "required": true,
            "schema": {
              "type": "integer",
              "format": "int32"
            }
          },
          {
            "name": "lineId",
            "in": "path",
            "description": "Line id (`lines[].id`).",
            "required": true,
            "schema": {
              "type": "integer",
              "format": "int32"
            }
          }
        ],
        "requestBody": {
          "description": "The new destination.",
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/RedirectTransferLineRequest"
              }
            },
            "text/json": {
              "schema": {
                "$ref": "#/components/schemas/RedirectTransferLineRequest"
              }
            },
            "application/*+json": {
              "schema": {
                "$ref": "#/components/schemas/RedirectTransferLineRequest"
              }
            }
          }
        },
        "responses": {
          "401": {
            "description": "Unauthorized",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ProblemDetails"
                }
              }
            }
          },
          "403": {
            "description": "Forbidden",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ProblemDetails"
                }
              }
            }
          },
          "429": {
            "description": "Too Many Requests",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ProblemDetails"
                }
              }
            }
          },
          "200": {
            "description": "Transfer id and confirmation (e.g. `Line 1207 redirected.`).",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiOperationResponse"
                }
              }
            }
          },
          "400": {
            "description": "Malformed body or missing `Idempotency-Key`.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ValidationProblemDetails"
                }
              }
            }
          },
          "404": {
            "description": "No such transfer in this organization, or no such line on it.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ProblemDetails"
                }
              }
            }
          },
          "409": {
            "description": "The line is delivered, failed or returned, or already dropped off at its waypoint (detail: `Line 1207 was already dropped off at waypoint 1; it cannot be redirected.`), the transfer is completed / cancelled / failed / awaiting approval, a job-site or sales-order transfer, or the key's creator is unknown on this server.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ProblemDetails"
                }
              }
            }
          },
          "422": {
            "description": "Missing `destinationType` or an unusable `destinationId` (field listed in `errors`).",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ValidationProblemDetails"
                }
              }
            }
          },
          "500": {
            "description": "The change could not be saved. Retry with the same `Idempotency-Key`.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ProblemDetails"
                }
              }
            }
          }
        }
      }
    },
    "/api/v1/transfers/{id}/lines/{lineId}/unpick": {
      "post": {
        "tags": [
          "Transfers"
        ],
        "summary": "Removes picked units from one line (the scan dialog's unpick / reset).",
        "description": "Requires `ManageTransfers` and an `Idempotency-Key` header. Body optional:\n`{ \"quantity\": 1 }` removes one picked unit; omit it to clear all of the line's picks.\nSame state rule as `…/lines/{lineId}/pick`. At 0 picked the line is no longer marked scanned.",
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "description": "Transfer id from `/api/v1/transfers`.",
            "required": true,
            "schema": {
              "type": "integer",
              "format": "int32"
            }
          },
          {
            "name": "lineId",
            "in": "path",
            "description": "Line id (`lines[].id`).",
            "required": true,
            "schema": {
              "type": "integer",
              "format": "int32"
            }
          }
        ],
        "requestBody": {
          "description": "Optional number of units.",
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/TransferLinePickRequest"
              }
            },
            "text/json": {
              "schema": {
                "$ref": "#/components/schemas/TransferLinePickRequest"
              }
            },
            "application/*+json": {
              "schema": {
                "$ref": "#/components/schemas/TransferLinePickRequest"
              }
            }
          }
        },
        "responses": {
          "401": {
            "description": "Unauthorized",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ProblemDetails"
                }
              }
            }
          },
          "403": {
            "description": "Forbidden",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ProblemDetails"
                }
              }
            }
          },
          "429": {
            "description": "Too Many Requests",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ProblemDetails"
                }
              }
            }
          },
          "200": {
            "description": "Transfer id and the line's progress (e.g. `Line 1207: 1 of 3 picked.`).",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiOperationResponse"
                }
              }
            }
          },
          "400": {
            "description": "Malformed body, missing `Idempotency-Key`, or `quantity` outside 1–1000 (listed in `errors.quantity`).",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ValidationProblemDetails"
                }
              }
            }
          },
          "404": {
            "description": "No such transfer in this organization, or no such line on it.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ProblemDetails"
                }
              }
            }
          },
          "409": {
            "description": "Nothing picked on the line or more units than are picked, not `Pending`, awaiting approval, a job-site or sales-order transfer, or the key's creator is unknown on this server.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ProblemDetails"
                }
              }
            }
          },
          "500": {
            "description": "The change could not be saved. Retry with the same `Idempotency-Key`.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ProblemDetails"
                }
              }
            }
          }
        }
      }
    },
    "/api/v1/transfers/{id}/pick": {
      "post": {
        "tags": [
          "Transfers"
        ],
        "summary": "Marks every line of a standalone transfer fully picked (the scan dialog's \"Pick All\").",
        "description": "Requires `ManageTransfers` and an `Idempotency-Key` header; no body. Only for a\nstandalone transfer that is `Pending` and not awaiting approval — the only state in\nwhich the app offers scanning. Each line's `quantityPicked` becomes its `quantity`;\nthe transfer can then be started. Picking again is harmless (\"Every line was already fully picked.\").",
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "description": "Transfer id from `/api/v1/transfers`.",
            "required": true,
            "schema": {
              "type": "integer",
              "format": "int32"
            }
          }
        ],
        "responses": {
          "401": {
            "description": "Unauthorized",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ProblemDetails"
                }
              }
            }
          },
          "403": {
            "description": "Forbidden",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ProblemDetails"
                }
              }
            }
          },
          "429": {
            "description": "Too Many Requests",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ProblemDetails"
                }
              }
            }
          },
          "200": {
            "description": "Transfer id and confirmation message (e.g. `Picked 2 line(s), 5 unit(s). The transfer can be started.`).",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiOperationResponse"
                }
              }
            }
          },
          "400": {
            "description": "Missing `Idempotency-Key`.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ValidationProblemDetails"
                }
              }
            }
          },
          "404": {
            "description": "No such transfer in this organization.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ProblemDetails"
                }
              }
            }
          },
          "409": {
            "description": "A job-site or sales-order transfer, a transfer awaiting approval or no longer `Pending`, or the key's creator is unknown on this server.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ProblemDetails"
                }
              }
            }
          },
          "500": {
            "description": "The change could not be saved. Retry with the same `Idempotency-Key`.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ProblemDetails"
                }
              }
            }
          }
        }
      }
    },
    "/api/v1/transfers/{id}/reject": {
      "post": {
        "tags": [
          "Transfers"
        ],
        "summary": "Rejects a transfer that is awaiting approval (Pending Approvals → Reject).",
        "description": "Requires `ManageTransfers` and an `Idempotency-Key` header. Body:\n`{ \"reason\": \"Van 2 is out of service this week.\" }` — the reason is required. Only a\ntransfer whose `approvalStatus` is `Pending` and whose `status` is still\n`Pending` or `Draft` can be rejected. The transfer is\ncancelled, the reason is stored as `rejectionReason` and the requester is emailed.",
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "description": "Transfer id from `/api/v1/transfers`.",
            "required": true,
            "schema": {
              "type": "integer",
              "format": "int32"
            }
          }
        ],
        "requestBody": {
          "description": "The rejection reason.",
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/RejectTransferRequest"
              }
            },
            "text/json": {
              "schema": {
                "$ref": "#/components/schemas/RejectTransferRequest"
              }
            },
            "application/*+json": {
              "schema": {
                "$ref": "#/components/schemas/RejectTransferRequest"
              }
            }
          }
        },
        "responses": {
          "401": {
            "description": "Unauthorized",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ProblemDetails"
                }
              }
            }
          },
          "403": {
            "description": "Forbidden",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ProblemDetails"
                }
              }
            }
          },
          "429": {
            "description": "Too Many Requests",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ProblemDetails"
                }
              }
            }
          },
          "200": {
            "description": "Transfer id and confirmation message (`Transfer rejected.`).",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiOperationResponse"
                }
              }
            }
          },
          "400": {
            "description": "Missing `Idempotency-Key` or a blank `reason` (listed in `errors`).",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ValidationProblemDetails"
                }
              }
            }
          },
          "404": {
            "description": "No such transfer in this organization.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ProblemDetails"
                }
              }
            }
          },
          "409": {
            "description": "The transfer has no pending approval (none requested, already decided, or the transfer was since cancelled, completed or moved), or the key's creator is unknown on this server.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ProblemDetails"
                }
              }
            }
          },
          "500": {
            "description": "The rejection could not be saved. Retry with the same `Idempotency-Key`.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ProblemDetails"
                }
              }
            }
          }
        }
      }
    },
    "/api/v1/transfers/{id}/scan": {
      "post": {
        "tags": [
          "Transfers"
        ],
        "summary": "Scans a barcode into a transfer (the scan dialog): one more unit picked on the matching line.",
        "description": "Requires `ManageTransfers` and an `Idempotency-Key` header (use a new key per scan —\na replayed key does not scan again). Body: `{ \"barcode\": \"0012345678905\", \"locationId\": 1 }`.\nThe barcode may be a unit's barcode or an item template's; the first line of that item (at\n`locationId` when given) that is not fully picked gets one more picked unit. Same state\nrule as `…/lines/{lineId}/pick`.",
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "description": "Transfer id from `/api/v1/transfers`.",
            "required": true,
            "schema": {
              "type": "integer",
              "format": "int32"
            }
          }
        ],
        "requestBody": {
          "description": "The scanned barcode and optional location.",
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/ScanTransferBarcodeRequest"
              }
            },
            "text/json": {
              "schema": {
                "$ref": "#/components/schemas/ScanTransferBarcodeRequest"
              }
            },
            "application/*+json": {
              "schema": {
                "$ref": "#/components/schemas/ScanTransferBarcodeRequest"
              }
            }
          }
        },
        "responses": {
          "401": {
            "description": "Unauthorized",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ProblemDetails"
                }
              }
            }
          },
          "403": {
            "description": "Forbidden",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ProblemDetails"
                }
              }
            }
          },
          "429": {
            "description": "Too Many Requests",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ProblemDetails"
                }
              }
            }
          },
          "200": {
            "description": "Transfer id and the matched line's progress (e.g. `Scanned Copper 12/2: line 1207 has 2 of 3 picked.`).",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiOperationResponse"
                }
              }
            }
          },
          "400": {
            "description": "Malformed body or missing `Idempotency-Key`.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ValidationProblemDetails"
                }
              }
            }
          },
          "404": {
            "description": "No such transfer in this organization.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ProblemDetails"
                }
              }
            }
          },
          "409": {
            "description": "The item is not on the transfer (at that location) or its line is fully picked, not `Pending`, awaiting approval, a job-site or sales-order transfer, or the key's creator is unknown on this server.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ProblemDetails"
                }
              }
            }
          },
          "422": {
            "description": "No item or item template has the barcode (`barcode` in `errors`).",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ValidationProblemDetails"
                }
              }
            }
          },
          "500": {
            "description": "The change could not be saved. Retry with a new `Idempotency-Key`.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ProblemDetails"
                }
              }
            }
          }
        }
      }
    },
    "/api/v1/transfers/{id}/start": {
      "post": {
        "tags": [
          "Transfers"
        ],
        "summary": "Starts a picked standalone transfer (\"Mark as In Transit\").",
        "description": "Requires `ManageTransfers` and an `Idempotency-Key` header; no body. The transfer\nmust be a standalone transfer, `Pending`, not awaiting approval, and every line fully\npicked (`POST /api/v1/transfers/{id}/pick`). The transfer and its lines become\n`InTransit` and its units are flagged in transit until it is completed or failed.",
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "description": "Transfer id from `/api/v1/transfers`.",
            "required": true,
            "schema": {
              "type": "integer",
              "format": "int32"
            }
          }
        ],
        "responses": {
          "401": {
            "description": "Unauthorized",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ProblemDetails"
                }
              }
            }
          },
          "403": {
            "description": "Forbidden",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ProblemDetails"
                }
              }
            }
          },
          "429": {
            "description": "Too Many Requests",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ProblemDetails"
                }
              }
            }
          },
          "200": {
            "description": "Transfer id and confirmation message (`Transfer is in transit.`).",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiOperationResponse"
                }
              }
            }
          },
          "400": {
            "description": "Missing `Idempotency-Key`.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ValidationProblemDetails"
                }
              }
            }
          },
          "404": {
            "description": "No such transfer in this organization.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ProblemDetails"
                }
              }
            }
          },
          "409": {
            "description": "A job-site or sales-order transfer, awaiting approval, not `Pending`, lines not fully picked (detail names them), or the key's creator is unknown on this server.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ProblemDetails"
                }
              }
            }
          },
          "500": {
            "description": "The change could not be saved. Retry with the same `Idempotency-Key`.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ProblemDetails"
                }
              }
            }
          }
        }
      }
    },
    "/api/v1/transfers/{id}/submit": {
      "post": {
        "tags": [
          "Transfers"
        ],
        "summary": "Submits a Draft standalone transfer (the Details page's \"Submit Transfer\"): it becomes Pending.",
        "description": "Requires `ManageTransfers` and an `Idempotency-Key` header; no body. Only a\nstandalone transfer in `Draft` (transfers created by `POST /api/v1/transfers` are\nalready `Pending`). The approval rule of a new transfer applies: when its value reaches\nthe organization's threshold an approval is requested (`approvalStatus``Pending`,\nmanagers are emailed) and only approve / reject apply until it is decided.",
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "description": "Transfer id from `/api/v1/transfers`.",
            "required": true,
            "schema": {
              "type": "integer",
              "format": "int32"
            }
          }
        ],
        "responses": {
          "401": {
            "description": "Unauthorized",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ProblemDetails"
                }
              }
            }
          },
          "403": {
            "description": "Forbidden",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ProblemDetails"
                }
              }
            }
          },
          "429": {
            "description": "Too Many Requests",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ProblemDetails"
                }
              }
            }
          },
          "200": {
            "description": "Transfer id and confirmation message (e.g. `Transfer submitted. Pick its lines, then start it.`).",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiOperationResponse"
                }
              }
            }
          },
          "400": {
            "description": "Missing `Idempotency-Key`.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ValidationProblemDetails"
                }
              }
            }
          },
          "404": {
            "description": "No such transfer in this organization.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ProblemDetails"
                }
              }
            }
          },
          "409": {
            "description": "Not `Draft`, a job-site or sales-order transfer, or the key's creator is unknown on this server.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ProblemDetails"
                }
              }
            }
          },
          "500": {
            "description": "The change could not be saved. Retry with the same `Idempotency-Key`.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ProblemDetails"
                }
              }
            }
          }
        }
      }
    },
    "/api/v1/transfers/{id}/waypoints/{waypointId}/arrive": {
      "post": {
        "tags": [
          "Transfers"
        ],
        "summary": "Arrives at a waypoint: the lines assigned to it are dropped off there.",
        "description": "Requires `ManageTransfers` and an `Idempotency-Key` header; no body. The transfer must\nbe a standalone transfer in an in-transit status and the waypoint (`waypoints[].id`)\n`Pending`. The units of every line whose `destinationWaypointId` is this waypoint move\nto its location (with their bundles); the waypoint becomes `Arrived` and the transfer\n`AtWaypoint`. The final destination is reached with `POST …/complete`.",
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "description": "Transfer id from `/api/v1/transfers`.",
            "required": true,
            "schema": {
              "type": "integer",
              "format": "int32"
            }
          },
          {
            "name": "waypointId",
            "in": "path",
            "description": "Waypoint id (`waypoints[].id` of `GET /api/v1/transfers/{id}`).",
            "required": true,
            "schema": {
              "type": "integer",
              "format": "int32"
            }
          }
        ],
        "responses": {
          "401": {
            "description": "Unauthorized",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ProblemDetails"
                }
              }
            }
          },
          "403": {
            "description": "Forbidden",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ProblemDetails"
                }
              }
            }
          },
          "429": {
            "description": "Too Many Requests",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ProblemDetails"
                }
              }
            }
          },
          "200": {
            "description": "Transfer id and confirmation (e.g. `Waypoint marked as arrived. 1 item(s) delivered to Van 2.`).",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiOperationResponse"
                }
              }
            }
          },
          "400": {
            "description": "Missing `Idempotency-Key`.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ValidationProblemDetails"
                }
              }
            }
          },
          "404": {
            "description": "No such transfer in this organization, or no such waypoint on it.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ProblemDetails"
                }
              }
            }
          },
          "409": {
            "description": "The waypoint is not `Pending`, the transfer is not in transit, a job-site or sales-order transfer, or the key's creator is unknown on this server.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ProblemDetails"
                }
              }
            }
          },
          "500": {
            "description": "The change could not be saved. Retry with the same `Idempotency-Key`.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ProblemDetails"
                }
              }
            }
          }
        }
      }
    },
    "/api/v1/transfers/{id}/waypoints/{waypointId}/deliver": {
      "post": {
        "tags": [
          "Transfers"
        ],
        "summary": "Records delivery of every line at an arrived waypoint (Waypoint Delivery → Deliver All Items).",
        "description": "Requires `ManageTransfers` and an `Idempotency-Key` header; no body. The waypoint must\nbe `Arrived` (the stock already moved on arrival); a \"Delivered at …\" note is added to each\nof its lines. Same transfer rule as `…/arrive`.",
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "description": "Transfer id from `/api/v1/transfers`.",
            "required": true,
            "schema": {
              "type": "integer",
              "format": "int32"
            }
          },
          {
            "name": "waypointId",
            "in": "path",
            "description": "Waypoint id (`waypoints[].id`).",
            "required": true,
            "schema": {
              "type": "integer",
              "format": "int32"
            }
          }
        ],
        "responses": {
          "401": {
            "description": "Unauthorized",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ProblemDetails"
                }
              }
            }
          },
          "403": {
            "description": "Forbidden",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ProblemDetails"
                }
              }
            }
          },
          "429": {
            "description": "Too Many Requests",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ProblemDetails"
                }
              }
            }
          },
          "200": {
            "description": "Transfer id and confirmation.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiOperationResponse"
                }
              }
            }
          },
          "400": {
            "description": "Missing `Idempotency-Key`.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ValidationProblemDetails"
                }
              }
            }
          },
          "404": {
            "description": "No such transfer in this organization, or no such waypoint on it.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ProblemDetails"
                }
              }
            }
          },
          "409": {
            "description": "The waypoint is not `Arrived`, the transfer is not in transit, a job-site or sales-order transfer, or the key's creator is unknown on this server.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ProblemDetails"
                }
              }
            }
          },
          "500": {
            "description": "The change could not be saved. Retry with the same `Idempotency-Key`.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ProblemDetails"
                }
              }
            }
          }
        }
      }
    },
    "/api/v1/transfers/{id}/waypoints/{waypointId}/depart": {
      "post": {
        "tags": [
          "Transfers"
        ],
        "summary": "Departs from an arrived waypoint towards the next stop.",
        "description": "Requires `ManageTransfers` and an `Idempotency-Key` header; no body. The waypoint must\nbe `Arrived`; it becomes `Departed` and the transfer `InTransitToWaypoint` (heading\nto the next waypoint, `currentWaypointId`) or `InTransitToDestination`. Same transfer\nrule as `…/arrive`.",
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "description": "Transfer id from `/api/v1/transfers`.",
            "required": true,
            "schema": {
              "type": "integer",
              "format": "int32"
            }
          },
          {
            "name": "waypointId",
            "in": "path",
            "description": "Waypoint id (`waypoints[].id`).",
            "required": true,
            "schema": {
              "type": "integer",
              "format": "int32"
            }
          }
        ],
        "responses": {
          "401": {
            "description": "Unauthorized",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ProblemDetails"
                }
              }
            }
          },
          "403": {
            "description": "Forbidden",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ProblemDetails"
                }
              }
            }
          },
          "429": {
            "description": "Too Many Requests",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ProblemDetails"
                }
              }
            }
          },
          "200": {
            "description": "Transfer id and confirmation (`Waypoint status updated to Departed`).",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiOperationResponse"
                }
              }
            }
          },
          "400": {
            "description": "Missing `Idempotency-Key`.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ValidationProblemDetails"
                }
              }
            }
          },
          "404": {
            "description": "No such transfer in this organization, or no such waypoint on it.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ProblemDetails"
                }
              }
            }
          },
          "409": {
            "description": "The waypoint is not `Arrived`, the transfer is not in transit, a job-site or sales-order transfer, or the key's creator is unknown on this server.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ProblemDetails"
                }
              }
            }
          },
          "500": {
            "description": "The change could not be saved. Retry with the same `Idempotency-Key`.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ProblemDetails"
                }
              }
            }
          }
        }
      }
    },
    "/api/v1/transfers/{id}/waypoints/{waypointId}/skip": {
      "post": {
        "tags": [
          "Transfers"
        ],
        "summary": "Skips a waypoint, redirecting the lines still assigned to it (Skip Waypoint).",
        "description": "Requires `ManageTransfers` and an `Idempotency-Key` header. Body (required when lines\nare still to be dropped off there): `{ \"destinationType\": \"Waypoint\", \"destinationId\": 42 }`\n(a later `Pending` waypoint), `{ \"destinationType\": \"Destination\" }` or\n`{ \"destinationType\": \"Location\", \"destinationId\": 4 }` (an active location, added as a\ndrop-off waypoint at the end of the route). The waypoint must be `Pending` (an\n`Arrived` one already had its lines dropped off - leave it with `…/depart`); it becomes `Skipped` and the transfer moves on to the next `Pending`\nwaypoint or `InTransitToDestination`. Same transfer rule as `…/arrive`.",
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "description": "Transfer id from `/api/v1/transfers`.",
            "required": true,
            "schema": {
              "type": "integer",
              "format": "int32"
            }
          },
          {
            "name": "waypointId",
            "in": "path",
            "description": "Waypoint id (`waypoints[].id`).",
            "required": true,
            "schema": {
              "type": "integer",
              "format": "int32"
            }
          }
        ],
        "requestBody": {
          "description": "Where the waypoint's lines go instead.",
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/SkipTransferWaypointRequest"
              }
            },
            "text/json": {
              "schema": {
                "$ref": "#/components/schemas/SkipTransferWaypointRequest"
              }
            },
            "application/*+json": {
              "schema": {
                "$ref": "#/components/schemas/SkipTransferWaypointRequest"
              }
            }
          }
        },
        "responses": {
          "401": {
            "description": "Unauthorized",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ProblemDetails"
                }
              }
            }
          },
          "403": {
            "description": "Forbidden",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ProblemDetails"
                }
              }
            }
          },
          "429": {
            "description": "Too Many Requests",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ProblemDetails"
                }
              }
            }
          },
          "200": {
            "description": "Transfer id and confirmation (e.g. `Waypoint skipped. 2 item(s) redirected.`).",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiOperationResponse"
                }
              }
            }
          },
          "400": {
            "description": "Malformed body or missing `Idempotency-Key`.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ValidationProblemDetails"
                }
              }
            }
          },
          "404": {
            "description": "No such transfer in this organization, or no such waypoint on it.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ProblemDetails"
                }
              }
            }
          },
          "409": {
            "description": "The waypoint is `Arrived`, `Departed` or already `Skipped`, the transfer is not in transit, a job-site or sales-order transfer, or the key's creator is unknown on this server.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ProblemDetails"
                }
              }
            }
          },
          "422": {
            "description": "Lines still assigned and no `destinationType`, or an unusable `destinationId` (field listed in `errors`).",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ValidationProblemDetails"
                }
              }
            }
          },
          "500": {
            "description": "The change could not be saved. Retry with the same `Idempotency-Key`.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ProblemDetails"
                }
              }
            }
          }
        }
      }
    },
    "/api/v1/users": {
      "get": {
        "tags": [
          "Users"
        ],
        "summary": "Lists the organization's users with the schedule groups each belongs to.",
        "description": "Users are keyed by string ids, so this collection is <b>not</b> cursor-paginated: the\nwhole list is returned in one response (ordered by first then last name) with\n`nextCursor` always `null`. The response carries an `ETag`; send it as\n`If-None-Match` to get `304` when nothing changed. Example:\n`GET /api/v1/users?scheduleGroupId=5` returns the members of group 5.\nAlso available as CSV (`Accept: text/csv` or `?format=csv`); paging cursor in `X-Next-Cursor`.",
        "parameters": [
          {
            "name": "includeInactive",
            "in": "query",
            "description": "Also return deactivated accounts (default `false`).",
            "schema": {
              "type": "boolean",
              "default": false
            }
          },
          {
            "name": "scheduleGroupId",
            "in": "query",
            "description": "Only users who are members of this schedule group.",
            "schema": {
              "type": "integer",
              "format": "int32"
            }
          },
          {
            "name": "format",
            "in": "query",
            "description": "Alternative to the Accept header: `csv` returns text/csv.",
            "schema": {
              "enum": [
                "csv"
              ],
              "type": "string"
            }
          }
        ],
        "responses": {
          "401": {
            "description": "Unauthorized",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ProblemDetails"
                }
              }
            }
          },
          "403": {
            "description": "The `scheduling` module is not active for this organization.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ProblemDetails"
                }
              }
            }
          },
          "429": {
            "description": "Too Many Requests",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ProblemDetails"
                }
              }
            }
          },
          "200": {
            "description": "All matching users.",
            "headers": {
              "X-Next-Cursor": {
                "description": "text/csv only: the page's nextCursor. Absent on the last page. Pass it as ?after= (or follow the Link header) to fetch the next page.",
                "schema": {
                  "type": "string"
                }
              },
              "Link": {
                "description": "text/csv only, alongside X-Next-Cursor: <absolute URL of the next page>; rel=\"next\".",
                "schema": {
                  "type": "string"
                }
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/PagedResponseOfUserSummaryDto"
                }
              },
              "text/csv": {
                "schema": {
                  "type": "string",
                  "description": "CSV document: UTF-8 with BOM, CRLF rows, RFC 4180 quoting. The header row holds the JSON property names of one item (nested objects flattened as parent.child); one row per item of the page."
                }
              }
            }
          },
          "304": {
            "description": "Unchanged since the supplied `If-None-Match`."
          },
          "404": {
            "description": "scheduleGroupId is not a group of this organization.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ProblemDetails"
                }
              }
            }
          }
        }
      },
      "post": {
        "tags": [
          "Users"
        ],
        "summary": "Creates an account in the organization, like Users → Add User in the app.",
        "description": "Requires `ManageUsers` and an `Idempotency-Key`. The sync server creates the account\n(the same call the Add User page makes); it can sign in to the web app at once and on devices at\ntheir next sign-in. The key's creator must be an OrganizationAdmin (or higher); `role` may be\nat most the creator's own; SystemAdmin is never assignable; and every permission the new account\nwould get (its role's, plus `customRoleId`'s) must be held by the API key and by its creator.\nThe password must meet the Flex password rule and is never returned. An active account counts as\na billed seat once the organization is past its included seats. Example:\n`{ \"email\": \"sam@example.com\", \"firstName\": \"Sam\", \"lastName\": \"Carter\", \"password\": \"Welcome#2026\", \"role\": \"Manager\" }`.",
        "requestBody": {
          "description": "The new account.",
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/CreateUserApiRequest"
              }
            },
            "text/json": {
              "schema": {
                "$ref": "#/components/schemas/CreateUserApiRequest"
              }
            },
            "application/*+json": {
              "schema": {
                "$ref": "#/components/schemas/CreateUserApiRequest"
              }
            }
          }
        },
        "responses": {
          "401": {
            "description": "Unauthorized",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ProblemDetails"
                }
              }
            }
          },
          "403": {
            "description": "A hierarchy or grant rule refused it (the detail says which).",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ProblemDetails"
                }
              }
            }
          },
          "429": {
            "description": "Too Many Requests",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ProblemDetails"
                }
              }
            }
          },
          "201": {
            "description": "Created; `Location` points at the new account.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/UserDetailDto"
                }
              }
            }
          },
          "202": {
            "description": "The sync server created the account but this host has not received it yet (it appears after the next sync).",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/UserPendingDto"
                }
              }
            }
          },
          "400": {
            "description": "Malformed body, an unknown `role`, or missing `Idempotency-Key`.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ValidationProblemDetails"
                }
              }
            }
          },
          "409": {
            "description": "The key's creator is unknown on this server.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ProblemDetails"
                }
              }
            }
          },
          "422": {
            "description": "A value is unacceptable: e-mail already in use, weak password, unknown `customRoleId`.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ValidationProblemDetails"
                }
              }
            }
          },
          "502": {
            "description": "The sync server answered with an unexpected error.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ProblemDetails"
                }
              }
            }
          },
          "503": {
            "description": "This server cannot reach the sync server (not registered or offline); nothing was created.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ProblemDetails"
                }
              }
            }
          }
        }
      }
    },
    "/api/v1/users/{id}": {
      "get": {
        "tags": [
          "Users"
        ],
        "summary": "Returns one account with its roles and effective permissions.",
        "description": "Requires `ManageUsers` (no module). Sends a weak `ETag`. Example:\n`GET /api/v1/users/8f0c3c1e-2b6d-4c55-9a51-0d3f3c1b7e21`.",
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "description": "User id (string GUID) from `GET /api/v1/users`.",
            "required": true,
            "schema": {
              "type": "string"
            }
          }
        ],
        "responses": {
          "401": {
            "description": "Unauthorized",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ProblemDetails"
                }
              }
            }
          },
          "403": {
            "description": "Forbidden",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ProblemDetails"
                }
              }
            }
          },
          "429": {
            "description": "Too Many Requests",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ProblemDetails"
                }
              }
            }
          },
          "200": {
            "description": "The account.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/UserDetailDto"
                }
              }
            }
          },
          "304": {
            "description": "Unchanged since the supplied ETag."
          },
          "404": {
            "description": "No such user in this organization.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ProblemDetails"
                }
              }
            }
          }
        }
      },
      "patch": {
        "tags": [
          "Users"
        ],
        "summary": "Edits an account; only the fields sent change.",
        "description": "Requires `ManageUsers` and an `Idempotency-Key`. The account must belong to the\norganization, must not be the key creator's own, and must hold a strictly lesser role than the\ncreator (an OrganizationAdmin manages Managers and Users). `role` may rank up to the creator's\nown, so an OrganizationAdmin can promote a Manager or User to `OrganizationAdmin`. `role` and `isActive` go through the sync server like the user list's buttons;\nnames and `customRoleIds` are written here. Whatever the edit adds to the account's\npermissions (all of them when it reactivates the account) must be held by the API key and its\ncreator. Demoting or deactivating an Organization Admin is refused (403): use\n`POST /api/v1/users/{id}/removal-requests` (their approval or 14 days' notice). Every rule is\nchecked before anything changes. Example:\n`PATCH /api/v1/users/8f0c3c1e-2b6d-4c55-9a51-0d3f3c1b7e21 { \"role\": \"Manager\", \"customRoleIds\": [4] }`.",
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "description": "User id (string GUID).",
            "required": true,
            "schema": {
              "type": "string"
            }
          }
        ],
        "requestBody": {
          "description": "The fields to change.",
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/UpdateUserApiRequest"
              }
            },
            "text/json": {
              "schema": {
                "$ref": "#/components/schemas/UpdateUserApiRequest"
              }
            },
            "application/*+json": {
              "schema": {
                "$ref": "#/components/schemas/UpdateUserApiRequest"
              }
            }
          }
        },
        "responses": {
          "401": {
            "description": "Unauthorized",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ProblemDetails"
                }
              }
            }
          },
          "403": {
            "description": "A hierarchy or grant rule refused it (the detail says which).",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ProblemDetails"
                }
              }
            }
          },
          "429": {
            "description": "Too Many Requests",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ProblemDetails"
                }
              }
            }
          },
          "200": {
            "description": "The account after the edit.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/UserDetailDto"
                }
              }
            }
          },
          "400": {
            "description": "Malformed body, an unknown `role`, or missing `Idempotency-Key`.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ValidationProblemDetails"
                }
              }
            }
          },
          "404": {
            "description": "No such user in this organization.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ProblemDetails"
                }
              }
            }
          },
          "409": {
            "description": "The key's creator is unknown on this server, or the change would remove the organization's last active Organization Admin.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ProblemDetails"
                }
              }
            }
          },
          "422": {
            "description": "A value is unacceptable (blank name, unknown custom role id).",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ValidationProblemDetails"
                }
              }
            }
          },
          "502": {
            "description": "The sync server answered with an unexpected error.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ProblemDetails"
                }
              }
            }
          },
          "503": {
            "description": "This server cannot reach the sync server; the role / active changes were not made.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ProblemDetails"
                }
              }
            }
          }
        }
      }
    },
    "/api/v1/users/{id}/deactivate": {
      "post": {
        "tags": [
          "Users"
        ],
        "summary": "Deactivates an account: it can no longer sign in and stops counting as a seat.",
        "description": "Requires `ManageUsers` and an `Idempotency-Key`; no body. Same rules as\n`PATCH { \"isActive\": false }` - an Organization Admin needs a removal request instead. Already\ninactive → 200 unchanged. Example:\n`POST /api/v1/users/8f0c3c1e-2b6d-4c55-9a51-0d3f3c1b7e21/deactivate`.",
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "description": "User id (string GUID).",
            "required": true,
            "schema": {
              "type": "string"
            }
          }
        ],
        "responses": {
          "401": {
            "description": "Unauthorized",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ProblemDetails"
                }
              }
            }
          },
          "403": {
            "description": "A hierarchy rule refused it (an Organization Admin needs a removal request).",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ProblemDetails"
                }
              }
            }
          },
          "429": {
            "description": "Too Many Requests",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ProblemDetails"
                }
              }
            }
          },
          "200": {
            "description": "The account after the change.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/UserDetailDto"
                }
              }
            }
          },
          "400": {
            "description": "Missing `Idempotency-Key`.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ValidationProblemDetails"
                }
              }
            }
          },
          "404": {
            "description": "No such user in this organization.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ProblemDetails"
                }
              }
            }
          },
          "409": {
            "description": "The key's creator is unknown on this server, or it would remove the organization's last active Organization Admin.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ProblemDetails"
                }
              }
            }
          },
          "502": {
            "description": "The sync server answered with an unexpected error.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ProblemDetails"
                }
              }
            }
          },
          "503": {
            "description": "This server cannot reach the sync server; nothing was changed.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ProblemDetails"
                }
              }
            }
          }
        }
      }
    },
    "/api/v1/users/{id}/reactivate": {
      "post": {
        "tags": [
          "Users"
        ],
        "summary": "Reactivates a deactivated account.",
        "description": "Requires `ManageUsers` and an `Idempotency-Key`; no body. Same rules as\n`PATCH { \"isActive\": true }`, including the grant limit: every permission the account holds\nmust be held by the API key and its creator. Already active → 200 unchanged. Example:\n`POST /api/v1/users/8f0c3c1e-2b6d-4c55-9a51-0d3f3c1b7e21/reactivate`.",
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "description": "User id (string GUID).",
            "required": true,
            "schema": {
              "type": "string"
            }
          }
        ],
        "responses": {
          "401": {
            "description": "Unauthorized",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ProblemDetails"
                }
              }
            }
          },
          "403": {
            "description": "A hierarchy or grant rule refused it.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ProblemDetails"
                }
              }
            }
          },
          "429": {
            "description": "Too Many Requests",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ProblemDetails"
                }
              }
            }
          },
          "200": {
            "description": "The account after the change.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/UserDetailDto"
                }
              }
            }
          },
          "400": {
            "description": "Missing `Idempotency-Key`.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ValidationProblemDetails"
                }
              }
            }
          },
          "404": {
            "description": "No such user in this organization.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ProblemDetails"
                }
              }
            }
          },
          "409": {
            "description": "The key's creator is unknown on this server.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ProblemDetails"
                }
              }
            }
          },
          "502": {
            "description": "The sync server answered with an unexpected error.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ProblemDetails"
                }
              }
            }
          },
          "503": {
            "description": "This server cannot reach the sync server; nothing was changed.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ProblemDetails"
                }
              }
            }
          }
        }
      }
    },
    "/api/v1/users/{id}/removal-requests": {
      "post": {
        "tags": [
          "UserRemovalRequests"
        ],
        "summary": "Asks to demote or deactivate an Organization Admin (or steps the key's creator down).",
        "description": "Requires `ManageUsers` and an `Idempotency-Key`. The key's creator must be an active\nOrganization Admin (a System Admin changes roles directly instead) and id an\nOrganization Admin of the same organization with no other pending request. The target is notified\n(in the app and by e-mail) and can approve it (applies at once) or contest it until\n`effectiveAt`, 14 days from now; otherwise it is applied shortly after that. When\nid is the key's creator, it is their own approval and applies at once (200).\nRefused (409) when it would leave the organization with no active Organization Admin. Example:\n`POST /api/v1/users/8f0c3c1e-2b6d-4c55-9a51-0d3f3c1b7e21/removal-requests { \"action\": \"Demote\", \"newRole\": \"Manager\" }`.",
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "description": "User id (string GUID) of the Organization Admin to remove.",
            "required": true,
            "schema": {
              "type": "string"
            }
          }
        ],
        "requestBody": {
          "description": "What to do.",
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/CreateAdminRemovalApiRequest"
              }
            },
            "text/json": {
              "schema": {
                "$ref": "#/components/schemas/CreateAdminRemovalApiRequest"
              }
            },
            "application/*+json": {
              "schema": {
                "$ref": "#/components/schemas/CreateAdminRemovalApiRequest"
              }
            }
          }
        },
        "responses": {
          "401": {
            "description": "Unauthorized",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ProblemDetails"
                }
              }
            }
          },
          "403": {
            "description": "The key's creator may not request this (not an Organization Admin, or a System Admin).",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ProblemDetails"
                }
              }
            }
          },
          "429": {
            "description": "Too Many Requests",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ProblemDetails"
                }
              }
            }
          },
          "200": {
            "description": "Self-removal: the change was made (`applied: true`).",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/AdminRemovalOutcomeDto"
                }
              }
            }
          },
          "201": {
            "description": "Requested; `Location` points at the list.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/AdminRemovalOutcomeDto"
                }
              }
            }
          },
          "400": {
            "description": "Malformed body or missing `Idempotency-Key`.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ValidationProblemDetails"
                }
              }
            }
          },
          "404": {
            "description": "No such user in this organization.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ProblemDetails"
                }
              }
            }
          },
          "409": {
            "description": "A request for this admin is already pending, it would remove the last active Organization Admin, or the key's creator is unknown here.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ProblemDetails"
                }
              }
            }
          },
          "422": {
            "description": "The account is not an Organization Admin, `newRole` is missing, or the account is already inactive.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ValidationProblemDetails"
                }
              }
            }
          },
          "502": {
            "description": "The sync server answered with an unexpected error.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ProblemDetails"
                }
              }
            }
          },
          "503": {
            "description": "This server cannot reach the sync server; nothing was requested.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ProblemDetails"
                }
              }
            }
          }
        }
      }
    },
    "/api/v1/users/{id}/removal-requests/{requestId}/approve": {
      "post": {
        "tags": [
          "UserRemovalRequests"
        ],
        "summary": "The admin being removed agrees: the change is made now.",
        "description": "Requires `ManageUsers` and an `Idempotency-Key`; no body. Only the admin named in the\nrequest (the key's creator must be them) can approve, at any time while it is `Pending` -\nalso after `effectiveAt` if it has not been applied yet. Refused (409) once closed, or when it\nwould remove the last active Organization Admin (the request then closes as `Cancelled`). Example:\n`POST /api/v1/users/8f0c3c1e-2b6d-4c55-9a51-0d3f3c1b7e21/removal-requests/12/approve`.",
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "description": "User id (string GUID) of the admin named in the request.",
            "required": true,
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "requestId",
            "in": "path",
            "description": "Removal request id from `GET /api/v1/users/removal-requests`.",
            "required": true,
            "schema": {
              "type": "integer",
              "format": "int32"
            }
          }
        ],
        "responses": {
          "401": {
            "description": "Unauthorized",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ProblemDetails"
                }
              }
            }
          },
          "403": {
            "description": "The key's creator is not the admin named in the request.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ProblemDetails"
                }
              }
            }
          },
          "429": {
            "description": "Too Many Requests",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ProblemDetails"
                }
              }
            }
          },
          "200": {
            "description": "Approved and applied.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/AdminRemovalOutcomeDto"
                }
              }
            }
          },
          "400": {
            "description": "Missing `Idempotency-Key`.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ValidationProblemDetails"
                }
              }
            }
          },
          "404": {
            "description": "No such request for this user in this organization.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ProblemDetails"
                }
              }
            }
          },
          "409": {
            "description": "The request is no longer pending, the change can no longer apply, or the key's creator is unknown here.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ProblemDetails"
                }
              }
            }
          },
          "502": {
            "description": "The sync server answered with an unexpected error.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ProblemDetails"
                }
              }
            }
          },
          "503": {
            "description": "This server cannot reach the sync server; nothing changed.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ProblemDetails"
                }
              }
            }
          }
        }
      }
    },
    "/api/v1/users/{id}/removal-requests/{requestId}/cancel": {
      "post": {
        "tags": [
          "UserRemovalRequests"
        ],
        "summary": "The requester withdraws a pending removal request.",
        "description": "Requires `ManageUsers` and an `Idempotency-Key`; no body. Only the admin who asked (the\nkey's creator must be them) or a System Admin, while the request is `Pending`. Example:\n`POST /api/v1/users/8f0c3c1e-2b6d-4c55-9a51-0d3f3c1b7e21/removal-requests/12/cancel`.",
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "description": "User id (string GUID) of the admin named in the request.",
            "required": true,
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "requestId",
            "in": "path",
            "description": "Removal request id from `GET /api/v1/users/removal-requests`.",
            "required": true,
            "schema": {
              "type": "integer",
              "format": "int32"
            }
          }
        ],
        "responses": {
          "401": {
            "description": "Unauthorized",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ProblemDetails"
                }
              }
            }
          },
          "403": {
            "description": "The key's creator did not make the request.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ProblemDetails"
                }
              }
            }
          },
          "429": {
            "description": "Too Many Requests",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ProblemDetails"
                }
              }
            }
          },
          "200": {
            "description": "Cancelled; nothing changed.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/AdminRemovalOutcomeDto"
                }
              }
            }
          },
          "400": {
            "description": "Missing `Idempotency-Key`.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ValidationProblemDetails"
                }
              }
            }
          },
          "404": {
            "description": "No such request for this user in this organization.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ProblemDetails"
                }
              }
            }
          },
          "409": {
            "description": "The request is no longer pending, or the key's creator is unknown here.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ProblemDetails"
                }
              }
            }
          },
          "502": {
            "description": "The sync server answered with an unexpected error.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ProblemDetails"
                }
              }
            }
          },
          "503": {
            "description": "This server cannot reach the sync server; nothing changed.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ProblemDetails"
                }
              }
            }
          }
        }
      }
    },
    "/api/v1/users/{id}/removal-requests/{requestId}/contest": {
      "post": {
        "tags": [
          "UserRemovalRequests"
        ],
        "summary": "The admin being removed objects: the request closes and nothing changes.",
        "description": "Requires `ManageUsers` and an `Idempotency-Key`; no body. Only the admin named in the\nrequest (the key's creator must be them), and only before `effectiveAt`: after the 14-day\nnotice period it can no longer be contested (409). Example:\n`POST /api/v1/users/8f0c3c1e-2b6d-4c55-9a51-0d3f3c1b7e21/removal-requests/12/contest`.",
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "description": "User id (string GUID) of the admin named in the request.",
            "required": true,
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "requestId",
            "in": "path",
            "description": "Removal request id from `GET /api/v1/users/removal-requests`.",
            "required": true,
            "schema": {
              "type": "integer",
              "format": "int32"
            }
          }
        ],
        "responses": {
          "401": {
            "description": "Unauthorized",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ProblemDetails"
                }
              }
            }
          },
          "403": {
            "description": "The key's creator is not the admin named in the request.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ProblemDetails"
                }
              }
            }
          },
          "429": {
            "description": "Too Many Requests",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ProblemDetails"
                }
              }
            }
          },
          "200": {
            "description": "Contested; nothing changed.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/AdminRemovalOutcomeDto"
                }
              }
            }
          },
          "400": {
            "description": "Missing `Idempotency-Key`.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ValidationProblemDetails"
                }
              }
            }
          },
          "404": {
            "description": "No such request for this user in this organization.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ProblemDetails"
                }
              }
            }
          },
          "409": {
            "description": "The request is no longer pending, the notice period has ended, or the key's creator is unknown here.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ProblemDetails"
                }
              }
            }
          },
          "502": {
            "description": "The sync server answered with an unexpected error.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ProblemDetails"
                }
              }
            }
          },
          "503": {
            "description": "This server cannot reach the sync server; nothing changed.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ProblemDetails"
                }
              }
            }
          }
        }
      }
    },
    "/api/v1/users/removal-requests": {
      "get": {
        "tags": [
          "UserRemovalRequests"
        ],
        "summary": "Lists the organization's Organization Admin removal requests, pending first.",
        "description": "Requires `ManageUsers`. Not cursor-paginated: pending requests plus (with\n`includeClosed=true`) the latest closed ones, at most 100, in one response with\n`nextCursor` always `null`. Sends a weak `ETag`. Example:\n`GET /api/v1/users/removal-requests?includeClosed=true`.\nAlso available as CSV (`Accept: text/csv` or `?format=csv`); paging cursor in `X-Next-Cursor`.",
        "parameters": [
          {
            "name": "includeClosed",
            "in": "query",
            "description": "Also return approved, contested, cancelled and applied requests (default `false`).",
            "schema": {
              "type": "boolean",
              "default": false
            }
          },
          {
            "name": "format",
            "in": "query",
            "description": "Alternative to the Accept header: `csv` returns text/csv.",
            "schema": {
              "enum": [
                "csv"
              ],
              "type": "string"
            }
          }
        ],
        "responses": {
          "401": {
            "description": "Unauthorized",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ProblemDetails"
                }
              }
            }
          },
          "403": {
            "description": "The key's creator is not an Organization Admin.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ProblemDetails"
                }
              }
            }
          },
          "429": {
            "description": "Too Many Requests",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ProblemDetails"
                }
              }
            }
          },
          "200": {
            "description": "The requests.",
            "headers": {
              "X-Next-Cursor": {
                "description": "text/csv only: the page's nextCursor. Absent on the last page. Pass it as ?after= (or follow the Link header) to fetch the next page.",
                "schema": {
                  "type": "string"
                }
              },
              "Link": {
                "description": "text/csv only, alongside X-Next-Cursor: <absolute URL of the next page>; rel=\"next\".",
                "schema": {
                  "type": "string"
                }
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/PagedResponseOfAdminRemovalRequestDto"
                }
              },
              "text/csv": {
                "schema": {
                  "type": "string",
                  "description": "CSV document: UTF-8 with BOM, CRLF rows, RFC 4180 quoting. The header row holds the JSON property names of one item (nested objects flattened as parent.child); one row per item of the page."
                }
              }
            }
          },
          "304": {
            "description": "Unchanged since the supplied `If-None-Match`."
          },
          "409": {
            "description": "The key's creator is unknown on this server.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ProblemDetails"
                }
              }
            }
          },
          "502": {
            "description": "The sync server answered with an unexpected error.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ProblemDetails"
                }
              }
            }
          },
          "503": {
            "description": "This server cannot reach the sync server.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ProblemDetails"
                }
              }
            }
          }
        }
      }
    },
    "/api/v1/vendors": {
      "get": {
        "tags": [
          "Vendors"
        ],
        "summary": "Lists the organization's vendors, one page at a time.",
        "description": "Requires `ViewPurchaseOrders`. Deactivated vendors are excluded unless\n`includeInactive=true`. Cursor-paginated: pass `nextCursor` as `after`.\nExample: `GET /api/v1/vendors?search=acme`.\nAlso available as CSV (`Accept: text/csv` or `?format=csv`); paging cursor in `X-Next-Cursor`.",
        "parameters": [
          {
            "name": "search",
            "in": "query",
            "description": "Case-insensitive match against name, contact name and contact email. Example: `acme`.",
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "includeInactive",
            "in": "query",
            "description": "Also return deactivated vendors (default `false`).",
            "schema": {
              "type": "boolean"
            }
          },
          {
            "name": "updatedSince",
            "in": "query",
            "description": "Only vendors created or updated at/after this instant (ISO-8601 UTC, e.g. `2026-09-01T00:00:00Z`).",
            "schema": {
              "type": "string",
              "format": "date-time"
            }
          },
          {
            "name": "after",
            "in": "query",
            "description": "Cursor from the previous page's `nextCursor` (a vendor SyncId). Omit for the first page.",
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "limit",
            "in": "query",
            "description": "Page size, 1..200 (default 50).",
            "schema": {
              "type": "integer",
              "format": "int32"
            }
          },
          {
            "name": "format",
            "in": "query",
            "description": "Alternative to the Accept header: `csv` returns text/csv.",
            "schema": {
              "enum": [
                "csv"
              ],
              "type": "string"
            }
          }
        ],
        "responses": {
          "401": {
            "description": "Unauthorized",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ProblemDetails"
                }
              }
            }
          },
          "403": {
            "description": "Forbidden",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ProblemDetails"
                }
              }
            }
          },
          "429": {
            "description": "Too Many Requests",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ProblemDetails"
                }
              }
            }
          },
          "200": {
            "description": "One page of vendors and the cursor for the next page (null on the last page).",
            "headers": {
              "X-Next-Cursor": {
                "description": "text/csv only: the page's nextCursor. Absent on the last page. Pass it as ?after= (or follow the Link header) to fetch the next page.",
                "schema": {
                  "type": "string"
                }
              },
              "Link": {
                "description": "text/csv only, alongside X-Next-Cursor: <absolute URL of the next page>; rel=\"next\".",
                "schema": {
                  "type": "string"
                }
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/PagedResponseOfVendorDto"
                }
              },
              "text/csv": {
                "schema": {
                  "type": "string",
                  "description": "CSV document: UTF-8 with BOM, CRLF rows, RFC 4180 quoting. The header row holds the JSON property names of one item (nested objects flattened as parent.child); one row per item of the page."
                }
              }
            }
          },
          "400": {
            "description": "Unusable `after` cursor.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ValidationProblemDetails"
                }
              }
            }
          }
        }
      },
      "post": {
        "tags": [
          "Vendors"
        ],
        "summary": "Creates a vendor.",
        "description": "Requires `ManageVendors` and an `Idempotency-Key` header. Same as \"New Vendor\" in the\napp: the name must be unique in the organization (active or not). Attributed to the key's creator.\nExample: `{ \"name\": \"Acme Supply\", \"contactEmail\": \"orders@acme.example\" }`.",
        "requestBody": {
          "description": "The vendor's fields; `name` is required.",
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/VendorWriteApiRequest"
              }
            },
            "text/json": {
              "schema": {
                "$ref": "#/components/schemas/VendorWriteApiRequest"
              }
            },
            "application/*+json": {
              "schema": {
                "$ref": "#/components/schemas/VendorWriteApiRequest"
              }
            }
          }
        },
        "responses": {
          "401": {
            "description": "Unauthorized",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ProblemDetails"
                }
              }
            }
          },
          "403": {
            "description": "Forbidden",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ProblemDetails"
                }
              }
            }
          },
          "429": {
            "description": "Too Many Requests",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ProblemDetails"
                }
              }
            }
          },
          "201": {
            "description": "Created; `Location` points at the new vendor.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/VendorDto"
                }
              }
            }
          },
          "400": {
            "description": "Malformed body or missing `Idempotency-Key`.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ValidationProblemDetails"
                }
              }
            }
          },
          "409": {
            "description": "A vendor with this name already exists, or the key's creator is unknown on this server.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ProblemDetails"
                }
              }
            }
          },
          "422": {
            "description": "A field is unacceptable (blank name, invalid email, too long); the field is listed in `errors`.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ValidationProblemDetails"
                }
              }
            }
          }
        }
      }
    },
    "/api/v1/vendors/{id}": {
      "get": {
        "tags": [
          "Vendors"
        ],
        "summary": "Returns one vendor.",
        "description": "Requires `ViewPurchaseOrders`. `id` may be the numeric id or the `syncId`. Sends a weak `ETag`.",
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "description": "Numeric id or SyncId. Example: `7`.",
            "required": true,
            "schema": {
              "type": "string"
            }
          }
        ],
        "responses": {
          "401": {
            "description": "Unauthorized",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ProblemDetails"
                }
              }
            }
          },
          "403": {
            "description": "Forbidden",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ProblemDetails"
                }
              }
            }
          },
          "429": {
            "description": "Too Many Requests",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ProblemDetails"
                }
              }
            }
          },
          "200": {
            "description": "The vendor.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/VendorDto"
                }
              }
            }
          },
          "304": {
            "description": "Unchanged since the supplied ETag."
          },
          "404": {
            "description": "No such vendor in this organization.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ProblemDetails"
                }
              }
            }
          }
        }
      },
      "patch": {
        "tags": [
          "Vendors"
        ],
        "summary": "Edits a vendor; only the fields sent change.",
        "description": "Requires `ManageVendors` and an `Idempotency-Key` header. `\"\"` clears a field\n(not `name`); `\"isActive\": false` deactivates the vendor. Renaming onto another vendor's\nname is refused. Existing purchase orders keep the vendor name they were placed with.\nExample: `PATCH /api/v1/vendors/7 { \"contactPhone\": \"555-0199\" }`.",
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "description": "Numeric vendor id. Example: `7`.",
            "required": true,
            "schema": {
              "type": "integer",
              "format": "int32"
            }
          }
        ],
        "requestBody": {
          "description": "The fields to change.",
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/VendorWriteApiRequest"
              }
            },
            "text/json": {
              "schema": {
                "$ref": "#/components/schemas/VendorWriteApiRequest"
              }
            },
            "application/*+json": {
              "schema": {
                "$ref": "#/components/schemas/VendorWriteApiRequest"
              }
            }
          }
        },
        "responses": {
          "401": {
            "description": "Unauthorized",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ProblemDetails"
                }
              }
            }
          },
          "403": {
            "description": "Forbidden",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ProblemDetails"
                }
              }
            }
          },
          "429": {
            "description": "Too Many Requests",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ProblemDetails"
                }
              }
            }
          },
          "200": {
            "description": "The vendor after the edit.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/VendorDto"
                }
              }
            }
          },
          "400": {
            "description": "Malformed body or missing `Idempotency-Key`.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ValidationProblemDetails"
                }
              }
            }
          },
          "404": {
            "description": "No such vendor in this organization.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ProblemDetails"
                }
              }
            }
          },
          "409": {
            "description": "Another vendor already has this name, or the key's creator is unknown on this server.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ProblemDetails"
                }
              }
            }
          },
          "422": {
            "description": "A field is unacceptable; the field is listed in `errors`.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ValidationProblemDetails"
                }
              }
            }
          }
        }
      }
    },
    "/api/v1/webhooks": {
      "get": {
        "tags": [
          "Webhooks"
        ],
        "summary": "Lists the organization's webhook subscriptions.",
        "description": "Requires `ManageOrganizations`. The signing secret is never returned here — only once, when the subscription is created.",
        "responses": {
          "401": {
            "description": "Unauthorized",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ProblemDetails"
                }
              }
            }
          },
          "403": {
            "description": "Forbidden",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ProblemDetails"
                }
              }
            }
          },
          "429": {
            "description": "Too Many Requests",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ProblemDetails"
                }
              }
            }
          },
          "200": {
            "description": "The organization's subscriptions.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/WebhooksResponse"
                }
              }
            }
          }
        }
      },
      "post": {
        "tags": [
          "Webhooks"
        ],
        "summary": "Registers a webhook subscription (an instant-trigger endpoint).",
        "description": "Requires `ManageOrganizations` and an `Idempotency-Key` header. The response includes the\nHMAC `signingSecret`<b>once</b> — store it to verify the `X-Flex-Signature-V2` (timestamped, replay-safe) and `X-Flex-Signature` headers; it is\nnever retrievable again. The URL must be a public `https` endpoint. Attributed to the key's\ncreator. Example: `{ \"url\": \"https://hooks.zapier.com/…\", \"events\": [\"job.completed\"] }`.",
        "requestBody": {
          "description": "The target URL and the events to subscribe to.",
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/CreateWebhookSubscriptionRequest"
              }
            },
            "text/json": {
              "schema": {
                "$ref": "#/components/schemas/CreateWebhookSubscriptionRequest"
              }
            },
            "application/*+json": {
              "schema": {
                "$ref": "#/components/schemas/CreateWebhookSubscriptionRequest"
              }
            }
          }
        },
        "responses": {
          "401": {
            "description": "Unauthorized",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ProblemDetails"
                }
              }
            }
          },
          "403": {
            "description": "Forbidden",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ProblemDetails"
                }
              }
            }
          },
          "429": {
            "description": "Too Many Requests",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ProblemDetails"
                }
              }
            }
          },
          "201": {
            "description": "Created; `Location` points at the subscription. Body includes `signingSecret`.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/WebhookSubscriptionDto"
                }
              }
            }
          },
          "400": {
            "description": "Malformed body or missing `Idempotency-Key`.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ValidationProblemDetails"
                }
              }
            }
          },
          "409": {
            "description": "The key's creator is unknown on this server.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ProblemDetails"
                }
              }
            }
          },
          "422": {
            "description": "The URL or events are unacceptable; the field is listed in `errors`.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ValidationProblemDetails"
                }
              }
            }
          }
        }
      }
    },
    "/api/v1/webhooks/{id}": {
      "delete": {
        "tags": [
          "Webhooks"
        ],
        "summary": "Deletes a webhook subscription (stops delivery).",
        "description": "Requires `ManageOrganizations`. Idempotent: deleting an already-deleted or unknown subscription returns `404`.",
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "description": "Numeric subscription id. Example: `42`.",
            "required": true,
            "schema": {
              "type": "integer",
              "format": "int32"
            }
          }
        ],
        "responses": {
          "401": {
            "description": "Unauthorized",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ProblemDetails"
                }
              }
            }
          },
          "403": {
            "description": "Forbidden",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ProblemDetails"
                }
              }
            }
          },
          "429": {
            "description": "Too Many Requests",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ProblemDetails"
                }
              }
            }
          },
          "204": {
            "description": "Deleted."
          },
          "404": {
            "description": "No such subscription in this organization.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ProblemDetails"
                }
              }
            }
          }
        }
      }
    },
    "/api/v1/webhooks/events": {
      "get": {
        "tags": [
          "Webhooks"
        ],
        "summary": "Lists the event names a subscription can subscribe to.",
        "description": "Requires `ManageOrganizations`. Use these values in the `events` array of\n`POST /api/v1/webhooks`. Integration platforms use this to populate a trigger picker.\nExample: `GET /api/v1/webhooks/events`.",
        "responses": {
          "401": {
            "description": "Unauthorized",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ProblemDetails"
                }
              }
            }
          },
          "403": {
            "description": "Forbidden",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ProblemDetails"
                }
              }
            }
          },
          "429": {
            "description": "Too Many Requests",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ProblemDetails"
                }
              }
            }
          },
          "200": {
            "description": "The subscribable events.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/WebhookEventsResponse"
                }
              }
            }
          }
        }
      }
    }
  },
  "components": {
    "schemas": {
      "AddJobChargeApiRequest": {
        "required": [
          "name"
        ],
        "type": "object",
        "properties": {
          "name": {
            "maxLength": 200,
            "minLength": 0,
            "type": "string",
            "description": "Charge name shown on the estimate, ≤ 200 characters. Required. Example: `Disposal fee`."
          },
          "price": {
            "minimum": 0,
            "type": "number",
            "description": "Amount charged to the customer, in the organization's currency, ≥ 0. Example: `75.00`.",
            "format": "double"
          },
          "description": {
            "maxLength": 500,
            "minLength": 0,
            "type": "string",
            "description": "Optional longer description, ≤ 500 characters.",
            "nullable": true
          }
        },
        "additionalProperties": false,
        "description": "Body of `POST /api/v1/jobs/{id}/charges`: add a custom charge."
      },
      "AddJobItemApiRequest": {
        "type": "object",
        "properties": {
          "itemTemplateId": {
            "maximum": 2147483647,
            "minimum": 1,
            "type": "integer",
            "description": "Item template id from `GET /api/v1/items`; must belong to your organization. Parent, sell-by-unit and add-on items are added through the app instead.",
            "format": "int32"
          },
          "quantity": {
            "maximum": 10000,
            "minimum": 1,
            "type": "integer",
            "description": "Whole units, 1–10000. Default 1.",
            "format": "int32"
          },
          "unitCost": {
            "minimum": 0,
            "type": "number",
            "description": "Cost per unit to the organization, in its currency, ≥ 0. Omit to use the template's base cost. Example: `12.50`.",
            "format": "double",
            "nullable": true
          },
          "unitPrice": {
            "minimum": 0,
            "type": "number",
            "description": "Price per unit charged to the customer, ≥ 0. Omit to let pricing resolve the tier/base price; a value that differs from the base price is kept as a manual override. Example: `19.99`.",
            "format": "double",
            "nullable": true
          },
          "notes": {
            "maxLength": 500,
            "minLength": 0,
            "type": "string",
            "description": "Line note, ≤ 500 characters.",
            "nullable": true
          }
        },
        "additionalProperties": false,
        "description": "Body of `POST /api/v1/jobs/{id}/items`: add a material line."
      },
      "AddJobLaborApiRequest": {
        "required": [
          "description"
        ],
        "type": "object",
        "properties": {
          "description": {
            "maxLength": 200,
            "minLength": 0,
            "type": "string",
            "description": "What the labor is, ≤ 200 characters. Required. Example: `Install and commission unit`."
          },
          "laborRateId": {
            "maximum": 2147483647,
            "minimum": 1,
            "type": "integer",
            "description": "Id of one of your organization's labor rates. The customer rate is that rate's hourly figure × `multiplier`.",
            "format": "int32"
          },
          "taskTemplateId": {
            "type": "integer",
            "description": "Optional task template id (must belong to your organization).",
            "format": "int32",
            "nullable": true
          },
          "estimatedHours": {
            "minimum": 0,
            "type": "number",
            "description": "Estimated hours, decimal ≥ 0. Example: `2.5`.",
            "format": "double"
          },
          "multiplier": {
            "minimum": 0,
            "type": "number",
            "description": "Multiplier applied to the labor rate's hourly rate (default 1). Example: `1.5` for overtime.",
            "format": "double"
          },
          "customerPrice": {
            "minimum": 0,
            "type": "number",
            "description": "Explicit per-job customer price for $0-rate variable-price labor, in the organization's currency, ≥ 0. Omit to bill rate × hours.",
            "format": "double",
            "nullable": true
          },
          "notes": {
            "maxLength": 500,
            "minLength": 0,
            "type": "string",
            "description": "Line note, ≤ 500 characters.",
            "nullable": true
          }
        },
        "additionalProperties": false,
        "description": "Body of `POST /api/v1/jobs/{id}/labor`: add a labor line."
      },
      "AddSalesOrderLinesApiRequest": {
        "required": [
          "lines"
        ],
        "type": "object",
        "properties": {
          "lines": {
            "maxItems": 200,
            "minItems": 1,
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/CreateSalesOrderLineApiRequest"
            },
            "description": "Lines to add, 1–200 entries, each as on create; checked before anything is written. A line for an item already on the order (not in a live transfer) is merged into it, as the app does."
          }
        },
        "additionalProperties": false,
        "description": "Body of `POST /api/v1/sales-orders/{id}/lines`. Example: `{ \"lines\": [ { \"itemId\": 5, \"quantity\": 2, \"unitPrice\": 125.00 } ] }`."
      },
      "AdjustStockRequest": {
        "type": "object",
        "properties": {
          "locationId": {
            "type": "integer",
            "description": "Stock location the units are added to / removed from (see `GET /api/v1/locations`).\nOmit or null for the organization's \"No Location\" bucket. Job sites are refused (422).",
            "format": "int32",
            "nullable": true
          },
          "delta": {
            "maximum": 1000,
            "minimum": -1000,
            "type": "integer",
            "description": "Whole units to add (positive) or remove (negative); never zero. Removal takes the oldest\navailable units first and is refused with 409 when fewer are on hand than requested.\nExample: `5` receives five units, `-1` writes one off. At most 1000 units per call\n(each unit is a physical stock row); split larger changes into several calls.",
            "format": "int32"
          },
          "reason": {
            "maxLength": 500,
            "minLength": 0,
            "type": "string",
            "description": "Optional reason recorded in each unit's change history (e.g. `\"Cycle count\"`). Max 500 characters.",
            "nullable": true
          }
        },
        "additionalProperties": false,
        "description": "Body of `POST /api/v1/items/{id}/stock/adjust`: add or remove whole units of an item\nat one stock location. Example: `{ \"locationId\": 3, \"delta\": -2, \"reason\": \"Damaged in van\" }`."
      },
      "AdminRemovalOutcomeDto": {
        "type": "object",
        "properties": {
          "request": {
            "$ref": "#/components/schemas/AdminRemovalRequestDto"
          },
          "applied": {
            "type": "boolean",
            "description": "True when this call demoted / deactivated the account (approval or self-removal)."
          },
          "message": {
            "type": "string",
            "description": "What happened. Example: `\"Removal requested: pat@example.com will be demoted to Manager on 2026-10-07 (UTC) unless they contest it, or sooner if they approve.\"`"
          }
        },
        "additionalProperties": false,
        "description": "Answer of the removal-request writes: the request after the call, and whether the change was made by it."
      },
      "AdminRemovalRequestDto": {
        "type": "object",
        "properties": {
          "id": {
            "type": "integer",
            "description": "Request id. Example: `12`.",
            "format": "int32"
          },
          "targetUserId": {
            "type": "string",
            "description": "User id (string GUID) of the Organization Admin being removed. Example: `\"8f0c3c1e-2b6d-4c55-9a51-0d3f3c1b7e21\"`."
          },
          "targetEmail": {
            "type": "string",
            "description": "E-mail of the admin being removed. Example: `\"pat@example.com\"`."
          },
          "targetName": {
            "type": "string",
            "description": "Display name of the admin being removed, when recorded. Example: `\"Pat Lee\"`.",
            "nullable": true
          },
          "requestedByUserId": {
            "type": "string",
            "description": "User id (string GUID) of the Organization Admin who asked; the same as `targetUserId` for a self-removal."
          },
          "requestedByEmail": {
            "type": "string",
            "description": "E-mail of the admin who asked. Example: `\"sam@example.com\"`."
          },
          "requestedByName": {
            "type": "string",
            "description": "Display name of the admin who asked, when recorded. Example: `\"Sam Carter\"`.",
            "nullable": true
          },
          "action": {
            "$ref": "#/components/schemas/ApiAdminRemovalAction"
          },
          "newRole": {
            "type": "string",
            "description": "For `Demote`: `Manager` or `User`; null for `Deactivate`.",
            "nullable": true
          },
          "reason": {
            "type": "string",
            "description": "The requester's optional note (up to 500 characters). Example: `\"Leaving the company on Friday\"`.",
            "nullable": true
          },
          "status": {
            "$ref": "#/components/schemas/ApiAdminRemovalStatus"
          },
          "createdAt": {
            "type": "string",
            "description": "When it was requested (ISO-8601 UTC). Example: `\"2026-09-23T14:05:00Z\"`.",
            "format": "date-time"
          },
          "effectiveAt": {
            "type": "string",
            "description": "End of the notice period (ISO-8601 UTC, `createdAt` + 14 days; equal to `createdAt` for a\nself-removal): the target can contest until then, and it is applied shortly after. Example: `\"2026-10-07T14:05:00Z\"`.",
            "format": "date-time"
          },
          "daysRemaining": {
            "type": "integer",
            "description": "Whole days left until `effectiveAt`, rounded up; 0 once it has passed or the request is closed. Example: `14`.",
            "format": "int32"
          },
          "resolvedAt": {
            "type": "string",
            "description": "When the request closed (ISO-8601 UTC); null while `Pending`.",
            "format": "date-time",
            "nullable": true
          },
          "resolvedByEmail": {
            "type": "string",
            "description": "E-mail of whoever closed it; null when the notice period ran out or it lapsed.",
            "nullable": true
          },
          "resolutionNote": {
            "type": "string",
            "description": "Why a request lapsed or was superseded. Example: `\"The requester is no longer an active Organization Admin.\"`",
            "nullable": true
          }
        },
        "additionalProperties": false,
        "description": "A request to demote or deactivate an Organization Admin. Removing one needs the admin's own\napproval or a 14-day notice period during which they can contest it."
      },
      "ApiActivityDto": {
        "type": "object",
        "properties": {
          "id": {
            "type": "integer",
            "description": "Log row id, increasing in the order rows were written; also the pagination cursor. Example: `48213`.",
            "format": "int64"
          },
          "apiKeyId": {
            "type": "integer",
            "description": "Id of the API key that made the request (see Settings → Integrations → API Keys). Example: `3`.",
            "format": "int32",
            "nullable": true
          },
          "keyName": {
            "type": "string",
            "description": "Name the key was given at creation; null when the key no longer exists. Example: `\"Claude assistant\"`.",
            "nullable": true
          },
          "keyPrefix": {
            "type": "string",
            "description": "Non-secret key prefix shown on the API Keys page. Example: `\"flx_live_a1b2c3d4\"`.",
            "nullable": true
          },
          "method": {
            "type": "string",
            "description": "HTTP method. Example: `\"POST\"`."
          },
          "path": {
            "type": "string",
            "description": "Request path without the query string. Example: `\"/api/v1/jobs/42/status\"` or `\"/mcp\"`."
          },
          "mcpMethod": {
            "type": "string",
            "description": "For `/mcp` requests, the JSON-RPC method (`\"tools/call\"`, `\"tools/list\"`, `\"initialize\"`, ...; `\"batch\"` for a JSON-RPC batch); null for `/api/v1`.",
            "nullable": true
          },
          "mcpTool": {
            "type": "string",
            "description": "For an MCP `tools/call`, the tool that was called; null otherwise. Example: `\"update_job_status\"`.",
            "nullable": true
          },
          "statusCode": {
            "type": "integer",
            "description": "HTTP status code returned. An MCP tool failure is still `200` (the error is inside the JSON-RPC result). Example: `201`.",
            "format": "int32"
          },
          "durationMs": {
            "type": "integer",
            "description": "Server processing time in milliseconds. Example: `37`.",
            "format": "int64"
          },
          "remoteIp": {
            "type": "string",
            "description": "Client IP address as seen by the server. Example: `\"203.0.113.9\"`.",
            "nullable": true
          },
          "createdAt": {
            "type": "string",
            "description": "UTC time the request started (ISO-8601). Example: `\"2026-09-23T14:05:11Z\"`.",
            "format": "date-time"
          },
          "authFailure": {
            "type": "string",
            "description": "Why a genuine key was refused before reaching the endpoint: `\"key_expired\"` (status\n`401`, the key's expiry had passed) or `\"ip_not_allowed\"` (status `403`, the\nclient IP is outside the key's allowed IP ranges). Null for every other request, including\npermission denials. Example: `\"ip_not_allowed\"`.",
            "nullable": true
          }
        },
        "additionalProperties": false,
        "description": "One logged request to `/api/v1` or `/mcp` made with one of the calling\norganization's API keys, as returned by `GET /api/v1/activity` — \"what did the\nintegration or AI agent do\". Never carries the token, request bodies or tool arguments.\nRows are kept 90 days."
      },
      "ApiAdminRemovalAction": {
        "enum": [
          "Demote",
          "Deactivate"
        ],
        "type": "string",
        "description": "What a removal request does to the Organization Admin."
      },
      "ApiAdminRemovalStatus": {
        "enum": [
          "Pending",
          "Approved",
          "Contested",
          "Cancelled",
          "Applied"
        ],
        "type": "string",
        "description": "Where a removal request stands. Only `Pending` is open."
      },
      "ApiAssignableRole": {
        "enum": [
          "OrganizationAdmin",
          "Manager",
          "User"
        ],
        "type": "string",
        "description": "A standard role an API caller may assign. `SystemAdmin` is never assignable through the API.\nRanked OrganizationAdmin (3) > Manager (2) > User (1)."
      },
      "ApiDemotionRole": {
        "enum": [
          "Manager",
          "User"
        ],
        "type": "string",
        "description": "A role an Organization Admin may be demoted to."
      },
      "ApiOperationResponse": {
        "type": "object",
        "properties": {
          "id": {
            "type": "integer",
            "description": "Id of the resource the operation acted on (e.g. the job id).",
            "format": "int32"
          },
          "message": {
            "type": "string",
            "description": "Human-readable confirmation from the service (e.g. \"Job started.\"). May be null.",
            "nullable": true
          },
          "warning": {
            "type": "string",
            "description": "Non-fatal problem encountered while completing the operation (e.g. a notification email that could not be sent). Null when there was none.",
            "nullable": true
          }
        },
        "additionalProperties": false,
        "description": "Body of a successful workflow-style write that has no richer resource to return\n(assign, start, complete, void…). Id is the affected resource's id;\nWarning carries a non-fatal note from the service (e.g. an email that\ncould not be sent)."
      },
      "AssignJobRequest": {
        "required": [
          "userIds"
        ],
        "type": "object",
        "properties": {
          "userIds": {
            "type": "array",
            "items": {
              "type": "string"
            },
            "description": "Identity user ids (see `GET /api/v1/users`) to assign. All must be active members of the organization."
          },
          "replace": {
            "type": "boolean",
            "description": "`true`: the list becomes the complete set of assignees (people not listed are\n            unassigned; an empty list unassigns everyone). `false` (default): the listed\n            people are added to whoever is already assigned."
          }
        },
        "additionalProperties": false,
        "description": "Body of `POST /api/v1/jobs/{id}/assignees`."
      },
      "CancelJobRequest": {
        "type": "object",
        "properties": {
          "reason": {
            "maxLength": 1000,
            "minLength": 0,
            "type": "string",
            "description": "Why the job is being cancelled (recorded on the job; \"No reason provided\" when omitted).",
            "nullable": true
          },
          "issueStoreCredit": {
            "type": "boolean",
            "description": "When the job holds an uncollected down payment, convert it to customer store credit\ninstead of blocking the cancellation. Requires the key to hold `ManageStoreCredit`."
          }
        },
        "additionalProperties": false,
        "description": "Body of `POST /api/v1/jobs/{id}/cancel`."
      },
      "CancelSalesOrderApiRequest": {
        "type": "object",
        "properties": {
          "reason": {
            "maxLength": 500,
            "minLength": 0,
            "type": "string",
            "description": "Why the order is cancelled, up to 500 characters; omitted or blank = `No reason provided` (as the app's Cancel page). Example: `Customer changed their mind`.",
            "nullable": true
          }
        },
        "additionalProperties": false,
        "description": "Body of `POST /api/v1/sales-orders/{id}/cancel`; optional. Example: `{ \"reason\": \"Customer changed their mind\" }`."
      },
      "CategoryDeletedResponse": {
        "type": "object",
        "properties": {
          "id": {
            "type": "integer",
            "description": "The deleted category's id, e.g. `4`.",
            "format": "int32"
          },
          "reassignedItemCount": {
            "type": "integer",
            "description": "Items whose primary `categoryId` was this category and now is the organization's default \"No Category\" (not listed by the API).",
            "format": "int32"
          },
          "removedAssignmentCount": {
            "type": "integer",
            "description": "Item and inventory-unit category assignments removed with the category.",
            "format": "int32"
          }
        },
        "additionalProperties": false,
        "description": "Body of `DELETE /api/v1/categories/{id}`: what the delete changed."
      },
      "CategoryDto": {
        "type": "object",
        "properties": {
          "id": {
            "type": "integer",
            "description": "Numeric category id, e.g. `4`; the `categoryId` of items and the id used by `/api/v1/categories/{id}/properties`.",
            "format": "int32"
          },
          "syncId": {
            "type": "string",
            "description": "Globally unique sync id (GUID). Use it as the `after` pagination cursor.",
            "format": "uuid"
          },
          "name": {
            "type": "string",
            "description": "Category name, e.g. `\"Lumber\"`."
          },
          "description": {
            "type": "string",
            "description": "Optional description; null when unset.",
            "nullable": true
          },
          "parentCategoryId": {
            "type": "integer",
            "description": "Id of the parent category; null for a top-level category.",
            "format": "int32",
            "nullable": true
          },
          "isTemplateBuilderCategory": {
            "type": "boolean",
            "description": "True when the category was generated by a Template Builder (its name and place in the tree are managed there, not on the Item Categories page)."
          },
          "createdAt": {
            "type": "string",
            "description": "UTC creation timestamp (ISO-8601).",
            "format": "date-time"
          },
          "updatedAt": {
            "type": "string",
            "description": "UTC timestamp of the last edit of the category itself; null when never edited. Property changes do not move it.",
            "format": "date-time",
            "nullable": true
          }
        },
        "additionalProperties": false,
        "description": "An item category of the calling organization, as returned by `GET /api/v1/categories`.\nCategories form a tree via `parentCategoryId`; an item's `categoryId` points here.\nSystem categories (the built-in \"No Category\") are not listed."
      },
      "CategoryPropertiesResponse": {
        "type": "object",
        "properties": {
          "categoryId": {
            "type": "integer",
            "description": "The category id, e.g. `4`.",
            "format": "int32"
          },
          "properties": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/CategoryPropertyDto"
            },
            "description": "Own and inherited properties; a property of this category overrides a parent's of the same name."
          }
        },
        "additionalProperties": false,
        "description": "Body of `GET /api/v1/categories/{id}/properties` and of every category-property write: the\ncategory's properties — its own plus those inherited from parent categories — ordered by\n`displayOrder`."
      },
      "CategoryPropertyDto": {
        "type": "object",
        "properties": {
          "id": {
            "type": "integer",
            "description": "Property id, e.g. `57`; used by `PATCH` / `DELETE /api/v1/categories/{categoryId}/properties/{propertyId}` (on the defining category).",
            "format": "int32"
          },
          "categoryId": {
            "type": "integer",
            "description": "Id of the category that defines the property: this category, or a parent when `isInherited`.",
            "format": "int32"
          },
          "name": {
            "type": "string",
            "description": "Property name, e.g. `\"Species\"` (unique per category, case-insensitive)."
          },
          "dataType": {
            "$ref": "#/components/schemas/PropertyDataType"
          },
          "defaultValue": {
            "type": "string",
            "description": "Default value items get when the property is copied to them, in the type's format (numbers `\"12.5\"`, booleans `\"true\"`/`\"false\"`, dates `yyyy-MM-dd`); `\"\"` = none."
          },
          "options": {
            "type": "array",
            "items": {
              "type": "string"
            },
            "description": "Allowed values for `Dropdown` / `MultiSelect`, e.g. `[\"Pine\",\"Oak\"]`; empty for other types."
          },
          "isRequired": {
            "type": "boolean",
            "description": "True when the property is marked required in the app."
          },
          "displayOrder": {
            "type": "integer",
            "description": "Sort position (ascending) in which the app lists the category's properties, e.g. `0`.",
            "format": "int32"
          },
          "isInherited": {
            "type": "boolean",
            "description": "True when the property is defined on a parent category (edit it there)."
          },
          "overridesParent": {
            "type": "boolean",
            "description": "True when this category's property overrides a parent category's property of the same name."
          }
        },
        "additionalProperties": false,
        "description": "One property (custom-field definition) that items in a category carry."
      },
      "CompleteTransferRequest": {
        "type": "object",
        "properties": {
          "failedLines": {
            "maxItems": 100,
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/FailedTransferLineRequest"
            },
            "description": "Lines (ids from the transfer's `lines[].id`) to mark failed before completing; they stay where they are instead of moving. Up to 100.",
            "nullable": true
          }
        },
        "additionalProperties": false,
        "description": "Optional body of `POST /api/v1/transfers/{id}/complete`: lines that did not arrive.\nExample: `{ \"failedLines\": [ { \"lineId\": 911, \"reason\": \"Damaged in transit\" } ] }`. Omit\nthe body (or send `{}`) when everything arrived."
      },
      "CreateAdminRemovalApiRequest": {
        "required": [
          "action"
        ],
        "type": "object",
        "properties": {
          "action": {
            "$ref": "#/components/schemas/ApiAdminRemovalAction"
          },
          "newRole": {
            "$ref": "#/components/schemas/ApiDemotionRole"
          },
          "reason": {
            "maxLength": 500,
            "minLength": 0,
            "type": "string",
            "description": "Optional note shown to the admin being removed, up to 500 characters. Example: `\"Leaving the company on Friday\"`.",
            "nullable": true
          }
        },
        "additionalProperties": false,
        "description": "Body of `POST /api/v1/users/{id}/removal-requests`. Examples:\n`{ \"action\": \"Demote\", \"newRole\": \"Manager\", \"reason\": \"Moving to the field team\" }`,\n`{ \"action\": \"Deactivate\" }`."
      },
      "CreateCategoryApiRequest": {
        "required": [
          "name"
        ],
        "type": "object",
        "properties": {
          "name": {
            "maxLength": 100,
            "minLength": 0,
            "type": "string",
            "description": "Category name, 1-100 characters (trimmed), unique among the categories with the same parent (case-insensitive), e.g. `\"Hardwood\"`."
          },
          "description": {
            "maxLength": 2000,
            "minLength": 0,
            "type": "string",
            "description": "Optional description, at most 2000 characters; blank is stored as none.",
            "nullable": true
          },
          "parentCategoryId": {
            "type": "integer",
            "description": "Parent category id (a category of your organization that is not Template Builder-generated), e.g. `1`; omit, null or `0` for a top-level category.",
            "format": "int32",
            "nullable": true
          }
        },
        "additionalProperties": false,
        "description": "Body of `POST /api/v1/categories`. Example:\n`{ \"name\": \"Hardwood\", \"description\": \"Oak, maple, walnut\", \"parentCategoryId\": 1 }`."
      },
      "CreateCategoryPropertyApiRequest": {
        "required": [
          "name"
        ],
        "type": "object",
        "properties": {
          "name": {
            "maxLength": 100,
            "minLength": 0,
            "type": "string",
            "description": "Property name, 1-100 characters, unique in the category (case-insensitive); `Cost` and `Unit Price` are reserved."
          },
          "dataType": {
            "$ref": "#/components/schemas/PropertyDataType"
          },
          "defaultValue": {
            "maxLength": 4000,
            "minLength": 0,
            "type": "string",
            "description": "Optional default value in the type's format (see `defaultValue` on GET), at most 4000 characters; a `Dropdown` / `MultiSelect` default must be among `options`. A required `Boolean` defaults to `\"false\"`.",
            "nullable": true
          },
          "options": {
            "maxItems": 200,
            "type": "array",
            "items": {
              "type": "string"
            },
            "description": "Options for a `Dropdown` / `MultiSelect` property (at most 200, each 1-200 characters, no commas, no duplicates); refused for other types.",
            "nullable": true
          },
          "isRequired": {
            "type": "boolean",
            "description": "Marks the property required in the app (default false)."
          },
          "displayOrder": {
            "type": "integer",
            "description": "Sort position, 0 or more; omitted = after the category's last property.",
            "format": "int32",
            "nullable": true
          }
        },
        "additionalProperties": false,
        "description": "Body of `POST /api/v1/categories/{id}/properties`. Example:\n`{ \"name\": \"Species\", \"dataType\": \"Dropdown\", \"options\": [\"Pine\", \"Oak\"], \"defaultValue\": \"Pine\" }`."
      },
      "CreateInvoiceFromJobRequest": {
        "type": "object",
        "properties": {
          "jobId": {
            "maximum": 2147483647,
            "minimum": 1,
            "type": "integer",
            "description": "Numeric id of the job to bill.",
            "format": "int32"
          },
          "invoiceNumber": {
            "maxLength": 50,
            "minLength": 0,
            "type": "string",
            "description": "Invoice number. Defaults to `INV-{jobNumber}`, or `INV-{jobNumber}-{n}` for the n-th invoice on the job.",
            "nullable": true
          },
          "invoiceDate": {
            "type": "string",
            "description": "Issue date (UTC, date part only). Defaults to today.",
            "format": "date-time",
            "nullable": true
          },
          "dueDate": {
            "type": "string",
            "description": "Due date (UTC, date part only). Defaults from the customer's payment terms; must not precede `invoiceDate`.",
            "format": "date-time",
            "nullable": true
          },
          "taxRate": {
            "maximum": 100,
            "minimum": 0,
            "type": "number",
            "description": "Tax rate percentage. Defaults to the job's rate; always 0 for a tax-exempt customer.",
            "format": "double",
            "nullable": true
          },
          "terms": {
            "type": "string",
            "description": "Payment terms text printed on the invoice. Defaults to the customer's terms (e.g. \"Net 30\").",
            "nullable": true
          },
          "notes": {
            "type": "string",
            "description": "Notes shown to the customer.",
            "nullable": true
          },
          "paymentInstructions": {
            "type": "string",
            "description": "Payment instructions printed on the invoice. Defaults to the organization's default instructions.",
            "nullable": true
          },
          "applyStoreCredit": {
            "type": "boolean",
            "description": "Apply `storeCreditToApply` of the customer's store credit to the new invoice."
          },
          "storeCreditToApply": {
            "minimum": 0,
            "type": "number",
            "description": "Store credit amount to apply when `applyStoreCredit` is true (capped at the customer's balance and the invoice balance). Ignored otherwise.",
            "format": "double"
          }
        },
        "additionalProperties": false,
        "description": "Body of `POST /api/v1/invoices/from-job`. Only `jobId` is required; every\nomitted field defaults exactly as the Generate Invoice page does (number\n`INV-{jobNumber}` or `INV-{jobNumber}-{n}`, today, due date from the customer's\npayment terms, the job's tax rate, the organization's payment instructions). Amounts are\nnever accepted: the subtotal and taxable portion come from the job — or, once a finalized\ninvoice exists, from its unbilled remainder (a supplemental invoice)."
      },
      "CreateItemApiRequest": {
        "required": [
          "name"
        ],
        "type": "object",
        "properties": {
          "name": {
            "maxLength": 100,
            "minLength": 0,
            "type": "string",
            "description": "Display name; required, 1-100 characters. An active item with the same name is refused (409)."
          },
          "description": {
            "maxLength": 2000,
            "minLength": 0,
            "type": "string",
            "description": "Optional free-text description, max 2000 characters.",
            "nullable": true
          },
          "categoryId": {
            "type": "integer",
            "description": "Primary category id (see the item's `categoryId` on GET). Omit or null for the organization's default \"No Category\". Unknown or another organization's id → 422.",
            "format": "int32",
            "nullable": true
          },
          "unitPrice": {
            "maximum": 1000000000,
            "minimum": 0,
            "type": "number",
            "description": "Starting selling price, 0 or more, stored to 2 decimals as the `Unit Price` property. Omit for 0.",
            "format": "double",
            "nullable": true
          },
          "cost": {
            "maximum": 1000000000,
            "minimum": 0,
            "type": "number",
            "description": "Starting purchase cost, 0 or more, stored to 2 decimals as the `Cost` property. Omit for 0.",
            "format": "double",
            "nullable": true
          }
        },
        "additionalProperties": false,
        "description": "Body of `POST /api/v1/items`: create a catalog item (template). Same rules as adding an\nitem in the app: the organization's default category when `categoryId` is omitted,\n`Cost` and `Unit Price` properties created (0 unless supplied) and the category's\nproperties copied onto the item. Example:\n`{ \"name\": \"1/2\\\" PEX Coupling\", \"categoryId\": 4, \"unitPrice\": 1.25, \"cost\": 0.40 }`."
      },
      "CreateItemPropertyApiRequest": {
        "required": [
          "name"
        ],
        "type": "object",
        "properties": {
          "name": {
            "maxLength": 100,
            "minLength": 0,
            "type": "string",
            "description": "Property name, 1-100 characters, unique on the item (case-insensitive); `Cost` and `Unit Price` are reserved."
          },
          "dataType": {
            "$ref": "#/components/schemas/PropertyDataType"
          },
          "value": {
            "maxLength": 4000,
            "minLength": 0,
            "type": "string",
            "description": "Starting value in the type's format (see `value` on GET), at most 4000 characters; omit for none.",
            "nullable": true
          }
        },
        "additionalProperties": false,
        "description": "Body of `POST /api/v1/items/{id}/properties`: add an item-level custom field to one item.\nExample: `{ \"name\": \"Warranty (years)\", \"dataType\": \"Number\", \"value\": \"2\" }`."
      },
      "CreateJobApiRequest": {
        "required": [
          "customerId"
        ],
        "type": "object",
        "properties": {
          "customerId": {
            "maximum": 2147483647,
            "minimum": 1,
            "type": "integer",
            "description": "Customer the job is for (must belong to the key's organization).",
            "format": "int32"
          },
          "customerAddressId": {
            "type": "integer",
            "description": "Optional site address id; must belong to `customerId`.",
            "format": "int32",
            "nullable": true
          },
          "jobNumber": {
            "maxLength": 50,
            "minLength": 0,
            "type": "string",
            "description": "Optional job number. Omit to have one generated; a duplicate is suffixed to stay unique.",
            "nullable": true
          },
          "jobType": {
            "maxLength": 100,
            "minLength": 0,
            "type": "string",
            "description": "Free-text job type, e.g. `Installation` (default), `Repair`, `Service`."
          },
          "description": {
            "maxLength": 500,
            "minLength": 0,
            "type": "string",
            "description": "Short description of the work, shown in lists. ≤ 500 characters. Example: `Replace water heater`.",
            "nullable": true
          },
          "notes": {
            "type": "string",
            "description": "Free-form internal notes (not shown to the customer). No length limit.",
            "nullable": true
          },
          "isDraft": {
            "type": "boolean",
            "description": "Save as a draft (status `Draft`) instead of an estimate. Ignored when\n`statusId` / `statusKey` is given."
          },
          "publishImmediately": {
            "type": "boolean",
            "description": "Create and publish in one call — the \"Create & Publish\" button. The job starts as a\n`Draft` and is then published exactly as `POST /api/v1/jobs/{id}/finalize` would\n(needs at least one charge and a unit price on every material line), landing on\n`Estimated`, or `Scheduled` when `scheduledDate` is given. When it cannot\nbe published yet the job is still created as a `Draft` (201) and `warning` on the\nresponse says why. Takes precedence over `isDraft`; 400 when `statusId` /\n`statusKey` names a non-Draft status. Default `false`."
          },
          "statusId": {
            "type": "integer",
            "description": "Initial status definition id (one of the org's statuses). Mutually exclusive with `statusKey`.",
            "format": "int32",
            "nullable": true
          },
          "statusKey": {
            "maxLength": 100,
            "minLength": 0,
            "type": "string",
            "description": "Initial status by name — a status definition name (case-insensitive) or a system\nstatus key such as `Draft`, `Estimated`, `Scheduled`. Mutually\nexclusive with `statusId`.",
            "nullable": true
          },
          "scheduledDate": {
            "type": "string",
            "description": "Scheduled start as the organization's local wall clock, `yyyy-MM-ddTHH:mm:ss` with no UTC offset (the `date-time` format here is a local time, not RFC 3339 with zone); a trailing `Z` is converted to local time. Example: `2026-09-08T08:30:00`.",
            "format": "date-time",
            "nullable": true
          },
          "estimateChecklistTemplateId": {
            "type": "integer",
            "description": "Optional id of one of the organization's estimate pre-inspection checklist templates; its sections and items are copied onto the job.",
            "format": "int32",
            "nullable": true
          },
          "items": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/CreateJobItemApiRequest"
            },
            "description": "Material lines (item templates with quantity and pricing). May be empty."
          },
          "laborItems": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/CreateJobLaborApiRequest"
            },
            "description": "Labor lines (hours × rates). May be empty."
          },
          "customCharges": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/CreateJobCustomChargeApiRequest"
            },
            "description": "Custom charges (fees, disposal, permits …) with a price and optional cost. May be empty."
          }
        },
        "additionalProperties": false,
        "description": "Body of `POST /api/v1/jobs` (API_AND_MCP.md, Phase 1b). Mirrors what the MVC\nCreate page posts, minus the browser-only fields; totals are computed server-side\nfrom the lines instead of being trusted from the client."
      },
      "CreateJobCustomChargeApiRequest": {
        "required": [
          "name"
        ],
        "type": "object",
        "properties": {
          "name": {
            "maxLength": 200,
            "minLength": 0,
            "type": "string",
            "description": "Charge name shown on the estimate, ≤ 200 characters. Required. Example: `Disposal fee`."
          },
          "description": {
            "maxLength": 500,
            "minLength": 0,
            "type": "string",
            "description": "Optional longer description, ≤ 500 characters.",
            "nullable": true
          },
          "price": {
            "minimum": 0,
            "type": "number",
            "description": "Amount charged to the customer, in the organization's currency, ≥ 0. Example: `75.00`.",
            "format": "double"
          },
          "cost": {
            "minimum": 0,
            "type": "number",
            "description": "Cost to the organization (for margin), in its currency, ≥ 0. Default 0.",
            "format": "double"
          }
        },
        "additionalProperties": false,
        "description": "A custom charge on a new job."
      },
      "CreateJobItemApiRequest": {
        "type": "object",
        "properties": {
          "itemTemplateId": {
            "maximum": 2147483647,
            "minimum": 1,
            "type": "integer",
            "description": "Item template id from `GET /api/v1/items`; must belong to the key's organization.",
            "format": "int32"
          },
          "quantity": {
            "maximum": 2147483647,
            "minimum": 1,
            "type": "integer",
            "description": "Whole units, ≥ 1. Default 1.",
            "format": "int32"
          },
          "unitCost": {
            "minimum": 0,
            "type": "number",
            "description": "Cost per unit to the organization, in its currency, ≥ 0. Example: `12.50`.",
            "format": "double"
          },
          "unitPrice": {
            "minimum": 0,
            "type": "number",
            "description": "Price per unit charged to the customer, in the organization's currency, ≥ 0. Example: `19.99`.",
            "format": "double"
          },
          "discountPercent": {
            "maximum": 100,
            "minimum": 0,
            "type": "number",
            "description": "Line discount as a percentage, 0–100 (`10` = 10 % off `quantity` × `unitPrice`). Default 0.",
            "format": "double"
          },
          "notes": {
            "maxLength": 500,
            "minLength": 0,
            "type": "string",
            "description": "Line note, ≤ 500 characters.",
            "nullable": true
          }
        },
        "additionalProperties": false,
        "description": "A material line on a new job."
      },
      "CreateJobLaborApiRequest": {
        "required": [
          "description"
        ],
        "type": "object",
        "properties": {
          "description": {
            "maxLength": 200,
            "minLength": 0,
            "type": "string",
            "description": "What the labor is, ≤ 200 characters. Required. Example: `Install and commission unit`."
          },
          "taskTemplateId": {
            "type": "integer",
            "description": "Optional id of the organization's task template this line derives from.",
            "format": "int32",
            "nullable": true
          },
          "laborRateId": {
            "type": "integer",
            "description": "Optional id of the organization's labor rate the hourly figures were taken from (the figures themselves are still sent explicitly).",
            "format": "int32",
            "nullable": true
          },
          "estimatedHours": {
            "minimum": 0,
            "type": "number",
            "description": "Estimated hours, decimal ≥ 0. Example: `2.5`.",
            "format": "double"
          },
          "customerRate": {
            "minimum": 0,
            "type": "number",
            "description": "Hourly rate billed to the customer.",
            "format": "double"
          },
          "employeeCost": {
            "minimum": 0,
            "type": "number",
            "description": "Hourly cost of the technician.",
            "format": "double"
          },
          "overheadFactor": {
            "minimum": 0,
            "type": "number",
            "description": "Multiplier applied to `employeeCost` for overhead (default 1).",
            "format": "double"
          },
          "notes": {
            "maxLength": 500,
            "minLength": 0,
            "type": "string",
            "description": "Line note, ≤ 500 characters.",
            "nullable": true
          }
        },
        "additionalProperties": false,
        "description": "A labor line on a new job."
      },
      "CreatePurchaseOrderApiRequest": {
        "type": "object",
        "properties": {
          "orderNumber": {
            "maxLength": 50,
            "minLength": 0,
            "type": "string",
            "description": "PO number, up to 50 characters. Omit to have one generated the way the app does:\n`{vendor name, letters and digits}-{yyyy-MM-dd}-{nnnn}` (e.g. `AcmeSupply-2026-09-22-0001`),\nor `PO-{yyyy}-{nnnn}` without a vendor.",
            "nullable": true
          },
          "externalOrderNumber": {
            "maxLength": 50,
            "minLength": 0,
            "type": "string",
            "description": "The vendor's own order or confirmation number, up to 50 characters. Example: `SO-88213`.",
            "nullable": true
          },
          "vendorId": {
            "type": "integer",
            "description": "An active vendor from `GET /api/v1/vendors`. Omit for a one-off vendor named in `vendorName`. Example: `7`.",
            "format": "int32",
            "nullable": true
          },
          "vendorName": {
            "maxLength": 200,
            "minLength": 0,
            "type": "string",
            "description": "Vendor name on the order, up to 200 characters. Defaults to the vendor record's name when `vendorId` is given.",
            "nullable": true
          },
          "vendorContactEmail": {
            "maxLength": 100,
            "minLength": 0,
            "type": "string",
            "description": "Contact email for this order, up to 100 characters. Defaults to the vendor record's contact email.",
            "nullable": true
          },
          "vendorContactPhone": {
            "maxLength": 20,
            "minLength": 0,
            "type": "string",
            "description": "Contact phone for this order, up to 20 characters. Defaults to the vendor record's contact phone.",
            "nullable": true
          },
          "orderDate": {
            "type": "string",
            "description": "Order date (organization-local calendar date, no offset). Defaults to today. Example: `2026-09-22`.",
            "format": "date-time",
            "nullable": true
          },
          "expectedDeliveryDate": {
            "type": "string",
            "description": "Expected delivery date (organization-local calendar date, no offset); optional. Example: `2026-09-30`.",
            "format": "date-time",
            "nullable": true
          },
          "totalCost": {
            "maximum": 999999999,
            "minimum": 0,
            "type": "number",
            "description": "Order total as agreed with the vendor, in `currency`; optional. Example: `74.40`.",
            "format": "double",
            "nullable": true
          },
          "currency": {
            "maxLength": 3,
            "minLength": 3,
            "type": "string",
            "description": "ISO 4217 currency code, 3 letters. Defaults to `USD`.",
            "nullable": true
          },
          "receivingLocationId": {
            "maximum": 2147483647,
            "minimum": 1,
            "type": "integer",
            "description": "Where the order will be received: an active, top-level location that is not a job site (see `GET /api/v1/locations?parentLocationId=0`). Example: `1`.",
            "format": "int32"
          },
          "notes": {
            "maxLength": 4000,
            "minLength": 0,
            "type": "string",
            "description": "Free-text notes on the order.",
            "nullable": true
          },
          "lines": {
            "maxItems": 200,
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/PurchaseOrderLineApiRequest"
            },
            "description": "Lines to order, 0..200, in display order. More can be added later with `POST /api/v1/purchase-orders/{id}/lines`.",
            "nullable": true
          }
        },
        "additionalProperties": false,
        "description": "Body of `POST /api/v1/purchase-orders`: a new `Draft` order, optionally with its lines.\nExample: `{ \"vendorId\": 7, \"receivingLocationId\": 1, \"lines\": [ { \"itemId\": 12, \"quantityOrdered\": 24, \"unitPrice\": 3.10 } ] }`."
      },
      "CreateSalesOrderApiRequest": {
        "type": "object",
        "properties": {
          "customerId": {
            "maximum": 2147483647,
            "minimum": 1,
            "type": "integer",
            "description": "Customer the order is for (see `/api/v1/customers`); must be an existing customer of this organization that is not marked Difficult. Example: `42`.",
            "format": "int32"
          },
          "customerPONumber": {
            "maxLength": 100,
            "minLength": 0,
            "type": "string",
            "description": "The customer's own purchase-order reference, up to 100 characters. Example: `PO-7781`.",
            "nullable": true
          },
          "deliveryAddressId": {
            "type": "integer",
            "description": "Id of one of the customer's addresses to deliver to (`/api/v1/customers/{id}/locations`); omit for none. Example: `77`.",
            "format": "int32",
            "nullable": true
          },
          "orderDate": {
            "type": "string",
            "description": "Order date, an organization-local calendar date (`yyyy-MM-dd`; no UTC offset). Defaults to today's date (server clock), like the New Sales Order page's pre-filled value. Example: `2026-09-23`.",
            "format": "date-time",
            "nullable": true
          },
          "requestedDeliveryDate": {
            "type": "string",
            "description": "Requested delivery date, an organization-local calendar date (`yyyy-MM-dd`). Example: `2026-09-30`.",
            "format": "date-time",
            "nullable": true
          },
          "paymentTerms": {
            "maxLength": 100,
            "minLength": 0,
            "type": "string",
            "description": "Payment terms text, up to 100 characters. Defaults to the organization's default payment terms (else `Due on Receipt`). Example: `Net 30`.",
            "nullable": true
          },
          "taxRate": {
            "maximum": 100,
            "minimum": 0,
            "type": "number",
            "description": "Tax rate in percent, 0–100 (8.25 = 8.25 %). Omit or send 0 for the organization's default tax rate; a tax-exempt customer always gets 0. Example: `8.25`.",
            "format": "double",
            "nullable": true
          },
          "notes": {
            "type": "string",
            "description": "Free-text notes on the order. Example: `Leave at loading dock.`",
            "nullable": true
          },
          "lines": {
            "maxItems": 200,
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/CreateSalesOrderLineApiRequest"
            },
            "description": "Lines to add, 0–200 entries (an order with no lines can be filled in the app). Every line is\nchecked before anything is written; lines for the same item are merged into one line\n(quantities add up, the later non-zero price wins), exactly as adding items in the app."
          }
        },
        "additionalProperties": false,
        "description": "Body of `POST /api/v1/sales-orders`. Example:\n`{ \"customerId\": 42, \"customerPONumber\": \"PO-7781\", \"lines\": [ { \"itemId\": 5, \"quantity\": 10, \"unitPrice\": 125.00 } ] }`."
      },
      "CreateSalesOrderLineApiRequest": {
        "type": "object",
        "properties": {
          "itemId": {
            "maximum": 2147483647,
            "minimum": 1,
            "type": "integer",
            "description": "Catalog item id from `/api/v1/items`; must be a non-deleted item of this organization. Example: `5`.",
            "format": "int32"
          },
          "quantity": {
            "maximum": 1000000,
            "minimum": 1,
            "type": "integer",
            "description": "Quantity ordered, whole units, 1–1,000,000. Example: `10`.",
            "format": "int32"
          },
          "unitPrice": {
            "maximum": 1000000000,
            "minimum": 0,
            "type": "number",
            "description": "Unit price in the organization's currency, 0–1,000,000,000; the line total is `quantity` × `unitPrice`, and the lines together may total at most 1,000,000,000,000 (else 422 on `lines`). Example: `125.00`.",
            "format": "double"
          },
          "unitCost": {
            "maximum": 1000000000,
            "minimum": 0,
            "type": "number",
            "description": "Unit cost in the organization's currency, 0–1,000,000,000; omit when unknown (stored as 0). Example: `80.00`.",
            "format": "double",
            "nullable": true
          },
          "notes": {
            "type": "string",
            "description": "Line notes. Example: `Cut to 25 ft lengths`.",
            "nullable": true
          }
        },
        "additionalProperties": false,
        "description": "One line of a sales-order create request."
      },
      "CreateStandaloneInvoiceApiRequest": {
        "required": [
          "lines"
        ],
        "type": "object",
        "properties": {
          "customerId": {
            "maximum": 2147483647,
            "minimum": 1,
            "type": "integer",
            "description": "Numeric id of the customer to bill (must be an active customer in your organization).",
            "format": "int32"
          },
          "customerAddressId": {
            "type": "integer",
            "description": "Optional site address id (a location belonging to the customer) to print on the invoice.",
            "format": "int32",
            "nullable": true
          },
          "invoiceNumber": {
            "maxLength": 50,
            "minLength": 0,
            "type": "string",
            "description": "Invoice number. Defaults to the next `INV-SA-…` number.",
            "nullable": true
          },
          "invoiceDate": {
            "type": "string",
            "description": "Issue date (UTC, date part only). Defaults to today.",
            "format": "date-time",
            "nullable": true
          },
          "dueDate": {
            "type": "string",
            "description": "Due date (UTC, date part only). Defaults from `paymentTerms`; must not precede `invoiceDate`.",
            "format": "date-time",
            "nullable": true
          },
          "description": {
            "maxLength": 2000,
            "minLength": 0,
            "type": "string",
            "description": "Short description shown in lists.",
            "nullable": true
          },
          "taxRate": {
            "maximum": 100,
            "minimum": 0,
            "type": "number",
            "description": "Tax rate percentage. Defaults to the organization's default rate; always 0 for a tax-exempt customer.",
            "format": "double",
            "nullable": true
          },
          "paymentTerms": {
            "$ref": "#/components/schemas/PaymentTerms"
          },
          "notes": {
            "type": "string",
            "description": "Notes shown to the customer.",
            "nullable": true
          },
          "paymentInstructions": {
            "type": "string",
            "description": "Payment instructions printed on the invoice. Defaults to the organization's default instructions.",
            "nullable": true
          },
          "lines": {
            "minItems": 1,
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/StandaloneInvoiceLineApiRequest"
            },
            "description": "Lines to bill, in display order. At least one."
          },
          "applyStoreCredit": {
            "type": "boolean",
            "description": "Apply `storeCreditToApply` of the customer's store credit to the new invoice."
          },
          "storeCreditToApply": {
            "minimum": 0,
            "type": "number",
            "description": "Store credit amount to apply when `applyStoreCredit` is true (capped at the customer's balance and the invoice total). Ignored otherwise.",
            "format": "double"
          }
        },
        "additionalProperties": false,
        "description": "Body of `POST /api/v1/invoices` — a standalone invoice (not tied to a job) with its\nown lines. Only `customerId` and at least one line are required; every omitted field\ndefaults exactly as the New Invoice page does (number `INV-SA-…`, today, the\norganization's default payment terms, tax rate and payment instructions). The subtotal is\nalways the sum of the lines; it is never accepted directly."
      },
      "CreateTransferLineRequest": {
        "type": "object",
        "properties": {
          "itemId": {
            "maximum": 2147483647,
            "minimum": 1,
            "type": "integer",
            "description": "Item (catalog template) id from `GET /api/v1/items`. Example: `12`.",
            "format": "int32"
          },
          "quantity": {
            "maximum": 1000,
            "minimum": 1,
            "type": "integer",
            "description": "Whole units to move, 1–1000. Example: `3`.",
            "format": "int32"
          }
        },
        "additionalProperties": false,
        "description": "One line of `POST /api/v1/transfers`. Example: `{ \"itemId\": 12, \"quantity\": 3 }`."
      },
      "CreateTransferRequest": {
        "required": [
          "lines"
        ],
        "type": "object",
        "properties": {
          "sourceLocationId": {
            "maximum": 2147483647,
            "minimum": 1,
            "type": "integer",
            "description": "Location the stock leaves (see `GET /api/v1/locations`). Must be active and must hold enough available units of every line's item. Example: `1`.",
            "format": "int32"
          },
          "destinationLocationId": {
            "maximum": 2147483647,
            "minimum": 1,
            "type": "integer",
            "description": "Location the stock arrives at; must be active and different from `sourceLocationId`. Example: `4`.",
            "format": "int32"
          },
          "notes": {
            "maxLength": 2000,
            "minLength": 0,
            "type": "string",
            "description": "Optional free-text note shown on the transfer, up to 2000 characters. Example: `Restock van 2`.",
            "nullable": true
          },
          "lines": {
            "maxItems": 100,
            "minItems": 1,
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/CreateTransferLineRequest"
            },
            "description": "What to move: 1–100 lines. Lines for the same item add up."
          }
        },
        "additionalProperties": false,
        "description": "Body of `POST /api/v1/transfers`: move whole units of catalog items from one location to\nanother (a standalone transfer, like Transfers → Create). Example:\n`{ \"sourceLocationId\": 1, \"destinationLocationId\": 4, \"notes\": \"Restock van 2\", \"lines\": [ { \"itemId\": 12, \"quantity\": 3 } ] }`."
      },
      "CreateUserApiRequest": {
        "required": [
          "email",
          "firstName",
          "lastName",
          "password"
        ],
        "type": "object",
        "properties": {
          "email": {
            "maxLength": 256,
            "minLength": 0,
            "type": "string",
            "description": "Login e-mail address, unique across Flex, up to 256 characters. Example: `\"sam@example.com\"`.",
            "format": "email"
          },
          "firstName": {
            "maxLength": 100,
            "minLength": 0,
            "type": "string",
            "description": "First name, 1..100 characters. Example: `\"Sam\"`."
          },
          "lastName": {
            "maxLength": 100,
            "minLength": 0,
            "type": "string",
            "description": "Last name, 1..100 characters. Example: `\"Carter\"`."
          },
          "password": {
            "maxLength": 128,
            "minLength": 0,
            "type": "string",
            "description": "Initial password: at least 8 characters with an uppercase letter, a lowercase letter, a digit and a\nsymbol. Pass it to the person securely; they can change it under Account → Settings. Never\nreturned. Example: `\"Welcome#2026\"`."
          },
          "role": {
            "$ref": "#/components/schemas/ApiAssignableRole"
          },
          "customRoleId": {
            "type": "integer",
            "description": "Optional custom role id (from `GET /api/v1/roles`) to add. The account then gets `User` as\nits standard role, so leave `role` out or send `User`. Example: `4`.",
            "format": "int32",
            "nullable": true
          }
        },
        "additionalProperties": false,
        "description": "Body of `POST /api/v1/users`. Example:\n`{ \"email\": \"sam@example.com\", \"firstName\": \"Sam\", \"lastName\": \"Carter\", \"password\": \"Welcome#2026\", \"role\": \"Manager\" }`."
      },
      "CreateWebhookSubscriptionRequest": {
        "type": "object",
        "properties": {
          "url": {
            "type": "string",
            "description": "The absolute `https` URL to POST events to. Public hosts only (no localhost/private ranges). Required.",
            "nullable": true
          },
          "events": {
            "type": "array",
            "items": {
              "type": "string"
            },
            "description": "The event names to subscribe to (at least one; see `GET /api/v1/webhooks/events`). Example: `[\"job.completed\"]`.",
            "nullable": true
          }
        },
        "additionalProperties": false,
        "description": "Request body for `POST /api/v1/webhooks` (register an instant-trigger endpoint)."
      },
      "CustomRoleWriteApiRequest": {
        "type": "object",
        "properties": {
          "name": {
            "maxLength": 50,
            "minLength": 0,
            "type": "string",
            "description": "Role name, 1..50 characters, unique in the organization and not a built-in role name. Example: `\"Dispatcher\"`.",
            "nullable": true
          },
          "description": {
            "maxLength": 200,
            "minLength": 0,
            "type": "string",
            "description": "Free-text description, up to 200 characters.",
            "nullable": true
          },
          "permissions": {
            "maxItems": 100,
            "type": "array",
            "items": {
              "type": "string"
            },
            "description": "FlexPermissions names the role grants (see `get_org_capabilities` / the permission catalog). Each\nmust be held both by the API key and by the key's creator. Example: `[\"ViewJobs\", \"ManageJobs\"]`.",
            "nullable": true
          }
        },
        "additionalProperties": false,
        "description": "Body of `POST /api/v1/roles` (`name` required) and `PATCH /api/v1/roles/{id}` (only the\nfields sent change; `permissions` replaces the whole set). Example:\n`{ \"name\": \"Dispatcher\", \"description\": \"Schedules crews\", \"permissions\": [\"ViewJobs\", \"ManageJobs\"] }`."
      },
      "CustomerDto": {
        "type": "object",
        "properties": {
          "id": {
            "type": "integer",
            "description": "Numeric id used in URLs (e.g. `/api/v1/customers/42`). Stable within this organization.",
            "format": "int32"
          },
          "syncId": {
            "type": "string",
            "description": "Globally unique sync id (GUID). Use it as the `after` pagination cursor and when correlating with offline devices.",
            "format": "uuid"
          },
          "name": {
            "type": "string",
            "description": "Display name, e.g. `\"Jane Doe\"`. Always present."
          },
          "companyName": {
            "type": "string",
            "description": "Company / trading name when the customer is a business, e.g. `\"Doe Roofing LLC\"`.",
            "nullable": true
          },
          "customerType": {
            "type": "string",
            "description": "Customer type: `EndCustomer`, `Installer`, `Distributor` or `Commercial`."
          },
          "primaryContact": {
            "type": "string",
            "description": "Name of the primary contact person, if different from `name`.",
            "nullable": true
          },
          "email": {
            "type": "string",
            "description": "Contact e-mail address.",
            "nullable": true
          },
          "phone": {
            "type": "string",
            "description": "Main phone number as entered (no normalisation).",
            "nullable": true
          },
          "mobilePhone": {
            "type": "string",
            "description": "Mobile phone number as entered.",
            "nullable": true
          },
          "address": {
            "type": "string",
            "description": "Primary (billing) street address line 1.",
            "nullable": true
          },
          "address2": {
            "type": "string",
            "description": "Primary address line 2 (suite, unit).",
            "nullable": true
          },
          "city": {
            "type": "string",
            "description": "Primary address city.",
            "nullable": true
          },
          "state": {
            "type": "string",
            "description": "Primary address state / province.",
            "nullable": true
          },
          "postalCode": {
            "type": "string",
            "description": "Primary address ZIP / postal code.",
            "nullable": true
          },
          "country": {
            "type": "string",
            "description": "Primary address country.",
            "nullable": true
          },
          "isTaxExempt": {
            "type": "boolean",
            "description": "True when the customer is tax exempt; null when not recorded.",
            "nullable": true
          },
          "priceTier": {
            "type": "string",
            "description": "Price tier applied to this customer: `Standard`, `Silver`, `Gold`, `Platinum` or `Custom`."
          },
          "paymentTerms": {
            "type": "string",
            "description": "Default payment terms: `DueOnReceipt`, `Net15`, `Net30`, `Net45`, `Net60`, `COD` or `Prepaid`."
          },
          "creditLimit": {
            "type": "number",
            "description": "Credit limit in the organization's currency, e.g. `5000.00`.",
            "format": "double"
          },
          "currentBalance": {
            "type": "number",
            "description": "Outstanding balance owed by the customer, e.g. `1250.50`.",
            "format": "double"
          },
          "storeCreditBalance": {
            "type": "number",
            "description": "Store credit available to the customer, e.g. `0.00`.",
            "format": "double"
          },
          "status": {
            "type": "string",
            "description": "Status flag: `None`, `Preferred` or `Difficult`."
          },
          "taxId": {
            "type": "string",
            "description": "Tax identification number, if recorded.",
            "nullable": true
          },
          "warningNotes": {
            "type": "string",
            "description": "Warning shown to staff when the customer is selected (e.g. `\"Pays late\"`).",
            "nullable": true
          },
          "notes": {
            "type": "string",
            "description": "Free-form internal notes.",
            "nullable": true
          },
          "isActive": {
            "type": "boolean",
            "description": "False when the customer has been deactivated (kept for history, hidden from pickers)."
          },
          "createdAt": {
            "type": "string",
            "description": "UTC creation timestamp, e.g. `2026-03-14T09:12:00Z`.",
            "format": "date-time"
          },
          "updatedAt": {
            "type": "string",
            "description": "UTC timestamp of the last edit; null when never edited after creation.",
            "format": "date-time",
            "nullable": true
          }
        },
        "additionalProperties": false,
        "description": "A customer of the calling organization as returned by `GET /api/v1/customers` and\n`GET /api/v1/customers/{id}`. Money values are decimals in the organization's\ncurrency; dates are ISO-8601 UTC. Additional job-site addresses are a separate\ncollection at `/api/v1/customers/{id}/locations`."
      },
      "CustomerLocationDto": {
        "type": "object",
        "properties": {
          "id": {
            "type": "integer",
            "description": "Numeric id of the location. Stable within this organization.",
            "format": "int32"
          },
          "syncId": {
            "type": "string",
            "description": "Globally unique sync id (GUID). Use it as the `after` pagination cursor.",
            "format": "uuid"
          },
          "customerId": {
            "type": "integer",
            "description": "Id of the owning customer (matches the `{id}` in the URL).",
            "format": "int32"
          },
          "name": {
            "type": "string",
            "description": "Label given to the location, e.g. `\"Main Office\"` or `\"Rental Property 1\"`."
          },
          "address": {
            "type": "string",
            "description": "Street address line 1.",
            "nullable": true
          },
          "address2": {
            "type": "string",
            "description": "Street address line 2 (suite, unit).",
            "nullable": true
          },
          "city": {
            "type": "string",
            "description": "City.",
            "nullable": true
          },
          "state": {
            "type": "string",
            "description": "State / province.",
            "nullable": true
          },
          "postalCode": {
            "type": "string",
            "description": "ZIP / postal code.",
            "nullable": true
          },
          "country": {
            "type": "string",
            "description": "Country.",
            "nullable": true
          },
          "isMailingAddress": {
            "type": "boolean",
            "description": "True when this is the customer's mailing address."
          },
          "isActive": {
            "type": "boolean",
            "description": "False when the location has been retired and should not be offered for new jobs."
          },
          "notes": {
            "type": "string",
            "description": "Free-text notes about the site (gate codes, parking), max 500 characters.",
            "nullable": true
          },
          "createdAt": {
            "type": "string",
            "description": "UTC creation timestamp.",
            "format": "date-time"
          },
          "updatedAt": {
            "type": "string",
            "description": "UTC timestamp of the last edit; null when never edited.",
            "format": "date-time",
            "nullable": true
          }
        },
        "additionalProperties": false,
        "description": "An additional address / job site belonging to a customer, as returned by\n`GET /api/v1/customers/{id}/locations`. Jobs can reference one of these as the\nsite to work at; the customer's primary address is on the customer itself."
      },
      "CustomerLocationWriteRequest": {
        "type": "object",
        "properties": {
          "name": {
            "maxLength": 100,
            "minLength": 0,
            "type": "string",
            "description": "Label for the location, 1–100 characters. Example: `\"Main Office\"`.",
            "nullable": true
          },
          "address": {
            "maxLength": 200,
            "minLength": 0,
            "type": "string",
            "description": "Street address line 1, up to 200 characters. Example: `\"12 Oak St\"`.",
            "nullable": true
          },
          "address2": {
            "maxLength": 200,
            "minLength": 0,
            "type": "string",
            "description": "Street address line 2 (suite, unit), up to 200 characters.",
            "nullable": true
          },
          "city": {
            "maxLength": 100,
            "minLength": 0,
            "type": "string",
            "description": "City, up to 100 characters. Example: `\"Springfield\"`.",
            "nullable": true
          },
          "state": {
            "maxLength": 50,
            "minLength": 0,
            "type": "string",
            "description": "State / province, up to 50 characters. Example: `\"IL\"`.",
            "nullable": true
          },
          "postalCode": {
            "maxLength": 20,
            "minLength": 0,
            "type": "string",
            "description": "ZIP / postal code, up to 20 characters. Example: `\"62701\"`.",
            "nullable": true
          },
          "country": {
            "maxLength": 100,
            "minLength": 0,
            "type": "string",
            "description": "Country, up to 100 characters. Example: `\"USA\"`.",
            "nullable": true
          },
          "isMailingAddress": {
            "type": "boolean",
            "description": "True makes this the customer's mailing address and unmarks every other location of the\ncustomer. Default `false` on create — except that a customer's first location is always\nthe mailing address.",
            "nullable": true
          },
          "isActive": {
            "type": "boolean",
            "description": "False retires the location (kept for history, no longer offered for new jobs). Default `true` on create.",
            "nullable": true
          },
          "notes": {
            "maxLength": 500,
            "minLength": 0,
            "type": "string",
            "description": "Free-text notes about the site (gate codes, parking), up to 500 characters.",
            "nullable": true
          }
        },
        "additionalProperties": false,
        "description": "Body of `POST /api/v1/customers/{id}/locations` and\n`PATCH /api/v1/customers/{id}/locations/{locationId}`. On create, `name`,\n`address`, `city`, `state` and `postalCode` are required (as on the\nCustomers → Addresses form). On PATCH only the fields sent change, `\"\"` clears an optional\nfield, and the required ones cannot be cleared.\nExample: `{ \"name\": \"Rental Property 1\", \"address\": \"12 Oak St\", \"city\": \"Springfield\", \"state\": \"IL\", \"postalCode\": \"62701\" }`."
      },
      "CustomerStatus": {
        "enum": [
          "None",
          "Preferred",
          "Difficult"
        ],
        "type": "string"
      },
      "CustomerType": {
        "enum": [
          "EndCustomer",
          "Installer",
          "Distributor",
          "Commercial"
        ],
        "type": "string"
      },
      "CustomerWebhookData": {
        "type": "object",
        "properties": {
          "id": {
            "type": "integer",
            "format": "int32"
          },
          "name": {
            "type": "string",
            "nullable": true
          },
          "email": {
            "type": "string",
            "nullable": true
          },
          "customerType": {
            "type": "string",
            "nullable": true
          }
        },
        "additionalProperties": false,
        "description": "Customer summary carried by `customer.created`."
      },
      "CustomerWriteRequest": {
        "required": [
          "name"
        ],
        "type": "object",
        "properties": {
          "name": {
            "maxLength": 200,
            "minLength": 0,
            "type": "string",
            "description": "Display name, e.g. `\"Jane Doe\"`. Required, ≤ 200 characters."
          },
          "companyName": {
            "maxLength": 200,
            "minLength": 0,
            "type": "string",
            "description": "Company / trading name, e.g. `\"Doe Roofing LLC\"`. ≤ 200.",
            "nullable": true
          },
          "customerType": {
            "$ref": "#/components/schemas/CustomerType"
          },
          "primaryContact": {
            "maxLength": 100,
            "minLength": 0,
            "type": "string",
            "description": "Primary contact person, if different from `name`. ≤ 100.",
            "nullable": true
          },
          "email": {
            "maxLength": 256,
            "minLength": 0,
            "type": "string",
            "description": "Contact e-mail address. ≤ 256.",
            "format": "email",
            "nullable": true
          },
          "phone": {
            "maxLength": 20,
            "minLength": 0,
            "type": "string",
            "description": "Main phone number, stored as entered. ≤ 20.",
            "format": "tel",
            "nullable": true
          },
          "mobilePhone": {
            "maxLength": 20,
            "minLength": 0,
            "type": "string",
            "description": "Mobile phone number, stored as entered. ≤ 20.",
            "format": "tel",
            "nullable": true
          },
          "address": {
            "maxLength": 200,
            "minLength": 0,
            "type": "string",
            "description": "Primary (billing) street address line 1. ≤ 200. On create, any of address/city/state/postalCode also creates a \"Primary Address\" location.",
            "nullable": true
          },
          "address2": {
            "maxLength": 200,
            "minLength": 0,
            "type": "string",
            "description": "Primary address line 2 (suite, unit). ≤ 200.",
            "nullable": true
          },
          "city": {
            "maxLength": 100,
            "minLength": 0,
            "type": "string",
            "description": "Primary address city. ≤ 100.",
            "nullable": true
          },
          "state": {
            "maxLength": 50,
            "minLength": 0,
            "type": "string",
            "description": "Primary address state / province. ≤ 50.",
            "nullable": true
          },
          "postalCode": {
            "maxLength": 20,
            "minLength": 0,
            "type": "string",
            "description": "Primary address ZIP / postal code. ≤ 20.",
            "nullable": true
          },
          "country": {
            "maxLength": 100,
            "minLength": 0,
            "type": "string",
            "description": "Primary address country. ≤ 100. Defaults to `\"USA\"` on create.",
            "nullable": true
          },
          "taxId": {
            "maxLength": 50,
            "minLength": 0,
            "type": "string",
            "description": "Tax identification number. ≤ 50.",
            "nullable": true
          },
          "isTaxExempt": {
            "type": "boolean",
            "description": "True when tax exempt; null when not recorded.",
            "nullable": true
          },
          "priceTier": {
            "$ref": "#/components/schemas/PriceTier"
          },
          "paymentTerms": {
            "$ref": "#/components/schemas/PaymentTerms"
          },
          "creditLimit": {
            "type": "number",
            "description": "Credit limit in the organization's currency, e.g. `5000.00`. Default 0.",
            "format": "double"
          },
          "isActive": {
            "type": "boolean",
            "description": "Update only — false deactivates the customer (kept for history, hidden from pickers). Ignored on create; new customers are always active."
          },
          "status": {
            "$ref": "#/components/schemas/CustomerStatus"
          },
          "warningNotes": {
            "maxLength": 1000,
            "minLength": 0,
            "type": "string",
            "description": "Warning shown to staff when the customer is selected. ≤ 1000.",
            "nullable": true
          },
          "notes": {
            "type": "string",
            "description": "Free-form internal notes.",
            "nullable": true
          }
        },
        "additionalProperties": false,
        "description": "Body of `POST /api/v1/customers` and `PUT /api/v1/customers/{id}`. PUT replaces\nevery editable field, so send the current value of anything you want to keep; omitted\noptional fields become null. Enum fields accept their names (case-insensitive) or\nnumeric values. Balances (`currentBalance`, `storeCreditBalance`) are\nmaintained by invoicing and cannot be set here."
      },
      "FailedTransferLineRequest": {
        "required": [
          "reason"
        ],
        "type": "object",
        "properties": {
          "lineId": {
            "maximum": 2147483647,
            "minimum": 1,
            "type": "integer",
            "description": "Transfer line id (`lines[].id` of `GET /api/v1/transfers/{id}`). The line must be `InTransit` (or already `Failed`). Example: `911`.",
            "format": "int32"
          },
          "reason": {
            "maxLength": 500,
            "minLength": 1,
            "type": "string",
            "description": "Why the line failed (required, 1–500 characters); recorded on the line. Example: `Damaged in transit`."
          }
        },
        "additionalProperties": false,
        "description": "One failed line of `POST /api/v1/transfers/{id}/complete`."
      },
      "InvoiceDetail": {
        "type": "object",
        "properties": {
          "invoice": {
            "$ref": "#/components/schemas/InvoiceSummary"
          },
          "lines": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/InvoiceLineDto"
            },
            "description": "Lines in display order."
          },
          "payments": {
            "$ref": "#/components/schemas/PaymentsSummary"
          },
          "terms": {
            "type": "string",
            "description": "Payment terms text.",
            "nullable": true
          },
          "notes": {
            "type": "string",
            "description": "Invoice notes shown to the customer.",
            "nullable": true
          },
          "paymentInstructions": {
            "type": "string",
            "description": "Instructions printed on the invoice.",
            "nullable": true
          },
          "paymentLinkUrl": {
            "type": "string",
            "description": "Online payment link, if one was generated.",
            "nullable": true
          },
          "taxRate": {
            "type": "number",
            "description": "Tax rate applied (percentage).",
            "format": "double"
          }
        },
        "additionalProperties": false,
        "description": "Full invoice as returned by `GET /api/v1/invoices/{id}`."
      },
      "InvoiceLineDto": {
        "type": "object",
        "properties": {
          "id": {
            "type": "integer",
            "description": "Line id.",
            "format": "int32"
          },
          "syncId": {
            "type": "string",
            "description": "Stable cross-device id.",
            "format": "uuid"
          },
          "description": {
            "type": "string",
            "description": "Line description."
          },
          "itemType": {
            "$ref": "#/components/schemas/InvoiceLineItemType"
          },
          "quantity": {
            "type": "number",
            "description": "Quantity billed.",
            "format": "double"
          },
          "unitOfMeasure": {
            "type": "string",
            "description": "Unit of measure, if any.",
            "nullable": true
          },
          "unitPrice": {
            "type": "number",
            "description": "Price per unit after discount.",
            "format": "double"
          },
          "originalUnitPrice": {
            "type": "number",
            "description": "Price per unit before discount, when a discount applied.",
            "format": "double",
            "nullable": true
          },
          "discountPercent": {
            "type": "number",
            "description": "Discount percentage applied to the line.",
            "format": "double"
          },
          "discountAmount": {
            "type": "number",
            "description": "Discount amount applied to the line.",
            "format": "double"
          },
          "total": {
            "type": "number",
            "description": "Quantity × unit price.",
            "format": "double"
          },
          "notes": {
            "type": "string",
            "description": "Line notes, if any.",
            "nullable": true
          },
          "sortOrder": {
            "type": "integer",
            "description": "Display order.",
            "format": "int32"
          }
        },
        "additionalProperties": false,
        "description": "One invoice line."
      },
      "InvoiceLineItemType": {
        "enum": [
          "Material",
          "Labor",
          "Service",
          "Other"
        ],
        "type": "string"
      },
      "InvoicePaymentDto": {
        "type": "object",
        "properties": {
          "id": {
            "type": "integer",
            "description": "Payment id.",
            "format": "int32"
          },
          "syncId": {
            "type": "string",
            "description": "Stable cross-device id.",
            "format": "uuid"
          },
          "amount": {
            "type": "number",
            "description": "Amount paid.",
            "format": "double"
          },
          "paymentDate": {
            "type": "string",
            "description": "Payment date, ISO-8601 UTC, e.g. `2026-09-05T14:30:00Z`.",
            "format": "date-time"
          },
          "paymentMethod": {
            "type": "string",
            "description": "Method (e.g. Cash, Card, Check), if recorded.",
            "nullable": true
          },
          "referenceNumber": {
            "type": "string",
            "description": "Check/transaction reference, if recorded.",
            "nullable": true
          }
        },
        "additionalProperties": false,
        "description": "A payment recorded against an invoice."
      },
      "InvoiceStatus": {
        "enum": [
          "Draft",
          "Sent",
          "Viewed",
          "PartiallyPaid",
          "Paid",
          "Overdue",
          "Cancelled",
          "Outstanding",
          "Consolidated"
        ],
        "type": "string"
      },
      "InvoiceSummary": {
        "type": "object",
        "properties": {
          "id": {
            "type": "integer",
            "description": "Numeric id, usable in `/api/v1/invoices/{id}`.",
            "format": "int32"
          },
          "syncId": {
            "type": "string",
            "description": "Stable cross-device id; also the pagination cursor.",
            "format": "uuid"
          },
          "invoiceNumber": {
            "type": "string",
            "description": "Human-readable invoice number, e.g. `INV-1042`."
          },
          "status": {
            "$ref": "#/components/schemas/InvoiceStatus"
          },
          "customerId": {
            "type": "integer",
            "description": "Billed customer id, if any.",
            "format": "int32",
            "nullable": true
          },
          "customerName": {
            "type": "string",
            "description": "Billed customer name, if any.",
            "nullable": true
          },
          "jobId": {
            "type": "integer",
            "description": "Job the invoice bills, if any.",
            "format": "int32",
            "nullable": true
          },
          "jobNumber": {
            "type": "string",
            "description": "Job number, if any.",
            "nullable": true
          },
          "salesOrderId": {
            "type": "integer",
            "description": "Sales order the invoice bills, if any.",
            "format": "int32",
            "nullable": true
          },
          "description": {
            "type": "string",
            "description": "Optional description.",
            "nullable": true
          },
          "invoiceDate": {
            "type": "string",
            "description": "Issue date, ISO-8601 UTC at midnight, e.g. `2026-09-05T00:00:00Z`.",
            "format": "date-time"
          },
          "dueDate": {
            "type": "string",
            "description": "Due date, ISO-8601 UTC at midnight, e.g. `2026-10-05T00:00:00Z`.",
            "format": "date-time"
          },
          "subtotal": {
            "type": "number",
            "description": "Sum of lines before tax.",
            "format": "double"
          },
          "taxAmount": {
            "type": "number",
            "description": "Tax charged.",
            "format": "double"
          },
          "total": {
            "type": "number",
            "description": "Subtotal plus tax.",
            "format": "double"
          },
          "amountPaid": {
            "type": "number",
            "description": "Payments applied.",
            "format": "double"
          },
          "storeCreditApplied": {
            "type": "number",
            "description": "Store credit applied.",
            "format": "double"
          },
          "balanceDue": {
            "type": "number",
            "description": "Total minus payments and store credit.",
            "format": "double"
          },
          "isConsolidated": {
            "type": "boolean",
            "description": "True when this invoice consolidates other invoices."
          },
          "consolidatedIntoInvoiceId": {
            "type": "integer",
            "description": "Id of the consolidated invoice that absorbed this one, if any.",
            "format": "int32",
            "nullable": true
          },
          "sentDate": {
            "type": "string",
            "description": "When the invoice was first sent, ISO-8601 UTC (e.g. `2026-09-05T14:30:00Z`); null if never sent.",
            "format": "date-time",
            "nullable": true
          },
          "paidDate": {
            "type": "string",
            "description": "When the invoice was paid in full, ISO-8601 UTC; null until paid.",
            "format": "date-time",
            "nullable": true
          },
          "createdAt": {
            "type": "string",
            "description": "Creation time, ISO-8601 UTC.",
            "format": "date-time"
          },
          "updatedAt": {
            "type": "string",
            "description": "Last modification time, ISO-8601 UTC; null when never edited. Use with the `updatedSince` filter to poll for changes.",
            "format": "date-time",
            "nullable": true
          }
        },
        "additionalProperties": false,
        "description": "Invoice header as listed by `GET /api/v1/invoices`. Timestamps are ISO-8601 UTC (`2026-09-05T14:30:00Z`)."
      },
      "InvoiceWebhookData": {
        "type": "object",
        "properties": {
          "id": {
            "type": "integer",
            "format": "int32"
          },
          "invoiceNumber": {
            "type": "string",
            "nullable": true
          },
          "jobId": {
            "type": "integer",
            "format": "int32",
            "nullable": true
          },
          "customerId": {
            "type": "integer",
            "format": "int32",
            "nullable": true
          },
          "status": {
            "type": "string",
            "nullable": true
          },
          "total": {
            "type": "number",
            "format": "double"
          },
          "amountPaid": {
            "type": "number",
            "format": "double"
          }
        },
        "additionalProperties": false,
        "description": "Invoice summary carried by `invoice.*` events."
      },
      "ItemBarcodeDto": {
        "type": "object",
        "properties": {
          "value": {
            "type": "string",
            "description": "Encoded value."
          },
          "type": {
            "type": "string",
            "description": "Symbology (e.g. `UPC`, `Code128`)."
          },
          "isPrimary": {
            "type": "boolean",
            "description": "True for the barcode printed on labels."
          }
        },
        "additionalProperties": false,
        "description": "A barcode attached to an item."
      },
      "ItemCategoriesResponse": {
        "type": "object",
        "properties": {
          "itemId": {
            "type": "integer",
            "description": "The item id, e.g. `12`.",
            "format": "int32"
          },
          "primaryCategoryId": {
            "type": "integer",
            "description": "The item's primary category (`categoryId` on `GET /api/v1/items/{id}`); null when none. Changed with `PATCH /api/v1/items/{id}`, not here.",
            "format": "int32",
            "nullable": true
          },
          "assignedCategoryIds": {
            "type": "array",
            "items": {
              "type": "integer",
              "format": "int32"
            },
            "description": "Categories assigned to the item by hand (Items page → Categories), e.g. `[4, 7]`; the ones `POST` / `DELETE /api/v1/items/{id}/categories/{categoryId}` change."
          },
          "categories": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/ItemCategoryMembershipDto"
            },
            "description": "Every category the item is in: the assigned ones, their parent categories and those its Template Builder gives it; ordered by name."
          }
        },
        "additionalProperties": false,
        "description": "Body of `GET /api/v1/items/{id}/categories` and of the item-category writes: the item's\nprimary category, the categories assigned to it by hand and every category it is in."
      },
      "ItemCategoryMembershipDto": {
        "type": "object",
        "properties": {
          "id": {
            "type": "integer",
            "description": "Category id, e.g. `4`.",
            "format": "int32"
          },
          "name": {
            "type": "string",
            "description": "Category name, e.g. `\"Lumber\"`."
          },
          "parentCategoryId": {
            "type": "integer",
            "description": "Parent category id; null for a top-level category.",
            "format": "int32",
            "nullable": true
          },
          "isAssigned": {
            "type": "boolean",
            "description": "True when the category is assigned to the item by hand (in `assignedCategoryIds`); false when it comes from an assigned child category or the item's Template Builder."
          },
          "isTemplateBuilderCategory": {
            "type": "boolean",
            "description": "True when the category was generated by a Template Builder."
          }
        },
        "additionalProperties": false,
        "description": "One category an item is in."
      },
      "ItemDetail": {
        "type": "object",
        "properties": {
          "item": {
            "$ref": "#/components/schemas/ItemSummary"
          },
          "properties": {
            "type": "object",
            "additionalProperties": {
              "type": "string"
            },
            "description": "Every template property by name (includes `Unit Price` and `Cost` when set)."
          },
          "priceTiers": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/ItemPriceTier"
            },
            "description": "Quantity-break pricing, ascending by quantity. Empty when the item has a single price."
          },
          "barcodes": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/ItemBarcodeDto"
            },
            "description": "Barcodes attached to the item."
          },
          "stock": {
            "$ref": "#/components/schemas/StockSummary"
          },
          "parentItemId": {
            "type": "integer",
            "description": "Id of the parent template for variant children; null otherwise.",
            "format": "int32",
            "nullable": true
          }
        },
        "additionalProperties": false,
        "description": "Full item as returned by `GET /api/v1/items/{id}`."
      },
      "ItemPriceTier": {
        "type": "object",
        "properties": {
          "minQuantity": {
            "type": "integer",
            "description": "Smallest quantity at which UnitPrice applies.",
            "format": "int32"
          },
          "unitPrice": {
            "type": "number",
            "description": "Price per unit at or above MinQuantity.",
            "format": "double"
          }
        },
        "additionalProperties": false,
        "description": "Quantity-break price."
      },
      "ItemPropertiesResponse": {
        "type": "object",
        "properties": {
          "itemId": {
            "type": "integer",
            "description": "The item (template) id, e.g. `12`.",
            "format": "int32"
          },
          "properties": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/ItemPropertyDto"
            },
            "description": "Every property of the item, including the pricing properties `Cost` and `Unit Price`."
          },
          "propagated": {
            "type": "integer",
            "description": "Writes only (0 on GET): how many inventory units' copies of the property were changed by the\nwrite (a new value flowing to units that were not individually customized, a new item-level\nproperty added to existing units, or the copies removed with a deleted one).",
            "format": "int32"
          }
        },
        "additionalProperties": false,
        "description": "Body of `GET /api/v1/items/{id}/properties` and of every custom-field write: all of an\nitem's properties (custom fields) with their definitions and current values, ordered by name."
      },
      "ItemPropertyDto": {
        "type": "object",
        "properties": {
          "id": {
            "type": "integer",
            "description": "Property id, e.g. `341`; used by the per-property routes.",
            "format": "int32"
          },
          "name": {
            "type": "string",
            "description": "Property name as shown in the app, e.g. `\"Color\"` (unique per item, case-insensitive)."
          },
          "value": {
            "type": "string",
            "description": "Current value as stored, never null (`\"\"` = no value). Numbers are decimal strings\n(`\"12.5\"`), booleans `\"true\"`/`\"false\"`, dates `yyyy-MM-dd`, MultiSelect a\ncomma-separated list of options (`\"Red,Blue\"`). A calculated property shows its last computed value."
          },
          "dataType": {
            "$ref": "#/components/schemas/PropertyDataType"
          },
          "options": {
            "type": "array",
            "items": {
              "type": "string"
            },
            "description": "Allowed values for `Dropdown` / `MultiSelect`, e.g. `[\"Red\",\"Blue\"]`; empty for other types (and for a list property that has no options yet, which then accepts any value)."
          },
          "isCalculated": {
            "type": "boolean",
            "description": "True when the value comes from a calculation (a formula authored in the app). Calculated\nproperties are read-only here: value writes answer `409`."
          },
          "calculationExpression": {
            "type": "string",
            "description": "The calculation's formula as stored (references appear as `{n}` placeholders), or null when not calculated. Informational only.",
            "nullable": true
          },
          "isItemLevel": {
            "type": "boolean",
            "description": "True for an item-level property added to this item only (Items page \"add property\", or\n`POST /api/v1/items/{id}/properties`); false for a property that came from the item's\nItem Template (template builder), its category or item creation."
          },
          "isCustomized": {
            "type": "boolean",
            "description": "True when a template-builder property's value was set on this item and so no longer follows\nthe Item Template (\"Customized\" in the app). `POST .../properties/{propertyId}/revert` undoes it."
          },
          "isPricing": {
            "type": "boolean",
            "description": "True for `Cost` and `Unit Price`: required on every item and changed through\n`PATCH /api/v1/items/{id}` (`cost` / `unitPrice`), never through the custom-field routes (`409`)."
          }
        },
        "additionalProperties": false,
        "description": "One custom field (property) of an item: its definition and the item's current value."
      },
      "ItemSummary": {
        "type": "object",
        "properties": {
          "id": {
            "type": "integer",
            "description": "Numeric id, usable in `/api/v1/items/{id}`.",
            "format": "int32"
          },
          "syncId": {
            "type": "string",
            "description": "Stable cross-device id; also the pagination cursor.",
            "format": "uuid"
          },
          "name": {
            "type": "string",
            "description": "Display name."
          },
          "description": {
            "type": "string",
            "description": "Free-text description, if any.",
            "nullable": true
          },
          "categoryId": {
            "type": "integer",
            "description": "Primary category id, if any.",
            "format": "int32",
            "nullable": true
          },
          "categoryName": {
            "type": "string",
            "description": "Primary category name, if any.",
            "nullable": true
          },
          "unitPrice": {
            "type": "number",
            "description": "Selling price from the template's `Unit Price` property; null when not priced.",
            "format": "double",
            "nullable": true
          },
          "cost": {
            "type": "number",
            "description": "Purchase cost from the template's `Cost` property; null when unknown.",
            "format": "double",
            "nullable": true
          },
          "onHand": {
            "type": "number",
            "description": "Units on hand across stock locations (job sites excluded). Decimal because items sold\nby sub-unit report partial rows; negative when the organization allows overselling.",
            "format": "double"
          },
          "isMiscItem": {
            "type": "boolean",
            "description": "True for ad-hoc \"misc\" items that never carry stock."
          },
          "isParentTemplate": {
            "type": "boolean",
            "description": "True for a grouping template whose children are the stocked variants."
          },
          "sellByUnits": {
            "type": "boolean",
            "description": "True when the item is sold in sub-units of a parent row."
          },
          "unitName": {
            "type": "string",
            "description": "Name of the sub-unit when SellByUnits is true.",
            "nullable": true
          },
          "createdAt": {
            "type": "string",
            "description": "UTC creation time.",
            "format": "date-time"
          },
          "updatedAt": {
            "type": "string",
            "description": "UTC last modification time; null when never edited.",
            "format": "date-time",
            "nullable": true
          },
          "preferredVendorId": {
            "type": "integer",
            "description": "Id of the vendor this item is normally reordered from (see `GET /api/v1/vendors`); null\nwhen none is set. Set on Item Templates → Edit or with `PATCH /api/v1/items/{id}`. Example: `3`.",
            "format": "int32",
            "nullable": true
          },
          "preferredVendorName": {
            "type": "string",
            "description": "Name of the preferred vendor; null when none is set. Example: `\"Ferguson\"`.",
            "nullable": true
          },
          "vendorPartNumber": {
            "type": "string",
            "description": "The vendor's own part / catalogue number for this item, max 100 characters; null when unknown. Example: `\"PX-12C\"`.",
            "nullable": true
          }
        },
        "additionalProperties": false,
        "description": "One sellable/stockable item (an `ItemTemplate`) as listed by `GET /api/v1/items`.\nPhysical rows in inventory are instances of a template; the API exposes templates\nbecause that is what agents price, sell and reorder."
      },
      "ItemUnitDto": {
        "type": "object",
        "properties": {
          "id": {
            "type": "integer",
            "description": "Unit id, e.g. `5012`; used by `PUT /api/v1/items/{id}/units/{unitId}/properties/{name}`.",
            "format": "int32"
          },
          "syncId": {
            "type": "string",
            "description": "Globally unique sync id (GUID). Use it as the `after` pagination cursor.",
            "format": "uuid"
          },
          "name": {
            "type": "string",
            "description": "The unit's display name (normally the item's name)."
          },
          "status": {
            "$ref": "#/components/schemas/ItemUnitStatus"
          },
          "locationId": {
            "type": "integer",
            "description": "Id of the location holding the unit (see `GET /api/v1/locations`); null when it has none.",
            "format": "int32",
            "nullable": true
          },
          "locationName": {
            "type": "string",
            "description": "Name of that location; null when the unit has none.",
            "nullable": true
          },
          "createdAt": {
            "type": "string",
            "description": "UTC creation timestamp (ISO-8601) — when the unit entered stock.",
            "format": "date-time"
          },
          "updatedAt": {
            "type": "string",
            "description": "UTC timestamp of the unit's last edit or move, including a change to one of its custom-field values; null when never changed.",
            "format": "date-time",
            "nullable": true
          },
          "properties": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/ItemUnitPropertyDto"
            },
            "description": "The unit's custom-field values, ordered by name. Includes `Cost` (the unit's own cost) and the pricing copy `Unit Price`, flagged `isPricing`."
          }
        },
        "additionalProperties": false,
        "description": "One inventory unit (a physical stock row) of an item, as returned by\n`GET /api/v1/items/{id}/units`, with its own copy of the item's custom fields."
      },
      "ItemUnitPropertyDto": {
        "type": "object",
        "properties": {
          "id": {
            "type": "integer",
            "description": "The unit's property id, e.g. `88213` (differs from the item's property id).",
            "format": "int32"
          },
          "name": {
            "type": "string",
            "description": "Property name, e.g. `\"Color\"`; the `{name}` of the per-unit PUT."
          },
          "value": {
            "type": "string",
            "description": "The unit's value as stored, never null (`\"\"` = none); formats as on `GET /api/v1/items/{id}/properties`."
          },
          "dataType": {
            "$ref": "#/components/schemas/PropertyDataType"
          },
          "isCalculated": {
            "type": "boolean",
            "description": "True when the value comes from a calculation on this unit; such values cannot be set (`409`)."
          },
          "isCustomized": {
            "type": "boolean",
            "description": "True when the value was set on this unit and no longer follows the item's value."
          },
          "isItemLevel": {
            "type": "boolean",
            "description": "True when the property does not come from the item's Item Template (an item-level property of the item, or one added on this unit)."
          },
          "isPricing": {
            "type": "boolean",
            "description": "True for `Cost` and `Unit Price`, which cannot be set per unit through the API (`409`)."
          }
        },
        "additionalProperties": false,
        "description": "One custom-field value on an inventory unit."
      },
      "ItemUnitStatus": {
        "enum": [
          "Available",
          "InTransit",
          "AtJobSite",
          "InBundle"
        ],
        "type": "string",
        "description": "Where an inventory unit stands, derived from its location and transfer state."
      },
      "ItemWebhookData": {
        "type": "object",
        "properties": {
          "id": {
            "type": "integer",
            "description": "The item (template) id as used by `/api/v1/items/{id}`.",
            "format": "int32"
          },
          "name": {
            "type": "string",
            "nullable": true
          },
          "categoryId": {
            "type": "integer",
            "format": "int32",
            "nullable": true
          },
          "isDeleted": {
            "type": "boolean"
          },
          "changedFields": {
            "type": "array",
            "items": {
              "type": "string"
            },
            "description": "camelCase names of the fields that changed in this save, any of `name`, `description`, `categoryId`, `isDeleted`, `preferredVendorId`, `vendorPartNumber` (the vendor pair since 2026-09-23), `unitPrice`, `cost`, `properties` (the item's custom fields) and `unitProperties` (custom-field values on its inventory units). Example: `[\"unitPrice\"]`.",
            "nullable": true
          },
          "changedProperties": {
            "type": "array",
            "items": {
              "type": "string"
            },
            "description": "Names of the item's custom fields added, deleted or edited (value, name, type or options) in this save, both names on a rename; omitted when none (2026-09-23, additive).",
            "nullable": true
          },
          "changedUnitProperties": {
            "type": "array",
            "items": {
              "type": "string"
            },
            "description": "Names of custom fields whose value changed on one or more of the item's inventory units in this save; omitted when none (2026-09-23, additive).",
            "nullable": true
          }
        },
        "additionalProperties": false,
        "description": "Item summary carried by `item.updated`."
      },
      "JobAssigneeDto": {
        "type": "object",
        "properties": {
          "userId": {
            "type": "string",
            "description": "Identity user id of the assignee (matches `/api/v1/users`)."
          },
          "name": {
            "type": "string",
            "description": "Display name (first + last, or email when no name is set). Example: `Maria Lopez`."
          },
          "scheduleGroupId": {
            "type": "integer",
            "description": "Schedule group (crew) the assignment was made under, or null.",
            "format": "int32",
            "nullable": true
          },
          "scheduleGroupName": {
            "type": "string",
            "description": "Display name of `scheduleGroupId`. Example: `Crew A`.",
            "nullable": true
          },
          "notes": {
            "type": "string",
            "description": "Note attached to the assignment, if any.",
            "nullable": true
          }
        },
        "additionalProperties": false,
        "description": "One staff member assigned to a job."
      },
      "JobCustomChargeDto": {
        "type": "object",
        "properties": {
          "id": {
            "type": "integer",
            "description": "Charge id — pass as `chargeId` to `PATCH`/`DELETE /api/v1/jobs/{id}/charges/{chargeId}`. Example: `4210`.",
            "format": "int32"
          },
          "syncId": {
            "type": "string",
            "description": "Stable cross-device identifier of the charge.",
            "format": "uuid"
          },
          "name": {
            "type": "string",
            "description": "Charge name shown on the job and invoice. Example: `Disposal fee`."
          },
          "description": {
            "type": "string",
            "description": "Longer description of the charge, or null when none. Example: `Haul-away of the old unit`.",
            "nullable": true
          },
          "price": {
            "type": "number",
            "description": "Price charged to the customer before discounts, in the organization's currency (e.g. `75.00`).",
            "format": "double"
          },
          "discountPercent": {
            "type": "number",
            "description": "Line discount as a percentage (0–100), applied before the flat discount.",
            "format": "double"
          },
          "discountAmount": {
            "type": "number",
            "description": "Flat discount taken off the charge after the percentage discount, in the organization's currency.",
            "format": "double"
          },
          "effectivePrice": {
            "type": "number",
            "description": "Price after both line discounts, in the organization's currency — the amount that rolls into `customChargesTotal`. Example: `67.50`.",
            "format": "double"
          }
        },
        "additionalProperties": false,
        "description": "A custom charge (fee, disposal, permit …) on a job."
      },
      "JobDetailDto": {
        "type": "object",
        "properties": {
          "id": {
            "type": "integer",
            "description": "Numeric id used in `/api/v1/jobs/{id}`. Unique within this organization. Example: `1042`.",
            "format": "int32"
          },
          "syncId": {
            "type": "string",
            "description": "Stable cross-device identifier (GUID) — also the pagination cursor value. Example: `3f2504e0-4f89-11d3-9a0c-0305e82c3301`.",
            "format": "uuid"
          },
          "jobNumber": {
            "type": "string",
            "description": "Human-readable job number shown to staff and customers. Example: `J-2026-0042`."
          },
          "jobType": {
            "type": "string",
            "description": "Free-text job category configured by the organization. Example: `Installation`, `Repair`.",
            "nullable": true
          },
          "status": {
            "type": "string",
            "description": "Workflow status name. One of `Draft`, `InspectionInProgress`, `Estimated`,\n`AwaitingOptionSelection`, `Scheduled`, `InProgress`, `Completed`,\n`Invoiced`, `Paid`, `Cancelled`, `Outstanding`."
          },
          "statusName": {
            "type": "string",
            "description": "Display label of the status as the organization has customised it (falls back to `status`). Example: `On Site`."
          },
          "paymentStatus": {
            "type": "string",
            "description": "Payment state, independent of workflow status: `Unpaid`, `Partial`, `Paid`, `Overpaid` or `Refunded`.",
            "nullable": true
          },
          "customerId": {
            "type": "integer",
            "description": "Id of the customer the job is for (see `/api/v1/customers/{id}`).",
            "format": "int32"
          },
          "customerName": {
            "type": "string",
            "description": "Customer display name at read time. Example: `Acme Property Management`."
          },
          "description": {
            "type": "string",
            "description": "Short description of the work. May be null for drafts.",
            "nullable": true
          },
          "createdAt": {
            "type": "string",
            "description": "When the job was created, ISO-8601 UTC. Example: `2026-09-01T14:03:22Z`.",
            "format": "date-time"
          },
          "updatedAt": {
            "type": "string",
            "description": "Last modification time, ISO-8601 UTC (creation time when never modified). Use with the\n`updatedSince` filter to poll for changes.",
            "format": "date-time"
          },
          "scheduledDate": {
            "type": "string",
            "description": "Scheduled start as the organization's local wall-clock time with no UTC offset (the\n`date-time` format here is a local time, not RFC 3339 with zone), e.g.\n`2026-09-08T08:30:00`. Null when unscheduled.",
            "format": "date-time",
            "nullable": true
          },
          "scheduledDurationMinutes": {
            "type": "integer",
            "description": "Planned duration in minutes (default 120 when scheduled without an explicit duration). Null when unscheduled.",
            "format": "int32",
            "nullable": true
          },
          "startDate": {
            "type": "string",
            "description": "When work actually started, ISO-8601 UTC. Null until the job is in progress.",
            "format": "date-time",
            "nullable": true
          },
          "completedDate": {
            "type": "string",
            "description": "When work was completed, ISO-8601 UTC. Null until completed.",
            "format": "date-time",
            "nullable": true
          },
          "totalPrice": {
            "type": "number",
            "description": "Customer price before tax, after the job-level discount, in the organization's currency (decimal, e.g. `1250.00`).",
            "format": "double"
          },
          "notes": {
            "type": "string",
            "description": "Internal notes typed on the job itself (not the timeline). Null when empty.",
            "nullable": true
          },
          "siteAddress": {
            "$ref": "#/components/schemas/JobSiteAddressDto"
          },
          "materialPrice": {
            "type": "number",
            "description": "Sum of material lines before job-level discount and tax (decimal currency, e.g. `800.00`).",
            "format": "double"
          },
          "laborPrice": {
            "type": "number",
            "description": "Sum of labor lines before job-level discount and tax (decimal currency).",
            "format": "double"
          },
          "customChargesTotal": {
            "type": "number",
            "description": "Sum of custom charges before job-level discount and tax (decimal currency).",
            "format": "double"
          },
          "discountPercent": {
            "type": "number",
            "description": "Job-level discount as a percentage of every line (0–100), or null when a fixed amount / no discount applies.",
            "format": "double",
            "nullable": true
          },
          "discountAmount": {
            "type": "number",
            "description": "Job-level fixed discount amount (decimal currency), or null when a percentage / no discount applies.",
            "format": "double",
            "nullable": true
          },
          "taxRate": {
            "type": "number",
            "description": "Tax rate locked in at creation, as a percentage (e.g. `8.25`).",
            "format": "double"
          },
          "downPaymentAmount": {
            "type": "number",
            "description": "Money collected before invoicing (decimal currency), or null when none.",
            "format": "double",
            "nullable": true
          },
          "assignees": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/JobAssigneeDto"
            },
            "description": "Staff assigned to the job (requires the Scheduling module to change). Empty when unassigned."
          },
          "items": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/JobMaterialLineDto"
            },
            "description": "Material lines (parts/products) on the job, excluding removed lines."
          },
          "laborItems": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/JobLaborLineDto"
            },
            "description": "Labor lines on the job, excluding removed lines."
          },
          "customCharges": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/JobCustomChargeDto"
            },
            "description": "Custom charges (fees, disposal, permits …) on the job, excluding removed charges. Edit or remove one with its `id` via `PATCH`/`DELETE /api/v1/jobs/{id}/charges/{chargeId}`."
          },
          "updates": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/JobNoteDto"
            },
            "description": "Timeline of notes, progress updates and status changes, oldest first."
          },
          "warning": {
            "type": "string",
            "description": "Set only on the `POST /api/v1/jobs` response when `publishImmediately` was\nrequested but the job could not be published yet (it was created as a `Draft`),\ne.g. `Not published yet: Cannot finalize estimate. All material items must have a unit price.`.\nAlways null on `GET`.",
            "nullable": true
          }
        },
        "additionalProperties": false,
        "description": "Full job as returned by `GET /api/v1/jobs/{id}`: everything in\n`JobSummaryDto` plus the job site, pricing breakdown, assignees, material\nand labor lines and the note/update timeline."
      },
      "JobLaborLineDto": {
        "type": "object",
        "properties": {
          "id": {
            "type": "integer",
            "description": "Line id.",
            "format": "int32"
          },
          "syncId": {
            "type": "string",
            "description": "Stable cross-device identifier of the line.",
            "format": "uuid"
          },
          "description": {
            "type": "string",
            "description": "What the work is. Example: `Replace torsion springs`."
          },
          "estimatedHours": {
            "type": "number",
            "description": "Hours quoted (decimal, e.g. `1.5`).",
            "format": "double"
          },
          "actualHours": {
            "type": "number",
            "description": "Hours actually worked (decimal). Zero until logged.",
            "format": "double"
          },
          "customerRate": {
            "type": "number",
            "description": "Hourly rate billed to the customer (decimal currency).",
            "format": "double"
          },
          "customerTotal": {
            "type": "number",
            "description": "Customer price for the line after line discount, before job discount and tax (decimal currency).",
            "format": "double"
          },
          "technicianId": {
            "type": "string",
            "description": "Identity user id of the technician the line is booked to, or null.",
            "nullable": true
          },
          "status": {
            "type": "string",
            "description": "Line state name from the LaborItemStatus enum (e.g. `Planned`, `Scheduled`, `Completed`)."
          },
          "notes": {
            "type": "string",
            "description": "Line note, or null when none. Example: `Overtime rate approved by owner`.",
            "nullable": true
          }
        },
        "additionalProperties": false,
        "description": "A labor line on a job."
      },
      "JobMaterialLineDto": {
        "type": "object",
        "properties": {
          "id": {
            "type": "integer",
            "description": "Line id.",
            "format": "int32"
          },
          "syncId": {
            "type": "string",
            "description": "Stable cross-device identifier of the line.",
            "format": "uuid"
          },
          "itemTemplateId": {
            "type": "integer",
            "description": "Item template (catalog product) id — see `/api/v1/items/{id}`.",
            "format": "int32"
          },
          "itemTemplateName": {
            "type": "string",
            "description": "Catalog product name at read time. Example: `Torsion Spring 2in x 32in`."
          },
          "quantity": {
            "type": "integer",
            "description": "Quantity ordered for the job (whole units).",
            "format": "int32"
          },
          "deliveredQuantity": {
            "type": "integer",
            "description": "Quantity already delivered to / used at the site.",
            "format": "int32"
          },
          "unitPrice": {
            "type": "number",
            "description": "Customer unit price before line discount (decimal currency).",
            "format": "double"
          },
          "discountPercent": {
            "type": "number",
            "description": "Line discount as a percentage (0–100).",
            "format": "double"
          },
          "discountAmount": {
            "type": "number",
            "description": "Line discount as a fixed amount (decimal currency).",
            "format": "double"
          },
          "totalPrice": {
            "type": "number",
            "description": "Extended customer price after line discount, before job discount and tax (decimal currency).",
            "format": "double"
          },
          "status": {
            "type": "string",
            "description": "Fulfilment state: `Planned`, `Reserved`, `Used` (delivered) or `Returned`."
          },
          "notes": {
            "type": "string",
            "description": "Line note, or null when none. Example: `Bring the 8-ft ladder`.",
            "nullable": true
          }
        },
        "additionalProperties": false,
        "description": "A material (part/product) line on a job."
      },
      "JobNoteDto": {
        "type": "object",
        "properties": {
          "id": {
            "type": "integer",
            "description": "Entry id.",
            "format": "int32"
          },
          "syncId": {
            "type": "string",
            "description": "Stable cross-device identifier of the entry.",
            "format": "uuid"
          },
          "type": {
            "type": "string",
            "description": "Kind of entry — JobUpdateType name such as `GeneralNote`, `ProgressUpdate`, `Issue`, `CustomerRequest`, `StatusChange`, `Photo` or `Signature`."
          },
          "description": {
            "type": "string",
            "description": "The note text."
          },
          "isInternal": {
            "type": "boolean",
            "description": "True when the note is staff-only and must not be shown to the customer."
          },
          "createdAt": {
            "type": "string",
            "description": "When the entry was written, ISO-8601 UTC.",
            "format": "date-time"
          },
          "createdByName": {
            "type": "string",
            "description": "Display name of the author, when recorded.",
            "nullable": true
          },
          "oldStatus": {
            "type": "string",
            "description": "For status-change entries: the previous status name; otherwise null.",
            "nullable": true
          },
          "newStatus": {
            "type": "string",
            "description": "For status-change entries: the new status name; otherwise null.",
            "nullable": true
          },
          "attachmentName": {
            "type": "string",
            "description": "For `Photo` / `Signature` entries: the attached image's file name (e.g.\n`J-1042-20260929-143015.jpg`); null for entries without an attachment. The image itself is\nviewable on the job's page in the web app and is included in the organization data export.",
            "nullable": true
          }
        },
        "additionalProperties": false,
        "description": "One entry on a job's timeline (note, progress update, issue, status change…)."
      },
      "JobPhotoDto": {
        "type": "object",
        "properties": {
          "id": {
            "type": "integer",
            "description": "Photo id — the same number as the entry's `id` in the job's `updates`. Use it to fetch or delete the photo. Example: `77`.",
            "format": "int32"
          },
          "syncId": {
            "type": "string",
            "description": "Stable cross-device identifier of the photo entry (GUID). Example: `3f2c1b9e-8a41-4c55-9d7e-0b6f3f7a1c20`.",
            "format": "uuid"
          },
          "caption": {
            "type": "string",
            "description": "Caption shown on the job's timeline; `Photo` when none was given. Up to 2000 characters. Example: `Rust on the bottom panel`."
          },
          "attachmentName": {
            "type": "string",
            "description": "The image's file name, e.g. `J-1042-20260929-143015.jpg`; null when none was recorded.",
            "nullable": true
          },
          "contentType": {
            "type": "string",
            "description": "MIME type of the image: `image/jpeg` or `image/png`."
          },
          "createdAt": {
            "type": "string",
            "description": "When the photo was added, ISO-8601 UTC. Example: `2026-09-29T14:30:15Z`.",
            "format": "date-time"
          },
          "createdByName": {
            "type": "string",
            "description": "Display name (or email) of the person who added the photo, when recorded. Example: `Maria Lopez`.",
            "nullable": true
          }
        },
        "additionalProperties": false,
        "description": "A photo on a job (a `Photo` entry of the job's timeline), as returned by\n`GET /api/v1/jobs/{id}/photos`. The image itself is at\n`GET /api/v1/jobs/{id}/photos/{photoId}/content`."
      },
      "JobSiteAddressDto": {
        "type": "object",
        "properties": {
          "id": {
            "type": "integer",
            "description": "Customer address id (see `/api/v1/customers/{id}/locations`).",
            "format": "int32"
          },
          "name": {
            "type": "string",
            "description": "Label the customer gave the address. Example: `Main Office`."
          },
          "address": {
            "type": "string",
            "description": "Street line 1.",
            "nullable": true
          },
          "address2": {
            "type": "string",
            "description": "Street line 2 (suite, unit).",
            "nullable": true
          },
          "city": {
            "type": "string",
            "description": "City.",
            "nullable": true
          },
          "state": {
            "type": "string",
            "description": "State / province.",
            "nullable": true
          },
          "postalCode": {
            "type": "string",
            "description": "Postal / ZIP code.",
            "nullable": true
          },
          "country": {
            "type": "string",
            "description": "Country, when recorded.",
            "nullable": true
          }
        },
        "additionalProperties": false,
        "description": "A customer address used as the job site."
      },
      "JobSummaryDto": {
        "type": "object",
        "properties": {
          "id": {
            "type": "integer",
            "description": "Numeric id used in `/api/v1/jobs/{id}`. Unique within this organization. Example: `1042`.",
            "format": "int32"
          },
          "syncId": {
            "type": "string",
            "description": "Stable cross-device identifier (GUID) — also the pagination cursor value. Example: `3f2504e0-4f89-11d3-9a0c-0305e82c3301`.",
            "format": "uuid"
          },
          "jobNumber": {
            "type": "string",
            "description": "Human-readable job number shown to staff and customers. Example: `J-2026-0042`."
          },
          "jobType": {
            "type": "string",
            "description": "Free-text job category configured by the organization. Example: `Installation`, `Repair`.",
            "nullable": true
          },
          "status": {
            "type": "string",
            "description": "Workflow status name. One of `Draft`, `InspectionInProgress`, `Estimated`,\n`AwaitingOptionSelection`, `Scheduled`, `InProgress`, `Completed`,\n`Invoiced`, `Paid`, `Cancelled`, `Outstanding`."
          },
          "statusName": {
            "type": "string",
            "description": "Display label of the status as the organization has customised it (falls back to `status`). Example: `On Site`."
          },
          "paymentStatus": {
            "type": "string",
            "description": "Payment state, independent of workflow status: `Unpaid`, `Partial`, `Paid`, `Overpaid` or `Refunded`.",
            "nullable": true
          },
          "customerId": {
            "type": "integer",
            "description": "Id of the customer the job is for (see `/api/v1/customers/{id}`).",
            "format": "int32"
          },
          "customerName": {
            "type": "string",
            "description": "Customer display name at read time. Example: `Acme Property Management`."
          },
          "description": {
            "type": "string",
            "description": "Short description of the work. May be null for drafts.",
            "nullable": true
          },
          "createdAt": {
            "type": "string",
            "description": "When the job was created, ISO-8601 UTC. Example: `2026-09-01T14:03:22Z`.",
            "format": "date-time"
          },
          "updatedAt": {
            "type": "string",
            "description": "Last modification time, ISO-8601 UTC (creation time when never modified). Use with the\n`updatedSince` filter to poll for changes.",
            "format": "date-time"
          },
          "scheduledDate": {
            "type": "string",
            "description": "Scheduled start as the organization's local wall-clock time with no UTC offset (the\n`date-time` format here is a local time, not RFC 3339 with zone), e.g.\n`2026-09-08T08:30:00`. Null when unscheduled.",
            "format": "date-time",
            "nullable": true
          },
          "scheduledDurationMinutes": {
            "type": "integer",
            "description": "Planned duration in minutes (default 120 when scheduled without an explicit duration). Null when unscheduled.",
            "format": "int32",
            "nullable": true
          },
          "startDate": {
            "type": "string",
            "description": "When work actually started, ISO-8601 UTC. Null until the job is in progress.",
            "format": "date-time",
            "nullable": true
          },
          "completedDate": {
            "type": "string",
            "description": "When work was completed, ISO-8601 UTC. Null until completed.",
            "format": "date-time",
            "nullable": true
          },
          "totalPrice": {
            "type": "number",
            "description": "Customer price before tax, after the job-level discount, in the organization's currency (decimal, e.g. `1250.00`).",
            "format": "double"
          }
        },
        "additionalProperties": false,
        "description": "One job as returned by `GET /api/v1/jobs`. A compact row for lists, boards and\nagents; call `GET /api/v1/jobs/{id}` for assignees, line items and notes."
      },
      "JobWebhookData": {
        "type": "object",
        "properties": {
          "id": {
            "type": "integer",
            "format": "int32"
          },
          "jobNumber": {
            "type": "string",
            "nullable": true
          },
          "customerId": {
            "type": "integer",
            "format": "int32",
            "nullable": true
          },
          "status": {
            "type": "string",
            "nullable": true
          },
          "previousStatus": {
            "type": "string",
            "nullable": true
          },
          "scheduledDate": {
            "type": "string",
            "format": "date-time",
            "nullable": true
          },
          "assigneeUserIds": {
            "type": "array",
            "items": {
              "type": "string"
            },
            "nullable": true
          }
        },
        "additionalProperties": false,
        "description": "Job summary carried by `job.*` events."
      },
      "LaborRateDto": {
        "type": "object",
        "properties": {
          "id": {
            "type": "integer",
            "description": "Numeric id, usable as a job labor line's `laborRateId`. Example: `4`.",
            "format": "int32"
          },
          "syncId": {
            "type": "string",
            "description": "Stable cross-device id; also the pagination cursor.",
            "format": "uuid"
          },
          "name": {
            "type": "string",
            "description": "Rate name, unique within the organization. Example: `Standard Electrician`."
          },
          "description": {
            "type": "string",
            "description": "Free-text notes about the rate; null when not recorded.",
            "nullable": true
          },
          "hourlyRate": {
            "type": "number",
            "description": "Hourly rate charged to the customer, in the organization's currency (decimal, e.g. `95.00`). `0` for variable-price charges priced per job.",
            "format": "double"
          },
          "rateType": {
            "type": "string",
            "description": "Kind of rate, as a string: one of `Standard`, `Emergency`, `Weekend`, `AfterHours`, `SkillLevel`, `Custom`, `Flat`."
          },
          "isActive": {
            "type": "boolean",
            "description": "False for a retired rate: kept for history, not offered on new work."
          },
          "createdAt": {
            "type": "string",
            "description": "UTC creation time (ISO-8601).",
            "format": "date-time"
          },
          "updatedAt": {
            "type": "string",
            "description": "UTC last modification time (ISO-8601); null when never edited after creation.",
            "format": "date-time",
            "nullable": true
          }
        },
        "additionalProperties": false,
        "description": "A labor rate as returned by `GET /api/v1/labor-rates`: a named hourly charge for a kind of\nwork. Use the id as a job labor line's `laborRateId` when adding labor to a job."
      },
      "LocationStock": {
        "type": "object",
        "properties": {
          "locationId": {
            "type": "integer",
            "description": "Location id.",
            "format": "int32"
          },
          "locationName": {
            "type": "string",
            "description": "Location name."
          },
          "quantity": {
            "type": "integer",
            "description": "Whole item rows at the location (not deleted, not in a job site).",
            "format": "int32"
          }
        },
        "additionalProperties": false,
        "description": "Stock held at one location."
      },
      "LocationSummary": {
        "type": "object",
        "properties": {
          "id": {
            "type": "integer",
            "description": "Numeric id, usable in `/api/v1/locations/{id}` and stock writes.",
            "format": "int32"
          },
          "syncId": {
            "type": "string",
            "description": "Stable cross-device id; also the pagination cursor.",
            "format": "uuid"
          },
          "name": {
            "type": "string",
            "description": "Display name, e.g. `Main Warehouse`."
          },
          "address": {
            "type": "string",
            "description": "Street address when recorded; null otherwise.",
            "nullable": true
          },
          "parentLocationId": {
            "type": "integer",
            "description": "Id of the containing location; null for a top-level location.",
            "format": "int32",
            "nullable": true
          },
          "locationType": {
            "type": "string",
            "description": "Name of the location's type as configured by the organization, e.g. `Warehouse`, `Vehicle`, `Job Site`."
          },
          "isJobSite": {
            "type": "boolean",
            "description": "True for a customer job site: stock there is \"delivered\", never counted as on hand, and cannot be adjusted or moved directly."
          },
          "isTransportVehicle": {
            "type": "boolean",
            "description": "True for a vehicle (van, truck) that carries stock."
          },
          "isSystemLocation": {
            "type": "boolean",
            "description": "True for the organization's built-in `No Location` bucket, the default for stock created without a location."
          },
          "status": {
            "type": "string",
            "description": "One of `Active`, `Inactive`, `Maintenance`, `Temporary`."
          },
          "jobId": {
            "type": "integer",
            "description": "For a job site: the job it belongs to; null otherwise.",
            "format": "int32",
            "nullable": true
          },
          "createdAt": {
            "type": "string",
            "description": "UTC creation time.",
            "format": "date-time"
          },
          "updatedAt": {
            "type": "string",
            "description": "UTC last modification time; null when never edited.",
            "format": "date-time",
            "nullable": true
          }
        },
        "additionalProperties": false,
        "description": "One stock location as listed by `GET /api/v1/locations`: a warehouse, shelf, vehicle or\njob site that inventory rows sit in. Use the id as `locationId` in stock adjustments and\nmoves. Locations form a tree through ParentLocationId."
      },
      "LowStockItemDto": {
        "type": "object",
        "properties": {
          "itemId": {
            "type": "integer",
            "description": "Item id, usable in `/api/v1/items/{id}`. Example: `42`.",
            "format": "int32"
          },
          "itemName": {
            "type": "string",
            "description": "Item display name. Example: `\"1/2\\\" PEX Coupling\"`."
          },
          "description": {
            "type": "string",
            "description": "Item description, if any.",
            "nullable": true
          },
          "reorderPointId": {
            "type": "integer",
            "description": "Id of the reorder point that triggered this line (see `GET /api/v1/items/{id}/reorder-points`).",
            "format": "int32"
          },
          "locationId": {
            "type": "integer",
            "description": "Location the reorder point watches; null = the item's stock at all locations together.",
            "format": "int32",
            "nullable": true
          },
          "locationName": {
            "type": "string",
            "description": "Name of LocationId; null for all locations.",
            "nullable": true
          },
          "onHand": {
            "type": "integer",
            "description": "Whole units counted for the reorder point: non-deleted stock rows at LocationId, or everywhere (job sites included) when it is null. Example: `3`.",
            "format": "int32"
          },
          "minimumStock": {
            "type": "integer",
            "description": "The reorder point: the item is listed while OnHand is strictly below this. Example: `10`.",
            "format": "int32"
          },
          "reorderQuantity": {
            "type": "integer",
            "description": "Quantity configured to reorder; null when the reorder point has none.",
            "format": "int32",
            "nullable": true
          },
          "suggestedOrderQuantity": {
            "type": "integer",
            "description": "Units to order: ReorderQuantity, or twice MinimumStock when unset (the Reorder page's default). Example: `20`.",
            "format": "int32"
          },
          "cost": {
            "type": "number",
            "description": "Purchase cost per unit from the item's `Cost` property, in the organization's currency; null when unknown. Example: `0.40`.",
            "format": "double",
            "nullable": true
          },
          "vendorId": {
            "type": "integer",
            "description": "Vendor to order from: the item's preferred vendor, else the vendor of the CSV import that created the item; null when unknown. Example: `3`.",
            "format": "int32",
            "nullable": true
          },
          "vendorName": {
            "type": "string",
            "description": "Name of VendorId; null when unknown.",
            "nullable": true
          },
          "isPreferredVendor": {
            "type": "boolean",
            "description": "True when VendorId is the item's preferred vendor; false when it came from the import (or there is none)."
          },
          "vendorPartNumber": {
            "type": "string",
            "description": "The vendor's part number for the item, if recorded. Example: `\"PX-12C\"`.",
            "nullable": true
          }
        },
        "additionalProperties": false,
        "description": "One item below one of its reorder points, as listed by `GET /api/v1/items/low-stock`.\nComputed on request from the reorder points (the app's Reorder → Thresholds) and current stock;\nthe same rule as the Reorder page's alerts, but reading it creates no alert."
      },
      "MeDto": {
        "type": "object",
        "properties": {
          "organizationId": {
            "type": "integer",
            "description": "Numeric id of the key's organization. Example: `5`.",
            "format": "int32"
          },
          "organizationName": {
            "type": "string",
            "description": "Display name of the organization. Example: `\"Hero Distribution\"`."
          },
          "permissions": {
            "type": "array",
            "items": {
              "type": "string"
            },
            "description": "The permission names this key can exercise (a subset of its creator's). Example: `[\"ViewJobs\",\"ManageJobs\"]`."
          }
        },
        "additionalProperties": false,
        "description": "Identity of the API key presented on the request — used by integration platforms (Zapier, Make) to\ntest a connection and label it. Reflects the key's organization and the permissions it currently holds."
      },
      "ModuleSummary": {
        "type": "object",
        "properties": {
          "key": {
            "type": "string",
            "description": "Stable module key (e.g. `scheduling`). Use this in code, never Name."
          },
          "name": {
            "type": "string",
            "description": "Display name."
          },
          "active": {
            "type": "boolean",
            "description": "True when the organization can use the module (included tier or activated)."
          }
        },
        "additionalProperties": false,
        "description": "One module from the catalog and whether the calling organization has it."
      },
      "ModulesResponse": {
        "type": "object",
        "properties": {
          "modules": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/ModuleSummary"
            },
            "description": "Every catalog module with its activation state for this organization."
          },
          "permissions": {
            "type": "array",
            "items": {
              "type": "string"
            },
            "description": "FlexPermissions names granted to the presented key."
          }
        },
        "additionalProperties": false,
        "description": "Capability discovery for an API key: the org's modules and the key's permissions."
      },
      "MoveStockRequest": {
        "type": "object",
        "properties": {
          "fromLocationId": {
            "maximum": 2147483647,
            "minimum": 1,
            "type": "integer",
            "description": "Location the units leave (see `GET /api/v1/locations`). Must hold at least `quantity` available units.",
            "format": "int32"
          },
          "toLocationId": {
            "type": "integer",
            "description": "Location the units arrive at. Omit or null for the organization's \"No Location\" bucket. Job sites are refused (422) — deliver there through a job or transfer.",
            "format": "int32",
            "nullable": true
          },
          "quantity": {
            "maximum": 1000,
            "minimum": 1,
            "type": "integer",
            "description": "Whole units to move, 1..1000. Oldest units at the source move first.",
            "format": "int32"
          },
          "reason": {
            "maxLength": 500,
            "minLength": 0,
            "type": "string",
            "description": "Optional note recorded in each moved unit's change history. Max 500 characters.",
            "nullable": true
          }
        },
        "additionalProperties": false,
        "description": "Body of `POST /api/v1/items/{id}/stock/move`: move whole units of an item between two\nstock locations. Example: `{ \"fromLocationId\": 1, \"toLocationId\": 4, \"quantity\": 3 }`."
      },
      "PagedResponseOfAdminRemovalRequestDto": {
        "type": "object",
        "properties": {
          "items": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/AdminRemovalRequestDto"
            },
            "description": "Items of this page in ascending creation order."
          },
          "nextCursor": {
            "type": "string",
            "description": "SyncId of the last item; pass it back as `?after=` to fetch the next page.\n`null` when this is the last page.",
            "nullable": true
          }
        },
        "additionalProperties": false,
        "description": "One page of a cursor-paginated /api/v1 collection (API_AND_MCP.md:\n`?after=<syncId>&limit=`)."
      },
      "PagedResponseOfApiActivityDto": {
        "type": "object",
        "properties": {
          "items": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/ApiActivityDto"
            },
            "description": "Items of this page in ascending creation order."
          },
          "nextCursor": {
            "type": "string",
            "description": "SyncId of the last item; pass it back as `?after=` to fetch the next page.\n`null` when this is the last page.",
            "nullable": true
          }
        },
        "additionalProperties": false,
        "description": "One page of a cursor-paginated /api/v1 collection (API_AND_MCP.md:\n`?after=<syncId>&limit=`)."
      },
      "PagedResponseOfCategoryDto": {
        "type": "object",
        "properties": {
          "items": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/CategoryDto"
            },
            "description": "Items of this page in ascending creation order."
          },
          "nextCursor": {
            "type": "string",
            "description": "SyncId of the last item; pass it back as `?after=` to fetch the next page.\n`null` when this is the last page.",
            "nullable": true
          }
        },
        "additionalProperties": false,
        "description": "One page of a cursor-paginated /api/v1 collection (API_AND_MCP.md:\n`?after=<syncId>&limit=`)."
      },
      "PagedResponseOfCustomerDto": {
        "type": "object",
        "properties": {
          "items": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/CustomerDto"
            },
            "description": "Items of this page in ascending creation order."
          },
          "nextCursor": {
            "type": "string",
            "description": "SyncId of the last item; pass it back as `?after=` to fetch the next page.\n`null` when this is the last page.",
            "nullable": true
          }
        },
        "additionalProperties": false,
        "description": "One page of a cursor-paginated /api/v1 collection (API_AND_MCP.md:\n`?after=<syncId>&limit=`)."
      },
      "PagedResponseOfCustomerLocationDto": {
        "type": "object",
        "properties": {
          "items": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/CustomerLocationDto"
            },
            "description": "Items of this page in ascending creation order."
          },
          "nextCursor": {
            "type": "string",
            "description": "SyncId of the last item; pass it back as `?after=` to fetch the next page.\n`null` when this is the last page.",
            "nullable": true
          }
        },
        "additionalProperties": false,
        "description": "One page of a cursor-paginated /api/v1 collection (API_AND_MCP.md:\n`?after=<syncId>&limit=`)."
      },
      "PagedResponseOfInvoiceSummary": {
        "type": "object",
        "properties": {
          "items": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/InvoiceSummary"
            },
            "description": "Items of this page in ascending creation order."
          },
          "nextCursor": {
            "type": "string",
            "description": "SyncId of the last item; pass it back as `?after=` to fetch the next page.\n`null` when this is the last page.",
            "nullable": true
          }
        },
        "additionalProperties": false,
        "description": "One page of a cursor-paginated /api/v1 collection (API_AND_MCP.md:\n`?after=<syncId>&limit=`)."
      },
      "PagedResponseOfItemSummary": {
        "type": "object",
        "properties": {
          "items": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/ItemSummary"
            },
            "description": "Items of this page in ascending creation order."
          },
          "nextCursor": {
            "type": "string",
            "description": "SyncId of the last item; pass it back as `?after=` to fetch the next page.\n`null` when this is the last page.",
            "nullable": true
          }
        },
        "additionalProperties": false,
        "description": "One page of a cursor-paginated /api/v1 collection (API_AND_MCP.md:\n`?after=<syncId>&limit=`)."
      },
      "PagedResponseOfItemUnitDto": {
        "type": "object",
        "properties": {
          "items": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/ItemUnitDto"
            },
            "description": "Items of this page in ascending creation order."
          },
          "nextCursor": {
            "type": "string",
            "description": "SyncId of the last item; pass it back as `?after=` to fetch the next page.\n`null` when this is the last page.",
            "nullable": true
          }
        },
        "additionalProperties": false,
        "description": "One page of a cursor-paginated /api/v1 collection (API_AND_MCP.md:\n`?after=<syncId>&limit=`)."
      },
      "PagedResponseOfJobPhotoDto": {
        "type": "object",
        "properties": {
          "items": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/JobPhotoDto"
            },
            "description": "Items of this page in ascending creation order."
          },
          "nextCursor": {
            "type": "string",
            "description": "SyncId of the last item; pass it back as `?after=` to fetch the next page.\n`null` when this is the last page.",
            "nullable": true
          }
        },
        "additionalProperties": false,
        "description": "One page of a cursor-paginated /api/v1 collection (API_AND_MCP.md:\n`?after=<syncId>&limit=`)."
      },
      "PagedResponseOfJobSummaryDto": {
        "type": "object",
        "properties": {
          "items": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/JobSummaryDto"
            },
            "description": "Items of this page in ascending creation order."
          },
          "nextCursor": {
            "type": "string",
            "description": "SyncId of the last item; pass it back as `?after=` to fetch the next page.\n`null` when this is the last page.",
            "nullable": true
          }
        },
        "additionalProperties": false,
        "description": "One page of a cursor-paginated /api/v1 collection (API_AND_MCP.md:\n`?after=<syncId>&limit=`)."
      },
      "PagedResponseOfLaborRateDto": {
        "type": "object",
        "properties": {
          "items": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/LaborRateDto"
            },
            "description": "Items of this page in ascending creation order."
          },
          "nextCursor": {
            "type": "string",
            "description": "SyncId of the last item; pass it back as `?after=` to fetch the next page.\n`null` when this is the last page.",
            "nullable": true
          }
        },
        "additionalProperties": false,
        "description": "One page of a cursor-paginated /api/v1 collection (API_AND_MCP.md:\n`?after=<syncId>&limit=`)."
      },
      "PagedResponseOfLocationSummary": {
        "type": "object",
        "properties": {
          "items": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/LocationSummary"
            },
            "description": "Items of this page in ascending creation order."
          },
          "nextCursor": {
            "type": "string",
            "description": "SyncId of the last item; pass it back as `?after=` to fetch the next page.\n`null` when this is the last page.",
            "nullable": true
          }
        },
        "additionalProperties": false,
        "description": "One page of a cursor-paginated /api/v1 collection (API_AND_MCP.md:\n`?after=<syncId>&limit=`)."
      },
      "PagedResponseOfLowStockItemDto": {
        "type": "object",
        "properties": {
          "items": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/LowStockItemDto"
            },
            "description": "Items of this page in ascending creation order."
          },
          "nextCursor": {
            "type": "string",
            "description": "SyncId of the last item; pass it back as `?after=` to fetch the next page.\n`null` when this is the last page.",
            "nullable": true
          }
        },
        "additionalProperties": false,
        "description": "One page of a cursor-paginated /api/v1 collection (API_AND_MCP.md:\n`?after=<syncId>&limit=`)."
      },
      "PagedResponseOfPurchaseOrderSummaryDto": {
        "type": "object",
        "properties": {
          "items": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/PurchaseOrderSummaryDto"
            },
            "description": "Items of this page in ascending creation order."
          },
          "nextCursor": {
            "type": "string",
            "description": "SyncId of the last item; pass it back as `?after=` to fetch the next page.\n`null` when this is the last page.",
            "nullable": true
          }
        },
        "additionalProperties": false,
        "description": "One page of a cursor-paginated /api/v1 collection (API_AND_MCP.md:\n`?after=<syncId>&limit=`)."
      },
      "PagedResponseOfReorderPointDto": {
        "type": "object",
        "properties": {
          "items": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/ReorderPointDto"
            },
            "description": "Items of this page in ascending creation order."
          },
          "nextCursor": {
            "type": "string",
            "description": "SyncId of the last item; pass it back as `?after=` to fetch the next page.\n`null` when this is the last page.",
            "nullable": true
          }
        },
        "additionalProperties": false,
        "description": "One page of a cursor-paginated /api/v1 collection (API_AND_MCP.md:\n`?after=<syncId>&limit=`)."
      },
      "PagedResponseOfRoleDto": {
        "type": "object",
        "properties": {
          "items": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/RoleDto"
            },
            "description": "Items of this page in ascending creation order."
          },
          "nextCursor": {
            "type": "string",
            "description": "SyncId of the last item; pass it back as `?after=` to fetch the next page.\n`null` when this is the last page.",
            "nullable": true
          }
        },
        "additionalProperties": false,
        "description": "One page of a cursor-paginated /api/v1 collection (API_AND_MCP.md:\n`?after=<syncId>&limit=`)."
      },
      "PagedResponseOfSalesOrderSummaryDto": {
        "type": "object",
        "properties": {
          "items": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/SalesOrderSummaryDto"
            },
            "description": "Items of this page in ascending creation order."
          },
          "nextCursor": {
            "type": "string",
            "description": "SyncId of the last item; pass it back as `?after=` to fetch the next page.\n`null` when this is the last page.",
            "nullable": true
          }
        },
        "additionalProperties": false,
        "description": "One page of a cursor-paginated /api/v1 collection (API_AND_MCP.md:\n`?after=<syncId>&limit=`)."
      },
      "PagedResponseOfScheduleGroupDto": {
        "type": "object",
        "properties": {
          "items": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/ScheduleGroupDto"
            },
            "description": "Items of this page in ascending creation order."
          },
          "nextCursor": {
            "type": "string",
            "description": "SyncId of the last item; pass it back as `?after=` to fetch the next page.\n`null` when this is the last page.",
            "nullable": true
          }
        },
        "additionalProperties": false,
        "description": "One page of a cursor-paginated /api/v1 collection (API_AND_MCP.md:\n`?after=<syncId>&limit=`)."
      },
      "PagedResponseOfTransferSummaryDto": {
        "type": "object",
        "properties": {
          "items": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/TransferSummaryDto"
            },
            "description": "Items of this page in ascending creation order."
          },
          "nextCursor": {
            "type": "string",
            "description": "SyncId of the last item; pass it back as `?after=` to fetch the next page.\n`null` when this is the last page.",
            "nullable": true
          }
        },
        "additionalProperties": false,
        "description": "One page of a cursor-paginated /api/v1 collection (API_AND_MCP.md:\n`?after=<syncId>&limit=`)."
      },
      "PagedResponseOfUserSummaryDto": {
        "type": "object",
        "properties": {
          "items": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/UserSummaryDto"
            },
            "description": "Items of this page in ascending creation order."
          },
          "nextCursor": {
            "type": "string",
            "description": "SyncId of the last item; pass it back as `?after=` to fetch the next page.\n`null` when this is the last page.",
            "nullable": true
          }
        },
        "additionalProperties": false,
        "description": "One page of a cursor-paginated /api/v1 collection (API_AND_MCP.md:\n`?after=<syncId>&limit=`)."
      },
      "PagedResponseOfVendorDto": {
        "type": "object",
        "properties": {
          "items": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/VendorDto"
            },
            "description": "Items of this page in ascending creation order."
          },
          "nextCursor": {
            "type": "string",
            "description": "SyncId of the last item; pass it back as `?after=` to fetch the next page.\n`null` when this is the last page.",
            "nullable": true
          }
        },
        "additionalProperties": false,
        "description": "One page of a cursor-paginated /api/v1 collection (API_AND_MCP.md:\n`?after=<syncId>&limit=`)."
      },
      "PaymentTerms": {
        "enum": [
          "DueOnReceipt",
          "Net15",
          "Net30",
          "Net45",
          "Net60",
          "COD",
          "Prepaid"
        ],
        "type": "string"
      },
      "PaymentsSummary": {
        "type": "object",
        "properties": {
          "count": {
            "type": "integer",
            "description": "Number of payments.",
            "format": "int32"
          },
          "totalPaid": {
            "type": "number",
            "description": "Sum of payment amounts.",
            "format": "double"
          },
          "lastPaymentDate": {
            "type": "string",
            "description": "Most recent payment date, ISO-8601 UTC; null when no payment has been recorded.",
            "format": "date-time",
            "nullable": true
          },
          "payments": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/InvoicePaymentDto"
            },
            "description": "Individual payments, oldest first."
          }
        },
        "additionalProperties": false,
        "description": "Payments applied to an invoice."
      },
      "PickSalesOrderApiRequest": {
        "type": "object",
        "properties": {
          "transferId": {
            "type": "integer",
            "description": "The order's Pending pick transfer to pick (the `transferId` returned by `/fulfill`). Omit when the order has exactly one Pending transfer. Example: `904`.",
            "format": "int32",
            "nullable": true
          },
          "lines": {
            "maxItems": 500,
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/PickSalesOrderLineApiRequest"
            },
            "description": "Units to pick per transfer line, 1–500 entries. Omit (or send an empty list) to pick every\nline in full — the app's \"Pick All\". Each entry adds `quantity` to the line's picked\ncount; every entry is checked before anything is written.",
            "nullable": true
          }
        },
        "additionalProperties": false,
        "description": "Body of `POST /api/v1/sales-orders/{id}/pick`. Example (pick everything):\n`{ \"transferId\": 904 }`; example (part of one line): `{ \"transferId\": 904, \"lines\": [ { \"transferItemId\": 5120, \"quantity\": 2 } ] }`."
      },
      "PickSalesOrderLineApiRequest": {
        "type": "object",
        "properties": {
          "transferItemId": {
            "maximum": 2147483647,
            "minimum": 1,
            "type": "integer",
            "description": "Transfer line id: `lines[].id` of `GET /api/v1/transfers/{transferId}`. Example: `5120`.",
            "format": "int32"
          },
          "quantity": {
            "maximum": 1000000,
            "minimum": 1,
            "type": "integer",
            "description": "Units to add to the line's picked count, from 1 up to the units still unpicked on the line (`quantity` − `quantityPicked`; more is 422). Example: `2`.",
            "format": "int32"
          }
        },
        "additionalProperties": false,
        "description": "One transfer line to pick."
      },
      "PriceTier": {
        "enum": [
          "Standard",
          "Silver",
          "Gold",
          "Platinum",
          "Custom"
        ],
        "type": "string"
      },
      "ProblemDetails": {
        "type": "object",
        "properties": {
          "type": {
            "type": "string",
            "description": "URI identifying the problem type (RFC 7807). Currently the generic reference for the HTTP status.",
            "nullable": true
          },
          "title": {
            "type": "string",
            "description": "Short human-readable summary of the status, e.g. \"Not Found\".",
            "nullable": true
          },
          "status": {
            "type": "integer",
            "description": "HTTP status code, repeated from the response line.",
            "format": "int32",
            "nullable": true
          },
          "detail": {
            "type": "string",
            "description": "Explanation specific to this occurrence, e.g. \"Job 42 was not found in this organization.\"",
            "nullable": true
          },
          "instance": {
            "type": "string",
            "description": "Request path the problem occurred on, when known.",
            "nullable": true
          }
        },
        "additionalProperties": { },
        "description": "RFC 7807 problem detail (application/problem+json). Also carries a traceId string to quote in support requests."
      },
      "PropertyDataType": {
        "enum": [
          "Text",
          "Number",
          "Date",
          "Boolean",
          "Dropdown",
          "MultiSelect"
        ],
        "type": "string"
      },
      "PurchaseOrderDetailDto": {
        "type": "object",
        "properties": {
          "id": {
            "type": "integer",
            "description": "Numeric id used in `/api/v1/purchase-orders/{id}`. Unique within this organization. Example: `412`.",
            "format": "int32"
          },
          "syncId": {
            "type": "string",
            "description": "Stable cross-device identifier (GUID) — also the pagination cursor value. Example: `3c9d1a2b-7e4f-4b6a-8c1d-5e6f7a8b9c0d`.",
            "format": "uuid"
          },
          "orderNumber": {
            "type": "string",
            "description": "The organization's purchase-order number. Example: `PO-ACME-20260915-01`."
          },
          "externalOrderNumber": {
            "type": "string",
            "description": "The vendor's own order or confirmation number; null when not recorded. Example: `SO-88213`.",
            "nullable": true
          },
          "status": {
            "type": "string",
            "description": "Order status name, derived by the app from the order's shipments and received quantities.\nOne of `Draft` (no shipment yet), `Ordered`, `Shipped`, `PartialShipped`,\n`Delivered` (at the receiving location, nothing received yet), `PartialReceived`,\n`Received` (every unit received and every shipment delivered), `Cancelled`."
          },
          "vendorId": {
            "type": "integer",
            "description": "Id of the vendor record; null for a one-off vendor typed on the order.",
            "format": "int32",
            "nullable": true
          },
          "vendorName": {
            "type": "string",
            "description": "Vendor name (the vendor record's name, else the one-off name on the order). Example: `Acme Supply`. Null when neither is set.",
            "nullable": true
          },
          "orderDate": {
            "type": "string",
            "description": "Order date as entered in the app: the organization's local calendar date, `yyyy-MM-ddT00:00:00` with no UTC offset (the `date-time` format here is a local date, not an RFC 3339 instant; do not convert it between time zones). Example: `2026-09-15T00:00:00`.",
            "format": "date-time"
          },
          "expectedDeliveryDate": {
            "type": "string",
            "description": "Expected delivery date as entered in the app, same local calendar-date format as `orderDate` (no UTC offset). Example: `2026-09-22T00:00:00`. Null when unknown.",
            "format": "date-time",
            "nullable": true
          },
          "receivedDate": {
            "type": "string",
            "description": "When the order became fully `Received`, ISO-8601 UTC; null until then.",
            "format": "date-time",
            "nullable": true
          },
          "totalCost": {
            "type": "number",
            "description": "Order total in `currency` as recorded on the order; null when not calculated. Example: `1249.50`.",
            "format": "double",
            "nullable": true
          },
          "currency": {
            "type": "string",
            "description": "ISO-4217 currency code of the prices on the order. Example: `USD`."
          },
          "receivingLocationId": {
            "type": "integer",
            "description": "Id of the location stock is received at by default (see `/api/v1/locations`). Example: `10`.",
            "format": "int32"
          },
          "receivingLocationName": {
            "type": "string",
            "description": "Name of `receivingLocationId`. Example: `Main Warehouse`.",
            "nullable": true
          },
          "lineCount": {
            "type": "integer",
            "description": "Number of lines on the order. Example: `3`.",
            "format": "int32"
          },
          "quantityOrdered": {
            "type": "integer",
            "description": "Units ordered across all lines. Example: `40`.",
            "format": "int32"
          },
          "quantityReceived": {
            "type": "integer",
            "description": "Units received across all lines. Example: `25`.",
            "format": "int32"
          },
          "createdAt": {
            "type": "string",
            "description": "Creation time, ISO-8601 UTC. Example: `2026-09-15T14:05:00Z`.",
            "format": "date-time"
          },
          "updatedAt": {
            "type": "string",
            "description": "Last modification time (including a receipt), ISO-8601 UTC; null when never modified since creation.",
            "format": "date-time",
            "nullable": true
          },
          "notes": {
            "type": "string",
            "description": "Free-text notes on the order. Example: `Deliver to dock 3.` Null when none.",
            "nullable": true
          },
          "lines": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/PurchaseOrderLineDto"
            },
            "description": "The order lines, in the order they were added."
          }
        },
        "additionalProperties": false,
        "description": "One purchase order with its lines, as returned by `GET /api/v1/purchase-orders/{id}`\nand by `POST /api/v1/purchase-orders/{id}/receive`."
      },
      "PurchaseOrderLineApiRequest": {
        "type": "object",
        "properties": {
          "itemId": {
            "type": "integer",
            "description": "Item to order (from `GET /api/v1/items`); not a draft or a sub-unit item. Example: `12`.",
            "format": "int32",
            "nullable": true
          },
          "bundleTemplateId": {
            "type": "integer",
            "description": "Bundle template to order instead of an item; receiving creates one bundle per unit.",
            "format": "int32",
            "nullable": true
          },
          "description": {
            "maxLength": 500,
            "minLength": 0,
            "type": "string",
            "description": "Line text, up to 500 characters. Defaults to the item or bundle name.",
            "nullable": true
          },
          "vendorPartNumber": {
            "maxLength": 100,
            "minLength": 0,
            "type": "string",
            "description": "The vendor's part number, up to 100 characters. Example: `RMX-122-250`.",
            "nullable": true
          },
          "quantityOrdered": {
            "maximum": 100000,
            "minimum": 1,
            "type": "integer",
            "description": "Whole units ordered, at least 1. Example: `24`.",
            "format": "int32"
          },
          "unitPrice": {
            "maximum": 999999999,
            "minimum": 0,
            "type": "number",
            "description": "Price per unit in the order's currency; becomes each received unit's `Cost`. Optional, not negative. Example: `3.10`.",
            "format": "double",
            "nullable": true
          },
          "notes": {
            "maxLength": 1000,
            "minLength": 0,
            "type": "string",
            "description": "Free-text note on the line.",
            "nullable": true
          }
        },
        "additionalProperties": false,
        "description": "One order line: exactly one of `itemId` or `bundleTemplateId`.\nExample: `{ \"itemId\": 12, \"quantityOrdered\": 24, \"unitPrice\": 3.10, \"vendorPartNumber\": \"RMX-122\" }`."
      },
      "PurchaseOrderLineDto": {
        "type": "object",
        "properties": {
          "id": {
            "type": "integer",
            "description": "Line id — the `lineId` to pass to `POST /api/v1/purchase-orders/{id}/receive`. Example: `1207`.",
            "format": "int32"
          },
          "itemId": {
            "type": "integer",
            "description": "Item (template) id as used by `/api/v1/items/{id}`; null for a bundle line.",
            "format": "int32",
            "nullable": true
          },
          "bundleTemplateId": {
            "type": "integer",
            "description": "Bundle template id for a bundle (kit) line; null for an item line.",
            "format": "int32",
            "nullable": true
          },
          "name": {
            "type": "string",
            "description": "What is ordered: the item or bundle name at read time, else the line description. Example: `Copper 12/2 Romex`.",
            "nullable": true
          },
          "description": {
            "type": "string",
            "description": "Line description as entered on the order. Example: `Copper 12/2 Romex, 250 ft`.",
            "nullable": true
          },
          "vendorPartNumber": {
            "type": "string",
            "description": "The vendor's part number; null when not recorded. Example: `RMX-122-250`.",
            "nullable": true
          },
          "quantityOrdered": {
            "type": "integer",
            "description": "Units ordered. Example: `10`.",
            "format": "int32"
          },
          "quantityShipped": {
            "type": "integer",
            "description": "Units the vendor has shipped, as recorded on the order. Example: `10`.",
            "format": "int32"
          },
          "quantityReceived": {
            "type": "integer",
            "description": "Units received into stock so far. Example: `6`.",
            "format": "int32"
          },
          "quantityRemaining": {
            "type": "integer",
            "description": "Units still to receive (`quantityOrdered` minus `quantityReceived`) — the most `receive` accepts for this line. Example: `4`.",
            "format": "int32"
          },
          "unitPrice": {
            "type": "number",
            "description": "Unit price in the order's `currency`; becomes the `Cost` property of each received item. Null when not priced. Example: `89.99`.",
            "format": "double",
            "nullable": true
          },
          "lineTotal": {
            "type": "number",
            "description": "Line total in the order's `currency`; null when not calculated. Example: `899.90`.",
            "format": "double",
            "nullable": true
          },
          "shipmentId": {
            "type": "integer",
            "description": "Id of the shipment the line travels on; null when not assigned to a shipment. Received items on a shipment with a shipping cost get `Shipping Cost` and `Total Cost` properties.",
            "format": "int32",
            "nullable": true
          }
        },
        "additionalProperties": false,
        "description": "One line of a purchase order: a catalog item or a bundle, with ordered and received quantities."
      },
      "PurchaseOrderSummaryDto": {
        "type": "object",
        "properties": {
          "id": {
            "type": "integer",
            "description": "Numeric id used in `/api/v1/purchase-orders/{id}`. Unique within this organization. Example: `412`.",
            "format": "int32"
          },
          "syncId": {
            "type": "string",
            "description": "Stable cross-device identifier (GUID) — also the pagination cursor value. Example: `3c9d1a2b-7e4f-4b6a-8c1d-5e6f7a8b9c0d`.",
            "format": "uuid"
          },
          "orderNumber": {
            "type": "string",
            "description": "The organization's purchase-order number. Example: `PO-ACME-20260915-01`."
          },
          "externalOrderNumber": {
            "type": "string",
            "description": "The vendor's own order or confirmation number; null when not recorded. Example: `SO-88213`.",
            "nullable": true
          },
          "status": {
            "type": "string",
            "description": "Order status name, derived by the app from the order's shipments and received quantities.\nOne of `Draft` (no shipment yet), `Ordered`, `Shipped`, `PartialShipped`,\n`Delivered` (at the receiving location, nothing received yet), `PartialReceived`,\n`Received` (every unit received and every shipment delivered), `Cancelled`."
          },
          "vendorId": {
            "type": "integer",
            "description": "Id of the vendor record; null for a one-off vendor typed on the order.",
            "format": "int32",
            "nullable": true
          },
          "vendorName": {
            "type": "string",
            "description": "Vendor name (the vendor record's name, else the one-off name on the order). Example: `Acme Supply`. Null when neither is set.",
            "nullable": true
          },
          "orderDate": {
            "type": "string",
            "description": "Order date as entered in the app: the organization's local calendar date, `yyyy-MM-ddT00:00:00` with no UTC offset (the `date-time` format here is a local date, not an RFC 3339 instant; do not convert it between time zones). Example: `2026-09-15T00:00:00`.",
            "format": "date-time"
          },
          "expectedDeliveryDate": {
            "type": "string",
            "description": "Expected delivery date as entered in the app, same local calendar-date format as `orderDate` (no UTC offset). Example: `2026-09-22T00:00:00`. Null when unknown.",
            "format": "date-time",
            "nullable": true
          },
          "receivedDate": {
            "type": "string",
            "description": "When the order became fully `Received`, ISO-8601 UTC; null until then.",
            "format": "date-time",
            "nullable": true
          },
          "totalCost": {
            "type": "number",
            "description": "Order total in `currency` as recorded on the order; null when not calculated. Example: `1249.50`.",
            "format": "double",
            "nullable": true
          },
          "currency": {
            "type": "string",
            "description": "ISO-4217 currency code of the prices on the order. Example: `USD`."
          },
          "receivingLocationId": {
            "type": "integer",
            "description": "Id of the location stock is received at by default (see `/api/v1/locations`). Example: `10`.",
            "format": "int32"
          },
          "receivingLocationName": {
            "type": "string",
            "description": "Name of `receivingLocationId`. Example: `Main Warehouse`.",
            "nullable": true
          },
          "lineCount": {
            "type": "integer",
            "description": "Number of lines on the order. Example: `3`.",
            "format": "int32"
          },
          "quantityOrdered": {
            "type": "integer",
            "description": "Units ordered across all lines. Example: `40`.",
            "format": "int32"
          },
          "quantityReceived": {
            "type": "integer",
            "description": "Units received across all lines. Example: `25`.",
            "format": "int32"
          },
          "createdAt": {
            "type": "string",
            "description": "Creation time, ISO-8601 UTC. Example: `2026-09-15T14:05:00Z`.",
            "format": "date-time"
          },
          "updatedAt": {
            "type": "string",
            "description": "Last modification time (including a receipt), ISO-8601 UTC; null when never modified since creation.",
            "format": "date-time",
            "nullable": true
          }
        },
        "additionalProperties": false,
        "description": "One purchase order as returned by `GET /api/v1/purchase-orders`: stock ordered from a\nvendor, to be received at a location. A flat row for lists and CSV; call\n`GET /api/v1/purchase-orders/{id}` for its lines."
      },
      "PurchaseOrderWebhookData": {
        "type": "object",
        "properties": {
          "id": {
            "type": "integer",
            "format": "int32"
          },
          "orderNumber": {
            "type": "string",
            "nullable": true
          },
          "vendorId": {
            "type": "integer",
            "format": "int32",
            "nullable": true
          },
          "status": {
            "type": "string",
            "nullable": true
          },
          "previousStatus": {
            "type": "string",
            "nullable": true
          },
          "total": {
            "type": "number",
            "format": "double",
            "nullable": true
          }
        },
        "additionalProperties": false,
        "description": "Purchase-order summary carried by `purchase_order.*` events; `previousStatus` is set only on `purchase_order.status_changed`."
      },
      "ReceivePurchaseOrderApiRequest": {
        "required": [
          "lines"
        ],
        "type": "object",
        "properties": {
          "lines": {
            "maxItems": 200,
            "minItems": 1,
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/ReceivePurchaseOrderLineApiRequest"
            },
            "description": "Lines to receive (1–200 entries). All are validated before anything is written; the\nfirst problem is reported and nothing is received."
          }
        },
        "additionalProperties": false,
        "description": "Body of `POST /api/v1/purchase-orders/{id}/receive`. Example:\n`{ \"lines\": [ { \"lineId\": 1207, \"quantity\": 4 }, { \"lineId\": 1208, \"quantity\": 1, \"locationId\": 11 } ] }`."
      },
      "ReceivePurchaseOrderLineApiRequest": {
        "type": "object",
        "properties": {
          "lineId": {
            "maximum": 2147483647,
            "minimum": 1,
            "type": "integer",
            "description": "The line's `id` from `GET /api/v1/purchase-orders/{id}`. Example: `1207`.",
            "format": "int32"
          },
          "quantity": {
            "maximum": 2147483647,
            "minimum": 1,
            "type": "integer",
            "description": "Whole units received now, 1 to the line's `quantityRemaining` (a line repeated in\nthe same request counts against the same remaining quantity). Example: `4`.",
            "format": "int32"
          },
          "locationId": {
            "type": "integer",
            "description": "Location the stock is created at (see `/api/v1/locations`); must be a location of\nthis organization that is not deleted. Omit to use the order's `receivingLocationId`.\nExample: `11`.",
            "format": "int32",
            "nullable": true
          }
        },
        "additionalProperties": false,
        "description": "One line of a receive request."
      },
      "RecordPaymentApiRequest": {
        "type": "object",
        "properties": {
          "amount": {
            "minimum": 0,
            "type": "number",
            "description": "Amount paid. May be 0 when only store credit is applied.",
            "format": "double"
          },
          "paymentDate": {
            "type": "string",
            "description": "Payment date (UTC). Defaults to now.",
            "format": "date-time",
            "nullable": true
          },
          "paymentMethod": {
            "maxLength": 50,
            "minLength": 0,
            "type": "string",
            "description": "How the customer paid, e.g. `Cash`, `Check`, `CreditCard`, `Other`. Required when `amount` is greater than 0.",
            "nullable": true
          },
          "referenceNumber": {
            "maxLength": 100,
            "minLength": 0,
            "type": "string",
            "description": "Check number or transaction reference.",
            "nullable": true
          },
          "applyStoreCredit": {
            "type": "boolean",
            "description": "Apply `storeCreditToApply` of the customer's store credit to the invoice."
          },
          "storeCreditToApply": {
            "minimum": 0,
            "type": "number",
            "description": "Store credit amount to apply when `applyStoreCredit` is true (capped at the customer's balance and the balance due). Ignored otherwise.",
            "format": "double"
          }
        },
        "additionalProperties": false,
        "description": "Body of `POST /api/v1/invoices/{id}/payments`. Records a payment and/or applies store\ncredit to a finalized invoice — the same rules as the Record Payment dialog: at least one of\n`amount` or store credit is required, a payment needs a `paymentMethod`, and the\ntotal may not exceed the balance due (extra money belongs in store credit, not on the invoice)."
      },
      "RedirectTransferLineRequest": {
        "required": [
          "destinationType"
        ],
        "type": "object",
        "properties": {
          "destinationType": {
            "$ref": "#/components/schemas/ViewModels.Jobs.TransferRedirectKind"
          },
          "destinationId": {
            "maximum": 2147483647,
            "minimum": 1,
            "type": "integer",
            "description": "Waypoint id or location id, per `destinationType`; ignored for `Destination`. Example: `4`.",
            "format": "int32",
            "nullable": true
          },
          "reason": {
            "maxLength": 500,
            "minLength": 0,
            "type": "string",
            "description": "Optional reason noted on the line, up to 500 characters (the app writes \"No reason provided\" when omitted). Example: `Side entrance`.",
            "nullable": true
          }
        },
        "additionalProperties": false,
        "description": "Body of `POST /api/v1/transfers/{id}/lines/{lineId}/redirect`. Example:\n`{ \"destinationType\": \"Location\", \"destinationId\": 4, \"reason\": \"Customer asked for the side entrance\" }`."
      },
      "RejectTransferRequest": {
        "required": [
          "reason"
        ],
        "type": "object",
        "properties": {
          "reason": {
            "maxLength": 1000,
            "minLength": 0,
            "type": "string",
            "description": "Why the transfer is rejected (required, 1–1000 characters). Stored on the approval and\nemailed to the person who requested it. Example: `Van 2 is out of service this week.`"
          }
        },
        "additionalProperties": false,
        "description": "Body of `POST /api/v1/transfers/{id}/reject`. Example:\n`{ \"reason\": \"Van 2 is out of service this week.\" }`."
      },
      "ReorderPointDto": {
        "type": "object",
        "properties": {
          "id": {
            "type": "integer",
            "description": "Reorder point id. Example: `7`.",
            "format": "int32"
          },
          "itemId": {
            "type": "integer",
            "description": "Item the reorder point belongs to.",
            "format": "int32"
          },
          "locationId": {
            "type": "integer",
            "description": "Location it watches; null = the item's stock at all locations together.",
            "format": "int32",
            "nullable": true
          },
          "locationName": {
            "type": "string",
            "description": "Name of LocationId; null for all locations.",
            "nullable": true
          },
          "minimumStock": {
            "type": "integer",
            "description": "Low-stock level: below this many units the item is listed by `GET /api/v1/items/low-stock`. Example: `10`.",
            "format": "int32"
          },
          "reorderQuantity": {
            "type": "integer",
            "description": "Units to reorder; null = twice MinimumStock. Example: `24`.",
            "format": "int32",
            "nullable": true
          },
          "isActive": {
            "type": "boolean",
            "description": "False when the reorder point is switched off (kept, but never low stock)."
          },
          "createdAt": {
            "type": "string",
            "description": "UTC creation time (`0001-01-01T00:00:00Z` for rows made before the API recorded it).",
            "format": "date-time"
          },
          "updatedAt": {
            "type": "string",
            "description": "UTC last modification time (same caveat as CreatedAt).",
            "format": "date-time"
          }
        },
        "additionalProperties": false,
        "description": "One reorder point (minimum-stock rule) of an item — a row of the app's Reorder → Thresholds."
      },
      "RescheduleJobRequest": {
        "required": [
          "start"
        ],
        "type": "object",
        "properties": {
          "start": {
            "type": "string",
            "description": "New start as the organization's local wall clock, `yyyy-MM-ddTHH:mm:ss` with no UTC offset (the `date-time` format here is a local time, not RFC 3339 with zone); a trailing `Z` is converted to local time. Example: `2026-09-08T08:30:00`.",
            "format": "date-time"
          },
          "end": {
            "type": "string",
            "description": "Optional new end, same local wall-clock format as `start`; when given it must be after `start` and sets the job's duration. Omit to keep the current duration.",
            "format": "date-time",
            "nullable": true
          },
          "note": {
            "maxLength": 1000,
            "minLength": 0,
            "type": "string",
            "description": "Optional reason recorded on the job.",
            "nullable": true
          }
        },
        "additionalProperties": false,
        "description": "Body of `POST /api/v1/jobs/{id}/reschedule`."
      },
      "ReturnSalesOrderItemsApiRequest": {
        "required": [
          "reason"
        ],
        "type": "object",
        "properties": {
          "reason": {
            "maxLength": 500,
            "minLength": 1,
            "type": "string",
            "description": "Why the goods come back, 1–500 characters (the return modal requires one); recorded on the order's return history. Example: `Damaged on arrival`."
          },
          "all": {
            "type": "boolean",
            "description": "`true` = the modal's \"Return All\": every completed transfer of the order is returned and cancelled. Default `false`."
          },
          "transferId": {
            "type": "integer",
            "description": "Quick-return one completed transfer of the order: all its units come back and the transfer is cancelled. Example: `904`.",
            "format": "int32",
            "nullable": true
          },
          "stockUnitIds": {
            "maxItems": 1000,
            "type": "array",
            "items": {
              "type": "integer",
              "format": "int32"
            },
            "description": "Delivered stock units to return (`transfers[].items[].stockUnitId` of `GET …/returnable-items`), up to 1,000. Example: `[4410, 4411]`.",
            "nullable": true
          },
          "untrackedLines": {
            "maxItems": 500,
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/ReturnSalesOrderUntrackedLineApiRequest"
            },
            "description": "Delivered lines with no stock unit behind them (misc items, shortfalls), returned for value only, up to 500 entries.",
            "nullable": true
          },
          "overrideLocationId": {
            "type": "integer",
            "description": "Where units go back to when their source location was deleted: one of `returnLocations[].locationId` of `GET …/returnable-items`. Omit otherwise (units return to their source). Example: `20`.",
            "format": "int32",
            "nullable": true
          }
        },
        "additionalProperties": false,
        "description": "Body of `POST /api/v1/sales-orders/{id}/returns`: exactly one of `all`,\n`transferId` or (`stockUnitIds` and/or `untrackedLines`). Example (selective):\n`{ \"reason\": \"Damaged on arrival\", \"stockUnitIds\": [ 4410, 4411 ] }`; example (everything):\n`{ \"reason\": \"Order returned\", \"all\": true }`."
      },
      "ReturnSalesOrderUntrackedLineApiRequest": {
        "type": "object",
        "properties": {
          "transferItemId": {
            "maximum": 2147483647,
            "minimum": 1,
            "type": "integer",
            "description": "Transfer line id (`transfers[].untrackedLines[].transferItemId`). Example: `5120`.",
            "format": "int32"
          },
          "quantity": {
            "maximum": 1000000,
            "minimum": 1,
            "type": "integer",
            "description": "Units to return, 1 up to `availableQuantity`. Example: `2`.",
            "format": "int32"
          }
        },
        "additionalProperties": false,
        "description": "One untracked delivered line to return."
      },
      "RoleDto": {
        "type": "object",
        "properties": {
          "id": {
            "type": "integer",
            "description": "Custom role id (use it as `customRoleId` / in `customRoleIds` on the user endpoints); null for a built-in role. Example: `4`.",
            "format": "int32",
            "nullable": true
          },
          "name": {
            "type": "string",
            "description": "Role name; for a built-in role this is the value to send as `role`. Example: `\"Manager\"`."
          },
          "description": {
            "type": "string",
            "description": "What the role is for. Example: `\"Manager with inventory and location management access\"`."
          },
          "isBuiltIn": {
            "type": "boolean",
            "description": "True for OrganizationAdmin / Manager / User, which cannot be edited."
          },
          "rank": {
            "type": "integer",
            "description": "Hierarchy rank: OrganizationAdmin 3, Manager 2, User 1; custom roles 0 (they add permissions, not rank).",
            "format": "int32"
          },
          "permissions": {
            "type": "array",
            "items": {
              "type": "string"
            },
            "description": "Permissions the role grants, as FlexPermissions names. Example: `[\"ViewInventory\", \"ManageJobs\"]`."
          },
          "createdAt": {
            "type": "string",
            "description": "UTC creation time (ISO-8601); null for built-in roles.",
            "format": "date-time",
            "nullable": true
          },
          "updatedAt": {
            "type": "string",
            "description": "UTC last modification time (ISO-8601); null for built-in roles.",
            "format": "date-time",
            "nullable": true
          }
        },
        "additionalProperties": false,
        "description": "A role accounts can hold, as returned by `/api/v1/roles`: the built-in `OrganizationAdmin`,\n`Manager` and `User` (never `SystemAdmin`), then the organization's custom roles."
      },
      "SalesOrderDetailDto": {
        "type": "object",
        "properties": {
          "id": {
            "type": "integer",
            "description": "Numeric id used in `/api/v1/sales-orders/{id}`. Unique within this organization. Example: `318`.",
            "format": "int32"
          },
          "syncId": {
            "type": "string",
            "description": "Stable cross-device identifier (GUID) — also the pagination cursor value. Example: `7b1e2c3d-4f5a-4b6c-9d8e-0f1a2b3c4d5e`.",
            "format": "uuid"
          },
          "orderNumber": {
            "type": "string",
            "description": "Order number assigned by the app, `SO` + two-digit year + two-digit month + a four-digit sequence per month. Example: `SO26090007`."
          },
          "status": {
            "type": "string",
            "description": "Order status. One of `Pending` (created, lines can be added, fulfilment not started or\nnot covering every line), `Picking` (a pick transfer covers every line and items are\nbeing picked), `Packed` (everything picked), `Shipped` (every transfer has shipping\ninfo), `Delivered` (every transfer completed), `Complete` (terminal: marked complete,\nor delivered and fully invoiced and paid) and `Cancelled` (terminal). Legacy rows may still say the\ndeprecated `Confirmed`. Over the API `/fulfill` starts fulfilment and the later\nstatuses follow from `/pick`, `/complete-pick`, `/ship` and `/deliver`, as in the app."
          },
          "paymentStatus": {
            "type": "string",
            "description": "Payment status: `Pending`, `Partial`, `Paid`, `Overdue`, `Refunded`, `Invoiced` or `Cancelled`. Example: `Pending`. Null on some legacy rows.",
            "nullable": true
          },
          "fulfillmentStatus": {
            "type": "string",
            "description": "Aggregate status of the order's transfers (the earliest one wins): `None` (no transfer yet), `Draft`, `Pending`, `InTransit`, `AtWaypoint`, `InTransitToWaypoint`, `InTransitToDestination`, `Completed`, `Cancelled`, `Failed`. Example: `Pending`.",
            "nullable": true
          },
          "customerId": {
            "type": "integer",
            "description": "Customer id (see `/api/v1/customers/{id}`). Example: `42`.",
            "format": "int32"
          },
          "customerName": {
            "type": "string",
            "description": "Customer name at read time. Example: `Acme Plumbing`.",
            "nullable": true
          },
          "customerPONumber": {
            "type": "string",
            "description": "The customer's own purchase-order reference; null when none. Example: `PO-7781`.",
            "nullable": true
          },
          "orderDate": {
            "type": "string",
            "description": "Order date as entered in the app: the organization's local calendar date with no UTC offset (not an instant; do not convert it between time zones). Example: `2026-09-23T00:00:00`.",
            "format": "date-time"
          },
          "requestedDeliveryDate": {
            "type": "string",
            "description": "Requested delivery date, same organization-local calendar-date format as `orderDate`; null when none. Example: `2026-09-30T00:00:00`.",
            "format": "date-time",
            "nullable": true
          },
          "subTotal": {
            "type": "number",
            "description": "Sum of the line totals, in the organization's currency. Example: `1250.00`.",
            "format": "double"
          },
          "taxRate": {
            "type": "number",
            "description": "Tax rate in percent (8.25 = 8.25 %). 0 for a tax-exempt customer. Example: `8.25`.",
            "format": "double"
          },
          "taxAmount": {
            "type": "number",
            "description": "Tax, `subTotal` × `taxRate` / 100, in the organization's currency. Example: `103.13`.",
            "format": "double"
          },
          "shippingCost": {
            "type": "number",
            "description": "Shipping charged on the order, in the organization's currency. Example: `25.00`.",
            "format": "double"
          },
          "totalAmount": {
            "type": "number",
            "description": "Order total, `subTotal` + `taxAmount` + `shippingCost`, in the organization's currency. Example: `1378.13`.",
            "format": "double"
          },
          "amountPaid": {
            "type": "number",
            "description": "Amount paid so far, in the organization's currency. Example: `500.00`.",
            "format": "double"
          },
          "balanceDue": {
            "type": "number",
            "description": "Still owed: `totalAmount` − `amountPaid` − store credit applied, in the organization's currency. Example: `878.13`.",
            "format": "double"
          },
          "lineCount": {
            "type": "integer",
            "description": "Number of lines on the order (removed lines excluded). Example: `3`.",
            "format": "int32"
          },
          "createdAt": {
            "type": "string",
            "description": "Creation time, ISO-8601 UTC. Example: `2026-09-23T14:05:00Z`.",
            "format": "date-time"
          },
          "updatedAt": {
            "type": "string",
            "description": "Last recorded modification (header or line edits, lines added or removed, status change, fulfilment and pick progress, `fulfillmentStatus` change), ISO-8601 UTC; null when never modified since creation. Payments do not move it (`paymentStatus` / `amountPaid` / `balanceDue` can change without it). Example: `2026-09-23T15:12:00Z`.",
            "format": "date-time",
            "nullable": true
          },
          "deliveryAddressId": {
            "type": "integer",
            "description": "Id of the customer address the order is delivered to; null when none. Example: `77`.",
            "format": "int32",
            "nullable": true
          },
          "paymentTerms": {
            "type": "string",
            "description": "Payment terms text. Example: `Net 30`. Null when none.",
            "nullable": true
          },
          "notes": {
            "type": "string",
            "description": "Free-text notes on the order. Example: `Leave at loading dock.` Null when none.",
            "nullable": true
          },
          "lines": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/SalesOrderLineDto"
            },
            "description": "The order lines (removed lines excluded), in the order they were added."
          }
        },
        "additionalProperties": false,
        "description": "One sales order with its lines, as returned by `GET /api/v1/sales-orders/{id}` and\n`POST /api/v1/sales-orders`."
      },
      "SalesOrderFulfillmentResponse": {
        "type": "object",
        "properties": {
          "salesOrderId": {
            "type": "integer",
            "description": "The sales order's id. Example: `318`.",
            "format": "int32"
          },
          "status": {
            "type": "string",
            "description": "Order status after fulfilment started, recalculated from its transfers: `Picking` when\nthe transfers cover every line, otherwise still `Pending` (some lines had no available\nstock; fulfil again once stock arrives). Example: `Picking`."
          },
          "transferId": {
            "type": "integer",
            "description": "Id of the pick transfer created (see `/api/v1/transfers/{id}`). Example: `904`.",
            "format": "int32",
            "nullable": true
          },
          "message": {
            "type": "string",
            "description": "Human-readable confirmation, as the app shows it. Example: `Order status updated to Picking`.",
            "nullable": true
          }
        },
        "additionalProperties": false,
        "description": "Response of `POST /api/v1/sales-orders/{id}/fulfill`."
      },
      "SalesOrderLineDto": {
        "type": "object",
        "properties": {
          "id": {
            "type": "integer",
            "description": "Line id. Example: `1502`.",
            "format": "int32"
          },
          "itemId": {
            "type": "integer",
            "description": "Catalog item id as used by `/api/v1/items/{id}`; null for a legacy free-text line.",
            "format": "int32",
            "nullable": true
          },
          "description": {
            "type": "string",
            "description": "Line description (the item name when the line was added). Example: `Copper 12/2 Romex`."
          },
          "quantityOrdered": {
            "type": "number",
            "description": "Quantity ordered. Example: `10`.",
            "format": "double"
          },
          "quantityPicked": {
            "type": "number",
            "description": "Quantity picked (scanned) so far. Example: `4`.",
            "format": "double"
          },
          "quantityShipped": {
            "type": "number",
            "description": "Quantity shipped so far. Example: `0`.",
            "format": "double"
          },
          "unitPrice": {
            "type": "number",
            "description": "Unit price, in the organization's currency. Example: `125.00`.",
            "format": "double"
          },
          "unitCost": {
            "type": "number",
            "description": "Unit cost, in the organization's currency; 0 when unknown. Example: `80.00`.",
            "format": "double"
          },
          "lineTotal": {
            "type": "number",
            "description": "Line total, `quantityOrdered` × `unitPrice`, in the organization's currency. Example: `1250.00`.",
            "format": "double"
          },
          "notes": {
            "type": "string",
            "description": "Line notes. Example: `Cut to 25 ft lengths`. Null when none.",
            "nullable": true
          }
        },
        "additionalProperties": false,
        "description": "One line of a sales order: a catalog item with ordered, picked and shipped quantities."
      },
      "SalesOrderLinePickResponse": {
        "type": "object",
        "properties": {
          "salesOrderId": {
            "type": "integer",
            "description": "The sales order's id. Example: `318`.",
            "format": "int32"
          },
          "lineId": {
            "type": "integer",
            "description": "The line picked; null for `/lines/pick-all`. Example: `1502`.",
            "format": "int32",
            "nullable": true
          },
          "itemName": {
            "type": "string",
            "description": "The picked item's name; null for `/lines/pick-all`. Example: `Copper 12/2 Romex`.",
            "nullable": true
          },
          "quantityPicked": {
            "type": "number",
            "description": "The line's picked quantity after the call (0 for `/lines/pick-all`). Example: `3`.",
            "format": "double"
          },
          "quantityOrdered": {
            "type": "number",
            "description": "The line's ordered quantity (0 for `/lines/pick-all`). Example: `10`.",
            "format": "double"
          },
          "orderComplete": {
            "type": "boolean",
            "description": "`/scan`, `/lines/{lineId}/pick`, `/lines/pick-all`: every line of the order is now picked in full. Always `false` for `picked-quantity`. Example: `false`."
          },
          "linesUpdated": {
            "type": "integer",
            "description": "`/lines/pick-all`: lines raised to their ordered quantity (0 = all were already picked). Example: `2`.",
            "format": "int32"
          },
          "unitsAdded": {
            "type": "number",
            "description": "`/lines/pick-all`: units added across those lines. Example: `7`.",
            "format": "double"
          },
          "status": {
            "type": "string",
            "description": "The order's status after the call (line picks never change it). Example: `Picking`."
          },
          "message": {
            "type": "string",
            "description": "Human-readable confirmation, as the app shows it; null for a single scan / pick. Example: `Picking progress saved`.",
            "nullable": true
          }
        },
        "additionalProperties": false,
        "description": "Response of the line-level pick endpoints (`/scan`, `/lines/{lineId}/pick`, `/lines/pick-all`, `/lines/{lineId}/picked-quantity`)."
      },
      "SalesOrderReturnLocationDto": {
        "type": "object",
        "properties": {
          "locationId": {
            "type": "integer",
            "description": "Location id — pass it as `overrideLocationId`. Example: `20`.",
            "format": "int32"
          },
          "name": {
            "type": "string",
            "description": "Location name. Example: `Main Warehouse`."
          },
          "path": {
            "type": "string",
            "description": "Full path in the location hierarchy. Example: `Main Warehouse > Aisle 3`.",
            "nullable": true
          }
        },
        "additionalProperties": false,
        "description": "A location units can be returned to."
      },
      "SalesOrderReturnResponse": {
        "type": "object",
        "properties": {
          "salesOrderId": {
            "type": "integer",
            "description": "The sales order's id. Example: `318`.",
            "format": "int32"
          },
          "status": {
            "type": "string",
            "description": "The order's status after the return (returns do not change it). Example: `Delivered`."
          },
          "fulfillmentStatus": {
            "type": "string",
            "description": "Aggregate status of the order's transfers after the return (a fully returned transfer is `Cancelled`). Example: `Cancelled`.",
            "nullable": true
          },
          "message": {
            "type": "string",
            "description": "Human-readable confirmation, as the app shows it. Example: `Successfully returned 2 item(s).`",
            "nullable": true
          },
          "itemsReturned": {
            "type": "integer",
            "description": "Units returned (stock units restocked plus untracked units by value). Example: `2`.",
            "format": "int32"
          },
          "transfersCancelled": {
            "type": "integer",
            "description": "Transfers cancelled because every line was returned. Example: `1`.",
            "format": "int32"
          },
          "storeCreditIssued": {
            "type": "number",
            "description": "Store credit issued to the customer for an overpayment the return created, in the organization's currency. Example: `250.00`.",
            "format": "double"
          },
          "overpaymentAmount": {
            "type": "number",
            "description": "Overpayment left after that credit (normally 0), in the organization's currency. Example: `0`.",
            "format": "double"
          }
        },
        "additionalProperties": false,
        "description": "Response of `POST /api/v1/sales-orders/{id}/returns`."
      },
      "SalesOrderReturnableItemsDto": {
        "type": "object",
        "properties": {
          "salesOrderId": {
            "type": "integer",
            "description": "The sales order's id. Example: `318`.",
            "format": "int32"
          },
          "transfers": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/SalesOrderReturnableTransferDto"
            },
            "description": "Completed transfers of the order with what is still returnable on each (empty when nothing was delivered)."
          },
          "needsReturnLocation": {
            "type": "boolean",
            "description": "`true` when some source location was deleted: pass `overrideLocationId` (one of `returnLocations`) to return those units. Example: `false`."
          },
          "returnLocations": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/SalesOrderReturnLocationDto"
            },
            "description": "The company locations units can be returned to when `needsReturnLocation` is true; empty otherwise."
          }
        },
        "additionalProperties": false,
        "description": "What `GET /api/v1/sales-orders/{id}/returnable-items` returns: the order's delivered goods that can still be returned."
      },
      "SalesOrderReturnableTransferDto": {
        "type": "object",
        "properties": {
          "transferId": {
            "type": "integer",
            "description": "Transfer id (`/api/v1/transfers/{id}`); pass as `transferId` to return it whole. Example: `904`.",
            "format": "int32"
          },
          "sourceLocationId": {
            "type": "integer",
            "description": "Where the units came from (they return there); null when unknown. Example: `20`.",
            "format": "int32",
            "nullable": true
          },
          "sourceLocationName": {
            "type": "string",
            "description": "Source location name at read time. Example: `Main Warehouse`.",
            "nullable": true
          },
          "sourceLocationDeleted": {
            "type": "boolean",
            "description": "`true` when the source location was deleted (returns then need `overrideLocationId`). Example: `false`."
          },
          "destinationLocationName": {
            "type": "string",
            "description": "Where the goods were delivered. Example: `Customer: Acme Plumbing - Main Office`.",
            "nullable": true
          },
          "transferDate": {
            "type": "string",
            "description": "When the transfer was created, ISO-8601 UTC. Example: `2026-09-23T14:05:00Z`.",
            "format": "date-time"
          },
          "items": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/SalesOrderReturnableUnitDto"
            },
            "description": "Delivered stock units still at the customer, one entry per unit."
          },
          "untrackedLines": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/SalesOrderReturnableUntrackedLineDto"
            },
            "description": "Delivered lines with no stock unit behind them, returnable for value only."
          }
        },
        "additionalProperties": false,
        "description": "One completed transfer of the order and its returnable content."
      },
      "SalesOrderReturnableUnitDto": {
        "type": "object",
        "properties": {
          "stockUnitId": {
            "type": "integer",
            "description": "Stock unit id — pass it in `stockUnitIds`. Example: `4410`.",
            "format": "int32"
          },
          "itemId": {
            "type": "integer",
            "description": "Catalog item (template) id as used by `/api/v1/items/{id}`; null for a unit without one. Example: `5`.",
            "format": "int32",
            "nullable": true
          },
          "name": {
            "type": "string",
            "description": "Item name. Example: `Copper 12/2 Romex`."
          },
          "transferItemId": {
            "type": "integer",
            "description": "The transfer line that delivered it. Example: `5120`.",
            "format": "int32"
          },
          "unitPrice": {
            "type": "number",
            "description": "Unit price charged on the order line, in the organization's currency (what a return credits before discount). Example: `125.00`.",
            "format": "double"
          },
          "discountPercent": {
            "type": "number",
            "description": "Line discount in percent, 0–100. Example: `0`.",
            "format": "double"
          },
          "isUnique": {
            "type": "boolean",
            "description": "`true` for a serialized (unique) unit. Example: `false`."
          }
        },
        "additionalProperties": false,
        "description": "One delivered stock unit that can be returned."
      },
      "SalesOrderReturnableUntrackedLineDto": {
        "type": "object",
        "properties": {
          "transferItemId": {
            "type": "integer",
            "description": "Transfer line id — pass it in `untrackedLines[].transferItemId`. Example: `5121`.",
            "format": "int32"
          },
          "itemId": {
            "type": "integer",
            "description": "Catalog item (template) id; null for a miscellaneous line. Example: `5`.",
            "format": "int32",
            "nullable": true
          },
          "name": {
            "type": "string",
            "description": "Item name. Example: `Delivery fee`."
          },
          "availableQuantity": {
            "type": "integer",
            "description": "Units still returnable (delivered minus already returned). Example: `2`.",
            "format": "int32"
          },
          "unitPrice": {
            "type": "number",
            "description": "Unit price, in the organization's currency. Example: `15.00`.",
            "format": "double"
          }
        },
        "additionalProperties": false,
        "description": "A delivered line with no stock unit (misc item or shortfall), returnable for value only."
      },
      "SalesOrderStepResponse": {
        "type": "object",
        "properties": {
          "salesOrderId": {
            "type": "integer",
            "description": "The sales order's id. Example: `318`.",
            "format": "int32"
          },
          "status": {
            "type": "string",
            "description": "Order status after the step: `Pending`, `Picking`, `Packed`, `Shipped`,\n`Delivered`, `Complete` or `Cancelled`. Recalculated from the order's\ntransfers, so always read it rather than assuming the next status. Example: `Packed`."
          },
          "fulfillmentStatus": {
            "type": "string",
            "description": "Aggregate status of the order's transfers (`None`, `Pending`, `InTransit`, `AtWaypoint`, `Completed`, `Cancelled`…), as `fulfillmentStatus` on the order. Example: `InTransit`.",
            "nullable": true
          },
          "transferId": {
            "type": "integer",
            "description": "The transfer the step acted on (resolved when the request did not name one); null for `/complete` and `/cancel`. Example: `904`.",
            "format": "int32",
            "nullable": true
          },
          "message": {
            "type": "string",
            "description": "Human-readable confirmation, as the app shows it. Example: `Transfer status updated to In Transit.`",
            "nullable": true
          },
          "unitsPicked": {
            "type": "integer",
            "description": "`/pick`: units marked picked by this call (0 for other steps). Example: `10`.",
            "format": "int32"
          },
          "itemsReturned": {
            "type": "integer",
            "description": "`/cancel`: delivered units returned to inventory (0 for other steps). Example: `0`.",
            "format": "int32"
          },
          "transfersCancelled": {
            "type": "integer",
            "description": "`/cancel`: transfers cancelled (0 for other steps). Example: `1`.",
            "format": "int32"
          }
        },
        "additionalProperties": false,
        "description": "Response of the sales-order fulfilment steps (`/pick`, `/complete-pick`, `/ship`, `/deliver`, `/complete`, `/cancel`)."
      },
      "SalesOrderSummaryDto": {
        "type": "object",
        "properties": {
          "id": {
            "type": "integer",
            "description": "Numeric id used in `/api/v1/sales-orders/{id}`. Unique within this organization. Example: `318`.",
            "format": "int32"
          },
          "syncId": {
            "type": "string",
            "description": "Stable cross-device identifier (GUID) — also the pagination cursor value. Example: `7b1e2c3d-4f5a-4b6c-9d8e-0f1a2b3c4d5e`.",
            "format": "uuid"
          },
          "orderNumber": {
            "type": "string",
            "description": "Order number assigned by the app, `SO` + two-digit year + two-digit month + a four-digit sequence per month. Example: `SO26090007`."
          },
          "status": {
            "type": "string",
            "description": "Order status. One of `Pending` (created, lines can be added, fulfilment not started or\nnot covering every line), `Picking` (a pick transfer covers every line and items are\nbeing picked), `Packed` (everything picked), `Shipped` (every transfer has shipping\ninfo), `Delivered` (every transfer completed), `Complete` (terminal: marked complete,\nor delivered and fully invoiced and paid) and `Cancelled` (terminal). Legacy rows may still say the\ndeprecated `Confirmed`. Over the API `/fulfill` starts fulfilment and the later\nstatuses follow from `/pick`, `/complete-pick`, `/ship` and `/deliver`, as in the app."
          },
          "paymentStatus": {
            "type": "string",
            "description": "Payment status: `Pending`, `Partial`, `Paid`, `Overdue`, `Refunded`, `Invoiced` or `Cancelled`. Example: `Pending`. Null on some legacy rows.",
            "nullable": true
          },
          "fulfillmentStatus": {
            "type": "string",
            "description": "Aggregate status of the order's transfers (the earliest one wins): `None` (no transfer yet), `Draft`, `Pending`, `InTransit`, `AtWaypoint`, `InTransitToWaypoint`, `InTransitToDestination`, `Completed`, `Cancelled`, `Failed`. Example: `Pending`.",
            "nullable": true
          },
          "customerId": {
            "type": "integer",
            "description": "Customer id (see `/api/v1/customers/{id}`). Example: `42`.",
            "format": "int32"
          },
          "customerName": {
            "type": "string",
            "description": "Customer name at read time. Example: `Acme Plumbing`.",
            "nullable": true
          },
          "customerPONumber": {
            "type": "string",
            "description": "The customer's own purchase-order reference; null when none. Example: `PO-7781`.",
            "nullable": true
          },
          "orderDate": {
            "type": "string",
            "description": "Order date as entered in the app: the organization's local calendar date with no UTC offset (not an instant; do not convert it between time zones). Example: `2026-09-23T00:00:00`.",
            "format": "date-time"
          },
          "requestedDeliveryDate": {
            "type": "string",
            "description": "Requested delivery date, same organization-local calendar-date format as `orderDate`; null when none. Example: `2026-09-30T00:00:00`.",
            "format": "date-time",
            "nullable": true
          },
          "subTotal": {
            "type": "number",
            "description": "Sum of the line totals, in the organization's currency. Example: `1250.00`.",
            "format": "double"
          },
          "taxRate": {
            "type": "number",
            "description": "Tax rate in percent (8.25 = 8.25 %). 0 for a tax-exempt customer. Example: `8.25`.",
            "format": "double"
          },
          "taxAmount": {
            "type": "number",
            "description": "Tax, `subTotal` × `taxRate` / 100, in the organization's currency. Example: `103.13`.",
            "format": "double"
          },
          "shippingCost": {
            "type": "number",
            "description": "Shipping charged on the order, in the organization's currency. Example: `25.00`.",
            "format": "double"
          },
          "totalAmount": {
            "type": "number",
            "description": "Order total, `subTotal` + `taxAmount` + `shippingCost`, in the organization's currency. Example: `1378.13`.",
            "format": "double"
          },
          "amountPaid": {
            "type": "number",
            "description": "Amount paid so far, in the organization's currency. Example: `500.00`.",
            "format": "double"
          },
          "balanceDue": {
            "type": "number",
            "description": "Still owed: `totalAmount` − `amountPaid` − store credit applied, in the organization's currency. Example: `878.13`.",
            "format": "double"
          },
          "lineCount": {
            "type": "integer",
            "description": "Number of lines on the order (removed lines excluded). Example: `3`.",
            "format": "int32"
          },
          "createdAt": {
            "type": "string",
            "description": "Creation time, ISO-8601 UTC. Example: `2026-09-23T14:05:00Z`.",
            "format": "date-time"
          },
          "updatedAt": {
            "type": "string",
            "description": "Last recorded modification (header or line edits, lines added or removed, status change, fulfilment and pick progress, `fulfillmentStatus` change), ISO-8601 UTC; null when never modified since creation. Payments do not move it (`paymentStatus` / `amountPaid` / `balanceDue` can change without it). Example: `2026-09-23T15:12:00Z`.",
            "format": "date-time",
            "nullable": true
          }
        },
        "additionalProperties": false,
        "description": "One sales order as returned by `GET /api/v1/sales-orders`: goods sold to a customer\nout of stock (no job). A flat row for lists and CSV; call\n`GET /api/v1/sales-orders/{id}` for its lines."
      },
      "SalesOrderTransferStepApiRequest": {
        "type": "object",
        "properties": {
          "transferId": {
            "type": "integer",
            "description": "The order's transfer to act on. Omit when the order has exactly one transfer in the state the step needs (`Pending` for complete-pick, in transit for deliver). Example: `904`.",
            "format": "int32",
            "nullable": true
          }
        },
        "additionalProperties": false,
        "description": "Body of `POST /api/v1/sales-orders/{id}/complete-pick` and `/deliver`; optional.\nExample: `{ \"transferId\": 904 }`."
      },
      "SalesOrderWebhookData": {
        "type": "object",
        "properties": {
          "id": {
            "type": "integer",
            "format": "int32"
          },
          "orderNumber": {
            "type": "string",
            "nullable": true
          },
          "customerId": {
            "type": "integer",
            "format": "int32"
          },
          "status": {
            "type": "string",
            "nullable": true
          },
          "previousStatus": {
            "type": "string",
            "nullable": true
          },
          "total": {
            "type": "number",
            "format": "double"
          }
        },
        "additionalProperties": false,
        "description": "Sales-order summary carried by `sales_order.*` events; `previousStatus` is set only on `sales_order.status_changed`."
      },
      "ScanSalesOrderBarcodeApiRequest": {
        "required": [
          "barcode"
        ],
        "type": "object",
        "properties": {
          "barcode": {
            "maxLength": 200,
            "minLength": 1,
            "type": "string",
            "description": "The scanned value: a stock unit's barcode or a catalog item's barcode, 1–200 characters. Example: `0012345678905`."
          }
        },
        "additionalProperties": false,
        "description": "Body of `POST /api/v1/sales-orders/{id}/scan`. Example: `{ \"barcode\": \"0012345678905\" }`."
      },
      "ScanTransferBarcodeRequest": {
        "required": [
          "barcode"
        ],
        "type": "object",
        "properties": {
          "barcode": {
            "maxLength": 200,
            "minLength": 1,
            "type": "string",
            "description": "The scanned value, 1–200 characters: an item (unit) barcode or an item template barcode. Example: `0012345678905`."
          },
          "locationId": {
            "maximum": 2147483647,
            "minimum": 1,
            "type": "integer",
            "description": "Optional: only lines whose units are at this location id (lines are grouped by the units' location, as in the app's scan dialog). Omit to match any line. Example: `1`.",
            "format": "int32",
            "nullable": true
          }
        },
        "additionalProperties": false,
        "description": "Body of `POST /api/v1/transfers/{id}/scan`: one scanned barcode = one more unit picked.\nExample: `{ \"barcode\": \"0012345678905\", \"locationId\": 1 }`."
      },
      "ScheduleEventAssigneeDto": {
        "type": "object",
        "properties": {
          "userId": {
            "type": "string",
            "description": "Identity user id (matches `/api/v1/users`)."
          },
          "name": {
            "type": "string",
            "description": "Display name. Example: `Maria Lopez`."
          },
          "groupId": {
            "type": "integer",
            "description": "Crew the assignment falls under (explicit, or the user's primary group), or null.",
            "format": "int32",
            "nullable": true
          },
          "groupName": {
            "type": "string",
            "description": "Crew display name, or null.",
            "nullable": true
          },
          "groupColor": {
            "type": "string",
            "description": "Crew hex colour, or null.",
            "nullable": true
          },
          "groupOrder": {
            "type": "integer",
            "description": "Crew sort order (lower first; `2147483647` when the user has no crew).",
            "format": "int32"
          }
        },
        "additionalProperties": false,
        "description": "An assignee on a schedule event."
      },
      "ScheduleEventDetailsDto": {
        "type": "object",
        "properties": {
          "jobNumber": {
            "type": "string",
            "description": "Human-readable job number. Example: `J-2026-0042`."
          },
          "customerName": {
            "type": "string",
            "description": "Customer display name."
          },
          "jobType": {
            "type": "string",
            "description": "Job category. Example: `Installation`.",
            "nullable": true
          },
          "status": {
            "type": "string",
            "description": "Workflow status name (see `JobSummaryDto.Status` for the list)."
          },
          "totalPrice": {
            "type": "number",
            "description": "Customer price before tax (decimal currency).",
            "format": "double"
          },
          "description": {
            "type": "string",
            "description": "Job description, empty string when none."
          },
          "scheduledTime": {
            "type": "string",
            "description": "Start time formatted for display. Example: `8:30 AM`."
          },
          "durationMinutes": {
            "type": "integer",
            "description": "Planned duration in minutes. Example: `120`.",
            "format": "int32"
          },
          "durationText": {
            "type": "string",
            "description": "Duration formatted for display. Example: `2h`."
          },
          "assignees": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/ScheduleEventAssigneeDto"
            },
            "description": "Assigned staff, ordered by crew then name."
          },
          "assigneeNames": {
            "type": "string",
            "description": "Comma-separated assignee names for one-line display. Empty when unassigned."
          },
          "groupIds": {
            "type": "array",
            "items": {
              "type": "integer",
              "format": "int32"
            },
            "description": "Distinct schedule group (crew) ids of the assignees."
          },
          "groupColor": {
            "type": "string",
            "description": "Hex colour of the first assignee's crew, used as the event accent; null when no crew colour applies.",
            "nullable": true
          }
        },
        "additionalProperties": false,
        "description": "Job facts attached to a `ScheduleEventDto`."
      },
      "ScheduleEventDto": {
        "type": "object",
        "properties": {
          "id": {
            "type": "integer",
            "description": "Job id — fetch details with `GET /api/v1/jobs/{id}`.",
            "format": "int32"
          },
          "title": {
            "type": "string",
            "description": "Event title: job number and customer, plus the description on a second line when present. Example: `J-2026-0042 - Acme Property`."
          },
          "start": {
            "type": "string",
            "description": "Start as the organization's local wall-clock time, `yyyy-MM-ddTHH:mm:ss` with no zone suffix. Example: `2026-09-08T08:30:00`."
          },
          "end": {
            "type": "string",
            "description": "End (start + duration), same format as `start`."
          },
          "url": {
            "type": "string",
            "description": "Relative API path of the job. Example: `/api/v1/jobs/1042`.",
            "nullable": true
          },
          "backgroundColor": {
            "type": "string",
            "description": "Hex colour for the event body, derived from the job status. Example: `#28a745`."
          },
          "borderColor": {
            "type": "string",
            "description": "Hex colour for the event border (currently equal to `backgroundColor`)."
          },
          "displayTime": {
            "type": "string",
            "description": "Start time formatted for display. Example: `8:30 AM`."
          },
          "extendedProps": {
            "$ref": "#/components/schemas/ScheduleEventDetailsDto"
          }
        },
        "additionalProperties": false,
        "description": "One scheduled job as a calendar event (`GET /api/v1/schedule`). Same shape as the\nfeed the web calendar consumes (`Jobs/GetCalendarEvents`) so a FullCalendar-style\nclient can render it directly."
      },
      "ScheduleGroupDto": {
        "type": "object",
        "properties": {
          "id": {
            "type": "integer",
            "description": "Numeric id of the group. Stable within this organization.",
            "format": "int32"
          },
          "syncId": {
            "type": "string",
            "description": "Globally unique sync id (GUID). Use it as the `after` pagination cursor.",
            "format": "uuid"
          },
          "name": {
            "type": "string",
            "description": "Group name, e.g. `\"Install Crew A\"`."
          },
          "description": {
            "type": "string",
            "description": "Optional description shown on the dispatch board.",
            "nullable": true
          },
          "colorHex": {
            "type": "string",
            "description": "Hex colour used for calendar / board rendering, e.g. `\"#2e7d32\"`; null when unset.",
            "nullable": true
          },
          "parentGroupId": {
            "type": "integer",
            "description": "Id of the parent group (e.g. the division a crew belongs to); null for top-level groups.",
            "format": "int32",
            "nullable": true
          },
          "parentGroupName": {
            "type": "string",
            "description": "Name of the parent group; null for top-level groups.",
            "nullable": true
          },
          "displayOrder": {
            "type": "integer",
            "description": "Sort position among siblings (ascending). Pages are returned in id order, so sort\nby this client-side when rendering a board.",
            "format": "int32"
          },
          "members": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/ScheduleGroupMemberDto"
            },
            "description": "Members of this group, leads first. Only active users are listed."
          },
          "createdAt": {
            "type": "string",
            "description": "UTC creation timestamp.",
            "format": "date-time"
          },
          "updatedAt": {
            "type": "string",
            "description": "UTC timestamp of the last edit; null when never edited.",
            "format": "date-time",
            "nullable": true
          }
        },
        "additionalProperties": false,
        "description": "A scheduling group (division, crew or team) of the calling organization, as returned by\n`GET /api/v1/schedule-groups`. Groups form a tree via `parentGroupId`;\na user may belong to several groups. Requires the `scheduling` module."
      },
      "ScheduleGroupMemberDto": {
        "type": "object",
        "properties": {
          "userId": {
            "type": "string",
            "description": "User id (string GUID) — matches `id` from `GET /api/v1/users` and job assignee ids."
          },
          "displayName": {
            "type": "string",
            "description": "User's display name, e.g. `\"Sam Carter\"`."
          },
          "isLead": {
            "type": "boolean",
            "description": "True for the crew lead / foreman of this group."
          }
        },
        "additionalProperties": false,
        "description": "A user's membership in a schedule group."
      },
      "ScheduleGroupMemberWriteApiRequest": {
        "type": "object",
        "properties": {
          "isLead": {
            "type": "boolean",
            "description": "True makes the user a crew lead / foreman of this group (listed first), false a regular member.\nOmit to keep the current flag (a newly added member is not a lead).",
            "nullable": true
          }
        },
        "additionalProperties": false,
        "description": "Body of `PUT /api/v1/schedule-groups/{id}/members/{userId}`. May be empty. Example: `{ \"isLead\": true }`."
      },
      "ScheduleGroupMembersApiRequest": {
        "required": [
          "userIds"
        ],
        "type": "object",
        "properties": {
          "userIds": {
            "maxItems": 500,
            "type": "array",
            "items": {
              "type": "string"
            },
            "description": "User ids (string GUIDs, from `GET /api/v1/users`) of every member, each an active user of this organization; `[]` removes everyone. At most 500."
          },
          "leadIds": {
            "maxItems": 500,
            "type": "array",
            "items": {
              "type": "string"
            },
            "description": "The crew leads / foremen among `userIds` (each must also be in `userIds`); omitted or `[]` = no leads.",
            "nullable": true
          }
        },
        "additionalProperties": false,
        "description": "Body of `PUT /api/v1/schedule-groups/{id}/members`: the group's complete membership.\nExample: `{ \"userIds\": [\"3f2a…\", \"9b1c…\"], \"leadIds\": [\"3f2a…\"] }`."
      },
      "ScheduleGroupOrderResponse": {
        "type": "object",
        "properties": {
          "parentGroupId": {
            "type": "integer",
            "description": "The parent whose children were reordered; null for the top level.",
            "format": "int32",
            "nullable": true
          },
          "items": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/ScheduleGroupDto"
            },
            "description": "The groups under that parent, in the new `displayOrder` (0, 1, 2, …)."
          }
        },
        "additionalProperties": false,
        "description": "Body of `POST /api/v1/schedule-groups/reorder`: the reordered siblings in their new order."
      },
      "ScheduleGroupReorderApiRequest": {
        "required": [
          "ids"
        ],
        "type": "object",
        "properties": {
          "parentGroupId": {
            "type": "integer",
            "description": "Parent whose child groups are reordered; omit or `0` for the top-level groups.",
            "format": "int32",
            "nullable": true
          },
          "ids": {
            "maxItems": 500,
            "type": "array",
            "items": {
              "type": "integer",
              "format": "int32"
            },
            "description": "Every group id under that parent exactly once, first to last (ids from `GET /api/v1/schedule-groups?parentGroupId=…`); at most 500."
          }
        },
        "additionalProperties": false,
        "description": "Body of `POST /api/v1/schedule-groups/reorder`: the new order of every group under one\nparent. Example: `{ \"parentGroupId\": 3, \"ids\": [9, 7, 8] }`."
      },
      "ScheduleGroupWriteApiRequest": {
        "type": "object",
        "properties": {
          "name": {
            "maxLength": 100,
            "minLength": 0,
            "type": "string",
            "description": "Group name, 1–100 characters. Required on create; cannot be cleared. Example: `\"Install Crew A\"`.",
            "nullable": true
          },
          "description": {
            "maxLength": 500,
            "minLength": 0,
            "type": "string",
            "description": "Optional description shown on the dispatch board, up to 500 characters; `\"\"` clears it.",
            "nullable": true
          },
          "colorHex": {
            "maxLength": 7,
            "minLength": 0,
            "type": "string",
            "description": "Colour for calendar / board rendering as 6-digit hex, e.g. `\"#2e7d32\"` (the `#` is optional; stored lower-case); `\"\"` clears it.",
            "nullable": true
          },
          "parentGroupId": {
            "type": "integer",
            "description": "Id of the parent group (e.g. the division a crew belongs to); `0` makes the group top-level.\nOmit on PATCH to keep the current parent (on create, omitted = top-level). A moved group is\nplaced last among its new siblings; a group cannot be moved under itself or its own descendants.",
            "format": "int32",
            "nullable": true
          }
        },
        "additionalProperties": false,
        "description": "Body of `POST /api/v1/schedule-groups` (`name` required) and\n`PATCH /api/v1/schedule-groups/{id}` (only the fields sent change; `\"\"` clears\n`description` / `colorHex`). Example:\n`{ \"name\": \"Install Crew A\", \"colorHex\": \"#2e7d32\", \"parentGroupId\": 3 }`."
      },
      "SetItemPropertyValueApiRequest": {
        "type": "object",
        "properties": {
          "value": {
            "maxLength": 4000,
            "minLength": 0,
            "type": "string",
            "description": "New value, at most 4000 characters, in the property's format (see `value` on GET); `\"\"` or null clears it.",
            "nullable": true
          }
        },
        "additionalProperties": false,
        "description": "Body of `PUT /api/v1/items/{id}/properties/{propertyId}/value`: replace one custom-field\nvalue. Example: `{ \"value\": \"Red\" }`."
      },
      "SetItemPropertyValuesApiRequest": {
        "required": [
          "values"
        ],
        "type": "object",
        "properties": {
          "values": {
            "type": "object",
            "additionalProperties": {
              "type": "string",
              "nullable": true
            },
            "description": "Property name (case-insensitive, as `name` on GET) → new value (string; `\"\"` or\nnull clears it). 1-100 entries. Values are checked against the property's\n`dataType` and `options` (see `value` on GET for the formats)."
          }
        },
        "additionalProperties": false,
        "description": "Body of `PATCH /api/v1/items/{id}/properties`: set several custom-field values at once, by\nproperty name. All or nothing — one refused value changes nothing. Example:\n`{ \"values\": { \"Color\": \"Red\", \"Length\": \"12\" } }`."
      },
      "SetItemUnitPropertyApiRequest": {
        "type": "object",
        "properties": {
          "value": {
            "maxLength": 4000,
            "minLength": 0,
            "type": "string",
            "description": "New value, at most 4000 characters, in the property's format (Number `\"12.5\"`, Boolean `\"true\"`/`\"false\"`, Date `yyyy-MM-dd`, Dropdown one option, MultiSelect comma-separated options); `\"\"` or null clears it.",
            "nullable": true
          }
        },
        "additionalProperties": false,
        "description": "Body of `PUT /api/v1/items/{id}/units/{unitId}/properties/{name}`: the unit's new value.\nExample: `{ \"value\": \"Blue\" }`."
      },
      "SetJobStatusRequest": {
        "type": "object",
        "properties": {
          "statusId": {
            "type": "integer",
            "description": "Target status definition id (one of the org's statuses).",
            "format": "int32",
            "nullable": true
          },
          "statusKey": {
            "maxLength": 100,
            "minLength": 0,
            "type": "string",
            "description": "Target status by name — a status definition name (case-insensitive) or a system status key such as `InProgress`, `Completed`.",
            "nullable": true
          },
          "note": {
            "maxLength": 1000,
            "minLength": 0,
            "type": "string",
            "description": "Optional note appended to the status-change history entry.",
            "nullable": true
          }
        },
        "additionalProperties": false,
        "description": "Body of `POST /api/v1/jobs/{id}/status`. Exactly one of `statusId` / `statusKey` is required."
      },
      "SetReorderPointApiRequest": {
        "required": [
          "minimumStock"
        ],
        "type": "object",
        "properties": {
          "locationId": {
            "type": "integer",
            "description": "Location to watch (see `GET /api/v1/locations`); omit or null for the item's stock at all locations together. Unknown or another organization's id → 422. Example: `3`.",
            "format": "int32",
            "nullable": true
          },
          "minimumStock": {
            "maximum": 2147483647,
            "minimum": 0,
            "type": "integer",
            "description": "Required. Low-stock level, a whole number 0 or more: the item is low while fewer units than this are on hand. Example: `10`.",
            "format": "int32"
          },
          "reorderQuantity": {
            "maximum": 2147483647,
            "minimum": 0,
            "type": "integer",
            "description": "Units to reorder, 0 or more; omit or null for the default of twice `minimumStock`. Example: `24`.",
            "format": "int32",
            "nullable": true
          },
          "isActive": {
            "type": "boolean",
            "description": "Whether the reorder point is on; omit for `true`. `false` keeps it but stops it listing the item as low stock.",
            "nullable": true
          }
        },
        "additionalProperties": false,
        "description": "Body of `PUT /api/v1/items/{id}/reorder-points`: create or update the item's reorder\npoint for one location (or for all locations together when `locationId` is omitted).\nExample: `{ \"locationId\": 3, \"minimumStock\": 10, \"reorderQuantity\": 24 }`."
      },
      "SetSalesOrderLinePickedApiRequest": {
        "required": [
          "quantityPicked"
        ],
        "type": "object",
        "properties": {
          "quantityPicked": {
            "maximum": 1000000,
            "minimum": 0,
            "type": "number",
            "description": "The line's new picked quantity, 0–1,000,000 (more than ordered is allowed, as in the app). Lower than now = unpick. Example: `3`.",
            "format": "double"
          }
        },
        "additionalProperties": false,
        "description": "Body of `PUT /api/v1/sales-orders/{id}/lines/{lineId}/picked-quantity`. Example: `{ \"quantityPicked\": 3 }`."
      },
      "ShipSalesOrderApiRequest": {
        "type": "object",
        "properties": {
          "transferId": {
            "type": "integer",
            "description": "The order's in-transit transfer being shipped. Omit when the order has exactly one transfer in transit. Example: `904`.",
            "format": "int32",
            "nullable": true
          },
          "withoutTracking": {
            "type": "boolean",
            "description": "`true` = the app's \"Ship without tracking\": no shipment record, the transfer is only\n            flagged shipped so it can be delivered. Then `carrier`, `trackingNumber` and the\n            other shipment fields must be omitted. Default `false`."
          },
          "carrier": {
            "maxLength": 100,
            "minLength": 0,
            "type": "string",
            "description": "Carrier name, up to 100 characters. Example: `UPS`.",
            "nullable": true
          },
          "trackingNumber": {
            "maxLength": 100,
            "minLength": 0,
            "type": "string",
            "description": "Carrier tracking number, up to 100 characters. Example: `1Z999AA10123456784`.",
            "nullable": true
          },
          "shippingMethod": {
            "maxLength": 50,
            "minLength": 0,
            "type": "string",
            "description": "Shipping method / service level, up to 50 characters. Example: `Ground`.",
            "nullable": true
          },
          "shipDate": {
            "type": "string",
            "description": "Ship date as ISO-8601 (stored as sent); omit for now (UTC). Example: `2026-09-23T15:00:00Z`.",
            "format": "date-time",
            "nullable": true
          },
          "estimatedDeliveryDate": {
            "type": "string",
            "description": "Estimated delivery date as ISO-8601 (stored as sent). Example: `2026-09-26`.",
            "format": "date-time",
            "nullable": true
          },
          "weight": {
            "maximum": 1000000,
            "minimum": 0,
            "type": "number",
            "description": "Package weight, 0–1,000,000, in whatever unit the organization uses (the app does not fix one). Example: `12.5`.",
            "format": "double",
            "nullable": true
          },
          "notes": {
            "type": "string",
            "description": "Free-text shipment notes. Example: `2 boxes`.",
            "nullable": true
          }
        },
        "additionalProperties": false,
        "description": "Body of `POST /api/v1/sales-orders/{id}/ship`. Example:\n`{ \"transferId\": 904, \"carrier\": \"UPS\", \"trackingNumber\": \"1Z999AA10123456784\", \"shippingMethod\": \"Ground\" }`;\nwithout tracking: `{ \"transferId\": 904, \"withoutTracking\": true }`."
      },
      "SkipTransferWaypointRequest": {
        "type": "object",
        "properties": {
          "destinationType": {
            "$ref": "#/components/schemas/ViewModels.Jobs.TransferRedirectKind"
          },
          "destinationId": {
            "maximum": 2147483647,
            "minimum": 1,
            "type": "integer",
            "description": "Waypoint id or location id, per `destinationType`; ignored for `Destination`. Example: `4`.",
            "format": "int32",
            "nullable": true
          }
        },
        "additionalProperties": false,
        "description": "Optional body of `POST /api/v1/transfers/{id}/waypoints/{waypointId}/skip`: where the lines\nstill assigned to the skipped waypoint go instead. Required (both fields) when it has such lines.\nExample: `{ \"destinationType\": \"Destination\" }`."
      },
      "StandaloneInvoiceLineApiRequest": {
        "required": [
          "description"
        ],
        "type": "object",
        "properties": {
          "description": {
            "maxLength": 500,
            "minLength": 0,
            "type": "string",
            "description": "What is being billed."
          },
          "itemType": {
            "$ref": "#/components/schemas/InvoiceLineItemType"
          },
          "quantity": {
            "minimum": 0.01,
            "type": "number",
            "description": "Quantity billed. Defaults to 1.",
            "format": "double"
          },
          "unitOfMeasure": {
            "maxLength": 50,
            "minLength": 0,
            "type": "string",
            "description": "Unit of measure label (e.g. `ea`, `hr`), if any.",
            "nullable": true
          },
          "unitPrice": {
            "minimum": 0,
            "type": "number",
            "description": "Price per unit.",
            "format": "double"
          }
        },
        "additionalProperties": false,
        "description": "One line of a standalone invoice."
      },
      "StockBatchEntry": {
        "required": [
          "action"
        ],
        "type": "object",
        "properties": {
          "action": {
            "$ref": "#/components/schemas/ViewModels.Items.StockBatchAction"
          },
          "itemId": {
            "maximum": 2147483647,
            "minimum": 1,
            "type": "integer",
            "description": "Numeric item id (see `GET /api/v1/items`). Example: `12`.",
            "format": "int32"
          },
          "locationId": {
            "type": "integer",
            "description": "`Adjust` only: the stock location adjusted. Omit or null for the organization's \"No Location\" bucket.",
            "format": "int32",
            "nullable": true
          },
          "delta": {
            "type": "integer",
            "description": "`Adjust` only: whole units to add (positive) or remove (negative), never zero, -1000..1000. Example: `-2`.",
            "format": "int32",
            "nullable": true
          },
          "fromLocationId": {
            "type": "integer",
            "description": "`Move` only: location the units leave. Required for a move.",
            "format": "int32",
            "nullable": true
          },
          "toLocationId": {
            "type": "integer",
            "description": "`Move` only: location the units arrive at. Omit or null for \"No Location\".",
            "format": "int32",
            "nullable": true
          },
          "quantity": {
            "type": "integer",
            "description": "`Move` only: whole units to move, 1..1000. Example: `3`.",
            "format": "int32",
            "nullable": true
          },
          "reason": {
            "maxLength": 500,
            "minLength": 0,
            "type": "string",
            "description": "Optional reason recorded in each affected unit's change history. Max 500 characters.",
            "nullable": true
          }
        },
        "additionalProperties": false,
        "description": "One operation of a stock batch: an adjustment (same rules as `/items/{id}/stock/adjust`) or a move (same rules as `/items/{id}/stock/move`)."
      },
      "StockBatchRequest": {
        "required": [
          "operations"
        ],
        "type": "object",
        "properties": {
          "operations": {
            "maxItems": 100,
            "minItems": 1,
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/StockBatchEntry"
            },
            "description": "1..100 operations, applied in the order given. At most 1000 units in total across the batch."
          }
        },
        "additionalProperties": false,
        "description": "Body of `POST /api/v1/items/stock/batch`: several adjustments and moves applied in order,\nall or nothing. Example:\n<c>{ \"operations\": [ { \"action\": \"Adjust\", \"itemId\": 12, \"locationId\": 3, \"delta\": 10, \"reason\": \"Delivery\" },\n{ \"action\": \"Move\", \"itemId\": 12, \"fromLocationId\": 3, \"toLocationId\": 4, \"quantity\": 2 } ] }</c>."
      },
      "StockBatchResponse": {
        "type": "object",
        "properties": {
          "message": {
            "type": "string",
            "description": "Human-readable confirmation, e.g. `Applied 2 stock operation(s).`"
          },
          "results": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/StockChangeResponse"
            },
            "description": "Per operation: the same shape as a single adjust/move response, with the item's stock after the whole batch."
          }
        },
        "additionalProperties": false,
        "description": "Result of `POST /api/v1/items/stock/batch`: one entry per operation, in request order."
      },
      "StockChangeResponse": {
        "type": "object",
        "properties": {
          "itemId": {
            "type": "integer",
            "description": "The item (template) whose stock changed.",
            "format": "int32"
          },
          "message": {
            "type": "string",
            "description": "Human-readable confirmation, e.g. `Moved 3 x 'Widget' from 'Main' to 'Van 2'.`"
          },
          "units": {
            "type": "integer",
            "description": "Whole units added, removed or moved (always positive).",
            "format": "int32"
          },
          "locationId": {
            "type": "integer",
            "description": "For an adjustment: the location adjusted. For a move: the source location.",
            "format": "int32"
          },
          "locationName": {
            "type": "string",
            "description": "Name of LocationId."
          },
          "destinationLocationId": {
            "type": "integer",
            "description": "For a move: the destination location; null for an adjustment.",
            "format": "int32",
            "nullable": true
          },
          "destinationLocationName": {
            "type": "string",
            "description": "Name of DestinationLocationId; null for an adjustment.",
            "nullable": true
          },
          "stock": {
            "$ref": "#/components/schemas/StockSummary"
          }
        },
        "additionalProperties": false,
        "description": "Result of a stock adjustment or move, with the item's stock position after the change."
      },
      "StockLowWebhookData": {
        "type": "object",
        "properties": {
          "alertId": {
            "type": "integer",
            "format": "int32"
          },
          "itemId": {
            "type": "integer",
            "description": "The item (template) id as used by `/api/v1/items/{id}`.",
            "format": "int32"
          },
          "itemName": {
            "type": "string",
            "nullable": true
          },
          "locationId": {
            "type": "integer",
            "description": "Location the threshold applies to; null when it is organization-wide.",
            "format": "int32",
            "nullable": true
          },
          "locationName": {
            "type": "string",
            "nullable": true
          },
          "currentStock": {
            "type": "integer",
            "format": "int32"
          },
          "threshold": {
            "type": "integer",
            "format": "int32"
          }
        },
        "additionalProperties": false,
        "description": "Low-stock alert carried by `inventory.low_stock`."
      },
      "StockSummary": {
        "type": "object",
        "properties": {
          "onHand": {
            "type": "number",
            "description": "Total on hand; same semantics as `onHand`.",
            "format": "double"
          },
          "byLocation": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/LocationStock"
            },
            "description": "Per-location breakdown of physical rows. Sums to the row count, which may differ from OnHand for oversold or sub-unit items."
          }
        },
        "additionalProperties": false,
        "description": "Stock position of an item."
      },
      "TransferDetailDto": {
        "type": "object",
        "properties": {
          "id": {
            "type": "integer",
            "description": "Numeric id used in `/api/v1/transfers/{id}`. Unique within this organization. Example: `318`.",
            "format": "int32"
          },
          "syncId": {
            "type": "string",
            "description": "Stable cross-device identifier (GUID) — also the pagination cursor value. Example: `0b8f4c1e-6f1a-4a5e-9d7c-2f3e4a5b6c7d`.",
            "format": "uuid"
          },
          "status": {
            "type": "string",
            "description": "Transfer status name. One of `Draft`, `Pending` (created, stock reserved, not yet\npicked up), `InTransit`, `AtWaypoint`, `InTransitToWaypoint`,\n`InTransitToDestination`, `Completed` (delivered), `Cancelled`, `Failed`."
          },
          "notes": {
            "type": "string",
            "description": "Free-text notes recorded on the transfer. Example: `Transfer for Job J-2026-0042`. Null when none.",
            "nullable": true
          },
          "jobId": {
            "type": "integer",
            "description": "Id of the job this transfer supplies (see `/api/v1/jobs/{id}`); null for a standalone or sales-order transfer.",
            "format": "int32",
            "nullable": true
          },
          "jobNumber": {
            "type": "string",
            "description": "Job number of `jobId` at read time. Example: `J-2026-0042`. Null when there is no job.",
            "nullable": true
          },
          "salesOrderId": {
            "type": "integer",
            "description": "Id of the sales order this transfer fulfils; null otherwise.",
            "format": "int32",
            "nullable": true
          },
          "isJobTransfer": {
            "type": "boolean",
            "description": "True for a transfer created for a job (materials to its job site, or a direct / counter sale). Standalone transfers (both flags false) are the ones `pick` / `start` / `complete` / `fail` accept; job-site and standalone transfers can be cancelled with `POST /api/v1/transfers/{id}/cancel`."
          },
          "isSalesOrderTransfer": {
            "type": "boolean",
            "description": "True for a transfer that fulfils a sales order."
          },
          "sourceLocationId": {
            "type": "integer",
            "description": "Id of the location the stock leaves (see `/api/v1/locations/{id}`); null when the lines come from several locations or from no location.",
            "format": "int32",
            "nullable": true
          },
          "sourceLocationName": {
            "type": "string",
            "description": "Name of `sourceLocationId`. Example: `Main Warehouse`. Null when there is none.",
            "nullable": true
          },
          "destinationLocationId": {
            "type": "integer",
            "description": "Id of the location the stock goes to — for a job-site transfer, the job's site location; null for a de-listing.",
            "format": "int32",
            "nullable": true
          },
          "destinationLocationName": {
            "type": "string",
            "description": "Name of `destinationLocationId`. Example: `Job Site - J-2026-0042`. Null when there is none.",
            "nullable": true
          },
          "requiresApproval": {
            "type": "boolean",
            "description": "True when the transfer's value exceeded the organization's approval threshold when it was created. Job-site transfers never require approval."
          },
          "approvalStatus": {
            "type": "string",
            "description": "State of the transfer's approval: `Pending` (awaiting `POST /api/v1/transfers/{id}/approve` or `/reject`), `Approved` or `Rejected`; null when the transfer never needed approval.",
            "nullable": true
          },
          "lineCount": {
            "type": "integer",
            "description": "Number of item lines on the transfer. Example: `4`.",
            "format": "int32"
          },
          "transferDate": {
            "type": "string",
            "description": "When the transfer was initiated, ISO-8601 UTC. Example: `2026-09-23T14:05:00Z`.",
            "format": "date-time"
          },
          "initiatedById": {
            "type": "string",
            "description": "Identity user id of the person who created the transfer (for an API/MCP-created transfer, the key's creator); null when unknown on this server.",
            "nullable": true
          },
          "completedById": {
            "type": "string",
            "description": "Identity user id of the person who completed (received) the transfer; null until it is completed.",
            "nullable": true
          },
          "createdAt": {
            "type": "string",
            "description": "Creation time, ISO-8601 UTC. Example: `2026-09-23T14:05:00Z`.",
            "format": "date-time"
          },
          "updatedAt": {
            "type": "string",
            "description": "Last modification time, ISO-8601 UTC; null when never modified since creation.",
            "format": "date-time",
            "nullable": true
          },
          "rejectionReason": {
            "type": "string",
            "description": "Why the transfer's approval was rejected (from `POST /api/v1/transfers/{id}/reject` or the app); null otherwise.",
            "nullable": true
          },
          "lines": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/TransferLineDto"
            },
            "description": "The item lines, in the order they were added."
          },
          "currentWaypointId": {
            "type": "integer",
            "description": "Id of the waypoint the transfer is heading to or stopped at (`waypoints[].id`); null when it has no waypoints or is heading to its final destination.",
            "format": "int32",
            "nullable": true
          },
          "waypoints": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/TransferWaypointDto"
            },
            "description": "Intermediate stops on the way to the destination, in route order (empty for a direct transfer). Worked with `POST /api/v1/transfers/{id}/waypoints/{waypointId}/arrive|deliver|depart|skip` while the transfer is in transit."
          }
        },
        "additionalProperties": false,
        "description": "One transfer with its item lines, as returned by `GET /api/v1/transfers/{id}` and by\n`POST /api/v1/jobs/{id}/transfer`."
      },
      "TransferLineDto": {
        "type": "object",
        "properties": {
          "id": {
            "type": "integer",
            "description": "Numeric line id. Example: `1207`.",
            "format": "int32"
          },
          "itemId": {
            "type": "integer",
            "description": "Item (template) id as used by `/api/v1/items/{id}`; null for a line that carries no catalogue item.",
            "format": "int32",
            "nullable": true
          },
          "itemName": {
            "type": "string",
            "description": "Item name at read time (falls back to the line's notes for a miscellaneous line). Example: `Copper 12/2 Romex`.",
            "nullable": true
          },
          "quantity": {
            "type": "integer",
            "description": "Units on the line. Example: `3`.",
            "format": "int32"
          },
          "quantityPicked": {
            "type": "integer",
            "description": "Units picked (scanned) so far; null or `0` before picking starts.",
            "format": "int32",
            "nullable": true
          },
          "quantityDelivered": {
            "type": "integer",
            "description": "Units delivered to the destination; null until delivery.",
            "format": "int32",
            "nullable": true
          },
          "quantityReturned": {
            "type": "integer",
            "description": "Units returned to the source after a failed delivery. Example: `0`.",
            "format": "int32"
          },
          "status": {
            "type": "string",
            "description": "Line status name. One of `Pending`, `InTransit`, `Delivered`, `Failed`, `Returned`."
          },
          "jobItemId": {
            "type": "integer",
            "description": "Id of the job material line this transfer line supplies (see the `items` of `/api/v1/jobs/{id}`); null when not tied to a job line.",
            "format": "int32",
            "nullable": true
          },
          "destinationLocationId": {
            "type": "integer",
            "description": "Per-line destination location id when it differs from the transfer's; null otherwise.",
            "format": "int32",
            "nullable": true
          },
          "destinationWaypointId": {
            "type": "integer",
            "description": "Waypoint (`waypoints[].id`) where this line is dropped off instead of the final destination; null when it goes to the destination.",
            "format": "int32",
            "nullable": true
          },
          "notes": {
            "type": "string",
            "description": "Free-text notes on the line. Example: `Stock shortfall: Copper 12/2 Romex`. Null when none.",
            "nullable": true
          }
        },
        "additionalProperties": false,
        "description": "One item line of a transfer."
      },
      "TransferLinePickRequest": {
        "type": "object",
        "properties": {
          "quantity": {
            "maximum": 1000,
            "minimum": 1,
            "type": "integer",
            "description": "Whole units to pick (at most what is left to pick) or to unpick (at most what is picked), 1–1000; null = all. Example: `2`.",
            "format": "int32",
            "nullable": true
          }
        },
        "additionalProperties": false,
        "description": "Optional body of `POST /api/v1/transfers/{id}/lines/{lineId}/pick` and `…/unpick`.\nExample: `{ \"quantity\": 2 }`. Omit the body (or `quantity`) to pick the rest of the\nline / clear all of its picks."
      },
      "TransferSummaryDto": {
        "type": "object",
        "properties": {
          "id": {
            "type": "integer",
            "description": "Numeric id used in `/api/v1/transfers/{id}`. Unique within this organization. Example: `318`.",
            "format": "int32"
          },
          "syncId": {
            "type": "string",
            "description": "Stable cross-device identifier (GUID) — also the pagination cursor value. Example: `0b8f4c1e-6f1a-4a5e-9d7c-2f3e4a5b6c7d`.",
            "format": "uuid"
          },
          "status": {
            "type": "string",
            "description": "Transfer status name. One of `Draft`, `Pending` (created, stock reserved, not yet\npicked up), `InTransit`, `AtWaypoint`, `InTransitToWaypoint`,\n`InTransitToDestination`, `Completed` (delivered), `Cancelled`, `Failed`."
          },
          "notes": {
            "type": "string",
            "description": "Free-text notes recorded on the transfer. Example: `Transfer for Job J-2026-0042`. Null when none.",
            "nullable": true
          },
          "jobId": {
            "type": "integer",
            "description": "Id of the job this transfer supplies (see `/api/v1/jobs/{id}`); null for a standalone or sales-order transfer.",
            "format": "int32",
            "nullable": true
          },
          "jobNumber": {
            "type": "string",
            "description": "Job number of `jobId` at read time. Example: `J-2026-0042`. Null when there is no job.",
            "nullable": true
          },
          "salesOrderId": {
            "type": "integer",
            "description": "Id of the sales order this transfer fulfils; null otherwise.",
            "format": "int32",
            "nullable": true
          },
          "isJobTransfer": {
            "type": "boolean",
            "description": "True for a transfer created for a job (materials to its job site, or a direct / counter sale). Standalone transfers (both flags false) are the ones `pick` / `start` / `complete` / `fail` accept; job-site and standalone transfers can be cancelled with `POST /api/v1/transfers/{id}/cancel`."
          },
          "isSalesOrderTransfer": {
            "type": "boolean",
            "description": "True for a transfer that fulfils a sales order."
          },
          "sourceLocationId": {
            "type": "integer",
            "description": "Id of the location the stock leaves (see `/api/v1/locations/{id}`); null when the lines come from several locations or from no location.",
            "format": "int32",
            "nullable": true
          },
          "sourceLocationName": {
            "type": "string",
            "description": "Name of `sourceLocationId`. Example: `Main Warehouse`. Null when there is none.",
            "nullable": true
          },
          "destinationLocationId": {
            "type": "integer",
            "description": "Id of the location the stock goes to — for a job-site transfer, the job's site location; null for a de-listing.",
            "format": "int32",
            "nullable": true
          },
          "destinationLocationName": {
            "type": "string",
            "description": "Name of `destinationLocationId`. Example: `Job Site - J-2026-0042`. Null when there is none.",
            "nullable": true
          },
          "requiresApproval": {
            "type": "boolean",
            "description": "True when the transfer's value exceeded the organization's approval threshold when it was created. Job-site transfers never require approval."
          },
          "approvalStatus": {
            "type": "string",
            "description": "State of the transfer's approval: `Pending` (awaiting `POST /api/v1/transfers/{id}/approve` or `/reject`), `Approved` or `Rejected`; null when the transfer never needed approval.",
            "nullable": true
          },
          "lineCount": {
            "type": "integer",
            "description": "Number of item lines on the transfer. Example: `4`.",
            "format": "int32"
          },
          "transferDate": {
            "type": "string",
            "description": "When the transfer was initiated, ISO-8601 UTC. Example: `2026-09-23T14:05:00Z`.",
            "format": "date-time"
          },
          "initiatedById": {
            "type": "string",
            "description": "Identity user id of the person who created the transfer (for an API/MCP-created transfer, the key's creator); null when unknown on this server.",
            "nullable": true
          },
          "completedById": {
            "type": "string",
            "description": "Identity user id of the person who completed (received) the transfer; null until it is completed.",
            "nullable": true
          },
          "createdAt": {
            "type": "string",
            "description": "Creation time, ISO-8601 UTC. Example: `2026-09-23T14:05:00Z`.",
            "format": "date-time"
          },
          "updatedAt": {
            "type": "string",
            "description": "Last modification time, ISO-8601 UTC; null when never modified since creation.",
            "format": "date-time",
            "nullable": true
          }
        },
        "additionalProperties": false,
        "description": "One inventory transfer as returned by `GET /api/v1/transfers`: stock moving from a\nlocation to another location or to a job site. A compact row for lists and CSV; call\n`GET /api/v1/transfers/{id}` for its item lines."
      },
      "TransferWaypointDto": {
        "type": "object",
        "properties": {
          "id": {
            "type": "integer",
            "description": "Numeric waypoint id, used in `/api/v1/transfers/{id}/waypoints/{waypointId}/…`. Example: `41`.",
            "format": "int32"
          },
          "order": {
            "type": "integer",
            "description": "Position on the route, 1 = first stop. Example: `1`.",
            "format": "int32"
          },
          "locationId": {
            "type": "integer",
            "description": "Id of the stop's location (see `/api/v1/locations/{id}`). Example: `4`.",
            "format": "int32"
          },
          "locationName": {
            "type": "string",
            "description": "Name of `locationId`. Example: `Van 2`. Null when the location was deleted.",
            "nullable": true
          },
          "status": {
            "type": "string",
            "description": "Waypoint status name: `Pending` (not reached), `Arrived`, `Departed`, `Skipped` or `Cancelled`."
          },
          "estimatedArrivalTime": {
            "type": "string",
            "description": "Planned arrival, ISO-8601 UTC; null when not planned.",
            "format": "date-time",
            "nullable": true
          },
          "actualArrivalTime": {
            "type": "string",
            "description": "When the transfer arrived, ISO-8601 UTC; null until then.",
            "format": "date-time",
            "nullable": true
          },
          "departureTime": {
            "type": "string",
            "description": "When the transfer left, ISO-8601 UTC; null until then.",
            "format": "date-time",
            "nullable": true
          },
          "notes": {
            "type": "string",
            "description": "Free-text notes on the stop (e.g. why it was skipped). Null when none.",
            "nullable": true
          }
        },
        "additionalProperties": false,
        "description": "One waypoint (intermediate stop) of a transfer."
      },
      "TransferWebhookData": {
        "type": "object",
        "properties": {
          "id": {
            "type": "integer",
            "format": "int32"
          },
          "sourceLocationId": {
            "type": "integer",
            "format": "int32",
            "nullable": true
          },
          "destinationLocationId": {
            "type": "integer",
            "format": "int32",
            "nullable": true
          },
          "jobId": {
            "type": "integer",
            "format": "int32",
            "nullable": true
          },
          "salesOrderId": {
            "type": "integer",
            "format": "int32",
            "nullable": true
          },
          "isJobTransfer": {
            "type": "boolean",
            "description": "True for a transfer created by a job (materials to a job site, direct or counter sale)."
          },
          "isSalesOrderTransfer": {
            "type": "boolean"
          },
          "status": {
            "type": "string",
            "nullable": true
          },
          "completedAt": {
            "type": "string",
            "format": "date-time",
            "nullable": true
          },
          "lines": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/TransferWebhookLine"
            },
            "description": "Item lines: template id, quantity and delivered quantity.",
            "nullable": true
          }
        },
        "additionalProperties": false,
        "description": "Transfer summary carried by `transfer.created`, `transfer.completed` and `transfer.cancelled`; `completedAt` is set only when `status` is `Completed`."
      },
      "TransferWebhookLine": {
        "type": "object",
        "properties": {
          "itemId": {
            "type": "integer",
            "format": "int32",
            "nullable": true
          },
          "quantity": {
            "type": "integer",
            "format": "int32"
          },
          "quantityDelivered": {
            "type": "integer",
            "format": "int32",
            "nullable": true
          },
          "status": {
            "type": "string",
            "nullable": true
          }
        },
        "additionalProperties": false,
        "description": "One line of a `transfer.*` payload."
      },
      "UpdateCategoryApiRequest": {
        "type": "object",
        "properties": {
          "name": {
            "maxLength": 100,
            "minLength": 0,
            "type": "string",
            "description": "New name, 1-100 characters (trimmed), unique among the categories with the same parent (case-insensitive).",
            "nullable": true
          },
          "description": {
            "maxLength": 2000,
            "minLength": 0,
            "type": "string",
            "description": "New description, at most 2000 characters; `\"\"` clears it.",
            "nullable": true
          },
          "parentCategoryId": {
            "type": "integer",
            "description": "New parent category id; `0` moves the category to the top level. The parent may not be the category itself or one of its descendants (no cycles), a system or a Template Builder-generated category.",
            "format": "int32",
            "nullable": true
          }
        },
        "additionalProperties": false,
        "description": "Body of `PATCH /api/v1/categories/{id}`: omitted (or null) fields are unchanged; at least\none is required. Example: `{ \"name\": \"Softwood\", \"parentCategoryId\": 0 }` renames the\ncategory and moves it to the top level."
      },
      "UpdateCategoryPropertyApiRequest": {
        "type": "object",
        "properties": {
          "name": {
            "maxLength": 100,
            "minLength": 0,
            "type": "string",
            "description": "New name, 1-100 characters, unique in the category (case-insensitive); `Cost` / `Unit Price` are reserved. Items already carrying the old name keep it.",
            "nullable": true
          },
          "dataType": {
            "$ref": "#/components/schemas/PropertyDataType"
          },
          "defaultValue": {
            "maxLength": 4000,
            "minLength": 0,
            "type": "string",
            "description": "New default value in the property's format, at most 4000 characters; `\"\"` clears it.",
            "nullable": true
          },
          "options": {
            "maxItems": 200,
            "type": "array",
            "items": {
              "type": "string"
            },
            "description": "Full replacement option list for a `Dropdown` / `MultiSelect` property (same rules as on create); `[]` clears it.",
            "nullable": true
          },
          "isRequired": {
            "type": "boolean",
            "description": "Marks the property required (true) or optional (false) in the app.",
            "nullable": true
          },
          "displayOrder": {
            "type": "integer",
            "description": "New sort position, 0 or more.",
            "format": "int32",
            "nullable": true
          }
        },
        "additionalProperties": false,
        "description": "Body of `PATCH /api/v1/categories/{id}/properties/{propertyId}`: omitted (or null) fields\nare unchanged; at least one is required. Example: `{ \"defaultValue\": \"Oak\", \"displayOrder\": 2 }`."
      },
      "UpdateInvoiceDraftApiRequest": {
        "type": "object",
        "properties": {
          "invoiceNumber": {
            "maxLength": 50,
            "minLength": 0,
            "type": "string",
            "description": "New invoice number shown to the customer (e.g. `INV-1042`). Omit to leave unchanged; blank is rejected.",
            "nullable": true
          },
          "invoiceDate": {
            "type": "string",
            "description": "New issue date (ISO-8601; the date part is used). Omit to leave unchanged.",
            "format": "date-time",
            "nullable": true
          },
          "dueDate": {
            "type": "string",
            "description": "New due date (ISO-8601; the date part is used). Omit to leave unchanged.",
            "format": "date-time",
            "nullable": true
          },
          "taxRate": {
            "maximum": 100,
            "minimum": 0,
            "type": "number",
            "description": "New tax rate as a percentage, 0–100 (e.g. `8.25`). Omit to leave unchanged.",
            "format": "double",
            "nullable": true
          },
          "terms": {
            "maxLength": 500,
            "minLength": 0,
            "type": "string",
            "description": "New payment terms printed on the invoice (e.g. `Due on Receipt`). Omit to leave unchanged; empty string clears it.",
            "nullable": true
          },
          "notes": {
            "type": "string",
            "description": "New customer-facing notes printed on the invoice. Omit to leave unchanged; empty string clears it.",
            "nullable": true
          },
          "paymentInstructions": {
            "type": "string",
            "description": "New payment instructions printed on the invoice. Omit to leave unchanged; empty string clears it.",
            "nullable": true
          }
        },
        "additionalProperties": false,
        "description": "Body of `PATCH /api/v1/invoices/{id}`: a partial edit of a <b>draft</b> invoice's header.\nOnly the fields you send change; omitted fields are left as they are. Totals are not editable\nhere — the subtotal comes from the job, so recompute it with\n`POST /api/v1/invoices/{id}/refresh-from-job` after the job's lines change."
      },
      "UpdateItemApiRequest": {
        "type": "object",
        "properties": {
          "name": {
            "maxLength": 100,
            "minLength": 0,
            "type": "string",
            "description": "New name, 1-100 characters; omit to keep. Whitespace-only → 422. Renaming also renames every inventory unit of the item.",
            "nullable": true
          },
          "description": {
            "maxLength": 2000,
            "minLength": 0,
            "type": "string",
            "description": "New description, max 2000 characters; omit to keep. Send `\"\"` (or whitespace only) to clear it.",
            "nullable": true
          },
          "categoryId": {
            "type": "integer",
            "description": "New primary category id; omit to keep. Changing it copies the category's properties onto the item. Unknown or another organization's id → 422.",
            "format": "int32",
            "nullable": true
          },
          "unitPrice": {
            "maximum": 1000000000,
            "minimum": 0,
            "type": "number",
            "description": "New selling price, 0 or more; omit to keep. Written to the item and its future stock only — existing units keep their own pricing.",
            "format": "double",
            "nullable": true
          },
          "cost": {
            "maximum": 1000000000,
            "minimum": 0,
            "type": "number",
            "description": "New purchase cost, 0 or more; omit to keep. Written to the item and its future stock only.",
            "format": "double",
            "nullable": true
          },
          "preferredVendorId": {
            "maximum": 2147483647,
            "minimum": 0,
            "type": "integer",
            "description": "New preferred vendor id (from `GET /api/v1/vendors`; must be an active vendor of the\norganization, 422 otherwise); omit to keep, send `0` to remove the link. Like\n`description` it is edited on the Item Templates page, so it requires `ManageTemplates`. Example: `3`.",
            "format": "int32",
            "nullable": true
          },
          "vendorPartNumber": {
            "maxLength": 100,
            "minLength": 0,
            "type": "string",
            "description": "New vendor part number, max 100 characters; omit to keep, send `\"\"` to clear it.\nRequires `ManageTemplates` (edited on the Item Templates page). Example: `\"PX-12C\"`.",
            "nullable": true
          },
          "hasAnyField": {
            "type": "boolean",
            "description": "True when at least one field was supplied.",
            "readOnly": true
          },
          "hasTemplatePageField": {
            "type": "boolean",
            "description": "True when a field only the Item Templates page edits (`ManageTemplates`) was supplied.",
            "readOnly": true
          }
        },
        "additionalProperties": false,
        "description": "Body of `PATCH /api/v1/items/{id}`: change some of an item's name, description,\ncategory, selling price, cost, preferred vendor or vendor part number. Partial update: a field that is omitted (or sent as\n`null`) is left unchanged, so send only what should change. At least one field is\nrequired. Example: `{ \"unitPrice\": 1.35 }` or `{ \"name\": \"PEX Coupling 1/2\\\"\", \"categoryId\": 7 }`."
      },
      "UpdateItemPropertyApiRequest": {
        "type": "object",
        "properties": {
          "name": {
            "maxLength": 100,
            "minLength": 0,
            "type": "string",
            "description": "New name, 1-100 characters, unique on the item (case-insensitive); `Cost` / `Unit Price` are reserved.",
            "nullable": true
          },
          "dataType": {
            "$ref": "#/components/schemas/PropertyDataType"
          },
          "options": {
            "maxItems": 200,
            "type": "array",
            "items": {
              "type": "string"
            },
            "description": "Full replacement option list for a `Dropdown` / `MultiSelect` property (at most 200 options, each 1-200 characters, no commas, no duplicates); `[]` clears it. Existing values are not re-checked.",
            "nullable": true
          }
        },
        "additionalProperties": false,
        "description": "Body of `PATCH /api/v1/items/{id}/properties/{propertyId}`: change a property's\ndefinition. Omitted (or null) fields are unchanged; at least one is required. Example:\n`{ \"dataType\": \"Dropdown\", \"options\": [\"Red\", \"Blue\"] }`."
      },
      "UpdateJobApiRequest": {
        "type": "object",
        "properties": {
          "jobType": {
            "maxLength": 100,
            "minLength": 0,
            "type": "string",
            "description": "New job type, e.g. `Installation` or `Service`. Omit to leave unchanged.",
            "nullable": true
          },
          "description": {
            "maxLength": 4000,
            "minLength": 0,
            "type": "string",
            "description": "New job description. Omit to leave unchanged.",
            "nullable": true
          },
          "notes": {
            "maxLength": 4000,
            "minLength": 0,
            "type": "string",
            "description": "New internal notes on the job. Omit to leave unchanged.",
            "nullable": true
          },
          "scheduledDate": {
            "type": "string",
            "description": "New scheduled start (ISO-8601 UTC, e.g. `2026-10-01T09:00:00Z`). Omit to leave unchanged.",
            "format": "date-time",
            "nullable": true
          },
          "customerId": {
            "type": "integer",
            "description": "Reassign the job to another customer of your organization (customer id from `GET /api/v1/customers`). Omit to leave unchanged.",
            "format": "int32",
            "nullable": true
          },
          "customerAddressId": {
            "type": "integer",
            "description": "Set the service address; it must be one of that customer's addresses. Omit to leave unchanged.",
            "format": "int32",
            "nullable": true
          }
        },
        "additionalProperties": false,
        "description": "Body of `PATCH /api/v1/jobs/{id}`: a partial edit of a job's core fields. Only the fields you send change; omitted fields are left as they are."
      },
      "UpdateJobChargeApiRequest": {
        "type": "object",
        "properties": {
          "name": {
            "maxLength": 200,
            "minLength": 0,
            "type": "string",
            "description": "New charge name, ≤ 200 characters. Omit to leave unchanged.",
            "nullable": true
          },
          "price": {
            "minimum": 0,
            "type": "number",
            "description": "New price, ≥ 0. Omit to leave unchanged.",
            "format": "double",
            "nullable": true
          },
          "description": {
            "maxLength": 500,
            "minLength": 0,
            "type": "string",
            "description": "New description. Omit to leave unchanged; send an empty string to clear it. ≤ 500 characters.",
            "nullable": true
          }
        },
        "additionalProperties": false,
        "description": "Body of `PATCH /api/v1/jobs/{id}/charges/{chargeId}`: partial edit of a custom charge. Omitted fields are left unchanged."
      },
      "UpdateJobItemApiRequest": {
        "type": "object",
        "properties": {
          "quantity": {
            "maximum": 10000,
            "minimum": 1,
            "type": "integer",
            "description": "New quantity, ≥ 1. Only Planned lines (or counter-sales lines) change; can't drop below what was already delivered. Omit to leave unchanged.",
            "format": "int32",
            "nullable": true
          },
          "unitPrice": {
            "minimum": 0,
            "type": "number",
            "description": "New unit price, ≥ 0, kept as a manual override unless it equals the resolved price. Omit to leave unchanged; set `clearPriceOverride` to revert to the tier/base price instead.",
            "format": "double",
            "nullable": true
          },
          "clearPriceOverride": {
            "type": "boolean",
            "description": "Revert the line to its tier/base-resolved price (clears any manual override). Takes precedence over `unitPrice`. Default `false`."
          },
          "notes": {
            "maxLength": 500,
            "minLength": 0,
            "type": "string",
            "description": "New line note. Omit to leave unchanged; send an empty string to clear it. ≤ 500 characters.",
            "nullable": true
          }
        },
        "additionalProperties": false,
        "description": "Body of `PATCH /api/v1/jobs/{id}/items/{itemId}`: partial edit of a material line. Omitted fields are left unchanged."
      },
      "UpdateJobLaborApiRequest": {
        "type": "object",
        "properties": {
          "laborRateId": {
            "type": "integer",
            "description": "New labor rate id (must belong to your organization). Changing it resets the customer rate and clears any per-job price. Omit to leave unchanged.",
            "format": "int32",
            "nullable": true
          },
          "estimatedHours": {
            "minimum": 0,
            "type": "number",
            "description": "New estimated hours, ≥ 0. Omit to leave unchanged.",
            "format": "double",
            "nullable": true
          },
          "actualHours": {
            "minimum": 0,
            "type": "number",
            "description": "New actual hours, ≥ 0. A value > 0 marks the line completed; 0 reverts it to planned. Omit to leave unchanged.",
            "format": "double",
            "nullable": true
          },
          "notes": {
            "maxLength": 500,
            "minLength": 0,
            "type": "string",
            "description": "New line note. Omit to leave unchanged; send an empty string to clear it. ≤ 500 characters.",
            "nullable": true
          }
        },
        "additionalProperties": false,
        "description": "Body of `PATCH /api/v1/jobs/{id}/labor/{laborId}`: partial edit of a labor line. Omitted fields are left unchanged."
      },
      "UpdateSalesOrderApiRequest": {
        "type": "object",
        "properties": {
          "orderNumber": {
            "maxLength": 50,
            "minLength": 0,
            "type": "string",
            "description": "New order number, 1–50 characters after trimming (blank is 422). Example: `SO26090007-B`.",
            "nullable": true
          },
          "customerPONumber": {
            "maxLength": 100,
            "minLength": 0,
            "type": "string",
            "description": "The customer's purchase-order reference, up to 100 characters; `\"\"` clears it. Example: `PO-7781`.",
            "nullable": true
          },
          "paymentTerms": {
            "maxLength": 100,
            "minLength": 0,
            "type": "string",
            "description": "Payment terms: one of `Due on Receipt`, `Net 15`, `Net 30`, `Net 45`, `Net 60`, `COD`, `Prepaid` (the Details page's list; anything else is 422); `\"\"` clears them. Example: `Net 30`.",
            "nullable": true
          },
          "requestedDeliveryDate": {
            "type": "string",
            "description": "Requested delivery date, an organization-local calendar date (`yyyy-MM-dd`, no UTC offset). Example: `2026-09-30`.",
            "format": "date-time",
            "nullable": true
          },
          "clearRequestedDeliveryDate": {
            "type": "boolean",
            "description": "`true` clears the requested delivery date; then omit `requestedDeliveryDate`. Default `false`."
          },
          "notes": {
            "type": "string",
            "description": "Free-text notes on the order, stored as sent; `\"\"` clears them. Example: `Leave at loading dock.`",
            "nullable": true
          },
          "taxRate": {
            "maximum": 100,
            "minimum": 0,
            "type": "number",
            "description": "Tax rate in percent, 0–100 (8.25 = 8.25 %); must stay 0 for a tax-exempt customer (422). Recomputes `taxAmount` and `totalAmount`. Example: `8.25`.",
            "format": "double",
            "nullable": true
          },
          "shippingCost": {
            "maximum": 1000000000,
            "minimum": 0,
            "type": "number",
            "description": "Shipping charged on the order, in the organization's currency, 0–1,000,000,000. Recomputes `totalAmount`; draft invoices get a matching \"Shipping & Handling\" line. Example: `25.00`.",
            "format": "double",
            "nullable": true
          }
        },
        "additionalProperties": false,
        "description": "Body of `PATCH /api/v1/sales-orders/{id}`: the header fields the app's Details page edits\ninline. Omitted (or null) members are left unchanged; send `\"\"` to clear an optional text\nfield. Example: `{ \"shippingCost\": 25.00, \"paymentTerms\": \"Net 30\" }`."
      },
      "UpdateSalesOrderLineApiRequest": {
        "type": "object",
        "properties": {
          "unitPrice": {
            "maximum": 1000000000,
            "minimum": 0,
            "type": "number",
            "description": "New unit price in the organization's currency, 0–1,000,000,000; the line total and the order totals are recomputed. Omit to keep. Example: `119.50`.",
            "format": "double",
            "nullable": true
          },
          "unitCost": {
            "maximum": 1000000000,
            "minimum": 0,
            "type": "number",
            "description": "New unit cost in the organization's currency, 0–1,000,000,000. Omit to keep. Example: `80.00`.",
            "format": "double",
            "nullable": true
          }
        },
        "additionalProperties": false,
        "description": "Body of `PATCH /api/v1/sales-orders/{id}/lines/{lineId}`: the Details page's inline price edit. Example: `{ \"unitPrice\": 119.50 }`."
      },
      "UpdateTransferLineRequest": {
        "type": "object",
        "properties": {
          "quantity": {
            "maximum": 1000,
            "minimum": 1,
            "type": "integer",
            "description": "New number of units, 1 up to the line's current `quantity` (a line can only shrink; the other units are released). Example: `2`.",
            "format": "int32"
          }
        },
        "additionalProperties": false,
        "description": "Body of `PATCH /api/v1/transfers/{id}/lines/{lineId}`: reduce the units a line moves.\nExample: `{ \"quantity\": 2 }`."
      },
      "UpdateUserApiRequest": {
        "type": "object",
        "properties": {
          "firstName": {
            "maxLength": 100,
            "minLength": 0,
            "type": "string",
            "description": "New first name, 1..100 characters. Example: `\"Samantha\"`.",
            "nullable": true
          },
          "lastName": {
            "maxLength": 100,
            "minLength": 0,
            "type": "string",
            "description": "New last name, 1..100 characters. Example: `\"Carter\"`.",
            "nullable": true
          },
          "role": {
            "$ref": "#/components/schemas/ApiAssignableRole"
          },
          "isActive": {
            "type": "boolean",
            "description": "`false` deactivates the account, `true` reactivates it (same as the deactivate / reactivate endpoints).",
            "nullable": true
          },
          "customRoleIds": {
            "maxItems": 50,
            "type": "array",
            "items": {
              "type": "integer",
              "format": "int32"
            },
            "description": "The complete set of custom role ids the account should hold; `[]` removes all. Example: `[4, 7]`.",
            "nullable": true
          }
        },
        "additionalProperties": false,
        "description": "Body of `PATCH /api/v1/users/{id}`; only the fields sent change. Example:\n`{ \"role\": \"Manager\", \"customRoleIds\": [4] }`."
      },
      "UserDetailDto": {
        "type": "object",
        "properties": {
          "id": {
            "type": "string",
            "description": "User id (string GUID). Example: `\"8f0c3c1e-2b6d-4c55-9a51-0d3f3c1b7e21\"`."
          },
          "email": {
            "type": "string",
            "description": "Login e-mail address. Example: `\"sam@example.com\"`.",
            "nullable": true
          },
          "firstName": {
            "type": "string",
            "description": "First name. Example: `\"Sam\"`.",
            "nullable": true
          },
          "lastName": {
            "type": "string",
            "description": "Last name. Example: `\"Carter\"`.",
            "nullable": true
          },
          "displayName": {
            "type": "string",
            "description": "Display name, e.g. `\"Sam Carter\"`; falls back to the e-mail address when no name is recorded."
          },
          "isActive": {
            "type": "boolean",
            "description": "False when the account is deactivated (it cannot sign in and holds no seat)."
          },
          "role": {
            "type": "string",
            "description": "Highest standard role the account holds — `OrganizationAdmin`, `Manager` or `User`\n(`SystemAdmin` for platform staff); null when it holds only custom roles.",
            "nullable": true
          },
          "customRoleIds": {
            "type": "array",
            "items": {
              "type": "integer",
              "format": "int32"
            },
            "description": "Ids of the custom roles the account holds (see `/api/v1/roles`). Empty when none."
          },
          "permissions": {
            "type": "array",
            "items": {
              "type": "string"
            },
            "description": "Effective permissions from all of the account's roles, in catalog order. Example: `[\"ViewInventory\", \"ViewJobs\"]`."
          }
        },
        "additionalProperties": false,
        "description": "An account of the calling organization, as returned by the user-administration endpoints\n(`GET /api/v1/users/{id}`, `POST /api/v1/users`, `PATCH /api/v1/users/{id}`).\nNever carries a password, hash or security stamp."
      },
      "UserPendingDto": {
        "type": "object",
        "properties": {
          "email": {
            "type": "string",
            "description": "The new account's e-mail address. Example: `\"sam@example.com\"`."
          },
          "message": {
            "type": "string",
            "description": "What happened. Example: `\"User sam@example.com created on server. They will appear locally after the next sync.\"`"
          }
        },
        "additionalProperties": false,
        "description": "`202` body of `POST /api/v1/users` when the sync server created the account but this host has\n            not received it yet; it appears (and `GET /api/v1/users/{id}` finds it) after the next sync."
      },
      "UserSummaryDto": {
        "type": "object",
        "properties": {
          "id": {
            "type": "string",
            "description": "User id (string GUID). Use it as an assignee id when scheduling jobs."
          },
          "displayName": {
            "type": "string",
            "description": "Display name, e.g. `\"Sam Carter\"`; falls back to the e-mail address when no name is recorded."
          },
          "email": {
            "type": "string",
            "description": "Login e-mail address, e.g. `sam@example.com`. Only present for callers whose key holds\n`ManageUsers`; it is null for a ViewJobs-only (scheduling) key, and also null when the\naccount has no address on file.",
            "nullable": true
          },
          "isActive": {
            "type": "boolean",
            "description": "False when the account is deactivated (such users are only returned when `includeInactive=true`)."
          },
          "scheduleGroupIds": {
            "type": "array",
            "items": {
              "type": "integer",
              "format": "int32"
            },
            "description": "Ids of the schedule groups the user belongs to (see `/api/v1/schedule-groups`). Empty when unassigned."
          }
        },
        "additionalProperties": false,
        "description": "A user of the calling organization as exposed to integrations by `GET /api/v1/users`:\ndisplay name and schedule-group membership only. No credentials, security stamps or\nrole details are ever returned. Requires the `scheduling` module."
      },
      "ValidationProblemDetails": {
        "type": "object",
        "properties": {
          "type": {
            "type": "string",
            "description": "URI identifying the problem type (RFC 7807). Currently the generic reference for the HTTP status.",
            "nullable": true
          },
          "title": {
            "type": "string",
            "description": "Short human-readable summary of the status, e.g. \"Not Found\".",
            "nullable": true
          },
          "status": {
            "type": "integer",
            "description": "HTTP status code, repeated from the response line.",
            "format": "int32",
            "nullable": true
          },
          "detail": {
            "type": "string",
            "description": "Explanation specific to this occurrence, e.g. \"Job 42 was not found in this organization.\"",
            "nullable": true
          },
          "instance": {
            "type": "string",
            "description": "Request path the problem occurred on, when known.",
            "nullable": true
          },
          "errors": {
            "type": "object",
            "additionalProperties": {
              "type": "array",
              "items": {
                "type": "string"
              }
            },
            "description": "Validation failures keyed by JSON field name in camelCase (an empty key holds body-level errors); each value is an array of messages, e.g. { \"name\": [\"The Name field is required.\"] }."
          }
        },
        "additionalProperties": { },
        "description": "RFC 7807 problem detail for 400 (malformed/invalid body) and 422 (semantically invalid) responses: the per-field messages are under errors. Also carries a traceId string to quote in support requests."
      },
      "VendorDto": {
        "type": "object",
        "properties": {
          "id": {
            "type": "integer",
            "description": "Numeric id, usable in `/api/v1/vendors/{id}` and as a purchase order's `vendorId`. Example: `7`.",
            "format": "int32"
          },
          "syncId": {
            "type": "string",
            "description": "Stable cross-device id; also the pagination cursor.",
            "format": "uuid"
          },
          "name": {
            "type": "string",
            "description": "Vendor name, unique in the organization. Example: `Acme Supply`."
          },
          "description": {
            "type": "string",
            "description": "Free-text notes about the vendor; null when not recorded.",
            "nullable": true
          },
          "contactName": {
            "type": "string",
            "description": "Main contact's name; null when not recorded. Example: `Dana Ruiz`.",
            "nullable": true
          },
          "contactEmail": {
            "type": "string",
            "description": "Main contact's email; null when not recorded. Example: `orders@acme.example`.",
            "nullable": true
          },
          "contactPhone": {
            "type": "string",
            "description": "Main contact's phone, as typed; null when not recorded. Example: `555-0100`.",
            "nullable": true
          },
          "website": {
            "type": "string",
            "description": "Vendor website; null when not recorded. Example: `https://acme.example`.",
            "nullable": true
          },
          "defaultShippingCompany": {
            "type": "string",
            "description": "Carrier the vendor usually ships with; null when not recorded. Example: `UPS`.",
            "nullable": true
          },
          "isActive": {
            "type": "boolean",
            "description": "False for a deactivated vendor: kept for history, not offered on new orders and refused as a new order's `vendorId`."
          },
          "createdAt": {
            "type": "string",
            "description": "UTC creation time (ISO-8601).",
            "format": "date-time"
          },
          "updatedAt": {
            "type": "string",
            "description": "UTC last modification time (ISO-8601).",
            "format": "date-time"
          }
        },
        "additionalProperties": false,
        "description": "A vendor (supplier) the organization buys from, as returned by `/api/v1/vendors`. Use the id\nas `vendorId` when creating a purchase order."
      },
      "VendorWebhookData": {
        "type": "object",
        "properties": {
          "id": {
            "type": "integer",
            "format": "int32"
          },
          "name": {
            "type": "string",
            "nullable": true
          },
          "isActive": {
            "type": "boolean"
          }
        },
        "additionalProperties": false,
        "description": "Vendor summary carried by `vendor.created` and `vendor.updated`."
      },
      "VendorWriteApiRequest": {
        "type": "object",
        "properties": {
          "name": {
            "maxLength": 100,
            "minLength": 0,
            "type": "string",
            "description": "Vendor name, 1..100 characters, unique in the organization. Required on create. Example: `Acme Supply`.",
            "nullable": true
          },
          "description": {
            "maxLength": 500,
            "minLength": 0,
            "type": "string",
            "description": "Free-text notes, up to 500 characters.",
            "nullable": true
          },
          "contactName": {
            "maxLength": 100,
            "minLength": 0,
            "type": "string",
            "description": "Main contact's name, up to 100 characters.",
            "nullable": true
          },
          "contactEmail": {
            "maxLength": 100,
            "minLength": 0,
            "type": "string",
            "description": "Main contact's email, up to 100 characters. Example: `orders@acme.example`.",
            "nullable": true
          },
          "contactPhone": {
            "maxLength": 20,
            "minLength": 0,
            "type": "string",
            "description": "Main contact's phone, up to 20 characters. Example: `555-0100`.",
            "nullable": true
          },
          "website": {
            "maxLength": 500,
            "minLength": 0,
            "type": "string",
            "description": "Vendor website, up to 500 characters.",
            "nullable": true
          },
          "defaultShippingCompany": {
            "maxLength": 100,
            "minLength": 0,
            "type": "string",
            "description": "Carrier the vendor usually ships with, up to 100 characters. Example: `UPS`.",
            "nullable": true
          },
          "isActive": {
            "type": "boolean",
            "description": "False deactivates the vendor (kept for history, not offered on new orders). Defaults to `true` on create.",
            "nullable": true
          }
        },
        "additionalProperties": false,
        "description": "Body of `POST /api/v1/vendors` (`name` required) and `PATCH /api/v1/vendors/{id}`\n(only the fields sent change; `\"\"` clears a field, except `name`).\nExample: `{ \"name\": \"Acme Supply\", \"contactEmail\": \"orders@acme.example\", \"defaultShippingCompany\": \"UPS\" }`."
      },
      "ViewModels.Items.StockBatchAction": {
        "enum": [
          "Adjust",
          "Move"
        ],
        "type": "string",
        "description": "What one entry of a stock batch does."
      },
      "ViewModels.Jobs.TransferRedirectKind": {
        "enum": [
          "Waypoint",
          "Destination",
          "Location"
        ],
        "type": "string",
        "description": "Where a redirected line, or a line on a skipped waypoint, goes instead: `Waypoint`, `Destination` or `Location`."
      },
      "VoidInvoiceRequest": {
        "type": "object",
        "properties": {
          "reason": {
            "maxLength": 1000,
            "minLength": 0,
            "type": "string",
            "description": "Why the invoice is being voided. Recorded in the job's history when the invoice bills a job.",
            "nullable": true
          }
        },
        "additionalProperties": false,
        "description": "Body of `POST /api/v1/invoices/{id}/void`. Optional — send no body (or `{}`) to\nvoid without a reason."
      },
      "WebhookEventDto": {
        "type": "object",
        "properties": {
          "name": {
            "type": "string",
            "description": "The event name, e.g. `job.completed`."
          }
        },
        "additionalProperties": false,
        "description": "One subscribable webhook event."
      },
      "WebhookEventsResponse": {
        "type": "object",
        "properties": {
          "events": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/WebhookEventDto"
            },
            "description": "Every subscribable event name."
          }
        },
        "additionalProperties": false,
        "description": "The events a subscription can subscribe to."
      },
      "WebhookSubscriptionDto": {
        "type": "object",
        "properties": {
          "id": {
            "type": "integer",
            "description": "Numeric id of the subscription. Example: `42`.",
            "format": "int32"
          },
          "url": {
            "type": "string",
            "description": "The absolute `https` URL that receives the event payloads."
          },
          "events": {
            "type": "array",
            "items": {
              "type": "string"
            },
            "description": "The subscribed event names (see `GET /api/v1/webhooks/events`). Example: `[\"job.completed\",\"invoice.paid\"]`."
          },
          "active": {
            "type": "boolean",
            "description": "Whether the subscription is currently delivering. Auto-disables after repeated failures."
          },
          "createdAt": {
            "type": "string",
            "description": "When the subscription was created, ISO-8601 UTC.",
            "format": "date-time"
          },
          "signingSecret": {
            "type": "string",
            "description": "The HMAC signing secret for verifying the `X-Flex-Signature-V2` (`t=<unix seconds>,v1=<hex>` over\n`\"<t>.<raw body>\"`) and legacy `X-Flex-Signature` headers. Returned <b>only</b> in the\nresponse to `POST /api/v1/webhooks` (shown once, never retrievable again); `null` otherwise.",
            "nullable": true
          }
        },
        "additionalProperties": false,
        "description": "A webhook subscription: the web host POSTs a signed JSON payload to `url` whenever one of the\nsubscribed `events` occurs. Used by integration platforms (Zapier, Make) to register an\ninstant-trigger endpoint per automation."
      },
      "WebhooksResponse": {
        "type": "object",
        "properties": {
          "webhooks": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/WebhookSubscriptionDto"
            },
            "description": "Every active subscription for the organization."
          }
        },
        "additionalProperties": false,
        "description": "The organization's webhook subscriptions."
      }
    },
    "securitySchemes": {
      "ApiKey": {
        "type": "http",
        "description": "API key created in Flex. Send as `Authorization: Bearer flx_live_…`.",
        "scheme": "bearer",
        "bearerFormat": "flx_live_…"
      }
    }
  },
  "security": [
    {
      "ApiKey": [ ]
    }
  ],
  "tags": [
    {
      "name": "Webhooks",
      "description": "Subscriptions are created in Flex under Settings → Integrations → Webhooks: a public https URL, the events to receive, and a signing secret shown once. Every delivery is an HTTP POST with `Content-Type: application/json` and the envelope `{ \"event\", \"occurredAt\", \"organizationId\", \"data\" }` (`occurredAt` is ISO-8601 UTC; `data` is the compact summary documented per event, camelCase, nulls omitted). Headers on every attempt: `X-Flex-Timestamp` (unix seconds when this attempt was signed), `X-Flex-Signature-V2: t=<same unix seconds>,v1=<lowercase hex>` (HMAC-SHA256, keyed with the subscription secret, of the string `<t>.<raw request body>`), `X-Flex-Delivery` (an opaque delivery id, identical on every retry of the same delivery), `X-Flex-Event` (the event name), and the legacy `X-Flex-Signature: sha256=<lowercase hex>` (HMAC-SHA256 of the raw body only, kept unchanged for existing integrations). Recommended verification (replay-safe): recompute the `X-Flex-Signature-V2` HMAC over the received bytes before parsing, compare in constant time, reject the request if `t` is more than 300 s (5 minutes) away from your clock, and ignore a `X-Flex-Delivery` you have already processed. Retries are re-signed with a fresh timestamp but keep the same `X-Flex-Delivery`. Any 2xx acknowledges the delivery; anything else, or no answer within 10 s, is retried with exponential backoff up to 5 attempts. After 20 consecutive failed deliveries the subscription is paused (shown inactive) until it is re-enabled. Redirects are never followed and private or link-local addresses are refused. Every attempt (status, timing, response) is listed in the delivery log in Settings."
    },
    {
      "name": "Activity",
      "description": "The organization's API and agent activity log (API_AND_MCP.md \"Activity log\"): one row\nper `/api/v1` or `/mcp` request made with any of its keys, including the MCP\ntool an agent called. Requires `ManageOrganizations` — the same gate as the API Keys\nand API activity pages. Read-only; served by `IApiActivityService`."
    },
    {
      "name": "Categories",
      "description": "Item categories, their properties — the custom fields every item in a category carries\n(Item Categories → Manage Properties in the app) — and the categories assigned to an item.\nReads need `ViewInventory`; writes need `ManageInventory` (the Item Categories page's\ngate) and an `Idempotency-Key`, and go through `ICategoryManagementService` /\n`ICategoryPropertyService`, the same Flex.Core code as that page. A property definition\napplies to items created in or moved to the category afterwards; existing items keep their\nproperties. The built-in \"No Category\" is not listed and cannot be changed."
    },
    {
      "name": "Customers",
      "description": "The calling organization's customers and their additional locations (job sites).\nMirrors the MVC Customers pages: reads need `ViewCustomers`, create/update need\n`ManageCustomers` on the API key (customer and location writes alike). Soft-deleted\ncustomers are never returned or editable, nor are their locations. Writes go through\n`ICustomerService` — the same code the UI uses."
    },
    {
      "name": "Invoices",
      "description": "The organization's invoices: headers, lines, payments and balance, plus creating a draft\nfrom a job or a standalone invoice from lines, finalizing, voiding, deleting drafts and recording payments.\nWrites go through `IInvoiceService` exactly like the MVC Invoices pages."
    },
    {
      "name": "ItemProperties",
      "description": "Custom fields: the properties of a catalog item (template) — their definitions (name, data\ntype, options) and the item's values. Permissions mirror the two app pages exactly: values\nand item-level properties are edited in the Items page's item modal (`ManageInventory`,\nthrough `IItemEditService`); a property's name, type and options, and deleting a\nproperty that came from the Item Template, are edited on the Item Templates page\n(`ManageTemplates`, through `IItemTemplateService`). Calculated properties are\nread-only and the pricing properties `Cost` / `Unit Price` are changed through\n`PATCH /api/v1/items/{id}`. Every write answers with the item's full property list."
    },
    {
      "name": "Items",
      "description": "The organization's items (ItemTemplates) with pricing and stock, plus create / partial\nupdate, deactivation and stock adjust/move. Stock semantics match the Items page:\nphysical rows outside job sites, minus oversold quantity when negative inventory is\nallowed, with sub-unit parents reporting a decimal on-hand. Create and update go through\n`IItemCatalogService` and deactivation through `IItemBulkActionService` — the\nsame code the Items pages use."
    },
    {
      "name": "ItemUnits",
      "description": "An item's inventory units — the physical stock rows counted by `onHand` — with their\nlocation, status and per-unit custom-field values. Each unit carries its own copy of the\nitem's custom fields: it follows the item's value until it is set on the unit (then\n`isCustomized`). Reads need `ViewInventory`; setting a unit's value needs\n`ManageInventory` (the Inventory edit page's gate) and goes through\n`IItemEditService`, the same Flex.Core code as that page."
    },
    {
      "name": "Jobs",
      "description": "Jobs (API_AND_MCP.md). Reads (Phase 1a) need `ViewJobs`; writes (Phase 1b) need\n`ManageJobs`, an `Idempotency-Key` header, and go through the same\n`IJobWorkflowService` methods as the MVC Jobs pages — this controller only\nvalidates ids against the key's organization and maps results to problem+json. Every\nquery is scoped to the presented key's organization; ids from other organizations\nanswer 404. Writes are attributed to the key's creator (`ApiActor`)."
    },
    {
      "name": "LaborRates",
      "description": "Read access to the organization's labor rates (API_AND_MCP.md) so callers can discover the\n`laborRateId` values that a job's labor line accepts. Requires `ViewJobs`. Retired\nrates are excluded unless `includeInactive=true`."
    },
    {
      "name": "Locations",
      "description": "Read access to the organization's stock locations (API_AND_MCP.md) so callers can discover\nthe `locationId` values that `GET /api/v1/items/{id}` reports stock against and that\n`POST /api/v1/items/{id}/stock/adjust` / `/stock/move` accept. Requires\n`ViewInventory`. Deleted locations are never returned."
    },
    {
      "name": "Me",
      "description": "Identity of the presented API key (API_AND_MCP.md). A connection-test endpoint for integration\nplatforms (Zapier, Make) and scripts: any valid key may call it — no extra permission — to confirm\nthe key works and learn its organization and effective permissions."
    },
    {
      "name": "Modules",
      "description": "Lets an agent or integration discover what it can do before calling anything else.\nReference implementation for the /api/v1 surface."
    },
    {
      "name": "PurchaseOrders",
      "description": "Purchase orders (API_AND_MCP.md). Reads need `ViewPurchaseOrders`; receiving needs\n`ManagePurchaseOrders` and an `Idempotency-Key` header and goes through\n`IPurchaseOrderService.ReceiveAsync`, the same Flex.Core path as the app's receiving\npanel — this controller only maps results to problem+json. Every query is scoped to the\npresented key's organization and skips deleted orders; ids from other organizations answer\n404. Receipts are attributed to the key's creator (`ApiActor`). Creating orders,\nadding lines and the status steps (ordered / shipped / delivered / cancel) go through\n`IPurchaseOrderWriteService`, the same path as the purchase-order pages; editing an\norder's header, removing lines and per-shipment tracking stay in the app."
    },
    {
      "name": "Reorder",
      "description": "Reorder points and low stock (API_AND_MCP.md \"Reorder points & vendor link\"): the app's\nReorder → Thresholds rows per item, and the items currently below one of them. Reads need\n`ViewInventory` (the office app shows the same low-stock list under it); setting a\nreorder point needs `ManageInventory`, the Thresholds page's gate. Everything goes\nthrough Flex.Core `IReorderPointService`; the low-stock list is computed on request and\nnever writes the alert rows the Reorder page creates."
    },
    {
      "name": "ReorderPoints",
      "description": "Reorder points across all items (API_AND_MCP.md \"Reorder points & vendor link\"): the\norg-wide list of the app's Reorder → Thresholds rows and deleting one. The per-item read and\ncreate-or-update live under `/api/v1/items/{id}/reorder-points`. Listing needs\n`ViewInventory`; deleting needs `ManageInventory`, the Thresholds page's gate.\nEverything goes through Flex.Core `IReorderPointService`."
    },
    {
      "name": "Roles",
      "description": "Roles (API_AND_MCP.md): the built-in OrganizationAdmin / Manager / User roles and the\norganization's custom roles, each with the permissions it grants. Reading needs\n`ManageUsers` (to pick a role for an account); creating, editing and deleting custom roles needs\n`ManageRoles` — the Roles pages' gate — and goes through Flex.Core\n`IRoleAdministrationService`, the same path as those pages. A role may only contain\npermissions held by both the API key and its creator. SystemAdmin is never listed."
    },
    {
      "name": "SalesOrders",
      "description": "Sales orders (API_AND_MCP.md). Reads need `ViewSalesOrders`; writes need\n`ManageSalesOrders` and an `Idempotency-Key` header and go through\n`ISalesOrderService` (create, Begin Fulfillment, Complete) and\n`ISalesOrderFulfillmentService` (pick, complete picking, ship, deliver, cancel) — the\nsame Flex.Core paths as the app's New Sales Order page, item modal, Details page and\nCancel page — this controller only maps results to problem+json. Every query is scoped to\nthe presented key's organization; ids from other organizations answer 404. Writes are\nattributed to the key's creator (`ApiActor`). Header / line edits, line picking, the\nmanual transfer and returns are in SalesOrdersController.Edits.cs; invoicing stays in the\napp. No module gate: sales orders are not a module."
    },
    {
      "name": "Schedule",
      "description": "The organization's calendar of scheduled jobs (API_AND_MCP.md, Phase 1a). Requires the\nScheduling module; without it every call answers 403. Same data and shape as the web\ncalendar feed (`Jobs/GetCalendarEvents`), produced by the same service."
    },
    {
      "name": "ScheduleGroups",
      "description": "The calling organization's schedule groups (divisions / crews / teams) and their members.\nRequires the `scheduling` module. Reads need `ViewJobs` — the same gate as the MVC\nassignment picker; writes need `ManageJobs` (the Schedule Groups page's gate) and an\n`Idempotency-Key`, and go through `IScheduleGroupService`, the same Flex.Core code as\nthat page. Deleted groups are never returned."
    },
    {
      "name": "Transfers",
      "description": "Inventory transfers (API_AND_MCP.md). Reads and writes both need `ManageTransfers` —\nthe Transfers pages and the office app require it for reads too and there is no separate\nview permission, so the API is never looser than the pages. Writes need an\n`Idempotency-Key` header and go through `ITransferWorkflowService`, the same\nFlex.Core façade the Job Details \"Create Transfer\" / \"Cancel\" buttons call, over\n`IStockTransferService` (the Transfers pages' engine) for standalone transfers — this\ncontroller only maps results to problem+json. Every query is scoped to the presented\nkey's organization; ids from other organizations answer 404. Writes are attributed to the\nkey's creator (`ApiActor`)."
    },
    {
      "name": "UserRemovalRequests",
      "description": "Organization Admin removal requests (permissions/ROLES_AND_PERMISSIONS.md \"Removing an Organization\nAdmin\"). Demoting or deactivating an Organization Admin is refused by `PATCH /api/v1/users/{id}`\nand `/deactivate`; instead an Organization Admin asks here, and the change is made when the\ntarget approves, or 14 days later unless the target contests. Asking for your own account steps\nyou down at once. Nobody can remove the organization's last active Organization Admin. Thin\nmapping onto Flex.Core `IAdminRemovalService` (the Users page's path, remote-first through\nthe sync server, which is the authority); every call is made as the key's creator, so approve /\ncontest work only with a key created by the admin being removed. Needs `ManageUsers`."
    },
    {
      "name": "Users",
      "description": "The calling organization's users. `GET /api/v1/users` is the scheduling roster (display\nnames and schedule-group membership; `scheduling` module + `ViewJobs`, the MVC\nassignment picker's gate). Everything else is account administration and needs\n`ManageUsers` — the Account/UserList page's permission — going through Flex.Core\n`IUserAdministrationService`, the same remote-first path as that page (the sync server\ncreates and re-roles accounts and enforces the hierarchy). Writes are attributed to the key's\ncreator and limited by `UserAdminRules`: OrganizationAdmin or higher only, never the\ncaller's own account, only accounts of a strictly lesser role, roles up to the creator's own\n(an OrganizationAdmin may promote a Manager / User to OrganizationAdmin), SystemAdmin never assignable,\nand nothing may grant a permission the API key (or its creator) does not hold."
    },
    {
      "name": "Vendors",
      "description": "Vendors (API_AND_MCP.md): the suppliers purchase orders are placed with. Reads need\n`ViewPurchaseOrders`; creating and editing need `ManageVendors` and an\n`Idempotency-Key` and go through `IVendorService`, the same Flex.Core path as the\nVendors pages and the purchase-order page's \"new vendor\" dialog."
    },
    {
      "name": "Webhooks",
      "description": "Manage webhook subscriptions over the API (API_AND_MCP.md) — the \"REST Hooks\" surface integration\nplatforms (Zapier, Make) use to register and remove an instant-trigger endpoint per automation.\nRequires `ManageOrganizations`. Web-host-only, like the rest of `/api/v1`."
    }
  ],
  "x-webhooks": {
    "job.created": {
      "post": {
        "tags": [
          "Webhooks"
        ],
        "summary": "A job or estimate is created — from the API, the web app or a synced device.",
        "description": "Outbound: Flex sends this request to your endpoint; you do not call it. Subscriptions are created in Flex under Settings → Integrations → Webhooks: a public https URL, the events to receive, and a signing secret shown once. Every delivery is an HTTP POST with `Content-Type: application/json` and the envelope `{ \"event\", \"occurredAt\", \"organizationId\", \"data\" }` (`occurredAt` is ISO-8601 UTC; `data` is the compact summary documented per event, camelCase, nulls omitted). Headers on every attempt: `X-Flex-Timestamp` (unix seconds when this attempt was signed), `X-Flex-Signature-V2: t=<same unix seconds>,v1=<lowercase hex>` (HMAC-SHA256, keyed with the subscription secret, of the string `<t>.<raw request body>`), `X-Flex-Delivery` (an opaque delivery id, identical on every retry of the same delivery), `X-Flex-Event` (the event name), and the legacy `X-Flex-Signature: sha256=<lowercase hex>` (HMAC-SHA256 of the raw body only, kept unchanged for existing integrations). Recommended verification (replay-safe): recompute the `X-Flex-Signature-V2` HMAC over the received bytes before parsing, compare in constant time, reject the request if `t` is more than 300 s (5 minutes) away from your clock, and ignore a `X-Flex-Delivery` you have already processed. Retries are re-signed with a fresh timestamp but keep the same `X-Flex-Delivery`. Any 2xx acknowledges the delivery; anything else, or no answer within 10 s, is retried with exponential backoff up to 5 attempts. After 20 consecutive failed deliveries the subscription is paused (shown inactive) until it is re-enabled. Redirects are never followed and private or link-local addresses are refused. Every attempt (status, timing, response) is listed in the delivery log in Settings.",
        "operationId": "webhook_job_created",
        "parameters": [
          {
            "name": "X-Flex-Signature",
            "in": "header",
            "description": "`sha256=<lowercase hex>` — HMAC-SHA256 of the raw request body keyed with the subscription secret. Legacy (no replay protection); prefer `X-Flex-Signature-V2`.",
            "required": true,
            "schema": {
              "type": "string",
              "example": "sha256=3f7a…c21e"
            }
          },
          {
            "name": "X-Flex-Signature-V2",
            "in": "header",
            "description": "`t=<unix seconds>,v1=<lowercase hex>` — HMAC-SHA256, keyed with the subscription secret, of the string `<t>.<raw request body>`. Reject the delivery when `t` is more than 300 s from your clock. Re-signed with a fresh `t` on every retry.",
            "required": true,
            "schema": {
              "type": "string",
              "example": "t=1790000000,v1=9b2e…4d10"
            }
          },
          {
            "name": "X-Flex-Timestamp",
            "in": "header",
            "description": "Unix time in seconds (UTC) when this attempt was signed; equal to `t` in `X-Flex-Signature-V2`.",
            "required": true,
            "schema": {
              "type": "string",
              "example": "1790000000"
            }
          },
          {
            "name": "X-Flex-Delivery",
            "in": "header",
            "description": "Opaque delivery id, the same on every retry of one delivery — store it (e.g. 24 h) and ignore repeats. Unique per subscription; do not parse it.",
            "required": true,
            "schema": {
              "type": "string",
              "example": "48213"
            }
          },
          {
            "name": "X-Flex-Event",
            "in": "header",
            "description": "The event name, identical to the envelope's `event` (handy for routing before parsing the body).",
            "required": true,
            "schema": {
              "type": "string",
              "example": "job.created"
            }
          }
        ],
        "requestBody": {
          "description": "The signed JSON envelope.",
          "content": {
            "application/json": {
              "schema": {
                "required": [
                  "event",
                  "occurredAt",
                  "organizationId",
                  "data"
                ],
                "type": "object",
                "properties": {
                  "event": {
                    "enum": [
                      "job.created"
                    ],
                    "type": "string",
                    "description": "Event name."
                  },
                  "occurredAt": {
                    "type": "string",
                    "description": "When the event occurred (ISO-8601 UTC).",
                    "format": "date-time"
                  },
                  "organizationId": {
                    "type": "integer",
                    "description": "The organization the subscription belongs to.",
                    "format": "int32"
                  },
                  "data": {
                    "$ref": "#/components/schemas/JobWebhookData"
                  }
                },
                "description": "Signed envelope Flex POSTs to the subscribed URL for `job.created`."
              }
            }
          },
          "required": true
        },
        "responses": {
          "2XX": {
            "description": "Delivery acknowledged. Any other status, or a timeout, is retried."
          }
        }
      }
    },
    "job.status_changed": {
      "post": {
        "tags": [
          "Webhooks"
        ],
        "summary": "A job moves to another status; `data.previousStatus` is the status it left.",
        "description": "Outbound: Flex sends this request to your endpoint; you do not call it. Subscriptions are created in Flex under Settings → Integrations → Webhooks: a public https URL, the events to receive, and a signing secret shown once. Every delivery is an HTTP POST with `Content-Type: application/json` and the envelope `{ \"event\", \"occurredAt\", \"organizationId\", \"data\" }` (`occurredAt` is ISO-8601 UTC; `data` is the compact summary documented per event, camelCase, nulls omitted). Headers on every attempt: `X-Flex-Timestamp` (unix seconds when this attempt was signed), `X-Flex-Signature-V2: t=<same unix seconds>,v1=<lowercase hex>` (HMAC-SHA256, keyed with the subscription secret, of the string `<t>.<raw request body>`), `X-Flex-Delivery` (an opaque delivery id, identical on every retry of the same delivery), `X-Flex-Event` (the event name), and the legacy `X-Flex-Signature: sha256=<lowercase hex>` (HMAC-SHA256 of the raw body only, kept unchanged for existing integrations). Recommended verification (replay-safe): recompute the `X-Flex-Signature-V2` HMAC over the received bytes before parsing, compare in constant time, reject the request if `t` is more than 300 s (5 minutes) away from your clock, and ignore a `X-Flex-Delivery` you have already processed. Retries are re-signed with a fresh timestamp but keep the same `X-Flex-Delivery`. Any 2xx acknowledges the delivery; anything else, or no answer within 10 s, is retried with exponential backoff up to 5 attempts. After 20 consecutive failed deliveries the subscription is paused (shown inactive) until it is re-enabled. Redirects are never followed and private or link-local addresses are refused. Every attempt (status, timing, response) is listed in the delivery log in Settings.",
        "operationId": "webhook_job_status_changed",
        "parameters": [
          {
            "name": "X-Flex-Signature",
            "in": "header",
            "description": "`sha256=<lowercase hex>` — HMAC-SHA256 of the raw request body keyed with the subscription secret. Legacy (no replay protection); prefer `X-Flex-Signature-V2`.",
            "required": true,
            "schema": {
              "type": "string",
              "example": "sha256=3f7a…c21e"
            }
          },
          {
            "name": "X-Flex-Signature-V2",
            "in": "header",
            "description": "`t=<unix seconds>,v1=<lowercase hex>` — HMAC-SHA256, keyed with the subscription secret, of the string `<t>.<raw request body>`. Reject the delivery when `t` is more than 300 s from your clock. Re-signed with a fresh `t` on every retry.",
            "required": true,
            "schema": {
              "type": "string",
              "example": "t=1790000000,v1=9b2e…4d10"
            }
          },
          {
            "name": "X-Flex-Timestamp",
            "in": "header",
            "description": "Unix time in seconds (UTC) when this attempt was signed; equal to `t` in `X-Flex-Signature-V2`.",
            "required": true,
            "schema": {
              "type": "string",
              "example": "1790000000"
            }
          },
          {
            "name": "X-Flex-Delivery",
            "in": "header",
            "description": "Opaque delivery id, the same on every retry of one delivery — store it (e.g. 24 h) and ignore repeats. Unique per subscription; do not parse it.",
            "required": true,
            "schema": {
              "type": "string",
              "example": "48213"
            }
          },
          {
            "name": "X-Flex-Event",
            "in": "header",
            "description": "The event name, identical to the envelope's `event` (handy for routing before parsing the body).",
            "required": true,
            "schema": {
              "type": "string",
              "example": "job.status_changed"
            }
          }
        ],
        "requestBody": {
          "description": "The signed JSON envelope.",
          "content": {
            "application/json": {
              "schema": {
                "required": [
                  "event",
                  "occurredAt",
                  "organizationId",
                  "data"
                ],
                "type": "object",
                "properties": {
                  "event": {
                    "enum": [
                      "job.status_changed"
                    ],
                    "type": "string",
                    "description": "Event name."
                  },
                  "occurredAt": {
                    "type": "string",
                    "description": "When the event occurred (ISO-8601 UTC).",
                    "format": "date-time"
                  },
                  "organizationId": {
                    "type": "integer",
                    "description": "The organization the subscription belongs to.",
                    "format": "int32"
                  },
                  "data": {
                    "$ref": "#/components/schemas/JobWebhookData"
                  }
                },
                "description": "Signed envelope Flex POSTs to the subscribed URL for `job.status_changed`."
              }
            }
          },
          "required": true
        },
        "responses": {
          "2XX": {
            "description": "Delivery acknowledged. Any other status, or a timeout, is retried."
          }
        }
      }
    },
    "job.scheduled": {
      "post": {
        "tags": [
          "Webhooks"
        ],
        "summary": "A job gets a start time or is rescheduled.",
        "description": "Outbound: Flex sends this request to your endpoint; you do not call it. Subscriptions are created in Flex under Settings → Integrations → Webhooks: a public https URL, the events to receive, and a signing secret shown once. Every delivery is an HTTP POST with `Content-Type: application/json` and the envelope `{ \"event\", \"occurredAt\", \"organizationId\", \"data\" }` (`occurredAt` is ISO-8601 UTC; `data` is the compact summary documented per event, camelCase, nulls omitted). Headers on every attempt: `X-Flex-Timestamp` (unix seconds when this attempt was signed), `X-Flex-Signature-V2: t=<same unix seconds>,v1=<lowercase hex>` (HMAC-SHA256, keyed with the subscription secret, of the string `<t>.<raw request body>`), `X-Flex-Delivery` (an opaque delivery id, identical on every retry of the same delivery), `X-Flex-Event` (the event name), and the legacy `X-Flex-Signature: sha256=<lowercase hex>` (HMAC-SHA256 of the raw body only, kept unchanged for existing integrations). Recommended verification (replay-safe): recompute the `X-Flex-Signature-V2` HMAC over the received bytes before parsing, compare in constant time, reject the request if `t` is more than 300 s (5 minutes) away from your clock, and ignore a `X-Flex-Delivery` you have already processed. Retries are re-signed with a fresh timestamp but keep the same `X-Flex-Delivery`. Any 2xx acknowledges the delivery; anything else, or no answer within 10 s, is retried with exponential backoff up to 5 attempts. After 20 consecutive failed deliveries the subscription is paused (shown inactive) until it is re-enabled. Redirects are never followed and private or link-local addresses are refused. Every attempt (status, timing, response) is listed in the delivery log in Settings.",
        "operationId": "webhook_job_scheduled",
        "parameters": [
          {
            "name": "X-Flex-Signature",
            "in": "header",
            "description": "`sha256=<lowercase hex>` — HMAC-SHA256 of the raw request body keyed with the subscription secret. Legacy (no replay protection); prefer `X-Flex-Signature-V2`.",
            "required": true,
            "schema": {
              "type": "string",
              "example": "sha256=3f7a…c21e"
            }
          },
          {
            "name": "X-Flex-Signature-V2",
            "in": "header",
            "description": "`t=<unix seconds>,v1=<lowercase hex>` — HMAC-SHA256, keyed with the subscription secret, of the string `<t>.<raw request body>`. Reject the delivery when `t` is more than 300 s from your clock. Re-signed with a fresh `t` on every retry.",
            "required": true,
            "schema": {
              "type": "string",
              "example": "t=1790000000,v1=9b2e…4d10"
            }
          },
          {
            "name": "X-Flex-Timestamp",
            "in": "header",
            "description": "Unix time in seconds (UTC) when this attempt was signed; equal to `t` in `X-Flex-Signature-V2`.",
            "required": true,
            "schema": {
              "type": "string",
              "example": "1790000000"
            }
          },
          {
            "name": "X-Flex-Delivery",
            "in": "header",
            "description": "Opaque delivery id, the same on every retry of one delivery — store it (e.g. 24 h) and ignore repeats. Unique per subscription; do not parse it.",
            "required": true,
            "schema": {
              "type": "string",
              "example": "48213"
            }
          },
          {
            "name": "X-Flex-Event",
            "in": "header",
            "description": "The event name, identical to the envelope's `event` (handy for routing before parsing the body).",
            "required": true,
            "schema": {
              "type": "string",
              "example": "job.scheduled"
            }
          }
        ],
        "requestBody": {
          "description": "The signed JSON envelope.",
          "content": {
            "application/json": {
              "schema": {
                "required": [
                  "event",
                  "occurredAt",
                  "organizationId",
                  "data"
                ],
                "type": "object",
                "properties": {
                  "event": {
                    "enum": [
                      "job.scheduled"
                    ],
                    "type": "string",
                    "description": "Event name."
                  },
                  "occurredAt": {
                    "type": "string",
                    "description": "When the event occurred (ISO-8601 UTC).",
                    "format": "date-time"
                  },
                  "organizationId": {
                    "type": "integer",
                    "description": "The organization the subscription belongs to.",
                    "format": "int32"
                  },
                  "data": {
                    "$ref": "#/components/schemas/JobWebhookData"
                  }
                },
                "description": "Signed envelope Flex POSTs to the subscribed URL for `job.scheduled`."
              }
            }
          },
          "required": true
        },
        "responses": {
          "2XX": {
            "description": "Delivery acknowledged. Any other status, or a timeout, is retried."
          }
        }
      }
    },
    "job.assigned": {
      "post": {
        "tags": [
          "Webhooks"
        ],
        "summary": "The crew assigned to a job changes; `data.assigneeUserIds` is the new crew.",
        "description": "Outbound: Flex sends this request to your endpoint; you do not call it. Subscriptions are created in Flex under Settings → Integrations → Webhooks: a public https URL, the events to receive, and a signing secret shown once. Every delivery is an HTTP POST with `Content-Type: application/json` and the envelope `{ \"event\", \"occurredAt\", \"organizationId\", \"data\" }` (`occurredAt` is ISO-8601 UTC; `data` is the compact summary documented per event, camelCase, nulls omitted). Headers on every attempt: `X-Flex-Timestamp` (unix seconds when this attempt was signed), `X-Flex-Signature-V2: t=<same unix seconds>,v1=<lowercase hex>` (HMAC-SHA256, keyed with the subscription secret, of the string `<t>.<raw request body>`), `X-Flex-Delivery` (an opaque delivery id, identical on every retry of the same delivery), `X-Flex-Event` (the event name), and the legacy `X-Flex-Signature: sha256=<lowercase hex>` (HMAC-SHA256 of the raw body only, kept unchanged for existing integrations). Recommended verification (replay-safe): recompute the `X-Flex-Signature-V2` HMAC over the received bytes before parsing, compare in constant time, reject the request if `t` is more than 300 s (5 minutes) away from your clock, and ignore a `X-Flex-Delivery` you have already processed. Retries are re-signed with a fresh timestamp but keep the same `X-Flex-Delivery`. Any 2xx acknowledges the delivery; anything else, or no answer within 10 s, is retried with exponential backoff up to 5 attempts. After 20 consecutive failed deliveries the subscription is paused (shown inactive) until it is re-enabled. Redirects are never followed and private or link-local addresses are refused. Every attempt (status, timing, response) is listed in the delivery log in Settings.",
        "operationId": "webhook_job_assigned",
        "parameters": [
          {
            "name": "X-Flex-Signature",
            "in": "header",
            "description": "`sha256=<lowercase hex>` — HMAC-SHA256 of the raw request body keyed with the subscription secret. Legacy (no replay protection); prefer `X-Flex-Signature-V2`.",
            "required": true,
            "schema": {
              "type": "string",
              "example": "sha256=3f7a…c21e"
            }
          },
          {
            "name": "X-Flex-Signature-V2",
            "in": "header",
            "description": "`t=<unix seconds>,v1=<lowercase hex>` — HMAC-SHA256, keyed with the subscription secret, of the string `<t>.<raw request body>`. Reject the delivery when `t` is more than 300 s from your clock. Re-signed with a fresh `t` on every retry.",
            "required": true,
            "schema": {
              "type": "string",
              "example": "t=1790000000,v1=9b2e…4d10"
            }
          },
          {
            "name": "X-Flex-Timestamp",
            "in": "header",
            "description": "Unix time in seconds (UTC) when this attempt was signed; equal to `t` in `X-Flex-Signature-V2`.",
            "required": true,
            "schema": {
              "type": "string",
              "example": "1790000000"
            }
          },
          {
            "name": "X-Flex-Delivery",
            "in": "header",
            "description": "Opaque delivery id, the same on every retry of one delivery — store it (e.g. 24 h) and ignore repeats. Unique per subscription; do not parse it.",
            "required": true,
            "schema": {
              "type": "string",
              "example": "48213"
            }
          },
          {
            "name": "X-Flex-Event",
            "in": "header",
            "description": "The event name, identical to the envelope's `event` (handy for routing before parsing the body).",
            "required": true,
            "schema": {
              "type": "string",
              "example": "job.assigned"
            }
          }
        ],
        "requestBody": {
          "description": "The signed JSON envelope.",
          "content": {
            "application/json": {
              "schema": {
                "required": [
                  "event",
                  "occurredAt",
                  "organizationId",
                  "data"
                ],
                "type": "object",
                "properties": {
                  "event": {
                    "enum": [
                      "job.assigned"
                    ],
                    "type": "string",
                    "description": "Event name."
                  },
                  "occurredAt": {
                    "type": "string",
                    "description": "When the event occurred (ISO-8601 UTC).",
                    "format": "date-time"
                  },
                  "organizationId": {
                    "type": "integer",
                    "description": "The organization the subscription belongs to.",
                    "format": "int32"
                  },
                  "data": {
                    "$ref": "#/components/schemas/JobWebhookData"
                  }
                },
                "description": "Signed envelope Flex POSTs to the subscribed URL for `job.assigned`."
              }
            }
          },
          "required": true
        },
        "responses": {
          "2XX": {
            "description": "Delivery acknowledged. Any other status, or a timeout, is retried."
          }
        }
      }
    },
    "job.completed": {
      "post": {
        "tags": [
          "Webhooks"
        ],
        "summary": "A job reaches a completed status (sent in addition to `job.status_changed`).",
        "description": "Outbound: Flex sends this request to your endpoint; you do not call it. Subscriptions are created in Flex under Settings → Integrations → Webhooks: a public https URL, the events to receive, and a signing secret shown once. Every delivery is an HTTP POST with `Content-Type: application/json` and the envelope `{ \"event\", \"occurredAt\", \"organizationId\", \"data\" }` (`occurredAt` is ISO-8601 UTC; `data` is the compact summary documented per event, camelCase, nulls omitted). Headers on every attempt: `X-Flex-Timestamp` (unix seconds when this attempt was signed), `X-Flex-Signature-V2: t=<same unix seconds>,v1=<lowercase hex>` (HMAC-SHA256, keyed with the subscription secret, of the string `<t>.<raw request body>`), `X-Flex-Delivery` (an opaque delivery id, identical on every retry of the same delivery), `X-Flex-Event` (the event name), and the legacy `X-Flex-Signature: sha256=<lowercase hex>` (HMAC-SHA256 of the raw body only, kept unchanged for existing integrations). Recommended verification (replay-safe): recompute the `X-Flex-Signature-V2` HMAC over the received bytes before parsing, compare in constant time, reject the request if `t` is more than 300 s (5 minutes) away from your clock, and ignore a `X-Flex-Delivery` you have already processed. Retries are re-signed with a fresh timestamp but keep the same `X-Flex-Delivery`. Any 2xx acknowledges the delivery; anything else, or no answer within 10 s, is retried with exponential backoff up to 5 attempts. After 20 consecutive failed deliveries the subscription is paused (shown inactive) until it is re-enabled. Redirects are never followed and private or link-local addresses are refused. Every attempt (status, timing, response) is listed in the delivery log in Settings.",
        "operationId": "webhook_job_completed",
        "parameters": [
          {
            "name": "X-Flex-Signature",
            "in": "header",
            "description": "`sha256=<lowercase hex>` — HMAC-SHA256 of the raw request body keyed with the subscription secret. Legacy (no replay protection); prefer `X-Flex-Signature-V2`.",
            "required": true,
            "schema": {
              "type": "string",
              "example": "sha256=3f7a…c21e"
            }
          },
          {
            "name": "X-Flex-Signature-V2",
            "in": "header",
            "description": "`t=<unix seconds>,v1=<lowercase hex>` — HMAC-SHA256, keyed with the subscription secret, of the string `<t>.<raw request body>`. Reject the delivery when `t` is more than 300 s from your clock. Re-signed with a fresh `t` on every retry.",
            "required": true,
            "schema": {
              "type": "string",
              "example": "t=1790000000,v1=9b2e…4d10"
            }
          },
          {
            "name": "X-Flex-Timestamp",
            "in": "header",
            "description": "Unix time in seconds (UTC) when this attempt was signed; equal to `t` in `X-Flex-Signature-V2`.",
            "required": true,
            "schema": {
              "type": "string",
              "example": "1790000000"
            }
          },
          {
            "name": "X-Flex-Delivery",
            "in": "header",
            "description": "Opaque delivery id, the same on every retry of one delivery — store it (e.g. 24 h) and ignore repeats. Unique per subscription; do not parse it.",
            "required": true,
            "schema": {
              "type": "string",
              "example": "48213"
            }
          },
          {
            "name": "X-Flex-Event",
            "in": "header",
            "description": "The event name, identical to the envelope's `event` (handy for routing before parsing the body).",
            "required": true,
            "schema": {
              "type": "string",
              "example": "job.completed"
            }
          }
        ],
        "requestBody": {
          "description": "The signed JSON envelope.",
          "content": {
            "application/json": {
              "schema": {
                "required": [
                  "event",
                  "occurredAt",
                  "organizationId",
                  "data"
                ],
                "type": "object",
                "properties": {
                  "event": {
                    "enum": [
                      "job.completed"
                    ],
                    "type": "string",
                    "description": "Event name."
                  },
                  "occurredAt": {
                    "type": "string",
                    "description": "When the event occurred (ISO-8601 UTC).",
                    "format": "date-time"
                  },
                  "organizationId": {
                    "type": "integer",
                    "description": "The organization the subscription belongs to.",
                    "format": "int32"
                  },
                  "data": {
                    "$ref": "#/components/schemas/JobWebhookData"
                  }
                },
                "description": "Signed envelope Flex POSTs to the subscribed URL for `job.completed`."
              }
            }
          },
          "required": true
        },
        "responses": {
          "2XX": {
            "description": "Delivery acknowledged. Any other status, or a timeout, is retried."
          }
        }
      }
    },
    "invoice.created": {
      "post": {
        "tags": [
          "Webhooks"
        ],
        "summary": "An invoice is created.",
        "description": "Outbound: Flex sends this request to your endpoint; you do not call it. Subscriptions are created in Flex under Settings → Integrations → Webhooks: a public https URL, the events to receive, and a signing secret shown once. Every delivery is an HTTP POST with `Content-Type: application/json` and the envelope `{ \"event\", \"occurredAt\", \"organizationId\", \"data\" }` (`occurredAt` is ISO-8601 UTC; `data` is the compact summary documented per event, camelCase, nulls omitted). Headers on every attempt: `X-Flex-Timestamp` (unix seconds when this attempt was signed), `X-Flex-Signature-V2: t=<same unix seconds>,v1=<lowercase hex>` (HMAC-SHA256, keyed with the subscription secret, of the string `<t>.<raw request body>`), `X-Flex-Delivery` (an opaque delivery id, identical on every retry of the same delivery), `X-Flex-Event` (the event name), and the legacy `X-Flex-Signature: sha256=<lowercase hex>` (HMAC-SHA256 of the raw body only, kept unchanged for existing integrations). Recommended verification (replay-safe): recompute the `X-Flex-Signature-V2` HMAC over the received bytes before parsing, compare in constant time, reject the request if `t` is more than 300 s (5 minutes) away from your clock, and ignore a `X-Flex-Delivery` you have already processed. Retries are re-signed with a fresh timestamp but keep the same `X-Flex-Delivery`. Any 2xx acknowledges the delivery; anything else, or no answer within 10 s, is retried with exponential backoff up to 5 attempts. After 20 consecutive failed deliveries the subscription is paused (shown inactive) until it is re-enabled. Redirects are never followed and private or link-local addresses are refused. Every attempt (status, timing, response) is listed in the delivery log in Settings.",
        "operationId": "webhook_invoice_created",
        "parameters": [
          {
            "name": "X-Flex-Signature",
            "in": "header",
            "description": "`sha256=<lowercase hex>` — HMAC-SHA256 of the raw request body keyed with the subscription secret. Legacy (no replay protection); prefer `X-Flex-Signature-V2`.",
            "required": true,
            "schema": {
              "type": "string",
              "example": "sha256=3f7a…c21e"
            }
          },
          {
            "name": "X-Flex-Signature-V2",
            "in": "header",
            "description": "`t=<unix seconds>,v1=<lowercase hex>` — HMAC-SHA256, keyed with the subscription secret, of the string `<t>.<raw request body>`. Reject the delivery when `t` is more than 300 s from your clock. Re-signed with a fresh `t` on every retry.",
            "required": true,
            "schema": {
              "type": "string",
              "example": "t=1790000000,v1=9b2e…4d10"
            }
          },
          {
            "name": "X-Flex-Timestamp",
            "in": "header",
            "description": "Unix time in seconds (UTC) when this attempt was signed; equal to `t` in `X-Flex-Signature-V2`.",
            "required": true,
            "schema": {
              "type": "string",
              "example": "1790000000"
            }
          },
          {
            "name": "X-Flex-Delivery",
            "in": "header",
            "description": "Opaque delivery id, the same on every retry of one delivery — store it (e.g. 24 h) and ignore repeats. Unique per subscription; do not parse it.",
            "required": true,
            "schema": {
              "type": "string",
              "example": "48213"
            }
          },
          {
            "name": "X-Flex-Event",
            "in": "header",
            "description": "The event name, identical to the envelope's `event` (handy for routing before parsing the body).",
            "required": true,
            "schema": {
              "type": "string",
              "example": "invoice.created"
            }
          }
        ],
        "requestBody": {
          "description": "The signed JSON envelope.",
          "content": {
            "application/json": {
              "schema": {
                "required": [
                  "event",
                  "occurredAt",
                  "organizationId",
                  "data"
                ],
                "type": "object",
                "properties": {
                  "event": {
                    "enum": [
                      "invoice.created"
                    ],
                    "type": "string",
                    "description": "Event name."
                  },
                  "occurredAt": {
                    "type": "string",
                    "description": "When the event occurred (ISO-8601 UTC).",
                    "format": "date-time"
                  },
                  "organizationId": {
                    "type": "integer",
                    "description": "The organization the subscription belongs to.",
                    "format": "int32"
                  },
                  "data": {
                    "$ref": "#/components/schemas/InvoiceWebhookData"
                  }
                },
                "description": "Signed envelope Flex POSTs to the subscribed URL for `invoice.created`."
              }
            }
          },
          "required": true
        },
        "responses": {
          "2XX": {
            "description": "Delivery acknowledged. Any other status, or a timeout, is retried."
          }
        }
      }
    },
    "invoice.sent": {
      "post": {
        "tags": [
          "Webhooks"
        ],
        "summary": "An invoice leaves Draft (finalized or marked sent), or is created already finalized.",
        "description": "Outbound: Flex sends this request to your endpoint; you do not call it. Subscriptions are created in Flex under Settings → Integrations → Webhooks: a public https URL, the events to receive, and a signing secret shown once. Every delivery is an HTTP POST with `Content-Type: application/json` and the envelope `{ \"event\", \"occurredAt\", \"organizationId\", \"data\" }` (`occurredAt` is ISO-8601 UTC; `data` is the compact summary documented per event, camelCase, nulls omitted). Headers on every attempt: `X-Flex-Timestamp` (unix seconds when this attempt was signed), `X-Flex-Signature-V2: t=<same unix seconds>,v1=<lowercase hex>` (HMAC-SHA256, keyed with the subscription secret, of the string `<t>.<raw request body>`), `X-Flex-Delivery` (an opaque delivery id, identical on every retry of the same delivery), `X-Flex-Event` (the event name), and the legacy `X-Flex-Signature: sha256=<lowercase hex>` (HMAC-SHA256 of the raw body only, kept unchanged for existing integrations). Recommended verification (replay-safe): recompute the `X-Flex-Signature-V2` HMAC over the received bytes before parsing, compare in constant time, reject the request if `t` is more than 300 s (5 minutes) away from your clock, and ignore a `X-Flex-Delivery` you have already processed. Retries are re-signed with a fresh timestamp but keep the same `X-Flex-Delivery`. Any 2xx acknowledges the delivery; anything else, or no answer within 10 s, is retried with exponential backoff up to 5 attempts. After 20 consecutive failed deliveries the subscription is paused (shown inactive) until it is re-enabled. Redirects are never followed and private or link-local addresses are refused. Every attempt (status, timing, response) is listed in the delivery log in Settings.",
        "operationId": "webhook_invoice_sent",
        "parameters": [
          {
            "name": "X-Flex-Signature",
            "in": "header",
            "description": "`sha256=<lowercase hex>` — HMAC-SHA256 of the raw request body keyed with the subscription secret. Legacy (no replay protection); prefer `X-Flex-Signature-V2`.",
            "required": true,
            "schema": {
              "type": "string",
              "example": "sha256=3f7a…c21e"
            }
          },
          {
            "name": "X-Flex-Signature-V2",
            "in": "header",
            "description": "`t=<unix seconds>,v1=<lowercase hex>` — HMAC-SHA256, keyed with the subscription secret, of the string `<t>.<raw request body>`. Reject the delivery when `t` is more than 300 s from your clock. Re-signed with a fresh `t` on every retry.",
            "required": true,
            "schema": {
              "type": "string",
              "example": "t=1790000000,v1=9b2e…4d10"
            }
          },
          {
            "name": "X-Flex-Timestamp",
            "in": "header",
            "description": "Unix time in seconds (UTC) when this attempt was signed; equal to `t` in `X-Flex-Signature-V2`.",
            "required": true,
            "schema": {
              "type": "string",
              "example": "1790000000"
            }
          },
          {
            "name": "X-Flex-Delivery",
            "in": "header",
            "description": "Opaque delivery id, the same on every retry of one delivery — store it (e.g. 24 h) and ignore repeats. Unique per subscription; do not parse it.",
            "required": true,
            "schema": {
              "type": "string",
              "example": "48213"
            }
          },
          {
            "name": "X-Flex-Event",
            "in": "header",
            "description": "The event name, identical to the envelope's `event` (handy for routing before parsing the body).",
            "required": true,
            "schema": {
              "type": "string",
              "example": "invoice.sent"
            }
          }
        ],
        "requestBody": {
          "description": "The signed JSON envelope.",
          "content": {
            "application/json": {
              "schema": {
                "required": [
                  "event",
                  "occurredAt",
                  "organizationId",
                  "data"
                ],
                "type": "object",
                "properties": {
                  "event": {
                    "enum": [
                      "invoice.sent"
                    ],
                    "type": "string",
                    "description": "Event name."
                  },
                  "occurredAt": {
                    "type": "string",
                    "description": "When the event occurred (ISO-8601 UTC).",
                    "format": "date-time"
                  },
                  "organizationId": {
                    "type": "integer",
                    "description": "The organization the subscription belongs to.",
                    "format": "int32"
                  },
                  "data": {
                    "$ref": "#/components/schemas/InvoiceWebhookData"
                  }
                },
                "description": "Signed envelope Flex POSTs to the subscribed URL for `invoice.sent`."
              }
            }
          },
          "required": true
        },
        "responses": {
          "2XX": {
            "description": "Delivery acknowledged. Any other status, or a timeout, is retried."
          }
        }
      }
    },
    "invoice.paid": {
      "post": {
        "tags": [
          "Webhooks"
        ],
        "summary": "An invoice is paid in full.",
        "description": "Outbound: Flex sends this request to your endpoint; you do not call it. Subscriptions are created in Flex under Settings → Integrations → Webhooks: a public https URL, the events to receive, and a signing secret shown once. Every delivery is an HTTP POST with `Content-Type: application/json` and the envelope `{ \"event\", \"occurredAt\", \"organizationId\", \"data\" }` (`occurredAt` is ISO-8601 UTC; `data` is the compact summary documented per event, camelCase, nulls omitted). Headers on every attempt: `X-Flex-Timestamp` (unix seconds when this attempt was signed), `X-Flex-Signature-V2: t=<same unix seconds>,v1=<lowercase hex>` (HMAC-SHA256, keyed with the subscription secret, of the string `<t>.<raw request body>`), `X-Flex-Delivery` (an opaque delivery id, identical on every retry of the same delivery), `X-Flex-Event` (the event name), and the legacy `X-Flex-Signature: sha256=<lowercase hex>` (HMAC-SHA256 of the raw body only, kept unchanged for existing integrations). Recommended verification (replay-safe): recompute the `X-Flex-Signature-V2` HMAC over the received bytes before parsing, compare in constant time, reject the request if `t` is more than 300 s (5 minutes) away from your clock, and ignore a `X-Flex-Delivery` you have already processed. Retries are re-signed with a fresh timestamp but keep the same `X-Flex-Delivery`. Any 2xx acknowledges the delivery; anything else, or no answer within 10 s, is retried with exponential backoff up to 5 attempts. After 20 consecutive failed deliveries the subscription is paused (shown inactive) until it is re-enabled. Redirects are never followed and private or link-local addresses are refused. Every attempt (status, timing, response) is listed in the delivery log in Settings.",
        "operationId": "webhook_invoice_paid",
        "parameters": [
          {
            "name": "X-Flex-Signature",
            "in": "header",
            "description": "`sha256=<lowercase hex>` — HMAC-SHA256 of the raw request body keyed with the subscription secret. Legacy (no replay protection); prefer `X-Flex-Signature-V2`.",
            "required": true,
            "schema": {
              "type": "string",
              "example": "sha256=3f7a…c21e"
            }
          },
          {
            "name": "X-Flex-Signature-V2",
            "in": "header",
            "description": "`t=<unix seconds>,v1=<lowercase hex>` — HMAC-SHA256, keyed with the subscription secret, of the string `<t>.<raw request body>`. Reject the delivery when `t` is more than 300 s from your clock. Re-signed with a fresh `t` on every retry.",
            "required": true,
            "schema": {
              "type": "string",
              "example": "t=1790000000,v1=9b2e…4d10"
            }
          },
          {
            "name": "X-Flex-Timestamp",
            "in": "header",
            "description": "Unix time in seconds (UTC) when this attempt was signed; equal to `t` in `X-Flex-Signature-V2`.",
            "required": true,
            "schema": {
              "type": "string",
              "example": "1790000000"
            }
          },
          {
            "name": "X-Flex-Delivery",
            "in": "header",
            "description": "Opaque delivery id, the same on every retry of one delivery — store it (e.g. 24 h) and ignore repeats. Unique per subscription; do not parse it.",
            "required": true,
            "schema": {
              "type": "string",
              "example": "48213"
            }
          },
          {
            "name": "X-Flex-Event",
            "in": "header",
            "description": "The event name, identical to the envelope's `event` (handy for routing before parsing the body).",
            "required": true,
            "schema": {
              "type": "string",
              "example": "invoice.paid"
            }
          }
        ],
        "requestBody": {
          "description": "The signed JSON envelope.",
          "content": {
            "application/json": {
              "schema": {
                "required": [
                  "event",
                  "occurredAt",
                  "organizationId",
                  "data"
                ],
                "type": "object",
                "properties": {
                  "event": {
                    "enum": [
                      "invoice.paid"
                    ],
                    "type": "string",
                    "description": "Event name."
                  },
                  "occurredAt": {
                    "type": "string",
                    "description": "When the event occurred (ISO-8601 UTC).",
                    "format": "date-time"
                  },
                  "organizationId": {
                    "type": "integer",
                    "description": "The organization the subscription belongs to.",
                    "format": "int32"
                  },
                  "data": {
                    "$ref": "#/components/schemas/InvoiceWebhookData"
                  }
                },
                "description": "Signed envelope Flex POSTs to the subscribed URL for `invoice.paid`."
              }
            }
          },
          "required": true
        },
        "responses": {
          "2XX": {
            "description": "Delivery acknowledged. Any other status, or a timeout, is retried."
          }
        }
      }
    },
    "invoice.deleted": {
      "post": {
        "tags": [
          "Webhooks"
        ],
        "summary": "A draft invoice is deleted — from the web app, `DELETE /api/v1/invoices/{id}`, MCP `delete_draft_invoice`, or a synced device regenerating its draft. `data` is the invoice as it was (status `Draft`); its id no longer resolves. Finalized invoices are never deleted (they are voided).",
        "description": "Outbound: Flex sends this request to your endpoint; you do not call it. Subscriptions are created in Flex under Settings → Integrations → Webhooks: a public https URL, the events to receive, and a signing secret shown once. Every delivery is an HTTP POST with `Content-Type: application/json` and the envelope `{ \"event\", \"occurredAt\", \"organizationId\", \"data\" }` (`occurredAt` is ISO-8601 UTC; `data` is the compact summary documented per event, camelCase, nulls omitted). Headers on every attempt: `X-Flex-Timestamp` (unix seconds when this attempt was signed), `X-Flex-Signature-V2: t=<same unix seconds>,v1=<lowercase hex>` (HMAC-SHA256, keyed with the subscription secret, of the string `<t>.<raw request body>`), `X-Flex-Delivery` (an opaque delivery id, identical on every retry of the same delivery), `X-Flex-Event` (the event name), and the legacy `X-Flex-Signature: sha256=<lowercase hex>` (HMAC-SHA256 of the raw body only, kept unchanged for existing integrations). Recommended verification (replay-safe): recompute the `X-Flex-Signature-V2` HMAC over the received bytes before parsing, compare in constant time, reject the request if `t` is more than 300 s (5 minutes) away from your clock, and ignore a `X-Flex-Delivery` you have already processed. Retries are re-signed with a fresh timestamp but keep the same `X-Flex-Delivery`. Any 2xx acknowledges the delivery; anything else, or no answer within 10 s, is retried with exponential backoff up to 5 attempts. After 20 consecutive failed deliveries the subscription is paused (shown inactive) until it is re-enabled. Redirects are never followed and private or link-local addresses are refused. Every attempt (status, timing, response) is listed in the delivery log in Settings.",
        "operationId": "webhook_invoice_deleted",
        "parameters": [
          {
            "name": "X-Flex-Signature",
            "in": "header",
            "description": "`sha256=<lowercase hex>` — HMAC-SHA256 of the raw request body keyed with the subscription secret. Legacy (no replay protection); prefer `X-Flex-Signature-V2`.",
            "required": true,
            "schema": {
              "type": "string",
              "example": "sha256=3f7a…c21e"
            }
          },
          {
            "name": "X-Flex-Signature-V2",
            "in": "header",
            "description": "`t=<unix seconds>,v1=<lowercase hex>` — HMAC-SHA256, keyed with the subscription secret, of the string `<t>.<raw request body>`. Reject the delivery when `t` is more than 300 s from your clock. Re-signed with a fresh `t` on every retry.",
            "required": true,
            "schema": {
              "type": "string",
              "example": "t=1790000000,v1=9b2e…4d10"
            }
          },
          {
            "name": "X-Flex-Timestamp",
            "in": "header",
            "description": "Unix time in seconds (UTC) when this attempt was signed; equal to `t` in `X-Flex-Signature-V2`.",
            "required": true,
            "schema": {
              "type": "string",
              "example": "1790000000"
            }
          },
          {
            "name": "X-Flex-Delivery",
            "in": "header",
            "description": "Opaque delivery id, the same on every retry of one delivery — store it (e.g. 24 h) and ignore repeats. Unique per subscription; do not parse it.",
            "required": true,
            "schema": {
              "type": "string",
              "example": "48213"
            }
          },
          {
            "name": "X-Flex-Event",
            "in": "header",
            "description": "The event name, identical to the envelope's `event` (handy for routing before parsing the body).",
            "required": true,
            "schema": {
              "type": "string",
              "example": "invoice.deleted"
            }
          }
        ],
        "requestBody": {
          "description": "The signed JSON envelope.",
          "content": {
            "application/json": {
              "schema": {
                "required": [
                  "event",
                  "occurredAt",
                  "organizationId",
                  "data"
                ],
                "type": "object",
                "properties": {
                  "event": {
                    "enum": [
                      "invoice.deleted"
                    ],
                    "type": "string",
                    "description": "Event name."
                  },
                  "occurredAt": {
                    "type": "string",
                    "description": "When the event occurred (ISO-8601 UTC).",
                    "format": "date-time"
                  },
                  "organizationId": {
                    "type": "integer",
                    "description": "The organization the subscription belongs to.",
                    "format": "int32"
                  },
                  "data": {
                    "$ref": "#/components/schemas/InvoiceWebhookData"
                  }
                },
                "description": "Signed envelope Flex POSTs to the subscribed URL for `invoice.deleted`."
              }
            }
          },
          "required": true
        },
        "responses": {
          "2XX": {
            "description": "Delivery acknowledged. Any other status, or a timeout, is retried."
          }
        }
      }
    },
    "customer.created": {
      "post": {
        "tags": [
          "Webhooks"
        ],
        "summary": "A customer is created.",
        "description": "Outbound: Flex sends this request to your endpoint; you do not call it. Subscriptions are created in Flex under Settings → Integrations → Webhooks: a public https URL, the events to receive, and a signing secret shown once. Every delivery is an HTTP POST with `Content-Type: application/json` and the envelope `{ \"event\", \"occurredAt\", \"organizationId\", \"data\" }` (`occurredAt` is ISO-8601 UTC; `data` is the compact summary documented per event, camelCase, nulls omitted). Headers on every attempt: `X-Flex-Timestamp` (unix seconds when this attempt was signed), `X-Flex-Signature-V2: t=<same unix seconds>,v1=<lowercase hex>` (HMAC-SHA256, keyed with the subscription secret, of the string `<t>.<raw request body>`), `X-Flex-Delivery` (an opaque delivery id, identical on every retry of the same delivery), `X-Flex-Event` (the event name), and the legacy `X-Flex-Signature: sha256=<lowercase hex>` (HMAC-SHA256 of the raw body only, kept unchanged for existing integrations). Recommended verification (replay-safe): recompute the `X-Flex-Signature-V2` HMAC over the received bytes before parsing, compare in constant time, reject the request if `t` is more than 300 s (5 minutes) away from your clock, and ignore a `X-Flex-Delivery` you have already processed. Retries are re-signed with a fresh timestamp but keep the same `X-Flex-Delivery`. Any 2xx acknowledges the delivery; anything else, or no answer within 10 s, is retried with exponential backoff up to 5 attempts. After 20 consecutive failed deliveries the subscription is paused (shown inactive) until it is re-enabled. Redirects are never followed and private or link-local addresses are refused. Every attempt (status, timing, response) is listed in the delivery log in Settings.",
        "operationId": "webhook_customer_created",
        "parameters": [
          {
            "name": "X-Flex-Signature",
            "in": "header",
            "description": "`sha256=<lowercase hex>` — HMAC-SHA256 of the raw request body keyed with the subscription secret. Legacy (no replay protection); prefer `X-Flex-Signature-V2`.",
            "required": true,
            "schema": {
              "type": "string",
              "example": "sha256=3f7a…c21e"
            }
          },
          {
            "name": "X-Flex-Signature-V2",
            "in": "header",
            "description": "`t=<unix seconds>,v1=<lowercase hex>` — HMAC-SHA256, keyed with the subscription secret, of the string `<t>.<raw request body>`. Reject the delivery when `t` is more than 300 s from your clock. Re-signed with a fresh `t` on every retry.",
            "required": true,
            "schema": {
              "type": "string",
              "example": "t=1790000000,v1=9b2e…4d10"
            }
          },
          {
            "name": "X-Flex-Timestamp",
            "in": "header",
            "description": "Unix time in seconds (UTC) when this attempt was signed; equal to `t` in `X-Flex-Signature-V2`.",
            "required": true,
            "schema": {
              "type": "string",
              "example": "1790000000"
            }
          },
          {
            "name": "X-Flex-Delivery",
            "in": "header",
            "description": "Opaque delivery id, the same on every retry of one delivery — store it (e.g. 24 h) and ignore repeats. Unique per subscription; do not parse it.",
            "required": true,
            "schema": {
              "type": "string",
              "example": "48213"
            }
          },
          {
            "name": "X-Flex-Event",
            "in": "header",
            "description": "The event name, identical to the envelope's `event` (handy for routing before parsing the body).",
            "required": true,
            "schema": {
              "type": "string",
              "example": "customer.created"
            }
          }
        ],
        "requestBody": {
          "description": "The signed JSON envelope.",
          "content": {
            "application/json": {
              "schema": {
                "required": [
                  "event",
                  "occurredAt",
                  "organizationId",
                  "data"
                ],
                "type": "object",
                "properties": {
                  "event": {
                    "enum": [
                      "customer.created"
                    ],
                    "type": "string",
                    "description": "Event name."
                  },
                  "occurredAt": {
                    "type": "string",
                    "description": "When the event occurred (ISO-8601 UTC).",
                    "format": "date-time"
                  },
                  "organizationId": {
                    "type": "integer",
                    "description": "The organization the subscription belongs to.",
                    "format": "int32"
                  },
                  "data": {
                    "$ref": "#/components/schemas/CustomerWebhookData"
                  }
                },
                "description": "Signed envelope Flex POSTs to the subscribed URL for `customer.created`."
              }
            }
          },
          "required": true
        },
        "responses": {
          "2XX": {
            "description": "Delivery acknowledged. Any other status, or a timeout, is retried."
          }
        }
      }
    },
    "transfer.completed": {
      "post": {
        "tags": [
          "Webhooks"
        ],
        "summary": "An inventory transfer is delivered — to a location, a job site, or as a direct/counter sale; `data.lines` lists item ids and quantities.",
        "description": "Outbound: Flex sends this request to your endpoint; you do not call it. Subscriptions are created in Flex under Settings → Integrations → Webhooks: a public https URL, the events to receive, and a signing secret shown once. Every delivery is an HTTP POST with `Content-Type: application/json` and the envelope `{ \"event\", \"occurredAt\", \"organizationId\", \"data\" }` (`occurredAt` is ISO-8601 UTC; `data` is the compact summary documented per event, camelCase, nulls omitted). Headers on every attempt: `X-Flex-Timestamp` (unix seconds when this attempt was signed), `X-Flex-Signature-V2: t=<same unix seconds>,v1=<lowercase hex>` (HMAC-SHA256, keyed with the subscription secret, of the string `<t>.<raw request body>`), `X-Flex-Delivery` (an opaque delivery id, identical on every retry of the same delivery), `X-Flex-Event` (the event name), and the legacy `X-Flex-Signature: sha256=<lowercase hex>` (HMAC-SHA256 of the raw body only, kept unchanged for existing integrations). Recommended verification (replay-safe): recompute the `X-Flex-Signature-V2` HMAC over the received bytes before parsing, compare in constant time, reject the request if `t` is more than 300 s (5 minutes) away from your clock, and ignore a `X-Flex-Delivery` you have already processed. Retries are re-signed with a fresh timestamp but keep the same `X-Flex-Delivery`. Any 2xx acknowledges the delivery; anything else, or no answer within 10 s, is retried with exponential backoff up to 5 attempts. After 20 consecutive failed deliveries the subscription is paused (shown inactive) until it is re-enabled. Redirects are never followed and private or link-local addresses are refused. Every attempt (status, timing, response) is listed in the delivery log in Settings.",
        "operationId": "webhook_transfer_completed",
        "parameters": [
          {
            "name": "X-Flex-Signature",
            "in": "header",
            "description": "`sha256=<lowercase hex>` — HMAC-SHA256 of the raw request body keyed with the subscription secret. Legacy (no replay protection); prefer `X-Flex-Signature-V2`.",
            "required": true,
            "schema": {
              "type": "string",
              "example": "sha256=3f7a…c21e"
            }
          },
          {
            "name": "X-Flex-Signature-V2",
            "in": "header",
            "description": "`t=<unix seconds>,v1=<lowercase hex>` — HMAC-SHA256, keyed with the subscription secret, of the string `<t>.<raw request body>`. Reject the delivery when `t` is more than 300 s from your clock. Re-signed with a fresh `t` on every retry.",
            "required": true,
            "schema": {
              "type": "string",
              "example": "t=1790000000,v1=9b2e…4d10"
            }
          },
          {
            "name": "X-Flex-Timestamp",
            "in": "header",
            "description": "Unix time in seconds (UTC) when this attempt was signed; equal to `t` in `X-Flex-Signature-V2`.",
            "required": true,
            "schema": {
              "type": "string",
              "example": "1790000000"
            }
          },
          {
            "name": "X-Flex-Delivery",
            "in": "header",
            "description": "Opaque delivery id, the same on every retry of one delivery — store it (e.g. 24 h) and ignore repeats. Unique per subscription; do not parse it.",
            "required": true,
            "schema": {
              "type": "string",
              "example": "48213"
            }
          },
          {
            "name": "X-Flex-Event",
            "in": "header",
            "description": "The event name, identical to the envelope's `event` (handy for routing before parsing the body).",
            "required": true,
            "schema": {
              "type": "string",
              "example": "transfer.completed"
            }
          }
        ],
        "requestBody": {
          "description": "The signed JSON envelope.",
          "content": {
            "application/json": {
              "schema": {
                "required": [
                  "event",
                  "occurredAt",
                  "organizationId",
                  "data"
                ],
                "type": "object",
                "properties": {
                  "event": {
                    "enum": [
                      "transfer.completed"
                    ],
                    "type": "string",
                    "description": "Event name."
                  },
                  "occurredAt": {
                    "type": "string",
                    "description": "When the event occurred (ISO-8601 UTC).",
                    "format": "date-time"
                  },
                  "organizationId": {
                    "type": "integer",
                    "description": "The organization the subscription belongs to.",
                    "format": "int32"
                  },
                  "data": {
                    "$ref": "#/components/schemas/TransferWebhookData"
                  }
                },
                "description": "Signed envelope Flex POSTs to the subscribed URL for `transfer.completed`."
              }
            }
          },
          "required": true
        },
        "responses": {
          "2XX": {
            "description": "Delivery acknowledged. Any other status, or a timeout, is retried."
          }
        }
      }
    },
    "transfer.created": {
      "post": {
        "tags": [
          "Webhooks"
        ],
        "summary": "An inventory transfer is created — a job-site transfer (API, MCP, app or synced device), a standalone transfer, or a direct/counter sale (which also sends `transfer.completed`). Sent once its lines are saved; `data.lines` lists the reserved item ids and quantities.",
        "description": "Outbound: Flex sends this request to your endpoint; you do not call it. Subscriptions are created in Flex under Settings → Integrations → Webhooks: a public https URL, the events to receive, and a signing secret shown once. Every delivery is an HTTP POST with `Content-Type: application/json` and the envelope `{ \"event\", \"occurredAt\", \"organizationId\", \"data\" }` (`occurredAt` is ISO-8601 UTC; `data` is the compact summary documented per event, camelCase, nulls omitted). Headers on every attempt: `X-Flex-Timestamp` (unix seconds when this attempt was signed), `X-Flex-Signature-V2: t=<same unix seconds>,v1=<lowercase hex>` (HMAC-SHA256, keyed with the subscription secret, of the string `<t>.<raw request body>`), `X-Flex-Delivery` (an opaque delivery id, identical on every retry of the same delivery), `X-Flex-Event` (the event name), and the legacy `X-Flex-Signature: sha256=<lowercase hex>` (HMAC-SHA256 of the raw body only, kept unchanged for existing integrations). Recommended verification (replay-safe): recompute the `X-Flex-Signature-V2` HMAC over the received bytes before parsing, compare in constant time, reject the request if `t` is more than 300 s (5 minutes) away from your clock, and ignore a `X-Flex-Delivery` you have already processed. Retries are re-signed with a fresh timestamp but keep the same `X-Flex-Delivery`. Any 2xx acknowledges the delivery; anything else, or no answer within 10 s, is retried with exponential backoff up to 5 attempts. After 20 consecutive failed deliveries the subscription is paused (shown inactive) until it is re-enabled. Redirects are never followed and private or link-local addresses are refused. Every attempt (status, timing, response) is listed in the delivery log in Settings.",
        "operationId": "webhook_transfer_created",
        "parameters": [
          {
            "name": "X-Flex-Signature",
            "in": "header",
            "description": "`sha256=<lowercase hex>` — HMAC-SHA256 of the raw request body keyed with the subscription secret. Legacy (no replay protection); prefer `X-Flex-Signature-V2`.",
            "required": true,
            "schema": {
              "type": "string",
              "example": "sha256=3f7a…c21e"
            }
          },
          {
            "name": "X-Flex-Signature-V2",
            "in": "header",
            "description": "`t=<unix seconds>,v1=<lowercase hex>` — HMAC-SHA256, keyed with the subscription secret, of the string `<t>.<raw request body>`. Reject the delivery when `t` is more than 300 s from your clock. Re-signed with a fresh `t` on every retry.",
            "required": true,
            "schema": {
              "type": "string",
              "example": "t=1790000000,v1=9b2e…4d10"
            }
          },
          {
            "name": "X-Flex-Timestamp",
            "in": "header",
            "description": "Unix time in seconds (UTC) when this attempt was signed; equal to `t` in `X-Flex-Signature-V2`.",
            "required": true,
            "schema": {
              "type": "string",
              "example": "1790000000"
            }
          },
          {
            "name": "X-Flex-Delivery",
            "in": "header",
            "description": "Opaque delivery id, the same on every retry of one delivery — store it (e.g. 24 h) and ignore repeats. Unique per subscription; do not parse it.",
            "required": true,
            "schema": {
              "type": "string",
              "example": "48213"
            }
          },
          {
            "name": "X-Flex-Event",
            "in": "header",
            "description": "The event name, identical to the envelope's `event` (handy for routing before parsing the body).",
            "required": true,
            "schema": {
              "type": "string",
              "example": "transfer.created"
            }
          }
        ],
        "requestBody": {
          "description": "The signed JSON envelope.",
          "content": {
            "application/json": {
              "schema": {
                "required": [
                  "event",
                  "occurredAt",
                  "organizationId",
                  "data"
                ],
                "type": "object",
                "properties": {
                  "event": {
                    "enum": [
                      "transfer.created"
                    ],
                    "type": "string",
                    "description": "Event name."
                  },
                  "occurredAt": {
                    "type": "string",
                    "description": "When the event occurred (ISO-8601 UTC).",
                    "format": "date-time"
                  },
                  "organizationId": {
                    "type": "integer",
                    "description": "The organization the subscription belongs to.",
                    "format": "int32"
                  },
                  "data": {
                    "$ref": "#/components/schemas/TransferWebhookData"
                  }
                },
                "description": "Signed envelope Flex POSTs to the subscribed URL for `transfer.created`."
              }
            }
          },
          "required": true
        },
        "responses": {
          "2XX": {
            "description": "Delivery acknowledged. Any other status, or a timeout, is retried."
          }
        }
      }
    },
    "transfer.cancelled": {
      "post": {
        "tags": [
          "Webhooks"
        ],
        "summary": "An inventory transfer is cancelled — from a job, the Transfers pages or the API, or because its approval was rejected.",
        "description": "Outbound: Flex sends this request to your endpoint; you do not call it. Subscriptions are created in Flex under Settings → Integrations → Webhooks: a public https URL, the events to receive, and a signing secret shown once. Every delivery is an HTTP POST with `Content-Type: application/json` and the envelope `{ \"event\", \"occurredAt\", \"organizationId\", \"data\" }` (`occurredAt` is ISO-8601 UTC; `data` is the compact summary documented per event, camelCase, nulls omitted). Headers on every attempt: `X-Flex-Timestamp` (unix seconds when this attempt was signed), `X-Flex-Signature-V2: t=<same unix seconds>,v1=<lowercase hex>` (HMAC-SHA256, keyed with the subscription secret, of the string `<t>.<raw request body>`), `X-Flex-Delivery` (an opaque delivery id, identical on every retry of the same delivery), `X-Flex-Event` (the event name), and the legacy `X-Flex-Signature: sha256=<lowercase hex>` (HMAC-SHA256 of the raw body only, kept unchanged for existing integrations). Recommended verification (replay-safe): recompute the `X-Flex-Signature-V2` HMAC over the received bytes before parsing, compare in constant time, reject the request if `t` is more than 300 s (5 minutes) away from your clock, and ignore a `X-Flex-Delivery` you have already processed. Retries are re-signed with a fresh timestamp but keep the same `X-Flex-Delivery`. Any 2xx acknowledges the delivery; anything else, or no answer within 10 s, is retried with exponential backoff up to 5 attempts. After 20 consecutive failed deliveries the subscription is paused (shown inactive) until it is re-enabled. Redirects are never followed and private or link-local addresses are refused. Every attempt (status, timing, response) is listed in the delivery log in Settings.",
        "operationId": "webhook_transfer_cancelled",
        "parameters": [
          {
            "name": "X-Flex-Signature",
            "in": "header",
            "description": "`sha256=<lowercase hex>` — HMAC-SHA256 of the raw request body keyed with the subscription secret. Legacy (no replay protection); prefer `X-Flex-Signature-V2`.",
            "required": true,
            "schema": {
              "type": "string",
              "example": "sha256=3f7a…c21e"
            }
          },
          {
            "name": "X-Flex-Signature-V2",
            "in": "header",
            "description": "`t=<unix seconds>,v1=<lowercase hex>` — HMAC-SHA256, keyed with the subscription secret, of the string `<t>.<raw request body>`. Reject the delivery when `t` is more than 300 s from your clock. Re-signed with a fresh `t` on every retry.",
            "required": true,
            "schema": {
              "type": "string",
              "example": "t=1790000000,v1=9b2e…4d10"
            }
          },
          {
            "name": "X-Flex-Timestamp",
            "in": "header",
            "description": "Unix time in seconds (UTC) when this attempt was signed; equal to `t` in `X-Flex-Signature-V2`.",
            "required": true,
            "schema": {
              "type": "string",
              "example": "1790000000"
            }
          },
          {
            "name": "X-Flex-Delivery",
            "in": "header",
            "description": "Opaque delivery id, the same on every retry of one delivery — store it (e.g. 24 h) and ignore repeats. Unique per subscription; do not parse it.",
            "required": true,
            "schema": {
              "type": "string",
              "example": "48213"
            }
          },
          {
            "name": "X-Flex-Event",
            "in": "header",
            "description": "The event name, identical to the envelope's `event` (handy for routing before parsing the body).",
            "required": true,
            "schema": {
              "type": "string",
              "example": "transfer.cancelled"
            }
          }
        ],
        "requestBody": {
          "description": "The signed JSON envelope.",
          "content": {
            "application/json": {
              "schema": {
                "required": [
                  "event",
                  "occurredAt",
                  "organizationId",
                  "data"
                ],
                "type": "object",
                "properties": {
                  "event": {
                    "enum": [
                      "transfer.cancelled"
                    ],
                    "type": "string",
                    "description": "Event name."
                  },
                  "occurredAt": {
                    "type": "string",
                    "description": "When the event occurred (ISO-8601 UTC).",
                    "format": "date-time"
                  },
                  "organizationId": {
                    "type": "integer",
                    "description": "The organization the subscription belongs to.",
                    "format": "int32"
                  },
                  "data": {
                    "$ref": "#/components/schemas/TransferWebhookData"
                  }
                },
                "description": "Signed envelope Flex POSTs to the subscribed URL for `transfer.cancelled`."
              }
            }
          },
          "required": true
        },
        "responses": {
          "2XX": {
            "description": "Delivery acknowledged. Any other status, or a timeout, is retried."
          }
        }
      }
    },
    "inventory.low_stock": {
      "post": {
        "tags": [
          "Webhooks"
        ],
        "summary": "Stock for an item drops below its configured minimum and a new low-stock alert is raised (once per alert, not per unit).",
        "description": "Outbound: Flex sends this request to your endpoint; you do not call it. Subscriptions are created in Flex under Settings → Integrations → Webhooks: a public https URL, the events to receive, and a signing secret shown once. Every delivery is an HTTP POST with `Content-Type: application/json` and the envelope `{ \"event\", \"occurredAt\", \"organizationId\", \"data\" }` (`occurredAt` is ISO-8601 UTC; `data` is the compact summary documented per event, camelCase, nulls omitted). Headers on every attempt: `X-Flex-Timestamp` (unix seconds when this attempt was signed), `X-Flex-Signature-V2: t=<same unix seconds>,v1=<lowercase hex>` (HMAC-SHA256, keyed with the subscription secret, of the string `<t>.<raw request body>`), `X-Flex-Delivery` (an opaque delivery id, identical on every retry of the same delivery), `X-Flex-Event` (the event name), and the legacy `X-Flex-Signature: sha256=<lowercase hex>` (HMAC-SHA256 of the raw body only, kept unchanged for existing integrations). Recommended verification (replay-safe): recompute the `X-Flex-Signature-V2` HMAC over the received bytes before parsing, compare in constant time, reject the request if `t` is more than 300 s (5 minutes) away from your clock, and ignore a `X-Flex-Delivery` you have already processed. Retries are re-signed with a fresh timestamp but keep the same `X-Flex-Delivery`. Any 2xx acknowledges the delivery; anything else, or no answer within 10 s, is retried with exponential backoff up to 5 attempts. After 20 consecutive failed deliveries the subscription is paused (shown inactive) until it is re-enabled. Redirects are never followed and private or link-local addresses are refused. Every attempt (status, timing, response) is listed in the delivery log in Settings.",
        "operationId": "webhook_inventory_low_stock",
        "parameters": [
          {
            "name": "X-Flex-Signature",
            "in": "header",
            "description": "`sha256=<lowercase hex>` — HMAC-SHA256 of the raw request body keyed with the subscription secret. Legacy (no replay protection); prefer `X-Flex-Signature-V2`.",
            "required": true,
            "schema": {
              "type": "string",
              "example": "sha256=3f7a…c21e"
            }
          },
          {
            "name": "X-Flex-Signature-V2",
            "in": "header",
            "description": "`t=<unix seconds>,v1=<lowercase hex>` — HMAC-SHA256, keyed with the subscription secret, of the string `<t>.<raw request body>`. Reject the delivery when `t` is more than 300 s from your clock. Re-signed with a fresh `t` on every retry.",
            "required": true,
            "schema": {
              "type": "string",
              "example": "t=1790000000,v1=9b2e…4d10"
            }
          },
          {
            "name": "X-Flex-Timestamp",
            "in": "header",
            "description": "Unix time in seconds (UTC) when this attempt was signed; equal to `t` in `X-Flex-Signature-V2`.",
            "required": true,
            "schema": {
              "type": "string",
              "example": "1790000000"
            }
          },
          {
            "name": "X-Flex-Delivery",
            "in": "header",
            "description": "Opaque delivery id, the same on every retry of one delivery — store it (e.g. 24 h) and ignore repeats. Unique per subscription; do not parse it.",
            "required": true,
            "schema": {
              "type": "string",
              "example": "48213"
            }
          },
          {
            "name": "X-Flex-Event",
            "in": "header",
            "description": "The event name, identical to the envelope's `event` (handy for routing before parsing the body).",
            "required": true,
            "schema": {
              "type": "string",
              "example": "inventory.low_stock"
            }
          }
        ],
        "requestBody": {
          "description": "The signed JSON envelope.",
          "content": {
            "application/json": {
              "schema": {
                "required": [
                  "event",
                  "occurredAt",
                  "organizationId",
                  "data"
                ],
                "type": "object",
                "properties": {
                  "event": {
                    "enum": [
                      "inventory.low_stock"
                    ],
                    "type": "string",
                    "description": "Event name."
                  },
                  "occurredAt": {
                    "type": "string",
                    "description": "When the event occurred (ISO-8601 UTC).",
                    "format": "date-time"
                  },
                  "organizationId": {
                    "type": "integer",
                    "description": "The organization the subscription belongs to.",
                    "format": "int32"
                  },
                  "data": {
                    "$ref": "#/components/schemas/StockLowWebhookData"
                  }
                },
                "description": "Signed envelope Flex POSTs to the subscribed URL for `inventory.low_stock`."
              }
            }
          },
          "required": true
        },
        "responses": {
          "2XX": {
            "description": "Delivery acknowledged. Any other status, or a timeout, is retried."
          }
        }
      }
    },
    "item.updated": {
      "post": {
        "tags": [
          "Webhooks"
        ],
        "summary": "An existing item's name, description, category, preferred vendor or vendor part number, unit price, cost or custom fields change (on the item or on its inventory units), or it is deleted/restored; `data.changedFields` says which, `data.changedProperties` / `data.changedUnitProperties` name the custom fields. One event per item per save.",
        "description": "Outbound: Flex sends this request to your endpoint; you do not call it. Subscriptions are created in Flex under Settings → Integrations → Webhooks: a public https URL, the events to receive, and a signing secret shown once. Every delivery is an HTTP POST with `Content-Type: application/json` and the envelope `{ \"event\", \"occurredAt\", \"organizationId\", \"data\" }` (`occurredAt` is ISO-8601 UTC; `data` is the compact summary documented per event, camelCase, nulls omitted). Headers on every attempt: `X-Flex-Timestamp` (unix seconds when this attempt was signed), `X-Flex-Signature-V2: t=<same unix seconds>,v1=<lowercase hex>` (HMAC-SHA256, keyed with the subscription secret, of the string `<t>.<raw request body>`), `X-Flex-Delivery` (an opaque delivery id, identical on every retry of the same delivery), `X-Flex-Event` (the event name), and the legacy `X-Flex-Signature: sha256=<lowercase hex>` (HMAC-SHA256 of the raw body only, kept unchanged for existing integrations). Recommended verification (replay-safe): recompute the `X-Flex-Signature-V2` HMAC over the received bytes before parsing, compare in constant time, reject the request if `t` is more than 300 s (5 minutes) away from your clock, and ignore a `X-Flex-Delivery` you have already processed. Retries are re-signed with a fresh timestamp but keep the same `X-Flex-Delivery`. Any 2xx acknowledges the delivery; anything else, or no answer within 10 s, is retried with exponential backoff up to 5 attempts. After 20 consecutive failed deliveries the subscription is paused (shown inactive) until it is re-enabled. Redirects are never followed and private or link-local addresses are refused. Every attempt (status, timing, response) is listed in the delivery log in Settings.",
        "operationId": "webhook_item_updated",
        "parameters": [
          {
            "name": "X-Flex-Signature",
            "in": "header",
            "description": "`sha256=<lowercase hex>` — HMAC-SHA256 of the raw request body keyed with the subscription secret. Legacy (no replay protection); prefer `X-Flex-Signature-V2`.",
            "required": true,
            "schema": {
              "type": "string",
              "example": "sha256=3f7a…c21e"
            }
          },
          {
            "name": "X-Flex-Signature-V2",
            "in": "header",
            "description": "`t=<unix seconds>,v1=<lowercase hex>` — HMAC-SHA256, keyed with the subscription secret, of the string `<t>.<raw request body>`. Reject the delivery when `t` is more than 300 s from your clock. Re-signed with a fresh `t` on every retry.",
            "required": true,
            "schema": {
              "type": "string",
              "example": "t=1790000000,v1=9b2e…4d10"
            }
          },
          {
            "name": "X-Flex-Timestamp",
            "in": "header",
            "description": "Unix time in seconds (UTC) when this attempt was signed; equal to `t` in `X-Flex-Signature-V2`.",
            "required": true,
            "schema": {
              "type": "string",
              "example": "1790000000"
            }
          },
          {
            "name": "X-Flex-Delivery",
            "in": "header",
            "description": "Opaque delivery id, the same on every retry of one delivery — store it (e.g. 24 h) and ignore repeats. Unique per subscription; do not parse it.",
            "required": true,
            "schema": {
              "type": "string",
              "example": "48213"
            }
          },
          {
            "name": "X-Flex-Event",
            "in": "header",
            "description": "The event name, identical to the envelope's `event` (handy for routing before parsing the body).",
            "required": true,
            "schema": {
              "type": "string",
              "example": "item.updated"
            }
          }
        ],
        "requestBody": {
          "description": "The signed JSON envelope.",
          "content": {
            "application/json": {
              "schema": {
                "required": [
                  "event",
                  "occurredAt",
                  "organizationId",
                  "data"
                ],
                "type": "object",
                "properties": {
                  "event": {
                    "enum": [
                      "item.updated"
                    ],
                    "type": "string",
                    "description": "Event name."
                  },
                  "occurredAt": {
                    "type": "string",
                    "description": "When the event occurred (ISO-8601 UTC).",
                    "format": "date-time"
                  },
                  "organizationId": {
                    "type": "integer",
                    "description": "The organization the subscription belongs to.",
                    "format": "int32"
                  },
                  "data": {
                    "$ref": "#/components/schemas/ItemWebhookData"
                  }
                },
                "description": "Signed envelope Flex POSTs to the subscribed URL for `item.updated`."
              }
            }
          },
          "required": true
        },
        "responses": {
          "2XX": {
            "description": "Delivery acknowledged. Any other status, or a timeout, is retried."
          }
        }
      }
    },
    "purchase_order.created": {
      "post": {
        "tags": [
          "Webhooks"
        ],
        "summary": "A purchase order is created (any status, including one entered already received).",
        "description": "Outbound: Flex sends this request to your endpoint; you do not call it. Subscriptions are created in Flex under Settings → Integrations → Webhooks: a public https URL, the events to receive, and a signing secret shown once. Every delivery is an HTTP POST with `Content-Type: application/json` and the envelope `{ \"event\", \"occurredAt\", \"organizationId\", \"data\" }` (`occurredAt` is ISO-8601 UTC; `data` is the compact summary documented per event, camelCase, nulls omitted). Headers on every attempt: `X-Flex-Timestamp` (unix seconds when this attempt was signed), `X-Flex-Signature-V2: t=<same unix seconds>,v1=<lowercase hex>` (HMAC-SHA256, keyed with the subscription secret, of the string `<t>.<raw request body>`), `X-Flex-Delivery` (an opaque delivery id, identical on every retry of the same delivery), `X-Flex-Event` (the event name), and the legacy `X-Flex-Signature: sha256=<lowercase hex>` (HMAC-SHA256 of the raw body only, kept unchanged for existing integrations). Recommended verification (replay-safe): recompute the `X-Flex-Signature-V2` HMAC over the received bytes before parsing, compare in constant time, reject the request if `t` is more than 300 s (5 minutes) away from your clock, and ignore a `X-Flex-Delivery` you have already processed. Retries are re-signed with a fresh timestamp but keep the same `X-Flex-Delivery`. Any 2xx acknowledges the delivery; anything else, or no answer within 10 s, is retried with exponential backoff up to 5 attempts. After 20 consecutive failed deliveries the subscription is paused (shown inactive) until it is re-enabled. Redirects are never followed and private or link-local addresses are refused. Every attempt (status, timing, response) is listed in the delivery log in Settings.",
        "operationId": "webhook_purchase_order_created",
        "parameters": [
          {
            "name": "X-Flex-Signature",
            "in": "header",
            "description": "`sha256=<lowercase hex>` — HMAC-SHA256 of the raw request body keyed with the subscription secret. Legacy (no replay protection); prefer `X-Flex-Signature-V2`.",
            "required": true,
            "schema": {
              "type": "string",
              "example": "sha256=3f7a…c21e"
            }
          },
          {
            "name": "X-Flex-Signature-V2",
            "in": "header",
            "description": "`t=<unix seconds>,v1=<lowercase hex>` — HMAC-SHA256, keyed with the subscription secret, of the string `<t>.<raw request body>`. Reject the delivery when `t` is more than 300 s from your clock. Re-signed with a fresh `t` on every retry.",
            "required": true,
            "schema": {
              "type": "string",
              "example": "t=1790000000,v1=9b2e…4d10"
            }
          },
          {
            "name": "X-Flex-Timestamp",
            "in": "header",
            "description": "Unix time in seconds (UTC) when this attempt was signed; equal to `t` in `X-Flex-Signature-V2`.",
            "required": true,
            "schema": {
              "type": "string",
              "example": "1790000000"
            }
          },
          {
            "name": "X-Flex-Delivery",
            "in": "header",
            "description": "Opaque delivery id, the same on every retry of one delivery — store it (e.g. 24 h) and ignore repeats. Unique per subscription; do not parse it.",
            "required": true,
            "schema": {
              "type": "string",
              "example": "48213"
            }
          },
          {
            "name": "X-Flex-Event",
            "in": "header",
            "description": "The event name, identical to the envelope's `event` (handy for routing before parsing the body).",
            "required": true,
            "schema": {
              "type": "string",
              "example": "purchase_order.created"
            }
          }
        ],
        "requestBody": {
          "description": "The signed JSON envelope.",
          "content": {
            "application/json": {
              "schema": {
                "required": [
                  "event",
                  "occurredAt",
                  "organizationId",
                  "data"
                ],
                "type": "object",
                "properties": {
                  "event": {
                    "enum": [
                      "purchase_order.created"
                    ],
                    "type": "string",
                    "description": "Event name."
                  },
                  "occurredAt": {
                    "type": "string",
                    "description": "When the event occurred (ISO-8601 UTC).",
                    "format": "date-time"
                  },
                  "organizationId": {
                    "type": "integer",
                    "description": "The organization the subscription belongs to.",
                    "format": "int32"
                  },
                  "data": {
                    "$ref": "#/components/schemas/PurchaseOrderWebhookData"
                  }
                },
                "description": "Signed envelope Flex POSTs to the subscribed URL for `purchase_order.created`."
              }
            }
          },
          "required": true
        },
        "responses": {
          "2XX": {
            "description": "Delivery acknowledged. Any other status, or a timeout, is retried."
          }
        }
      }
    },
    "purchase_order.status_changed": {
      "post": {
        "tags": [
          "Webhooks"
        ],
        "summary": "A purchase order moves to another status (Draft → Ordered → Shipped → Received, or Cancelled); `data.previousStatus` is the status it left.",
        "description": "Outbound: Flex sends this request to your endpoint; you do not call it. Subscriptions are created in Flex under Settings → Integrations → Webhooks: a public https URL, the events to receive, and a signing secret shown once. Every delivery is an HTTP POST with `Content-Type: application/json` and the envelope `{ \"event\", \"occurredAt\", \"organizationId\", \"data\" }` (`occurredAt` is ISO-8601 UTC; `data` is the compact summary documented per event, camelCase, nulls omitted). Headers on every attempt: `X-Flex-Timestamp` (unix seconds when this attempt was signed), `X-Flex-Signature-V2: t=<same unix seconds>,v1=<lowercase hex>` (HMAC-SHA256, keyed with the subscription secret, of the string `<t>.<raw request body>`), `X-Flex-Delivery` (an opaque delivery id, identical on every retry of the same delivery), `X-Flex-Event` (the event name), and the legacy `X-Flex-Signature: sha256=<lowercase hex>` (HMAC-SHA256 of the raw body only, kept unchanged for existing integrations). Recommended verification (replay-safe): recompute the `X-Flex-Signature-V2` HMAC over the received bytes before parsing, compare in constant time, reject the request if `t` is more than 300 s (5 minutes) away from your clock, and ignore a `X-Flex-Delivery` you have already processed. Retries are re-signed with a fresh timestamp but keep the same `X-Flex-Delivery`. Any 2xx acknowledges the delivery; anything else, or no answer within 10 s, is retried with exponential backoff up to 5 attempts. After 20 consecutive failed deliveries the subscription is paused (shown inactive) until it is re-enabled. Redirects are never followed and private or link-local addresses are refused. Every attempt (status, timing, response) is listed in the delivery log in Settings.",
        "operationId": "webhook_purchase_order_status_changed",
        "parameters": [
          {
            "name": "X-Flex-Signature",
            "in": "header",
            "description": "`sha256=<lowercase hex>` — HMAC-SHA256 of the raw request body keyed with the subscription secret. Legacy (no replay protection); prefer `X-Flex-Signature-V2`.",
            "required": true,
            "schema": {
              "type": "string",
              "example": "sha256=3f7a…c21e"
            }
          },
          {
            "name": "X-Flex-Signature-V2",
            "in": "header",
            "description": "`t=<unix seconds>,v1=<lowercase hex>` — HMAC-SHA256, keyed with the subscription secret, of the string `<t>.<raw request body>`. Reject the delivery when `t` is more than 300 s from your clock. Re-signed with a fresh `t` on every retry.",
            "required": true,
            "schema": {
              "type": "string",
              "example": "t=1790000000,v1=9b2e…4d10"
            }
          },
          {
            "name": "X-Flex-Timestamp",
            "in": "header",
            "description": "Unix time in seconds (UTC) when this attempt was signed; equal to `t` in `X-Flex-Signature-V2`.",
            "required": true,
            "schema": {
              "type": "string",
              "example": "1790000000"
            }
          },
          {
            "name": "X-Flex-Delivery",
            "in": "header",
            "description": "Opaque delivery id, the same on every retry of one delivery — store it (e.g. 24 h) and ignore repeats. Unique per subscription; do not parse it.",
            "required": true,
            "schema": {
              "type": "string",
              "example": "48213"
            }
          },
          {
            "name": "X-Flex-Event",
            "in": "header",
            "description": "The event name, identical to the envelope's `event` (handy for routing before parsing the body).",
            "required": true,
            "schema": {
              "type": "string",
              "example": "purchase_order.status_changed"
            }
          }
        ],
        "requestBody": {
          "description": "The signed JSON envelope.",
          "content": {
            "application/json": {
              "schema": {
                "required": [
                  "event",
                  "occurredAt",
                  "organizationId",
                  "data"
                ],
                "type": "object",
                "properties": {
                  "event": {
                    "enum": [
                      "purchase_order.status_changed"
                    ],
                    "type": "string",
                    "description": "Event name."
                  },
                  "occurredAt": {
                    "type": "string",
                    "description": "When the event occurred (ISO-8601 UTC).",
                    "format": "date-time"
                  },
                  "organizationId": {
                    "type": "integer",
                    "description": "The organization the subscription belongs to.",
                    "format": "int32"
                  },
                  "data": {
                    "$ref": "#/components/schemas/PurchaseOrderWebhookData"
                  }
                },
                "description": "Signed envelope Flex POSTs to the subscribed URL for `purchase_order.status_changed`."
              }
            }
          },
          "required": true
        },
        "responses": {
          "2XX": {
            "description": "Delivery acknowledged. Any other status, or a timeout, is retried."
          }
        }
      }
    },
    "sales_order.created": {
      "post": {
        "tags": [
          "Webhooks"
        ],
        "summary": "A sales order is created.",
        "description": "Outbound: Flex sends this request to your endpoint; you do not call it. Subscriptions are created in Flex under Settings → Integrations → Webhooks: a public https URL, the events to receive, and a signing secret shown once. Every delivery is an HTTP POST with `Content-Type: application/json` and the envelope `{ \"event\", \"occurredAt\", \"organizationId\", \"data\" }` (`occurredAt` is ISO-8601 UTC; `data` is the compact summary documented per event, camelCase, nulls omitted). Headers on every attempt: `X-Flex-Timestamp` (unix seconds when this attempt was signed), `X-Flex-Signature-V2: t=<same unix seconds>,v1=<lowercase hex>` (HMAC-SHA256, keyed with the subscription secret, of the string `<t>.<raw request body>`), `X-Flex-Delivery` (an opaque delivery id, identical on every retry of the same delivery), `X-Flex-Event` (the event name), and the legacy `X-Flex-Signature: sha256=<lowercase hex>` (HMAC-SHA256 of the raw body only, kept unchanged for existing integrations). Recommended verification (replay-safe): recompute the `X-Flex-Signature-V2` HMAC over the received bytes before parsing, compare in constant time, reject the request if `t` is more than 300 s (5 minutes) away from your clock, and ignore a `X-Flex-Delivery` you have already processed. Retries are re-signed with a fresh timestamp but keep the same `X-Flex-Delivery`. Any 2xx acknowledges the delivery; anything else, or no answer within 10 s, is retried with exponential backoff up to 5 attempts. After 20 consecutive failed deliveries the subscription is paused (shown inactive) until it is re-enabled. Redirects are never followed and private or link-local addresses are refused. Every attempt (status, timing, response) is listed in the delivery log in Settings.",
        "operationId": "webhook_sales_order_created",
        "parameters": [
          {
            "name": "X-Flex-Signature",
            "in": "header",
            "description": "`sha256=<lowercase hex>` — HMAC-SHA256 of the raw request body keyed with the subscription secret. Legacy (no replay protection); prefer `X-Flex-Signature-V2`.",
            "required": true,
            "schema": {
              "type": "string",
              "example": "sha256=3f7a…c21e"
            }
          },
          {
            "name": "X-Flex-Signature-V2",
            "in": "header",
            "description": "`t=<unix seconds>,v1=<lowercase hex>` — HMAC-SHA256, keyed with the subscription secret, of the string `<t>.<raw request body>`. Reject the delivery when `t` is more than 300 s from your clock. Re-signed with a fresh `t` on every retry.",
            "required": true,
            "schema": {
              "type": "string",
              "example": "t=1790000000,v1=9b2e…4d10"
            }
          },
          {
            "name": "X-Flex-Timestamp",
            "in": "header",
            "description": "Unix time in seconds (UTC) when this attempt was signed; equal to `t` in `X-Flex-Signature-V2`.",
            "required": true,
            "schema": {
              "type": "string",
              "example": "1790000000"
            }
          },
          {
            "name": "X-Flex-Delivery",
            "in": "header",
            "description": "Opaque delivery id, the same on every retry of one delivery — store it (e.g. 24 h) and ignore repeats. Unique per subscription; do not parse it.",
            "required": true,
            "schema": {
              "type": "string",
              "example": "48213"
            }
          },
          {
            "name": "X-Flex-Event",
            "in": "header",
            "description": "The event name, identical to the envelope's `event` (handy for routing before parsing the body).",
            "required": true,
            "schema": {
              "type": "string",
              "example": "sales_order.created"
            }
          }
        ],
        "requestBody": {
          "description": "The signed JSON envelope.",
          "content": {
            "application/json": {
              "schema": {
                "required": [
                  "event",
                  "occurredAt",
                  "organizationId",
                  "data"
                ],
                "type": "object",
                "properties": {
                  "event": {
                    "enum": [
                      "sales_order.created"
                    ],
                    "type": "string",
                    "description": "Event name."
                  },
                  "occurredAt": {
                    "type": "string",
                    "description": "When the event occurred (ISO-8601 UTC).",
                    "format": "date-time"
                  },
                  "organizationId": {
                    "type": "integer",
                    "description": "The organization the subscription belongs to.",
                    "format": "int32"
                  },
                  "data": {
                    "$ref": "#/components/schemas/SalesOrderWebhookData"
                  }
                },
                "description": "Signed envelope Flex POSTs to the subscribed URL for `sales_order.created`."
              }
            }
          },
          "required": true
        },
        "responses": {
          "2XX": {
            "description": "Delivery acknowledged. Any other status, or a timeout, is retried."
          }
        }
      }
    },
    "sales_order.status_changed": {
      "post": {
        "tags": [
          "Webhooks"
        ],
        "summary": "A sales order moves to another status (Pending → Picking → Packed → Shipped → Delivered, Complete or Cancelled); `data.previousStatus` is the status it left.",
        "description": "Outbound: Flex sends this request to your endpoint; you do not call it. Subscriptions are created in Flex under Settings → Integrations → Webhooks: a public https URL, the events to receive, and a signing secret shown once. Every delivery is an HTTP POST with `Content-Type: application/json` and the envelope `{ \"event\", \"occurredAt\", \"organizationId\", \"data\" }` (`occurredAt` is ISO-8601 UTC; `data` is the compact summary documented per event, camelCase, nulls omitted). Headers on every attempt: `X-Flex-Timestamp` (unix seconds when this attempt was signed), `X-Flex-Signature-V2: t=<same unix seconds>,v1=<lowercase hex>` (HMAC-SHA256, keyed with the subscription secret, of the string `<t>.<raw request body>`), `X-Flex-Delivery` (an opaque delivery id, identical on every retry of the same delivery), `X-Flex-Event` (the event name), and the legacy `X-Flex-Signature: sha256=<lowercase hex>` (HMAC-SHA256 of the raw body only, kept unchanged for existing integrations). Recommended verification (replay-safe): recompute the `X-Flex-Signature-V2` HMAC over the received bytes before parsing, compare in constant time, reject the request if `t` is more than 300 s (5 minutes) away from your clock, and ignore a `X-Flex-Delivery` you have already processed. Retries are re-signed with a fresh timestamp but keep the same `X-Flex-Delivery`. Any 2xx acknowledges the delivery; anything else, or no answer within 10 s, is retried with exponential backoff up to 5 attempts. After 20 consecutive failed deliveries the subscription is paused (shown inactive) until it is re-enabled. Redirects are never followed and private or link-local addresses are refused. Every attempt (status, timing, response) is listed in the delivery log in Settings.",
        "operationId": "webhook_sales_order_status_changed",
        "parameters": [
          {
            "name": "X-Flex-Signature",
            "in": "header",
            "description": "`sha256=<lowercase hex>` — HMAC-SHA256 of the raw request body keyed with the subscription secret. Legacy (no replay protection); prefer `X-Flex-Signature-V2`.",
            "required": true,
            "schema": {
              "type": "string",
              "example": "sha256=3f7a…c21e"
            }
          },
          {
            "name": "X-Flex-Signature-V2",
            "in": "header",
            "description": "`t=<unix seconds>,v1=<lowercase hex>` — HMAC-SHA256, keyed with the subscription secret, of the string `<t>.<raw request body>`. Reject the delivery when `t` is more than 300 s from your clock. Re-signed with a fresh `t` on every retry.",
            "required": true,
            "schema": {
              "type": "string",
              "example": "t=1790000000,v1=9b2e…4d10"
            }
          },
          {
            "name": "X-Flex-Timestamp",
            "in": "header",
            "description": "Unix time in seconds (UTC) when this attempt was signed; equal to `t` in `X-Flex-Signature-V2`.",
            "required": true,
            "schema": {
              "type": "string",
              "example": "1790000000"
            }
          },
          {
            "name": "X-Flex-Delivery",
            "in": "header",
            "description": "Opaque delivery id, the same on every retry of one delivery — store it (e.g. 24 h) and ignore repeats. Unique per subscription; do not parse it.",
            "required": true,
            "schema": {
              "type": "string",
              "example": "48213"
            }
          },
          {
            "name": "X-Flex-Event",
            "in": "header",
            "description": "The event name, identical to the envelope's `event` (handy for routing before parsing the body).",
            "required": true,
            "schema": {
              "type": "string",
              "example": "sales_order.status_changed"
            }
          }
        ],
        "requestBody": {
          "description": "The signed JSON envelope.",
          "content": {
            "application/json": {
              "schema": {
                "required": [
                  "event",
                  "occurredAt",
                  "organizationId",
                  "data"
                ],
                "type": "object",
                "properties": {
                  "event": {
                    "enum": [
                      "sales_order.status_changed"
                    ],
                    "type": "string",
                    "description": "Event name."
                  },
                  "occurredAt": {
                    "type": "string",
                    "description": "When the event occurred (ISO-8601 UTC).",
                    "format": "date-time"
                  },
                  "organizationId": {
                    "type": "integer",
                    "description": "The organization the subscription belongs to.",
                    "format": "int32"
                  },
                  "data": {
                    "$ref": "#/components/schemas/SalesOrderWebhookData"
                  }
                },
                "description": "Signed envelope Flex POSTs to the subscribed URL for `sales_order.status_changed`."
              }
            }
          },
          "required": true
        },
        "responses": {
          "2XX": {
            "description": "Delivery acknowledged. Any other status, or a timeout, is retried."
          }
        }
      }
    },
    "vendor.created": {
      "post": {
        "tags": [
          "Webhooks"
        ],
        "summary": "A vendor is created.",
        "description": "Outbound: Flex sends this request to your endpoint; you do not call it. Subscriptions are created in Flex under Settings → Integrations → Webhooks: a public https URL, the events to receive, and a signing secret shown once. Every delivery is an HTTP POST with `Content-Type: application/json` and the envelope `{ \"event\", \"occurredAt\", \"organizationId\", \"data\" }` (`occurredAt` is ISO-8601 UTC; `data` is the compact summary documented per event, camelCase, nulls omitted). Headers on every attempt: `X-Flex-Timestamp` (unix seconds when this attempt was signed), `X-Flex-Signature-V2: t=<same unix seconds>,v1=<lowercase hex>` (HMAC-SHA256, keyed with the subscription secret, of the string `<t>.<raw request body>`), `X-Flex-Delivery` (an opaque delivery id, identical on every retry of the same delivery), `X-Flex-Event` (the event name), and the legacy `X-Flex-Signature: sha256=<lowercase hex>` (HMAC-SHA256 of the raw body only, kept unchanged for existing integrations). Recommended verification (replay-safe): recompute the `X-Flex-Signature-V2` HMAC over the received bytes before parsing, compare in constant time, reject the request if `t` is more than 300 s (5 minutes) away from your clock, and ignore a `X-Flex-Delivery` you have already processed. Retries are re-signed with a fresh timestamp but keep the same `X-Flex-Delivery`. Any 2xx acknowledges the delivery; anything else, or no answer within 10 s, is retried with exponential backoff up to 5 attempts. After 20 consecutive failed deliveries the subscription is paused (shown inactive) until it is re-enabled. Redirects are never followed and private or link-local addresses are refused. Every attempt (status, timing, response) is listed in the delivery log in Settings.",
        "operationId": "webhook_vendor_created",
        "parameters": [
          {
            "name": "X-Flex-Signature",
            "in": "header",
            "description": "`sha256=<lowercase hex>` — HMAC-SHA256 of the raw request body keyed with the subscription secret. Legacy (no replay protection); prefer `X-Flex-Signature-V2`.",
            "required": true,
            "schema": {
              "type": "string",
              "example": "sha256=3f7a…c21e"
            }
          },
          {
            "name": "X-Flex-Signature-V2",
            "in": "header",
            "description": "`t=<unix seconds>,v1=<lowercase hex>` — HMAC-SHA256, keyed with the subscription secret, of the string `<t>.<raw request body>`. Reject the delivery when `t` is more than 300 s from your clock. Re-signed with a fresh `t` on every retry.",
            "required": true,
            "schema": {
              "type": "string",
              "example": "t=1790000000,v1=9b2e…4d10"
            }
          },
          {
            "name": "X-Flex-Timestamp",
            "in": "header",
            "description": "Unix time in seconds (UTC) when this attempt was signed; equal to `t` in `X-Flex-Signature-V2`.",
            "required": true,
            "schema": {
              "type": "string",
              "example": "1790000000"
            }
          },
          {
            "name": "X-Flex-Delivery",
            "in": "header",
            "description": "Opaque delivery id, the same on every retry of one delivery — store it (e.g. 24 h) and ignore repeats. Unique per subscription; do not parse it.",
            "required": true,
            "schema": {
              "type": "string",
              "example": "48213"
            }
          },
          {
            "name": "X-Flex-Event",
            "in": "header",
            "description": "The event name, identical to the envelope's `event` (handy for routing before parsing the body).",
            "required": true,
            "schema": {
              "type": "string",
              "example": "vendor.created"
            }
          }
        ],
        "requestBody": {
          "description": "The signed JSON envelope.",
          "content": {
            "application/json": {
              "schema": {
                "required": [
                  "event",
                  "occurredAt",
                  "organizationId",
                  "data"
                ],
                "type": "object",
                "properties": {
                  "event": {
                    "enum": [
                      "vendor.created"
                    ],
                    "type": "string",
                    "description": "Event name."
                  },
                  "occurredAt": {
                    "type": "string",
                    "description": "When the event occurred (ISO-8601 UTC).",
                    "format": "date-time"
                  },
                  "organizationId": {
                    "type": "integer",
                    "description": "The organization the subscription belongs to.",
                    "format": "int32"
                  },
                  "data": {
                    "$ref": "#/components/schemas/VendorWebhookData"
                  }
                },
                "description": "Signed envelope Flex POSTs to the subscribed URL for `vendor.created`."
              }
            }
          },
          "required": true
        },
        "responses": {
          "2XX": {
            "description": "Delivery acknowledged. Any other status, or a timeout, is retried."
          }
        }
      }
    },
    "vendor.updated": {
      "post": {
        "tags": [
          "Webhooks"
        ],
        "summary": "A vendor's details change — name, contact, website, default shipping company, or active/inactive.",
        "description": "Outbound: Flex sends this request to your endpoint; you do not call it. Subscriptions are created in Flex under Settings → Integrations → Webhooks: a public https URL, the events to receive, and a signing secret shown once. Every delivery is an HTTP POST with `Content-Type: application/json` and the envelope `{ \"event\", \"occurredAt\", \"organizationId\", \"data\" }` (`occurredAt` is ISO-8601 UTC; `data` is the compact summary documented per event, camelCase, nulls omitted). Headers on every attempt: `X-Flex-Timestamp` (unix seconds when this attempt was signed), `X-Flex-Signature-V2: t=<same unix seconds>,v1=<lowercase hex>` (HMAC-SHA256, keyed with the subscription secret, of the string `<t>.<raw request body>`), `X-Flex-Delivery` (an opaque delivery id, identical on every retry of the same delivery), `X-Flex-Event` (the event name), and the legacy `X-Flex-Signature: sha256=<lowercase hex>` (HMAC-SHA256 of the raw body only, kept unchanged for existing integrations). Recommended verification (replay-safe): recompute the `X-Flex-Signature-V2` HMAC over the received bytes before parsing, compare in constant time, reject the request if `t` is more than 300 s (5 minutes) away from your clock, and ignore a `X-Flex-Delivery` you have already processed. Retries are re-signed with a fresh timestamp but keep the same `X-Flex-Delivery`. Any 2xx acknowledges the delivery; anything else, or no answer within 10 s, is retried with exponential backoff up to 5 attempts. After 20 consecutive failed deliveries the subscription is paused (shown inactive) until it is re-enabled. Redirects are never followed and private or link-local addresses are refused. Every attempt (status, timing, response) is listed in the delivery log in Settings.",
        "operationId": "webhook_vendor_updated",
        "parameters": [
          {
            "name": "X-Flex-Signature",
            "in": "header",
            "description": "`sha256=<lowercase hex>` — HMAC-SHA256 of the raw request body keyed with the subscription secret. Legacy (no replay protection); prefer `X-Flex-Signature-V2`.",
            "required": true,
            "schema": {
              "type": "string",
              "example": "sha256=3f7a…c21e"
            }
          },
          {
            "name": "X-Flex-Signature-V2",
            "in": "header",
            "description": "`t=<unix seconds>,v1=<lowercase hex>` — HMAC-SHA256, keyed with the subscription secret, of the string `<t>.<raw request body>`. Reject the delivery when `t` is more than 300 s from your clock. Re-signed with a fresh `t` on every retry.",
            "required": true,
            "schema": {
              "type": "string",
              "example": "t=1790000000,v1=9b2e…4d10"
            }
          },
          {
            "name": "X-Flex-Timestamp",
            "in": "header",
            "description": "Unix time in seconds (UTC) when this attempt was signed; equal to `t` in `X-Flex-Signature-V2`.",
            "required": true,
            "schema": {
              "type": "string",
              "example": "1790000000"
            }
          },
          {
            "name": "X-Flex-Delivery",
            "in": "header",
            "description": "Opaque delivery id, the same on every retry of one delivery — store it (e.g. 24 h) and ignore repeats. Unique per subscription; do not parse it.",
            "required": true,
            "schema": {
              "type": "string",
              "example": "48213"
            }
          },
          {
            "name": "X-Flex-Event",
            "in": "header",
            "description": "The event name, identical to the envelope's `event` (handy for routing before parsing the body).",
            "required": true,
            "schema": {
              "type": "string",
              "example": "vendor.updated"
            }
          }
        ],
        "requestBody": {
          "description": "The signed JSON envelope.",
          "content": {
            "application/json": {
              "schema": {
                "required": [
                  "event",
                  "occurredAt",
                  "organizationId",
                  "data"
                ],
                "type": "object",
                "properties": {
                  "event": {
                    "enum": [
                      "vendor.updated"
                    ],
                    "type": "string",
                    "description": "Event name."
                  },
                  "occurredAt": {
                    "type": "string",
                    "description": "When the event occurred (ISO-8601 UTC).",
                    "format": "date-time"
                  },
                  "organizationId": {
                    "type": "integer",
                    "description": "The organization the subscription belongs to.",
                    "format": "int32"
                  },
                  "data": {
                    "$ref": "#/components/schemas/VendorWebhookData"
                  }
                },
                "description": "Signed envelope Flex POSTs to the subscribed URL for `vendor.updated`."
              }
            }
          },
          "required": true
        },
        "responses": {
          "2XX": {
            "description": "Delivery acknowledged. Any other status, or a timeout, is retried."
          }
        }
      }
    }
  }
}