{
  "openapi": "3.0.0",
  "info": {
    "title": "SmartPricingTable API",
    "version": "1.0.0",
    "description": "API documentation for SmartPricingTable application",
    "contact": {
      "name": "SmartPricingTable Support",
      "url": "https://smartpricingtable.com"
    }
  },
  "servers": [
    {
      "url": "http://localhost:3000",
      "description": "Development server"
    }
  ],
  "components": {
    "securitySchemes": {
      "BearerAuth": {
        "type": "http",
        "scheme": "bearer",
        "bearerFormat": "JWT",
        "description": "JWT access token obtained during authentication"
      },
      "ApiKeyAuth": {
        "type": "apiKey",
        "in": "header",
        "name": "Authorization",
        "description": "API Key authentication (format: Bearer YOUR_API_KEY)"
      }
    },
    "schemas": {
      "LineItem": {
        "type": "object",
        "properties": {
          "id": {
            "type": "string",
            "description": "Unique line item identifier"
          },
          "name": {
            "type": "string",
            "description": "Line item name/title"
          },
          "description": {
            "type": "string",
            "description": "HTML content for the item description"
          },
          "price": {
            "$ref": "#/components/schemas/PriceConfig"
          },
          "isOptional": {
            "type": "boolean",
            "description": "Whether this line item is optional for the client"
          },
          "isSelected": {
            "type": "boolean",
            "description": "Whether this optional item is currently selected"
          },
          "taxExempt": {
            "type": "boolean",
            "description": "Whether this item is exempt from sales tax"
          },
          "markupExempt": {
            "type": "boolean",
            "description": "Whether this item is exempt from markup"
          },
          "discountExempt": {
            "type": "boolean",
            "description": "Whether this item is exempt from proposal-wide percentage discounts"
          },
          "skipRounding": {
            "type": "boolean",
            "description": "Skip Use Rounded Amounts for this item. Ignored unless that\npricing table has Use Rounded Amounts on. `showCents` then shows cents on\nthis item only."
          },
          "showCents": {
            "type": "boolean",
            "description": "Show cents on this item. Only used when `skipRounding` is true."
          },
          "hidden": {
            "type": "boolean",
            "description": "Whether this item is hidden from the client view"
          },
          "locked": {
            "type": "boolean",
            "description": "Logic lock. Hidden always implies locked.\nLocked items are not in totals and cannot be changed except by the engine."
          },
          "hideModifications": {
            "type": "boolean",
            "description": "Whether to hide modifications/options for this item"
          },
          "internalNotes": {
            "type": "string",
            "description": "Admin-only internal notes (never shown to recipients unless Configure PDF includes them)"
          },
          "terms": {
            "type": "string",
            "description": "HTML added to [dynamic_terms] when this line item qualifies.\nGated by the dynamic_terms add-on."
          },
          "modificationsLabel": {
            "type": "string",
            "description": "Custom label for the modifications section (e.g., \"Options\", \"Add-ons\")"
          },
          "children": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/Modification"
            },
            "description": "Array of child modifications/options for this line item"
          },
          "startExpanded": {
            "type": "boolean",
            "description": "Whether modifications are expanded by default"
          },
          "order_key": {
            "type": "number",
            "description": "Sort position within the group"
          },
          "selectionType": {
            "type": "string",
            "enum": [
              "single",
              "multiple",
              "loose"
            ],
            "description": "How child modifications can be selected (single, multiple, etc.)"
          },
          "requireSelection": {
            "type": "boolean",
            "description": "When selectionType is loose, require the customer to select at least one modification (ignored when hideModifications is true)"
          },
          "label": {
            "type": "string",
            "description": "Custom display label for this line item"
          },
          "tags": {
            "type": "array",
            "items": {
              "type": "string"
            },
            "description": "Tags/labels applied to this item"
          },
          "integrations": {
            "type": "object",
            "properties": {
              "quickbooks": {
                "type": "object",
                "properties": {
                  "productId": {
                    "type": "string"
                  }
                },
                "additionalProperties": false
              }
            },
            "additionalProperties": false,
            "description": "External integration references for this item"
          },
          "externalItemId": {
            "type": [
              "string",
              "null"
            ],
            "description": "Optional customer-owned identifier (SKU, CRM id, etc.).\nPrintable characters, including spaces and punctuation. Max 255 characters.\nControl characters and non-ASCII are not allowed.\nShown in the editor as \"Item ID\" when the org setting `proposals.showItemId` is on.\nIncluded on authenticated API, webhooks, and JSON export. Never shown to recipients.\nNamed `externalItemId` so it is not confused with this object's `id` (SPT public id)."
          },
          "outOfPocket": {
            "anyOf": [
              {
                "$ref": "#/components/schemas/OutOfPocketConfig"
              },
              {
                "type": "null"
              }
            ],
            "description": "Fixed out-of-pocket (pass-through) amount billed to the client.\nGated by the out_of_pocket_costs module. Not marked up or taxed."
          },
          "internalCost": {
            "anyOf": [
              {
                "$ref": "#/components/schemas/InternalCostConfig"
              },
              {
                "type": "null"
              }
            ],
            "description": "What you pay (per unit when quantity applies). Editor-only.\nWritable on create/update as `internalCost` or `internal_cost`.\nGated by the profit_margins module. Never shown to customers."
          },
          "discount": {
            "anyOf": [
              {
                "$ref": "#/components/schemas/LineItemDiscountConfig"
              },
              {
                "type": "null"
              }
            ],
            "description": "Per-line discount (same object as `price.discount`).\nAccepted at the top level on create/update so Discount can be set\nwithout nesting. Pass null to clear."
          },
          "created_at": {
            "type": "string",
            "description": "When the item was created (ISO 8601 timestamp)"
          },
          "updated_at": {
            "type": "string",
            "description": "When the item was last updated (ISO 8601 timestamp)"
          },
          "archived_at": {
            "type": [
              "string",
              "null"
            ],
            "description": "When the item was archived (ISO 8601 timestamp), or null if active"
          },
          "is_archived": {
            "type": "boolean",
            "description": "Whether the item is archived"
          },
          "childCount": {
            "type": "number",
            "description": "Number of child modifications"
          },
          "parent_id": {
            "type": [
              "string",
              "null"
            ],
            "description": "The public_id of the parent group or scope that this item belongs to"
          },
          "in_library": {
            "type": "boolean",
            "description": "Whether this item belongs to the library (true) or a regular proposal (false/undefined)"
          },
          "linkedLibraryItemId": {
            "type": [
              "string",
              "null"
            ],
            "description": "Public ID of the library item this is linked to (templates only)"
          },
          "hasLinkedItems": {
            "type": "boolean",
            "description": "Whether this library item has other items linked to it (library items only)"
          }
        },
        "required": [
          "id",
          "name",
          "description",
          "price",
          "isOptional",
          "isSelected",
          "taxExempt",
          "markupExempt",
          "discountExempt",
          "children"
        ],
        "additionalProperties": false,
        "description": "A line item (product or service) within a group"
      },
      "Pagination": {
        "type": "object",
        "description": "Pagination metadata for list endpoints",
        "properties": {
          "page": {
            "type": "integer",
            "description": "Current page number",
            "example": 1
          },
          "limit": {
            "type": "integer",
            "description": "Items per page",
            "example": 20
          },
          "total": {
            "type": "integer",
            "description": "Total number of items",
            "example": 150
          },
          "pages": {
            "type": "integer",
            "description": "Total number of pages",
            "example": 8
          }
        }
      },
      "ProposalGroup": {
        "type": "object",
        "properties": {
          "kind": {
            "type": "string",
            "const": "group",
            "description": "Discriminator for PageChild union (set when part of children array)"
          },
          "id": {
            "type": "string",
            "description": "Unique group identifier"
          },
          "name": {
            "type": "string",
            "description": "Group name/title"
          },
          "description": {
            "type": "string",
            "description": "Group description text"
          },
          "internalNotes": {
            "type": "string",
            "description": "Admin-only internal notes (never shown to recipients unless Configure PDF includes them)"
          },
          "isOptional": {
            "type": "boolean",
            "description": "Whether this group is optional for the client"
          },
          "isSelected": {
            "type": "boolean",
            "description": "Whether this optional group is currently selected"
          },
          "hidden": {
            "type": "boolean",
            "description": "Hidden from the client (same meaning as line-item hidden)"
          },
          "locked": {
            "type": "boolean",
            "description": "Logic lock — not in totals, not writable"
          },
          "displayCost": {
            "type": "string",
            "enum": [
              "show_all",
              "hide_all",
              "hide_item_costs",
              "hide_group_cost"
            ],
            "description": "Whether to show the cost for this group"
          },
          "selectionType": {
            "type": "string",
            "enum": [
              "single",
              "multiple",
              "loose"
            ],
            "description": "How items within this group can be selected (single, multiple, etc.)"
          },
          "requireSelection": {
            "type": "boolean",
            "description": "When selectionType is loose, require the customer to select at least one optional line item"
          },
          "lineItems": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/LineItem"
            },
            "description": "Array of line items in this group"
          },
          "hideQuantities": {
            "type": "boolean",
            "description": "Whether to hide quantity columns"
          },
          "startExpanded": {
            "type": "boolean",
            "description": "Whether the group starts expanded in the proposal view"
          },
          "pdfPageBreak": {
            "type": "boolean",
            "description": "When generating a PDF, start this group at the top of a new page"
          },
          "quantity": {
            "$ref": "#/components/schemas/QuantityConfig"
          },
          "order_key": {
            "type": "number",
            "description": "Sort position of the group within the page"
          },
          "tags": {
            "type": "array",
            "items": {
              "type": "string"
            },
            "description": "Tags/labels applied to this group"
          },
          "linkedScopeId": {
            "type": [
              "string",
              "null"
            ],
            "description": "ID of the linked scope of work section"
          },
          "created_at": {
            "type": "string",
            "description": "When the group was created (ISO 8601 timestamp)"
          },
          "updated_at": {
            "type": "string",
            "description": "When the group was last updated (ISO 8601 timestamp)"
          },
          "metadata": {
            "$ref": "#/components/schemas/Record%3Cstring%2Cunknown%3E"
          },
          "archived_at": {
            "type": [
              "string",
              "null"
            ],
            "description": "When the group was archived (ISO 8601 timestamp), or null if active"
          },
          "is_archived": {
            "type": "boolean",
            "description": "Whether the group is archived"
          },
          "in_library": {
            "type": "boolean",
            "description": "Whether this group belongs to the library (true) or a regular proposal (false/undefined)"
          }
        },
        "required": [
          "id",
          "name",
          "isOptional",
          "isSelected",
          "displayCost",
          "selectionType",
          "lineItems"
        ],
        "additionalProperties": false,
        "description": "A group of line items within a proposal page"
      },
      "UpdateGroupApiInput": {
        "type": "object",
        "properties": {
          "kind": {
            "type": "string",
            "const": "group",
            "description": "Discriminator for PageChild union (set when part of children array)"
          },
          "name": {
            "type": "string",
            "description": "Group name/title"
          },
          "description": {
            "type": "string",
            "description": "Group description text"
          },
          "internalNotes": {
            "type": "string",
            "description": "Admin-only internal notes (never shown to recipients unless Configure PDF includes them)"
          },
          "isOptional": {
            "type": "boolean",
            "description": "Whether this group is optional for the client"
          },
          "isSelected": {
            "type": "boolean",
            "description": "Whether this optional group is currently selected"
          },
          "hidden": {
            "type": "boolean",
            "description": "Hidden from the client (same meaning as line-item hidden)"
          },
          "locked": {
            "type": "boolean",
            "description": "Logic lock — not in totals, not writable"
          },
          "displayCost": {
            "type": "string",
            "enum": [
              "show_all",
              "hide_all",
              "hide_item_costs",
              "hide_group_cost"
            ],
            "description": "Whether to show the cost for this group"
          },
          "selectionType": {
            "type": "string",
            "enum": [
              "single",
              "multiple",
              "loose"
            ],
            "description": "How items within this group can be selected (single, multiple, etc.)"
          },
          "requireSelection": {
            "type": "boolean",
            "description": "When selectionType is loose, require the customer to select at least one optional line item"
          },
          "hideQuantities": {
            "type": "boolean",
            "description": "Whether to hide quantity columns"
          },
          "startExpanded": {
            "type": "boolean",
            "description": "Whether the group starts expanded in the proposal view"
          },
          "pdfPageBreak": {
            "type": "boolean",
            "description": "When generating a PDF, start this group at the top of a new page"
          },
          "quantity": {
            "$ref": "#/components/schemas/QuantityConfig"
          },
          "order_key": {
            "type": "number",
            "description": "Sort position of the group within the page"
          },
          "tags": {
            "type": "array",
            "items": {
              "type": "string"
            },
            "description": "Tags/labels applied to this group"
          },
          "metadata": {
            "$ref": "#/components/schemas/Record%3Cstring%2Cunknown%3E"
          },
          "in_library": {
            "type": "boolean",
            "description": "Whether this group belongs to the library (true) or a regular proposal (false/undefined)"
          }
        },
        "additionalProperties": false
      },
      "CreateGroupContent": {
        "type": "object",
        "additionalProperties": false,
        "properties": {
          "kind": {
            "type": "string",
            "const": "group",
            "description": "Discriminator for PageChild union (set when part of children array)"
          },
          "description": {
            "type": "string",
            "description": "Group description text"
          },
          "internalNotes": {
            "type": "string",
            "description": "Admin-only internal notes (never shown to recipients unless Configure PDF includes them)"
          },
          "isOptional": {
            "type": "boolean",
            "description": "Whether this group is optional for the client"
          },
          "isSelected": {
            "type": "boolean",
            "description": "Whether this optional group is currently selected"
          },
          "hidden": {
            "type": "boolean",
            "description": "Hidden from the client (same meaning as line-item hidden)"
          },
          "locked": {
            "type": "boolean",
            "description": "Logic lock — not in totals, not writable"
          },
          "displayCost": {
            "type": "string",
            "enum": [
              "show_all",
              "hide_all",
              "hide_item_costs",
              "hide_group_cost"
            ],
            "description": "Whether to show the cost for this group"
          },
          "selectionType": {
            "type": "string",
            "enum": [
              "single",
              "multiple",
              "loose"
            ],
            "description": "How items within this group can be selected (single, multiple, etc.)"
          },
          "requireSelection": {
            "type": "boolean",
            "description": "When selectionType is loose, require the customer to select at least one optional line item"
          },
          "hideQuantities": {
            "type": "boolean",
            "description": "Whether to hide quantity columns"
          },
          "startExpanded": {
            "type": "boolean",
            "description": "Whether the group starts expanded in the proposal view"
          },
          "pdfPageBreak": {
            "type": "boolean",
            "description": "When generating a PDF, start this group at the top of a new page"
          },
          "quantity": {
            "$ref": "#/components/schemas/QuantityConfig"
          },
          "order_key": {
            "type": "number",
            "description": "Sort position of the group within the page"
          },
          "tags": {
            "type": "array",
            "items": {
              "type": "string"
            },
            "description": "Tags/labels applied to this group"
          },
          "metadata": {
            "$ref": "#/components/schemas/Record%3Cstring%2Cunknown%3E"
          },
          "in_library": {
            "type": "boolean",
            "description": "Whether this group belongs to the library (true) or a regular proposal (false/undefined)"
          },
          "name": {
            "type": "string",
            "description": "Group name/title"
          }
        },
        "description": "Group content fields for creation",
        "required": [
          "name"
        ]
      },
      "Modification": {
        "type": "object",
        "properties": {
          "id": {
            "type": "string",
            "description": "Unique identifier for the modification"
          },
          "name": {
            "type": "string",
            "description": "Name of the modification"
          },
          "description": {
            "type": "string",
            "description": "HTML description of the modification"
          },
          "internalNotes": {
            "type": "string",
            "description": "Admin-only internal notes (never shown to recipients unless Configure PDF includes them)"
          },
          "terms": {
            "type": "string",
            "description": "HTML added to [dynamic_terms] when this modification qualifies.\nGated by the dynamic_terms add-on."
          },
          "price": {
            "$ref": "#/components/schemas/PriceConfig",
            "description": "Price configuration"
          },
          "isOptional": {
            "type": "boolean",
            "description": "Whether this modification is optional"
          },
          "isSelected": {
            "type": "boolean",
            "description": "Whether this modification is currently selected"
          },
          "hidden": {
            "type": "boolean",
            "description": "Hidden from the client (same meaning as line-item hidden)"
          },
          "locked": {
            "type": "boolean",
            "description": "Logic lock — not in totals, not writable"
          },
          "taxExempt": {
            "type": "boolean",
            "description": "Whether this modification is exempt from tax calculations"
          },
          "markupExempt": {
            "type": "boolean",
            "description": "Whether this modification is exempt from markup calculations"
          },
          "discountExempt": {
            "type": "boolean",
            "description": "Whether this modification is exempt from percentage-based scope discounts"
          },
          "skipRounding": {
            "type": "boolean",
            "description": "Skip Use Rounded Amounts for this modification. Ignored unless\nthat pricing table has Use Rounded Amounts on."
          },
          "showCents": {
            "type": "boolean",
            "description": "Show cents on this modification. Only used when `skipRounding` is true."
          },
          "label": {
            "type": "string",
            "description": "Custom label for the modification"
          },
          "kind": {
            "type": "string",
            "enum": [
              "mod",
              "mod_divider"
            ],
            "description": "Item kind - 'mod' for regular modifications, 'mod_divider' for dividers"
          },
          "selectionType": {
            "type": "string",
            "enum": [
              "single",
              "multiple",
              "loose"
            ],
            "description": "Selection type for dividers - affects modifications below the divider"
          },
          "requireSelection": {
            "type": "boolean",
            "description": "When selectionType is loose, require the customer to select at least one modification under this divider"
          },
          "integrations": {
            "type": "object",
            "properties": {
              "quickbooks": {
                "type": "object",
                "properties": {
                  "productId": {
                    "type": "string"
                  }
                },
                "additionalProperties": false
              }
            },
            "additionalProperties": false,
            "description": "Integration-specific data"
          },
          "externalItemId": {
            "type": [
              "string",
              "null"
            ],
            "description": "Optional customer-owned identifier (SKU, CRM id, etc.).\nPrintable characters, including spaces and punctuation. Max 255 characters.\nControl characters and non-ASCII are not allowed.\nShown in the editor as \"Item ID\" when the org setting `proposals.showItemId` is on.\nIncluded on authenticated API, webhooks, and JSON export. Never shown to recipients.\nNamed `externalItemId` so it is not confused with this object's `id` (SPT public id)."
          },
          "outOfPocket": {
            "anyOf": [
              {
                "$ref": "#/components/schemas/OutOfPocketConfig"
              },
              {
                "type": "null"
              }
            ],
            "description": "Fixed out-of-pocket (pass-through) amount billed to the client.\nGated by the out_of_pocket_costs module. Not marked up or taxed."
          },
          "internalCost": {
            "anyOf": [
              {
                "$ref": "#/components/schemas/InternalCostConfig"
              },
              {
                "type": "null"
              }
            ],
            "description": "What you pay (per unit when quantity applies). Editor-only.\nWritable on create/update as `internalCost` or `internal_cost`.\nGated by the profit_margins module. Never shown to customers."
          },
          "discount": {
            "anyOf": [
              {
                "$ref": "#/components/schemas/LineItemDiscountConfig"
              },
              {
                "type": "null"
              }
            ],
            "description": "Per-line discount (same object as `price.discount`).\nAccepted at the top level on create/update. Pass null to clear."
          },
          "created_at": {
            "type": "string",
            "description": "When the modification was created (ISO 8601 timestamp)"
          },
          "updated_at": {
            "type": "string",
            "description": "When the modification was last updated (ISO 8601 timestamp)"
          },
          "archived_at": {
            "type": [
              "string",
              "null"
            ],
            "description": "When the modification was archived (ISO 8601 timestamp), or null if active"
          },
          "is_archived": {
            "type": "boolean",
            "description": "Whether this modification is archived"
          }
        },
        "required": [
          "id",
          "name",
          "price",
          "isOptional",
          "isSelected",
          "taxExempt",
          "markupExempt",
          "discountExempt"
        ],
        "additionalProperties": false,
        "description": "A modification (child item) of a line item - represents an option or variant"
      },
      "UpdateLineItemApiInput": {
        "type": "object",
        "additionalProperties": false,
        "properties": {
          "price": {
            "$ref": "#/components/schemas/PriceConfig",
            "description": "New price. This is how you change the cost of an existing line item\nor modification. It replaces the whole price: send `model`, `value`, and `frequency`\ntogether. For fixed, `value` is the unit price in dollars (major currency units),\nnot cents. For percent, `value` is a whole-number percent (15 = 15%).\nSending only `value` without `model` does not change the price.\nExample: `{ \"model\": \"fixed\", \"value\": 379, \"frequency\": \"once\" }`."
          },
          "internal_cost": {
            "anyOf": [
              {
                "$ref": "#/components/schemas/InternalCostConfig"
              },
              {
                "type": "null"
              }
            ],
            "description": "Alias for `internalCost` (snake_case)."
          },
          "name": {
            "type": "string",
            "description": "Line item name/title"
          },
          "description": {
            "type": "string",
            "description": "HTML content for the item description"
          },
          "isOptional": {
            "type": "boolean",
            "description": "Whether this line item is optional for the client"
          },
          "isSelected": {
            "type": "boolean",
            "description": "Whether this optional item is currently selected"
          },
          "taxExempt": {
            "type": "boolean",
            "description": "Whether this item is exempt from sales tax"
          },
          "markupExempt": {
            "type": "boolean",
            "description": "Whether this item is exempt from markup"
          },
          "discountExempt": {
            "type": "boolean",
            "description": "Whether this item is exempt from proposal-wide percentage discounts"
          },
          "skipRounding": {
            "type": "boolean",
            "description": "Skip Use Rounded Amounts for this item. Ignored unless that\npricing table has Use Rounded Amounts on. `showCents` then shows cents on\nthis item only."
          },
          "showCents": {
            "type": "boolean",
            "description": "Show cents on this item. Only used when `skipRounding` is true."
          },
          "hidden": {
            "type": "boolean",
            "description": "Whether this item is hidden from the client view"
          },
          "locked": {
            "type": "boolean",
            "description": "Logic lock. Hidden always implies locked.\nLocked items are not in totals and cannot be changed except by the engine."
          },
          "hideModifications": {
            "type": "boolean",
            "description": "Whether to hide modifications/options for this item"
          },
          "internalNotes": {
            "type": "string",
            "description": "Admin-only internal notes (never shown to recipients unless Configure PDF includes them)"
          },
          "terms": {
            "type": "string",
            "description": "HTML added to [dynamic_terms] when this line item qualifies.\nGated by the dynamic_terms add-on."
          },
          "modificationsLabel": {
            "type": "string",
            "description": "Custom label for the modifications section (e.g., \"Options\", \"Add-ons\")"
          },
          "startExpanded": {
            "type": "boolean",
            "description": "Whether modifications are expanded by default"
          },
          "order_key": {
            "type": "number",
            "description": "Sort position within the group"
          },
          "selectionType": {
            "type": "string",
            "enum": [
              "single",
              "multiple",
              "loose"
            ],
            "description": "How child modifications can be selected (single, multiple, etc.)"
          },
          "requireSelection": {
            "type": "boolean",
            "description": "When selectionType is loose, require the customer to select at least one modification (ignored when hideModifications is true)"
          },
          "label": {
            "type": "string",
            "description": "Custom display label for this line item"
          },
          "tags": {
            "type": "array",
            "items": {
              "type": "string"
            },
            "description": "Tags/labels applied to this item"
          },
          "integrations": {
            "type": "object",
            "properties": {
              "quickbooks": {
                "type": "object",
                "properties": {
                  "productId": {
                    "type": "string"
                  }
                },
                "additionalProperties": false
              }
            },
            "additionalProperties": false,
            "description": "External integration references for this item"
          },
          "externalItemId": {
            "type": [
              "string",
              "null"
            ],
            "description": "Optional customer-owned identifier (SKU, CRM id, etc.).\nPrintable characters, including spaces and punctuation. Max 255 characters.\nControl characters and non-ASCII are not allowed.\nShown in the editor as \"Item ID\" when the org setting `proposals.showItemId` is on.\nIncluded on authenticated API, webhooks, and JSON export. Never shown to recipients.\nNamed `externalItemId` so it is not confused with this object's `id` (SPT public id)."
          },
          "outOfPocket": {
            "anyOf": [
              {
                "$ref": "#/components/schemas/OutOfPocketConfig"
              },
              {
                "type": "null"
              }
            ],
            "description": "Fixed out-of-pocket (pass-through) amount billed to the client.\nGated by the out_of_pocket_costs module. Not marked up or taxed."
          },
          "internalCost": {
            "anyOf": [
              {
                "$ref": "#/components/schemas/InternalCostConfig"
              },
              {
                "type": "null"
              }
            ],
            "description": "What you pay (per unit when quantity applies). Editor-only.\nWritable on create/update as `internalCost` or `internal_cost`.\nGated by the profit_margins module. Never shown to customers."
          },
          "discount": {
            "anyOf": [
              {
                "$ref": "#/components/schemas/LineItemDiscountConfig"
              },
              {
                "type": "null"
              }
            ],
            "description": "Per-line discount (same object as `price.discount`).\nAccepted at the top level on create/update so Discount can be set\nwithout nesting. Pass null to clear."
          },
          "parent_id": {
            "type": [
              "string",
              "null"
            ],
            "description": "The public_id of the parent group or scope that this item belongs to"
          },
          "in_library": {
            "type": "boolean",
            "description": "Whether this item belongs to the library (true) or a regular proposal (false/undefined)"
          },
          "linkedLibraryItemId": {
            "type": [
              "string",
              "null"
            ],
            "description": "Public ID of the library item this is linked to (templates only)"
          },
          "hasLinkedItems": {
            "type": "boolean",
            "description": "Whether this library item has other items linked to it (library items only)"
          }
        },
        "description": "Request body for updating a line item or modification (PATCH /api/items/{id}).\nAll fields are optional; only the fields you send change. The body is flat\n(no `item` wrapper). `price` is a full replacement: to change the cost, send\nthe whole price object with `model`, `value` (dollars, not cents), and\n`frequency`, e.g. `{ \"price\": { \"model\": \"fixed\", \"value\": 379, \"frequency\": \"once\" } }`."
      },
      "CreateLineItemApiInput": {
        "type": "object",
        "additionalProperties": false,
        "properties": {
          "source_library_item_id": {
            "type": "string",
            "description": "Library row this copy was inserted from (used later to pair group overwrite)"
          },
          "internal_cost": {
            "anyOf": [
              {
                "$ref": "#/components/schemas/InternalCostConfig"
              },
              {
                "type": "null"
              }
            ],
            "description": "Alias for `internalCost` (snake_case)."
          },
          "description": {
            "type": "string",
            "description": "HTML content for the item description"
          },
          "price": {
            "$ref": "#/components/schemas/PriceConfig"
          },
          "isOptional": {
            "type": "boolean",
            "description": "Whether this line item is optional for the client"
          },
          "isSelected": {
            "type": "boolean",
            "description": "Whether this optional item is currently selected"
          },
          "taxExempt": {
            "type": "boolean",
            "description": "Whether this item is exempt from sales tax"
          },
          "markupExempt": {
            "type": "boolean",
            "description": "Whether this item is exempt from markup"
          },
          "discountExempt": {
            "type": "boolean",
            "description": "Whether this item is exempt from proposal-wide percentage discounts"
          },
          "skipRounding": {
            "type": "boolean",
            "description": "Skip Use Rounded Amounts for this item. Ignored unless that\npricing table has Use Rounded Amounts on. `showCents` then shows cents on\nthis item only."
          },
          "showCents": {
            "type": "boolean",
            "description": "Show cents on this item. Only used when `skipRounding` is true."
          },
          "hidden": {
            "type": "boolean",
            "description": "Whether this item is hidden from the client view"
          },
          "locked": {
            "type": "boolean",
            "description": "Logic lock. Hidden always implies locked.\nLocked items are not in totals and cannot be changed except by the engine."
          },
          "hideModifications": {
            "type": "boolean",
            "description": "Whether to hide modifications/options for this item"
          },
          "internalNotes": {
            "type": "string",
            "description": "Admin-only internal notes (never shown to recipients unless Configure PDF includes them)"
          },
          "terms": {
            "type": "string",
            "description": "HTML added to [dynamic_terms] when this line item qualifies.\nGated by the dynamic_terms add-on."
          },
          "modificationsLabel": {
            "type": "string",
            "description": "Custom label for the modifications section (e.g., \"Options\", \"Add-ons\")"
          },
          "children": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/Modification"
            },
            "description": "Array of child modifications/options for this line item"
          },
          "startExpanded": {
            "type": "boolean",
            "description": "Whether modifications are expanded by default"
          },
          "order_key": {
            "type": "number",
            "description": "Sort position within the group"
          },
          "selectionType": {
            "type": "string",
            "enum": [
              "single",
              "multiple",
              "loose"
            ],
            "description": "How child modifications can be selected (single, multiple, etc.)"
          },
          "requireSelection": {
            "type": "boolean",
            "description": "When selectionType is loose, require the customer to select at least one modification (ignored when hideModifications is true)"
          },
          "label": {
            "type": "string",
            "description": "Custom display label for this line item"
          },
          "tags": {
            "type": "array",
            "items": {
              "type": "string"
            },
            "description": "Tags/labels applied to this item"
          },
          "integrations": {
            "type": "object",
            "properties": {
              "quickbooks": {
                "type": "object",
                "properties": {
                  "productId": {
                    "type": "string"
                  }
                },
                "additionalProperties": false
              }
            },
            "additionalProperties": false,
            "description": "External integration references for this item"
          },
          "externalItemId": {
            "type": [
              "string",
              "null"
            ],
            "description": "Optional customer-owned identifier (SKU, CRM id, etc.).\nPrintable characters, including spaces and punctuation. Max 255 characters.\nControl characters and non-ASCII are not allowed.\nShown in the editor as \"Item ID\" when the org setting `proposals.showItemId` is on.\nIncluded on authenticated API, webhooks, and JSON export. Never shown to recipients.\nNamed `externalItemId` so it is not confused with this object's `id` (SPT public id)."
          },
          "outOfPocket": {
            "anyOf": [
              {
                "$ref": "#/components/schemas/OutOfPocketConfig"
              },
              {
                "type": "null"
              }
            ],
            "description": "Fixed out-of-pocket (pass-through) amount billed to the client.\nGated by the out_of_pocket_costs module. Not marked up or taxed."
          },
          "internalCost": {
            "anyOf": [
              {
                "$ref": "#/components/schemas/InternalCostConfig"
              },
              {
                "type": "null"
              }
            ],
            "description": "What you pay (per unit when quantity applies). Editor-only.\nWritable on create/update as `internalCost` or `internal_cost`.\nGated by the profit_margins module. Never shown to customers."
          },
          "discount": {
            "anyOf": [
              {
                "$ref": "#/components/schemas/LineItemDiscountConfig"
              },
              {
                "type": "null"
              }
            ],
            "description": "Per-line discount (same object as `price.discount`).\nAccepted at the top level on create/update so Discount can be set\nwithout nesting. Pass null to clear."
          },
          "parent_id": {
            "type": [
              "string",
              "null"
            ],
            "description": "The public_id of the parent group or scope that this item belongs to"
          },
          "in_library": {
            "type": "boolean",
            "description": "Whether this item belongs to the library (true) or a regular proposal (false/undefined)"
          },
          "linkedLibraryItemId": {
            "type": [
              "string",
              "null"
            ],
            "description": "Public ID of the library item this is linked to (templates only)"
          },
          "hasLinkedItems": {
            "type": "boolean",
            "description": "Whether this library item has other items linked to it (library items only)"
          },
          "name": {
            "type": "string",
            "description": "Line item name/title"
          }
        },
        "description": "API input for creating a line item",
        "required": [
          "name"
        ]
      },
      "ProposalPage": {
        "type": "object",
        "additionalProperties": false,
        "properties": {
          "id": {
            "type": "string",
            "description": "Unique page identifier"
          },
          "created_at": {
            "type": "string",
            "description": "When the page was created (ISO 8601 timestamp)"
          },
          "updated_at": {
            "type": "string",
            "description": "When the page was last updated (ISO 8601 timestamp)"
          },
          "name": {
            "type": "string",
            "description": "Page name/title"
          },
          "content": {
            "type": "string",
            "description": "Rich text content of the page"
          },
          "order_key": {
            "type": "number",
            "description": "Sort position of the page within the proposal"
          },
          "kind": {
            "type": "string",
            "description": "Type of page (e.g., content, pricing, signatures)"
          },
          "scopeId": {
            "type": "string",
            "description": "ID of the scope of work section this page belongs to"
          },
          "isScopeOfWork": {
            "type": "boolean",
            "description": "Whether this page is a scope of work pricing page"
          },
          "isSignaturesPage": {
            "type": "boolean",
            "description": "Whether this is the signatures page"
          },
          "screenshotUrl": {
            "type": [
              "string",
              "null"
            ],
            "description": "URL of the page screenshot thumbnail"
          },
          "showCents": {
            "type": "boolean",
            "description": "Whether to display cents in price values"
          },
          "useRoundedAmounts": {
            "type": "boolean",
            "description": "Use rounded whole-dollar line amounts in the price math. Only applies while showCents is off. Missing means off. Set from the account default when the proposal or template is created, or copied from the template a proposal is based on."
          },
          "salesTaxPercentage": {
            "type": "string",
            "description": "Sales tax percentage applied to this page"
          },
          "lineItemsStartExpanded": {
            "type": "string",
            "enum": [
              "none",
              "all",
              "first"
            ],
            "description": "Whether line items are expanded by default"
          },
          "selectionType": {
            "type": "string",
            "enum": [
              "single",
              "multiple",
              "loose"
            ],
            "description": "How items on this page can be selected"
          },
          "requireSelection": {
            "type": "boolean",
            "description": "When selectionType is loose, require the customer to select at least one optional group"
          },
          "tags": {
            "type": "array",
            "items": {
              "type": "string"
            },
            "description": "Tags/labels applied to this page"
          },
          "metadata": {
            "$ref": "#/components/schemas/Record%3Cstring%2Cunknown%3E"
          },
          "markupPercentage": {
            "type": "string",
            "description": "Markup percentage applied to items on this page"
          },
          "discounts": {
            "type": "array",
            "items": {
              "type": "object",
              "properties": {
                "id": {
                  "type": "string"
                },
                "type": {
                  "type": "string",
                  "enum": [
                    "fixed",
                    "percentage"
                  ]
                },
                "amount": {
                  "type": "string"
                },
                "frequency": {
                  "type": "string"
                }
              },
              "required": [
                "id",
                "type",
                "amount",
                "frequency"
              ],
              "additionalProperties": false
            },
            "description": "Discount configurations for this page"
          },
          "hideTotalsTop": {
            "type": "boolean",
            "description": "Whether to hide the totals row at the top of the page"
          },
          "hideTotalsBottom": {
            "type": "boolean",
            "description": "Whether to hide the totals row at the bottom of the page"
          },
          "labels": {
            "type": "object",
            "properties": {
              "grandTotalLabels": {
                "$ref": "#/components/schemas/Record%3Cstring%2Cstring%3E"
              },
              "tax": {
                "type": "string"
              },
              "outOfPocket": {
                "type": "string",
                "description": "Default out-of-pocket label for this pricing table."
              }
            },
            "additionalProperties": false,
            "description": "Custom label overrides for this page"
          },
          "displayHeading": {
            "type": "boolean",
            "description": "Whether to display the page heading"
          },
          "hideSection": {
            "type": "boolean",
            "description": "Hide this page from recipients (the editor \"Hide this Page\" toggle). Staff still see it in the proposal editor. Recipients do not see it on the public view, preview, sign, or default PDF. Hidden Scope of Work pages are left out of grand totals. This is not ArchivePage and not excludeFromPdf. Set false to show the page again."
          },
          "excludeFromPdf": {
            "type": "boolean",
            "description": "Whether to exclude this page from PDF exports"
          },
          "startNewPdfPage": {
            "type": "boolean",
            "description": "When false, this page continues on the same PDF page as the previous one. Omitted/undefined keeps the historical behavior: each page starts a new PDF page."
          },
          "designSettings": {
            "$ref": "#/components/schemas/PageDesignSettings"
          },
          "children": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/PageChild"
            },
            "description": "Unified ordered list of groups and side-by-sides (only on scope-of-work pages)"
          }
        },
        "required": [
          "name",
          "content",
          "order_key"
        ],
        "description": "A page within a proposal. Pages contain the content sections of a proposal."
      },
      "ProposalPageUpdateInput": {
        "type": "object",
        "additionalProperties": false,
        "properties": {
          "designSettings": {
            "anyOf": [
              {
                "$ref": "#/components/schemas/PageDesignSettings"
              },
              {
                "type": "null"
              }
            ]
          },
          "name": {
            "type": "string",
            "description": "Page name/title"
          },
          "content": {
            "type": "string",
            "description": "Rich text content of the page"
          },
          "order_key": {
            "type": "number",
            "description": "Sort position of the page within the proposal"
          },
          "kind": {
            "type": "string",
            "description": "Type of page (e.g., content, pricing, signatures)"
          },
          "scopeId": {
            "type": "string",
            "description": "ID of the scope of work section this page belongs to"
          },
          "isScopeOfWork": {
            "type": "boolean",
            "description": "Whether this page is a scope of work pricing page"
          },
          "isSignaturesPage": {
            "type": "boolean",
            "description": "Whether this is the signatures page"
          },
          "screenshotUrl": {
            "type": [
              "string",
              "null"
            ],
            "description": "URL of the page screenshot thumbnail"
          },
          "showCents": {
            "type": "boolean",
            "description": "Whether to display cents in price values"
          },
          "useRoundedAmounts": {
            "type": "boolean",
            "description": "Use rounded whole-dollar line amounts in the price math. Only applies while showCents is off. Missing means off. Set from the account default when the proposal or template is created, or copied from the template a proposal is based on."
          },
          "salesTaxPercentage": {
            "type": "string",
            "description": "Sales tax percentage applied to this page"
          },
          "lineItemsStartExpanded": {
            "type": "string",
            "enum": [
              "none",
              "all",
              "first"
            ],
            "description": "Whether line items are expanded by default"
          },
          "selectionType": {
            "type": "string",
            "enum": [
              "single",
              "multiple",
              "loose"
            ],
            "description": "How items on this page can be selected"
          },
          "requireSelection": {
            "type": "boolean",
            "description": "When selectionType is loose, require the customer to select at least one optional group"
          },
          "tags": {
            "type": "array",
            "items": {
              "type": "string"
            },
            "description": "Tags/labels applied to this page"
          },
          "metadata": {
            "$ref": "#/components/schemas/Record%3Cstring%2Cunknown%3E"
          },
          "markupPercentage": {
            "type": "string",
            "description": "Markup percentage applied to items on this page"
          },
          "discounts": {
            "type": "array",
            "items": {
              "type": "object",
              "properties": {
                "id": {
                  "type": "string"
                },
                "type": {
                  "type": "string",
                  "enum": [
                    "fixed",
                    "percentage"
                  ]
                },
                "amount": {
                  "type": "string"
                },
                "frequency": {
                  "type": "string"
                }
              },
              "required": [
                "id",
                "type",
                "amount",
                "frequency"
              ],
              "additionalProperties": false
            },
            "description": "Discount configurations for this page"
          },
          "hideTotalsTop": {
            "type": "boolean",
            "description": "Whether to hide the totals row at the top of the page"
          },
          "hideTotalsBottom": {
            "type": "boolean",
            "description": "Whether to hide the totals row at the bottom of the page"
          },
          "labels": {
            "type": "object",
            "properties": {
              "grandTotalLabels": {
                "$ref": "#/components/schemas/Record%3Cstring%2Cstring%3E"
              },
              "tax": {
                "type": "string"
              },
              "outOfPocket": {
                "type": "string",
                "description": "Default out-of-pocket label for this pricing table."
              }
            },
            "additionalProperties": false,
            "description": "Custom label overrides for this page"
          },
          "displayHeading": {
            "type": "boolean",
            "description": "Whether to display the page heading"
          },
          "hideSection": {
            "type": "boolean",
            "description": "Hide this page from recipients (the editor \"Hide this Page\" toggle). Staff still see it in the proposal editor. Recipients do not see it on the public view, preview, sign, or default PDF. Hidden Scope of Work pages are left out of grand totals. This is not ArchivePage and not excludeFromPdf. Set false to show the page again."
          },
          "excludeFromPdf": {
            "type": "boolean",
            "description": "Whether to exclude this page from PDF exports"
          },
          "startNewPdfPage": {
            "type": "boolean",
            "description": "When false, this page continues on the same PDF page as the previous one. Omitted/undefined keeps the historical behavior: each page starts a new PDF page."
          },
          "children": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/PageChild"
            },
            "description": "Unified ordered list of groups and side-by-sides (only on scope-of-work pages)"
          }
        }
      },
      "ProposalPageCreateInput": {
        "type": "object",
        "additionalProperties": false,
        "properties": {
          "content": {
            "type": "string",
            "description": "Rich text content of the page"
          },
          "order_key": {
            "type": "number",
            "description": "Sort position of the page within the proposal"
          },
          "kind": {
            "type": "string",
            "description": "Type of page (e.g., content, pricing, signatures)"
          },
          "scopeId": {
            "type": "string",
            "description": "ID of the scope of work section this page belongs to"
          },
          "isScopeOfWork": {
            "type": "boolean",
            "description": "Whether this page is a scope of work pricing page"
          },
          "isSignaturesPage": {
            "type": "boolean",
            "description": "Whether this is the signatures page"
          },
          "screenshotUrl": {
            "type": [
              "string",
              "null"
            ],
            "description": "URL of the page screenshot thumbnail"
          },
          "showCents": {
            "type": "boolean",
            "description": "Whether to display cents in price values"
          },
          "useRoundedAmounts": {
            "type": "boolean",
            "description": "Use rounded whole-dollar line amounts in the price math. Only applies while showCents is off. Missing means off. Set from the account default when the proposal or template is created, or copied from the template a proposal is based on."
          },
          "salesTaxPercentage": {
            "type": "string",
            "description": "Sales tax percentage applied to this page"
          },
          "lineItemsStartExpanded": {
            "type": "string",
            "enum": [
              "none",
              "all",
              "first"
            ],
            "description": "Whether line items are expanded by default"
          },
          "selectionType": {
            "type": "string",
            "enum": [
              "single",
              "multiple",
              "loose"
            ],
            "description": "How items on this page can be selected"
          },
          "requireSelection": {
            "type": "boolean",
            "description": "When selectionType is loose, require the customer to select at least one optional group"
          },
          "tags": {
            "type": "array",
            "items": {
              "type": "string"
            },
            "description": "Tags/labels applied to this page"
          },
          "metadata": {
            "$ref": "#/components/schemas/Record%3Cstring%2Cunknown%3E"
          },
          "markupPercentage": {
            "type": "string",
            "description": "Markup percentage applied to items on this page"
          },
          "discounts": {
            "type": "array",
            "items": {
              "type": "object",
              "properties": {
                "id": {
                  "type": "string"
                },
                "type": {
                  "type": "string",
                  "enum": [
                    "fixed",
                    "percentage"
                  ]
                },
                "amount": {
                  "type": "string"
                },
                "frequency": {
                  "type": "string"
                }
              },
              "required": [
                "id",
                "type",
                "amount",
                "frequency"
              ],
              "additionalProperties": false
            },
            "description": "Discount configurations for this page"
          },
          "hideTotalsTop": {
            "type": "boolean",
            "description": "Whether to hide the totals row at the top of the page"
          },
          "hideTotalsBottom": {
            "type": "boolean",
            "description": "Whether to hide the totals row at the bottom of the page"
          },
          "labels": {
            "type": "object",
            "properties": {
              "grandTotalLabels": {
                "$ref": "#/components/schemas/Record%3Cstring%2Cstring%3E"
              },
              "tax": {
                "type": "string"
              },
              "outOfPocket": {
                "type": "string",
                "description": "Default out-of-pocket label for this pricing table."
              }
            },
            "additionalProperties": false,
            "description": "Custom label overrides for this page"
          },
          "displayHeading": {
            "type": "boolean",
            "description": "Whether to display the page heading"
          },
          "hideSection": {
            "type": "boolean",
            "description": "Hide this page from recipients (the editor \"Hide this Page\" toggle). Staff still see it in the proposal editor. Recipients do not see it on the public view, preview, sign, or default PDF. Hidden Scope of Work pages are left out of grand totals. This is not ArchivePage and not excludeFromPdf. Set false to show the page again."
          },
          "excludeFromPdf": {
            "type": "boolean",
            "description": "Whether to exclude this page from PDF exports"
          },
          "startNewPdfPage": {
            "type": "boolean",
            "description": "When false, this page continues on the same PDF page as the previous one. Omitted/undefined keeps the historical behavior: each page starts a new PDF page."
          },
          "designSettings": {
            "$ref": "#/components/schemas/PageDesignSettings"
          },
          "children": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/PageChild"
            },
            "description": "Unified ordered list of groups and side-by-sides (only on scope-of-work pages)"
          },
          "name": {
            "type": "string",
            "description": "Page name/title"
          }
        },
        "required": [
          "name"
        ],
        "description": "Input type for creating a new page Extends ProposalPageBase and requires proposal_id"
      },
      "SendProposalEmailInput": {
        "type": "object",
        "description": "Body for POST /api/proposals/{id}/send-email. The API key user's email signature is appended unless include_signature is false.",
        "required": [
          "recipients",
          "subject",
          "message",
          "fromEmail"
        ],
        "properties": {
          "recipients": {
            "description": "Who receives the proposal email. An array of name and email, or a comma-separated list of email addresses.",
            "example": [
              {
                "name": "Jane Smith",
                "email": "jane@acme.example"
              }
            ],
            "oneOf": [
              {
                "type": "array",
                "items": {
                  "type": "object",
                  "properties": {
                    "name": {
                      "type": "string",
                      "description": "Recipient name.",
                      "example": "Jane Smith"
                    },
                    "email": {
                      "type": "string",
                      "description": "Recipient email address.",
                      "example": "jane@acme.example"
                    }
                  },
                  "required": [
                    "email"
                  ]
                }
              },
              {
                "type": "string",
                "description": "Comma-separated email addresses.",
                "example": "jane@acme.example, alex@acme.example"
              }
            ]
          },
          "subject": {
            "type": "string",
            "description": "Email subject. Tokens such as [recipient_company_name] are replaced for each recipient.",
            "example": "Proposal for Acme Co"
          },
          "message": {
            "type": "string",
            "description": "Email body as HTML. Tokens such as [recipient_contact_first_name] are replaced for each recipient. The sender signature is appended after this HTML.",
            "example": "<p>Hi [recipient_contact_first_name], your proposal is ready.</p>"
          },
          "fromEmail": {
            "type": "string",
            "description": "Address shown as the sender.",
            "example": "eddie@yourcompany.example"
          },
          "include_signature": {
            "type": "boolean",
            "description": "Append an email signature. Defaults to true. Set false to send message exactly as given.",
            "example": true
          },
          "signature_user_id": {
            "type": "string",
            "description": "Public id of an organization member whose saved signature to append instead of the API key user. Ignored when include_signature is false.",
            "example": "uAb12Cd34Ef56"
          }
        }
      },
      "CreateProposalApiInput": {
        "type": "object",
        "description": "Body for POST /api/proposals. name is required. Pass templateId to copy a template, and settings.recipient to set the client on that copy. Pass integrations.crm.provider and integrations.crm.deal_id to link a HubSpot, Pipedrive, Zoho, or Go High Level record on create. When Add tax from CRM is on, a HubSpot deal tax rate is written onto the new proposal pricing pages. salesTaxPercentage on this request wins over that rate.",
        "required": [
          "name"
        ],
        "properties": {
          "name": {
            "type": "string",
            "description": "Proposal title. Required even when creating from a template.",
            "example": "Acme website redesign"
          },
          "templateId": {
            "type": "string",
            "description": "Public id of a template. Get it from GET /api/proposals?is_template=true. The new proposal copies that template. Omit to create a blank proposal.",
            "example": "zw5FD5HGcfE3"
          },
          "is_template": {
            "type": "boolean",
            "description": "Set true to create a reusable template instead of a proposal. Leave unset or false when creating a proposal from a template."
          },
          "settings": {
            "type": "object",
            "description": "Proposal settings. When creating from a template, only the keys you send replace the template values. recipient replaces the template client entirely.",
            "properties": {
              "recipient": {
                "type": "object",
                "description": "The client this proposal is for.",
                "properties": {
                  "name": {
                    "type": "string",
                    "description": "Company name. This is the Client on the proposal.",
                    "example": "Acme Co"
                  },
                  "address": {
                    "type": "string",
                    "description": "Mailing address of the company.",
                    "example": "123 Main St, Tulsa, OK"
                  },
                  "website": {
                    "type": "string",
                    "description": "Company website.",
                    "example": "https://acme.example"
                  },
                  "contact": {
                    "type": "object",
                    "description": "Primary contact at the company.",
                    "properties": {
                      "name": {
                        "type": "string",
                        "description": "Contact full name.",
                        "example": "Jane Smith"
                      },
                      "email": {
                        "type": "string",
                        "description": "Contact email address.",
                        "example": "jane@acme.example"
                      },
                      "phone": {
                        "type": "string",
                        "description": "Contact phone number.",
                        "example": "555-0100"
                      }
                    }
                  }
                }
              }
            }
          },
          "integrations": {
            "type": "object",
            "description": "Optional. Link this proposal to a CRM record when it is created. The organization must already have that CRM connected.",
            "properties": {
              "crm": {
                "type": "object",
                "description": "CRM record to link. We look up deal_id on the connected account and link the proposal to it. Name matching is not used. When Add tax from CRM is on, a HubSpot deal tax rate is written onto the new proposal pricing pages.",
                "required": [
                  "provider",
                  "deal_id"
                ],
                "properties": {
                  "provider": {
                    "type": "string",
                    "enum": [
                      "hubspot",
                      "pipedrive",
                      "zoho",
                      "gohighlevel"
                    ],
                    "description": "Connected CRM. hubspot, pipedrive, zoho, or gohighlevel.",
                    "example": "hubspot"
                  },
                  "deal_id": {
                    "type": "string",
                    "description": "Record id in that CRM. A HubSpot, Pipedrive, or Zoho deal id, or a Go High Level opportunity id.",
                    "example": "12345678901"
                  }
                }
              }
            }
          }
        }
      },
      "CreateProposalResponse": {
        "type": "object",
        "properties": {
          "id": {
            "type": "string",
            "description": "Unique identifier for the created proposal"
          }
        },
        "required": [
          "id"
        ],
        "additionalProperties": false,
        "description": "Response from creating a new proposal. Only the ID is returned;\ncallers navigate to the proposal editor which fetches the full detail."
      },
      "PageDesignSettings": {
        "type": "object",
        "properties": {
          "backgroundColor": {
            "type": "string",
            "description": "Background color (CSS color value)"
          },
          "fontColor": {
            "type": "string",
            "description": "Font/text color (CSS color value)"
          },
          "backgroundImage": {
            "type": "string",
            "description": "URL of the background image"
          },
          "backgroundPosition": {
            "type": "string",
            "description": "CSS background-position value"
          },
          "backgroundSize": {
            "type": "string",
            "description": "CSS background-size value"
          },
          "backgroundRepeat": {
            "type": "string",
            "description": "CSS background-repeat value"
          },
          "backgroundAttachment": {
            "type": "string",
            "description": "CSS background-attachment value"
          },
          "backgroundOpacity": {
            "type": "number",
            "description": "Opacity of the background image as a percent from 0 to 100"
          },
          "contentWidth": {
            "type": "string",
            "enum": [
              "full",
              "medium",
              "compact",
              "narrow"
            ],
            "description": "Override proposal-level page content max-width for this page. Omit to inherit from proposal design.styling.content_width."
          },
          "contentAlign": {
            "type": "string",
            "enum": [
              "left",
              "center",
              "right"
            ],
            "description": "Override proposal-level content column alignment for this page. Omit to inherit from proposal design.styling.content_align."
          }
        },
        "additionalProperties": false,
        "description": "Visual design settings for a specific page"
      },
      "PageChild": {
        "anyOf": [
          {
            "type": "object",
            "additionalProperties": false,
            "properties": {
              "kind": {
                "type": "string",
                "const": "group",
                "description": "Discriminator for PageChild union (set when part of children array)"
              },
              "id": {
                "type": "string",
                "description": "Unique group identifier"
              },
              "name": {
                "type": "string",
                "description": "Group name/title"
              },
              "description": {
                "type": "string",
                "description": "Group description text"
              },
              "internalNotes": {
                "type": "string",
                "description": "Admin-only internal notes (never shown to recipients unless Configure PDF includes them)"
              },
              "isOptional": {
                "type": "boolean",
                "description": "Whether this group is optional for the client"
              },
              "isSelected": {
                "type": "boolean",
                "description": "Whether this optional group is currently selected"
              },
              "hidden": {
                "type": "boolean",
                "description": "Hidden from the client (same meaning as line-item hidden)"
              },
              "locked": {
                "type": "boolean",
                "description": "Logic lock — not in totals, not writable"
              },
              "displayCost": {
                "type": "string",
                "enum": [
                  "show_all",
                  "hide_all",
                  "hide_item_costs",
                  "hide_group_cost"
                ],
                "description": "Whether to show the cost for this group"
              },
              "selectionType": {
                "type": "string",
                "enum": [
                  "single",
                  "multiple",
                  "loose"
                ],
                "description": "How items within this group can be selected (single, multiple, etc.)"
              },
              "requireSelection": {
                "type": "boolean",
                "description": "When selectionType is loose, require the customer to select at least one optional line item"
              },
              "lineItems": {
                "type": "array",
                "items": {
                  "$ref": "#/components/schemas/LineItem"
                },
                "description": "Array of line items in this group"
              },
              "hideQuantities": {
                "type": "boolean",
                "description": "Whether to hide quantity columns"
              },
              "startExpanded": {
                "type": "boolean",
                "description": "Whether the group starts expanded in the proposal view"
              },
              "pdfPageBreak": {
                "type": "boolean",
                "description": "When generating a PDF, start this group at the top of a new page"
              },
              "quantity": {
                "$ref": "#/components/schemas/QuantityConfig"
              },
              "order_key": {
                "type": "number",
                "description": "Sort position of the group within the page"
              },
              "tags": {
                "type": "array",
                "items": {
                  "type": "string"
                },
                "description": "Tags/labels applied to this group"
              },
              "linkedScopeId": {
                "type": [
                  "string",
                  "null"
                ],
                "description": "ID of the linked scope of work section"
              },
              "created_at": {
                "type": "string",
                "description": "When the group was created (ISO 8601 timestamp)"
              },
              "updated_at": {
                "type": "string",
                "description": "When the group was last updated (ISO 8601 timestamp)"
              },
              "metadata": {
                "$ref": "#/components/schemas/Record%3Cstring%2Cunknown%3E"
              },
              "archived_at": {
                "type": [
                  "string",
                  "null"
                ],
                "description": "When the group was archived (ISO 8601 timestamp), or null if active"
              },
              "is_archived": {
                "type": "boolean",
                "description": "Whether the group is archived"
              },
              "in_library": {
                "type": "boolean",
                "description": "Whether this group belongs to the library (true) or a regular proposal (false/undefined)"
              }
            },
            "required": [
              "displayCost",
              "id",
              "isOptional",
              "isSelected",
              "kind",
              "lineItems",
              "name",
              "selectionType"
            ]
          },
          {
            "type": "object",
            "additionalProperties": false,
            "properties": {
              "kind": {
                "type": "string",
                "const": "side_by_side",
                "description": "Discriminator for PageChild union (set when part of children array)"
              },
              "id": {
                "type": "string",
                "description": "Unique identifier for the side-by-side item"
              },
              "name": {
                "type": "string",
                "description": "Display name"
              },
              "description": {
                "type": "string",
                "description": "Description text"
              },
              "style": {
                "type": "string",
                "enum": [
                  "standard",
                  "comparison"
                ],
                "description": "Layout style: 'standard' (default column cards) or 'comparison' (feature comparison grid)"
              },
              "selectionType": {
                "type": "string",
                "enum": [
                  "multiple",
                  "single",
                  "loose"
                ],
                "description": "How options can be selected (single, multiple, etc.)"
              },
              "columns": {
                "type": "array",
                "items": {
                  "type": "object",
                  "properties": {
                    "id": {
                      "type": "string"
                    },
                    "name": {
                      "type": "string"
                    },
                    "description": {
                      "type": "string"
                    },
                    "price": {
                      "type": "number"
                    },
                    "frequency": {
                      "type": "string"
                    },
                    "isSelected": {
                      "type": "boolean"
                    },
                    "includedFeatures": {
                      "type": "array",
                      "items": {
                        "type": "string"
                      },
                      "description": "Feature IDs included in this column (only used when style === 'comparison')"
                    },
                    "featureValues": {
                      "$ref": "#/components/schemas/Record%3Cstring%2Cstring%3E",
                      "description": "Per-feature text overrides keyed by feature ID (renders text instead of check/X)"
                    },
                    "feeType": {
                      "type": "string",
                      "enum": [
                        "fixed",
                        "text",
                        "none"
                      ],
                      "description": "Fee type: 'fixed' shows price, 'text' shows text override, 'none' hides price"
                    },
                    "priceTextOverride": {
                      "type": "string",
                      "description": "Text to display instead of price when feeType is 'text'"
                    },
                    "quantity": {
                      "type": "object",
                      "properties": {
                        "min": {
                          "type": "number"
                        },
                        "max": {
                          "type": "number"
                        },
                        "editableByRecipient": {
                          "type": "boolean"
                        },
                        "current": {
                          "type": "number"
                        },
                        "unitLabel": {
                          "anyOf": [
                            {
                              "type": "string"
                            },
                            {
                              "type": "object",
                              "properties": {
                                "singular": {
                                  "type": "string"
                                },
                                "plural": {
                                  "type": "string"
                                }
                              },
                              "required": [
                                "singular",
                                "plural"
                              ],
                              "additionalProperties": false
                            }
                          ]
                        }
                      },
                      "required": [
                        "min",
                        "max",
                        "editableByRecipient"
                      ],
                      "additionalProperties": false,
                      "description": "Quantity configuration for this column"
                    },
                    "design": {
                      "type": "object",
                      "properties": {
                        "background": {
                          "type": "string"
                        },
                        "borderWidth": {
                          "type": "number"
                        },
                        "borderColor": {
                          "type": "string"
                        },
                        "borderRadius": {
                          "type": "number"
                        },
                        "buttonColor": {
                          "type": "string"
                        },
                        "buttonTextColor": {
                          "type": "string"
                        },
                        "titleColor": {
                          "type": "string"
                        },
                        "priceColor": {
                          "type": "string"
                        },
                        "checkmarkColor": {
                          "type": "string"
                        },
                        "fontColor": {
                          "type": "string"
                        },
                        "quantityBackgroundColor": {
                          "type": "string"
                        },
                        "quantityFontColor": {
                          "type": "string"
                        }
                      },
                      "additionalProperties": false
                    }
                  },
                  "required": [
                    "id",
                    "name",
                    "description",
                    "price",
                    "frequency"
                  ],
                  "additionalProperties": false
                },
                "description": "Array of comparison columns"
              },
              "features": {
                "type": "array",
                "items": {
                  "$ref": "#/components/schemas/SideBySideFeature"
                },
                "description": "Shared feature list (only used when style === 'comparison')"
              },
              "order_key": {
                "type": "number",
                "description": "Sort position"
              },
              "containerDesign": {
                "type": "object",
                "properties": {
                  "background": {
                    "type": "string"
                  },
                  "fontColor": {
                    "type": "string"
                  },
                  "showHeader": {
                    "type": "boolean"
                  },
                  "sizeModifier": {
                    "type": "string",
                    "enum": [
                      "compact",
                      "default",
                      "wide"
                    ]
                  },
                  "borderColor": {
                    "type": "string"
                  },
                  "borderWidth": {
                    "type": "number"
                  },
                  "selectionOutlineColor": {
                    "type": "string"
                  }
                },
                "additionalProperties": false,
                "description": "Visual design settings for the container"
              },
              "featuresColumnDesign": {
                "type": "object",
                "properties": {
                  "background": {
                    "type": "string"
                  },
                  "fontColor": {
                    "type": "string"
                  },
                  "borderColor": {
                    "type": "string"
                  },
                  "borderWidth": {
                    "type": "number"
                  },
                  "borderRadius": {
                    "type": "number"
                  }
                },
                "additionalProperties": false,
                "description": "Design settings for the features column (only used when style === 'comparison')"
              },
              "headingDesign": {
                "type": "object",
                "properties": {
                  "background": {
                    "type": "string"
                  },
                  "fontColor": {
                    "type": "string"
                  },
                  "alignment": {
                    "type": "string",
                    "enum": [
                      "center",
                      "left",
                      "right"
                    ]
                  }
                },
                "additionalProperties": false,
                "description": "Design settings for headings (only used when style === 'comparison')"
              },
              "syncQuantitiesAcrossColumns": {
                "type": "boolean",
                "description": "Whether to sync quantities across all columns"
              },
              "taxExempt": {
                "type": "boolean",
                "description": "Whether this side-by-side is exempt from sales tax"
              },
              "markupExempt": {
                "type": "boolean",
                "description": "Whether this side-by-side is exempt from scope-level markup"
              },
              "discountExempt": {
                "type": "boolean",
                "description": "Whether this side-by-side is exempt from proposal-wide percentage discounts"
              },
              "linkedLibraryItemId": {
                "type": [
                  "string",
                  "null"
                ],
                "description": "Public ID of the library side-by-side this is linked to (templates only — never set on regular proposals)"
              },
              "hasLinkedItems": {
                "type": "boolean",
                "description": "Whether this library side-by-side has other items linked to it (library items only)"
              },
              "hidden": {
                "type": "boolean",
                "description": "Hidden from recipients by Logic. The price is left out while this is set."
              },
              "locked": {
                "type": "boolean",
                "description": "Set with hidden so a recipient cannot change the packages while it is hidden."
              }
            },
            "required": [
              "columns",
              "id",
              "kind",
              "name"
            ]
          }
        ],
        "description": "Discriminated union for page children (groups and side-by-sides). Both share order_key from the same namespace for interleaved ordering."
      },
      "QuantityConfig": {
        "type": "object",
        "properties": {
          "min": {
            "type": "number",
            "description": "Minimum quantity allowed"
          },
          "max": {
            "type": "number",
            "description": "Maximum quantity allowed"
          },
          "editableByRecipient": {
            "type": "boolean",
            "description": "Whether the recipient can edit the quantity"
          },
          "current": {
            "type": "number",
            "description": "Current quantity value"
          },
          "unitLabel": {
            "anyOf": [
              {
                "type": "string"
              },
              {
                "type": "object",
                "properties": {
                  "singular": {
                    "type": "string"
                  },
                  "plural": {
                    "type": "string"
                  }
                },
                "required": [
                  "singular",
                  "plural"
                ],
                "additionalProperties": false
              }
            ],
            "description": "Unit label. A string is automatically singularized/pluralized.\nAn object supplies explicit singular and plural forms (both required)."
          },
          "volumePricingEnabled": {
            "type": "boolean",
            "description": "When true, unit price is selected from volumeTiers based on current quantity\n(volume pricing). Only valid for fixed (non-range) line items and modifications."
          },
          "volumeTiers": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/VolumeTier"
            },
            "description": "Ordered volume pricing tiers (used when volumePricingEnabled is true)"
          }
        },
        "required": [
          "min",
          "max",
          "editableByRecipient"
        ],
        "additionalProperties": false,
        "description": "Configuration for item quantity settings"
      },
      "SideBySideFeature": {
        "type": "object",
        "properties": {
          "id": {
            "type": "string",
            "description": "Unique identifier for this feature"
          },
          "name": {
            "type": "string",
            "description": "Display name of the feature"
          },
          "order_key": {
            "type": "number",
            "description": "Sort position"
          },
          "type": {
            "type": "string",
            "enum": [
              "feature",
              "heading"
            ],
            "description": "Type of row: 'feature' (default) or 'heading'"
          }
        },
        "required": [
          "id",
          "name",
          "order_key"
        ],
        "additionalProperties": false,
        "description": "A feature row used in comparison-style side-by-side items"
      },
      "VolumeTier": {
        "type": "object",
        "properties": {
          "upTo": {
            "type": "number",
            "description": "Inclusive upper bound of this tier"
          },
          "unitPrice": {
            "type": [
              "number",
              "null"
            ],
            "description": "Unit price when quantity falls in this tier.\nNull means unset (UI shows blank). The first tier always mirrors the\nline item / modification fixed cost regardless of this value."
          }
        },
        "required": [
          "upTo",
          "unitPrice"
        ],
        "additionalProperties": false,
        "description": "A single volume-pricing tier. Tiers are ordered; quantity bands are\nderived as [min, tiers[0].upTo], then (prev.upTo+1)..tiers[i].upTo.\nThe first tier's unit price always mirrors the line item / modification fixed cost."
      },
      "InternalCostConfig": {
        "type": "object",
        "properties": {
          "amount": {
            "type": "number",
            "description": "Dollars you pay (per unit when quantity applies)."
          }
        },
        "required": [
          "amount"
        ],
        "additionalProperties": false,
        "description": "Internal (your) cost on a line item or modification. Used by the profit_margins add-on. Never shown to customers.\n\nAmount is per unit when the parent has quantity; totals multiply by qty. Frequency follows the parent sell price for profitability rollups.\n\nWritable on create/update as `internalCost` or `internal_cost`."
      },
      "PriceConfig": {
        "description": "Price configuration - discriminated union of pricing models\nThe 'model' field determines which configuration type is used",
        "oneOf": [
          {
            "$ref": "#/components/schemas/FixedPriceConfig"
          },
          {
            "$ref": "#/components/schemas/PercentPriceConfig"
          },
          {
            "$ref": "#/components/schemas/TextPriceConfig"
          },
          {
            "$ref": "#/components/schemas/NonePriceConfig"
          },
          {
            "$ref": "#/components/schemas/HourlyPriceConfig"
          }
        ],
        "discriminator": {
          "propertyName": "model",
          "mapping": {
            "fixed": "#/components/schemas/FixedPriceConfig",
            "percent": "#/components/schemas/PercentPriceConfig",
            "text": "#/components/schemas/TextPriceConfig",
            "none": "#/components/schemas/NonePriceConfig"
          }
        }
      },
      "OutOfPocketConfig": {
        "type": "object",
        "properties": {
          "amount": {
            "type": "number",
            "description": "Fixed dollar amount billed to the client (not marked up, not taxed)."
          },
          "frequency": {
            "$ref": "#/components/schemas/Frequency",
            "description": "Billing frequency. When omitted, inherit from the parent line/mod."
          },
          "label": {
            "type": "string",
            "description": "Display label override (e.g. \"Ad Spend\"). Default: \"Out-of-Pocket\"."
          }
        },
        "required": [
          "amount"
        ],
        "additionalProperties": false,
        "description": "Out-of-pocket (pass-through) cost on a line item or modification. Fixed dollars only — no ranges, quantity settings, or discounts.\n\nFrequency is independent of the parent sell price. When omitted, inherit from the parent line/mod. Label defaults to \"Out-of-Pocket\" when omitted."
      },
      "LineItemDiscountConfig": {
        "type": "object",
        "properties": {
          "added": {
            "type": "boolean",
            "description": "Whether the discount has been added. Defaults to true on write when omitted."
          },
          "isSelected": {
            "type": "boolean",
            "description": "Whether the discount is currently selected/applied. Defaults to true on write when omitted."
          },
          "scope": {
            "type": "string",
            "enum": [
              "unit",
              "total",
              "line",
              "group",
              "scope"
            ],
            "description": "Scope of the discount. `unit` = per quantity; `total` (also\naccepted as `line`) = the whole line. `group` / `scope` are accepted aliases\nfor total-style discounts."
          },
          "type": {
            "type": "string",
            "enum": [
              "percentage",
              "fixed"
            ],
            "description": "Discount type: 'percentage' or 'fixed' amount"
          },
          "amount": {
            "type": "number",
            "description": "Discount amount. For type=\"percentage\" this is a whole-number\npercent (10 = 10%). For type=\"fixed\" this is dollars (not cents)."
          }
        },
        "additionalProperties": {},
        "description": "Configuration for discounts applied to line items"
      },
      "Frequency": {
        "type": "string",
        "enum": [
          "once",
          "weekly",
          "biweekly",
          "monthly",
          "semi-monthly",
          "quarterly",
          "semi-annually",
          "annually",
          "every-2-years",
          "every-3-years",
          "every-4-years",
          "every-5-years",
          "every-6-years",
          "every-7-years",
          "every-8-years",
          "every-9-years",
          "every-10-years"
        ],
        "examples": [
          "monthly"
        ],
        "description": "Billing frequency for recurring items"
      },
      "FixedPriceConfig": {
        "type": "object",
        "properties": {
          "value": {
            "type": "number",
            "description": "Price value. Required for the fixed and percent models.\nFor fixed: the unit price in major currency units (dollars for USD, not cents), e.g. 379 = $379.00.\nFor percent: a whole-number percent, e.g. 15 = 15% (not 0.15).\nA negative value makes the item or modification a discount."
          },
          "maxValue": {
            "type": "number",
            "description": "Maximum price value for range pricing"
          },
          "isRange": {
            "type": "boolean",
            "description": "Whether this represents a price range"
          },
          "quantity": {
            "anyOf": [
              {
                "$ref": "#/components/schemas/QuantityConfig"
              },
              {
                "type": "null"
              }
            ],
            "description": "Quantity configuration"
          },
          "discount": {
            "anyOf": [
              {
                "$ref": "#/components/schemas/LineItemDiscountConfig"
              },
              {
                "type": "null"
              }
            ],
            "description": "Per-line discount. Writable on create/update (`price.discount`\nor top-level `discount`). Pass null to clear. Percentage `amount` is a\nwhole-number percent (10 = 10%); fixed `amount` is dollars."
          },
          "model": {
            "type": "string",
            "const": "fixed",
            "description": "Pricing model type - must be \"fixed\""
          },
          "frequency": {
            "$ref": "#/components/schemas/Frequency",
            "description": "Billing frequency"
          }
        },
        "required": [
          "model",
          "frequency"
        ],
        "additionalProperties": false,
        "description": "Fixed price configuration - item has a fixed price per unit"
      },
      "PercentPriceConfig": {
        "type": "object",
        "properties": {
          "value": {
            "type": "number",
            "description": "Price value. Required for the fixed and percent models.\nFor fixed: the unit price in major currency units (dollars for USD, not cents), e.g. 379 = $379.00.\nFor percent: a whole-number percent, e.g. 15 = 15% (not 0.15).\nA negative value makes the item or modification a discount."
          },
          "maxValue": {
            "type": "number",
            "description": "Maximum price value for range pricing"
          },
          "isRange": {
            "type": "boolean",
            "description": "Whether this represents a price range"
          },
          "quantity": {
            "anyOf": [
              {
                "$ref": "#/components/schemas/QuantityConfig"
              },
              {
                "type": "null"
              }
            ],
            "description": "Quantity configuration"
          },
          "discount": {
            "anyOf": [
              {
                "$ref": "#/components/schemas/LineItemDiscountConfig"
              },
              {
                "type": "null"
              }
            ],
            "description": "Per-line discount. Writable on create/update (`price.discount`\nor top-level `discount`). Pass null to clear. Percentage `amount` is a\nwhole-number percent (10 = 10%); fixed `amount` is dollars."
          },
          "model": {
            "type": "string",
            "const": "percent",
            "description": "Pricing model type - must be \"percent\""
          },
          "frequency": {
            "$ref": "#/components/schemas/Frequency",
            "description": "Billing frequency"
          },
          "percentageApplyTo": {
            "type": "string",
            "enum": [
              "line",
              "group",
              "scope"
            ],
            "description": "What the percentage applies to: 'line' (parent line item), 'group' (parent group total), or 'scope' (proposal total)"
          }
        },
        "required": [
          "model",
          "frequency"
        ],
        "additionalProperties": false,
        "description": "Percentage-based price configuration - item price is a percentage of another value"
      },
      "TextPriceConfig": {
        "type": "object",
        "properties": {
          "value": {
            "type": "number",
            "description": "Price value. Required for the fixed and percent models.\nFor fixed: the unit price in major currency units (dollars for USD, not cents), e.g. 379 = $379.00.\nFor percent: a whole-number percent, e.g. 15 = 15% (not 0.15).\nA negative value makes the item or modification a discount."
          },
          "maxValue": {
            "type": "number",
            "description": "Maximum price value for range pricing"
          },
          "isRange": {
            "type": "boolean",
            "description": "Whether this represents a price range"
          },
          "quantity": {
            "anyOf": [
              {
                "$ref": "#/components/schemas/QuantityConfig"
              },
              {
                "type": "null"
              }
            ],
            "description": "Quantity configuration"
          },
          "discount": {
            "anyOf": [
              {
                "$ref": "#/components/schemas/LineItemDiscountConfig"
              },
              {
                "type": "null"
              }
            ],
            "description": "Per-line discount. Writable on create/update (`price.discount`\nor top-level `discount`). Pass null to clear. Percentage `amount` is a\nwhole-number percent (10 = 10%); fixed `amount` is dollars."
          },
          "model": {
            "type": "string",
            "const": "text",
            "description": "Pricing model type - must be \"text\""
          },
          "frequency": {
            "$ref": "#/components/schemas/Frequency",
            "description": "Billing frequency"
          }
        },
        "required": [
          "model",
          "frequency"
        ],
        "additionalProperties": false,
        "description": "Text-based price configuration - price is entered as text (e.g., \"Contact for pricing\")"
      },
      "NonePriceConfig": {
        "type": "object",
        "properties": {
          "value": {
            "type": "number",
            "description": "Price value. Required for the fixed and percent models.\nFor fixed: the unit price in major currency units (dollars for USD, not cents), e.g. 379 = $379.00.\nFor percent: a whole-number percent, e.g. 15 = 15% (not 0.15).\nA negative value makes the item or modification a discount."
          },
          "maxValue": {
            "type": "number",
            "description": "Maximum price value for range pricing"
          },
          "isRange": {
            "type": "boolean",
            "description": "Whether this represents a price range"
          },
          "quantity": {
            "anyOf": [
              {
                "$ref": "#/components/schemas/QuantityConfig"
              },
              {
                "type": "null"
              }
            ],
            "description": "Quantity configuration"
          },
          "discount": {
            "anyOf": [
              {
                "$ref": "#/components/schemas/LineItemDiscountConfig"
              },
              {
                "type": "null"
              }
            ],
            "description": "Per-line discount. Writable on create/update (`price.discount`\nor top-level `discount`). Pass null to clear. Percentage `amount` is a\nwhole-number percent (10 = 10%); fixed `amount` is dollars."
          },
          "model": {
            "type": "string",
            "const": "none",
            "description": "Pricing model type - must be \"none\""
          }
        },
        "required": [
          "model"
        ],
        "additionalProperties": false,
        "description": "No price configuration - item has no price"
      },
      "HourlyPriceConfig": {
        "type": "object",
        "properties": {
          "value": {
            "type": "number",
            "description": "Price value. Required for the fixed and percent models.\nFor fixed: the unit price in major currency units (dollars for USD, not cents), e.g. 379 = $379.00.\nFor percent: a whole-number percent, e.g. 15 = 15% (not 0.15).\nA negative value makes the item or modification a discount."
          },
          "maxValue": {
            "type": "number",
            "description": "Maximum price value for range pricing"
          },
          "isRange": {
            "type": "boolean",
            "description": "Whether this represents a price range"
          },
          "quantity": {
            "anyOf": [
              {
                "$ref": "#/components/schemas/QuantityConfig"
              },
              {
                "type": "null"
              }
            ],
            "description": "Quantity configuration"
          },
          "discount": {
            "anyOf": [
              {
                "$ref": "#/components/schemas/LineItemDiscountConfig"
              },
              {
                "type": "null"
              }
            ],
            "description": "Per-line discount. Writable on create/update (`price.discount`\nor top-level `discount`). Pass null to clear. Percentage `amount` is a\nwhole-number percent (10 = 10%); fixed `amount` is dollars."
          },
          "model": {
            "type": "string",
            "const": "hourly",
            "description": "Pricing model type - must be \"hourly\""
          },
          "frequency": {
            "$ref": "#/components/schemas/Frequency",
            "description": "Billing frequency"
          },
          "hourlyTasks": {
            "$ref": "#/components/schemas/Record%3Cstring%2C(number%7Cstructure-385897607-7295-7324-385897607-7286-7331-385897607-7271-7332-385897607-7178-7333-385897607-6884-7422-385897607-0-9530%7Cnull)%3E",
            "description": "Hourly tasks with their hours (single number or range)"
          },
          "hourlyTaskUseRanges": {
            "type": "boolean",
            "description": "Whether hourly tasks use ranges"
          }
        },
        "required": [
          "model",
          "frequency"
        ],
        "additionalProperties": false,
        "description": "Hourly price configuration - item price is calculated from hourly tasks"
      }
    },
    "responses": {
      "UnauthorizedError": {
        "description": "Authentication required",
        "content": {
          "application/json": {
            "schema": {
              "type": "object",
              "required": [
                "error"
              ],
              "properties": {
                "error": {
                  "type": "object",
                  "required": [
                    "code",
                    "message"
                  ],
                  "properties": {
                    "code": {
                      "type": "string",
                      "enum": [
                        "UNAUTHORIZED"
                      ],
                      "description": "Machine-readable error code"
                    },
                    "message": {
                      "type": "string",
                      "description": "Human-readable error message"
                    },
                    "details": {
                      "type": "object",
                      "description": "Additional error details"
                    }
                  }
                }
              }
            },
            "example": {
              "error": {
                "code": "UNAUTHORIZED",
                "message": "Authentication required"
              }
            }
          }
        }
      },
      "ForbiddenError": {
        "description": "Insufficient permissions",
        "content": {
          "application/json": {
            "schema": {
              "type": "object",
              "required": [
                "error"
              ],
              "properties": {
                "error": {
                  "type": "object",
                  "required": [
                    "code",
                    "message"
                  ],
                  "properties": {
                    "code": {
                      "type": "string",
                      "enum": [
                        "FORBIDDEN"
                      ],
                      "description": "Machine-readable error code"
                    },
                    "message": {
                      "type": "string",
                      "description": "Human-readable error message"
                    },
                    "details": {
                      "type": "object",
                      "description": "Additional error details"
                    }
                  }
                }
              }
            },
            "example": {
              "error": {
                "code": "FORBIDDEN",
                "message": "Insufficient permissions"
              }
            }
          }
        }
      },
      "ValidationError": {
        "description": "Validation error",
        "content": {
          "application/json": {
            "schema": {
              "type": "object",
              "required": [
                "error"
              ],
              "properties": {
                "error": {
                  "type": "object",
                  "required": [
                    "code",
                    "message"
                  ],
                  "properties": {
                    "code": {
                      "type": "string",
                      "enum": [
                        "VALIDATION_ERROR"
                      ],
                      "description": "Machine-readable error code"
                    },
                    "message": {
                      "type": "string",
                      "description": "Human-readable error message"
                    },
                    "details": {
                      "type": "object",
                      "description": "Validation error details"
                    }
                  }
                }
              }
            },
            "example": {
              "error": {
                "code": "VALIDATION_ERROR",
                "message": "Invalid input data",
                "details": {
                  "name": "Name is required"
                }
              }
            }
          }
        }
      },
      "InternalServerError": {
        "description": "Internal server error",
        "content": {
          "application/json": {
            "schema": {
              "type": "object",
              "required": [
                "error"
              ],
              "properties": {
                "error": {
                  "type": "object",
                  "required": [
                    "code",
                    "message"
                  ],
                  "properties": {
                    "code": {
                      "type": "string",
                      "enum": [
                        "INTERNAL_ERROR"
                      ],
                      "description": "Machine-readable error code"
                    },
                    "message": {
                      "type": "string",
                      "description": "Human-readable error message"
                    },
                    "details": {
                      "type": "object",
                      "description": "Additional error details"
                    }
                  }
                }
              }
            },
            "example": {
              "error": {
                "code": "INTERNAL_ERROR",
                "message": "An unexpected error occurred"
              }
            }
          }
        }
      },
      "NotFoundOrNotAuthorizedError": {
        "description": "Not Found - Resource does not exist or access denied",
        "content": {
          "application/json": {
            "schema": {
              "type": "object",
              "required": [
                "error"
              ],
              "properties": {
                "error": {
                  "type": "object",
                  "required": [
                    "code",
                    "message"
                  ],
                  "properties": {
                    "code": {
                      "type": "string",
                      "enum": [
                        "NOT_FOUND"
                      ],
                      "description": "Machine-readable error code"
                    },
                    "message": {
                      "type": "string",
                      "description": "Human-readable error message"
                    },
                    "details": {
                      "type": "object",
                      "description": "Additional error details"
                    }
                  }
                }
              }
            },
            "example": {
              "error": {
                "code": "NOT_FOUND",
                "message": "Not Authorized or Not Found: Resource {id}"
              }
            }
          }
        }
      }
    }
  },
  "security": [
    {
      "BearerAuth": []
    },
    {
      "ApiKeyAuth": []
    }
  ],
  "paths": {
    "/api/groups/{id}/archive": {
      "post": {
        "operationId": "ArchiveGroup",
        "summary": "Archive a group",
        "description": "Remove a group from the proposal in a way you can undo. The archived group, and the line items shown inside it, are hidden from the proposal and left out of default list responses. Bring it back with UnarchiveGroup. Proposals that are pending signature, won, or lost cannot be edited (409).\n",
        "tags": [
          "Groups"
        ],
        "parameters": [
          {
            "in": "path",
            "name": "id",
            "required": true,
            "schema": {
              "type": "string"
            },
            "description": "Public ID of the group, as returned by the API (for example zw5FD5HGcfE3).",
            "example": "zw5FD5HGcfE3"
          }
        ],
        "responses": {
          "204": {
            "description": "Group archived. No response body."
          },
          "401": {
            "$ref": "#/components/responses/UnauthorizedError"
          },
          "404": {
            "description": "Group not found."
          },
          "409": {
            "description": "The proposal is locked (pending signature, won, lost, or a locked customer version)."
          },
          "500": {
            "$ref": "#/components/responses/InternalServerError"
          }
        }
      }
    },
    "/api/groups/{id}/duplicate": {
      "post": {
        "operationId": "DuplicateGroup",
        "summary": "Duplicate a group",
        "description": "Copy a group with its settings, line items, and their modifications. Prices are copied as they are. With an empty body the copy goes on the same Scope of Work page, named \"<original name> - Copy\".\nTo copy into another proposal, send proposalId (Public ID of the destination proposal) and parent_item_id (Public ID of a Scope of Work page in that proposal). A copy into another proposal keeps the original name unless you send name. The destination cannot be pending signature, won, or lost.\nOther modes: toLibrary true saves the group to your library, and toGroup overwrites an existing group with this group's content.\n",
        "tags": [
          "Groups"
        ],
        "parameters": [
          {
            "in": "path",
            "name": "id",
            "required": true,
            "schema": {
              "type": "string"
            },
            "description": "Public ID of the group to copy, as returned by the API (for example zw5FD5HGcfE3).",
            "example": "zw5FD5HGcfE3"
          }
        ],
        "requestBody": {
          "required": false,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "properties": {
                  "name": {
                    "type": "string",
                    "description": "Name for the copy. Default is the original name plus \" - Copy\" in the same proposal, or the original name when copying into another proposal.\n",
                    "example": "Website Development - Copy"
                  },
                  "includeItems": {
                    "type": "boolean",
                    "description": "Also copy the group's line items and their modifications.",
                    "default": true
                  },
                  "orderKey": {
                    "type": "number",
                    "description": "Position for the copy on the page. Optional.",
                    "example": 1.5
                  },
                  "proposalId": {
                    "type": "string",
                    "description": "Public ID of the destination proposal, to copy into another proposal."
                  },
                  "parent_item_id": {
                    "type": "string",
                    "description": "Public ID of the destination Scope of Work page. Required with proposalId."
                  },
                  "toLibrary": {
                    "type": "boolean",
                    "description": "Save a copy to your organization's library instead.",
                    "default": false
                  },
                  "tags": {
                    "type": "array",
                    "items": {
                      "type": "string"
                    },
                    "description": "With toLibrary. Tags for the library copy."
                  },
                  "selectionType": {
                    "type": "string",
                    "enum": [
                      "single",
                      "multiple",
                      "loose"
                    ],
                    "description": "With toLibrary. Selection type for the library copy."
                  },
                  "toGroup": {
                    "type": "string",
                    "description": "Public ID of an existing group to overwrite with this group's content (the \"update from\" action). No new group is created.\n"
                  }
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Target group updated (when toGroup is set)."
          },
          "201": {
            "description": "Copy created. data.id is the Public ID of the new group, and data.lineItems lists the copied line items with their new ids.\n"
          },
          "400": {
            "description": "Validation error, for example parent_item_id missing or not a Scope of Work page in the destination proposal, or the destination proposal is pending signature, won, or lost.\n"
          },
          "401": {
            "$ref": "#/components/responses/UnauthorizedError"
          },
          "404": {
            "description": "Group, destination proposal, or destination page not found."
          },
          "409": {
            "description": "The source proposal is locked (pending signature, won, lost, or a locked customer version)."
          },
          "500": {
            "$ref": "#/components/responses/InternalServerError"
          }
        }
      }
    },
    "/api/groups/{id}/items/reorder": {
      "post": {
        "operationId": "ReorderItems",
        "summary": "Reorder line items in a group",
        "description": "Set the display order of line items inside one group. Send itemIds as the Public IDs of the group's line items in the order you want; the first id is shown first. Every id must be a line item in this group, otherwise the request fails with 400 and nothing changes. Line items you leave out keep their current position value. To move one line item, or move it into a different group, use MoveItem. Proposals that are pending signature, won, or lost cannot be edited (409).\n",
        "tags": [
          "Line Items"
        ],
        "parameters": [
          {
            "in": "path",
            "name": "id",
            "required": true,
            "schema": {
              "type": "string"
            },
            "description": "Public ID of the group, as returned by the API (for example zw5FD5HGcfE3).",
            "example": "zw5FD5HGcfE3"
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "required": [
                  "itemIds"
                ],
                "properties": {
                  "itemIds": {
                    "type": "array",
                    "items": {
                      "type": "string"
                    },
                    "description": "Public IDs of line items in this group, in the desired order. Must not be empty.",
                    "example": [
                      "qeP5CKvwBveC",
                      "N66wN1Yhese9",
                      "Vv5EYHv5aNEx"
                    ]
                  }
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Items reordered successfully",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "message": {
                      "type": "string",
                      "example": "Items reordered successfully"
                    },
                    "data": {
                      "type": "object",
                      "properties": {
                        "reorderedCount": {
                          "type": "integer",
                          "description": "Number of items that were reordered",
                          "example": 3
                        }
                      }
                    }
                  }
                }
              }
            }
          },
          "400": {
            "$ref": "#/components/responses/ValidationError"
          },
          "401": {
            "$ref": "#/components/responses/UnauthorizedError"
          },
          "403": {
            "$ref": "#/components/responses/ForbiddenError"
          },
          "404": {
            "description": "Group not found."
          },
          "409": {
            "description": "The proposal is locked (pending signature, won, lost, or a locked customer version)."
          },
          "500": {
            "$ref": "#/components/responses/InternalServerError"
          }
        }
      }
    },
    "/api/groups/{id}/items": {
      "get": {
        "operationId": "ListItems",
        "summary": "List line items in a group",
        "description": "Return the line items in one group, in display order by default. Each line item includes its Public ID (id) and its price. For fixed prices, price.value is in dollars, not cents. To list a line item's modifications, use ListModifications. Archived and deleted line items are not included. Use a returned id with UpdateItem (PATCH /api/items/{id}) to change a line item's price.\n",
        "tags": [
          "Line Items"
        ],
        "parameters": [
          {
            "in": "path",
            "name": "id",
            "required": true,
            "schema": {
              "type": "string"
            },
            "description": "Public ID of the group, as returned by the API (for example zw5FD5HGcfE3).",
            "example": "zw5FD5HGcfE3"
          },
          {
            "in": "query",
            "name": "page",
            "schema": {
              "type": "integer",
              "minimum": 1,
              "default": 1
            },
            "description": "Page number for pagination"
          },
          {
            "in": "query",
            "name": "limit",
            "schema": {
              "type": "integer",
              "minimum": 1,
              "maximum": 100,
              "default": 20
            },
            "description": "Number of line items per page (max 100)."
          },
          {
            "in": "query",
            "name": "search",
            "schema": {
              "type": "string"
            },
            "description": "Search term to filter items by name"
          },
          {
            "in": "query",
            "name": "sort",
            "schema": {
              "type": "string",
              "enum": [
                "order_key",
                "name",
                "created_at",
                "updated_at"
              ],
              "default": "order_key"
            },
            "description": "Field to sort by"
          },
          {
            "in": "query",
            "name": "order",
            "schema": {
              "type": "string",
              "enum": [
                "asc",
                "desc"
              ],
              "default": "asc"
            },
            "description": "Sort order"
          }
        ],
        "responses": {
          "200": {
            "description": "Items retrieved successfully",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "data": {
                      "type": "array",
                      "items": {
                        "$ref": "#/components/schemas/LineItem"
                      }
                    },
                    "pagination": {
                      "$ref": "#/components/schemas/Pagination"
                    }
                  }
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/UnauthorizedError"
          },
          "404": {
            "description": "Group not found"
          },
          "500": {
            "$ref": "#/components/responses/InternalServerError"
          }
        }
      }
    },
    "/api/groups/{id}/move": {
      "post": {
        "operationId": "MoveGroup",
        "summary": "Move a group",
        "description": "Change a group's position on its Scope of Work page. Set after_id to the Public ID of another group on the same page to place this group right after it, or to the Public ID of the proposal to place it first. The group stays on its current page, and its line items move with it. To set the order of many groups at once, use ReorderProposalGroups. Proposals that are pending signature, won, or lost cannot be edited (409).\n",
        "tags": [
          "Groups"
        ],
        "parameters": [
          {
            "in": "path",
            "name": "id",
            "required": true,
            "schema": {
              "type": "string"
            },
            "description": "Public ID of the group to move, as returned by the API (for example zw5FD5HGcfE3).",
            "example": "zw5FD5HGcfE3"
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "required": [
                  "after_id"
                ],
                "properties": {
                  "after_id": {
                    "type": "string",
                    "description": "Public ID of the group to place this one after, or Public ID of the proposal to place it first on its page.\n",
                    "example": "ijklmnop5678"
                  }
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Group moved.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "data": {
                      "type": "object",
                      "properties": {
                        "id": {
                          "type": "string",
                          "description": "Public ID of the moved group.",
                          "example": "zw5FD5HGcfE3"
                        }
                      }
                    }
                  }
                }
              }
            }
          },
          "400": {
            "description": "after_id is missing, or it is not a group or the proposal this group belongs to."
          },
          "401": {
            "$ref": "#/components/responses/UnauthorizedError"
          },
          "404": {
            "description": "Group not found."
          },
          "409": {
            "description": "The proposal is locked (pending signature, won, lost, or a locked customer version)."
          },
          "500": {
            "$ref": "#/components/responses/InternalServerError"
          }
        }
      }
    },
    "/api/groups/{id}": {
      "get": {
        "operationId": "GetGroup",
        "summary": "Get a group",
        "description": "Return one group with its settings and its line items. Each line item includes its Public ID, its price, and its modifications (children). For fixed prices, price.value is in dollars, not cents. Use the line item and modification ids with UpdateItem and UpdateModification to change prices.\n",
        "tags": [
          "Groups"
        ],
        "parameters": [
          {
            "in": "path",
            "name": "id",
            "required": true,
            "schema": {
              "type": "string"
            },
            "description": "Public ID of the group, as returned by the API (for example zw5FD5HGcfE3).",
            "example": "zw5FD5HGcfE3"
          }
        ],
        "responses": {
          "200": {
            "description": "The group, including lineItems.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "data": {
                      "$ref": "#/components/schemas/ProposalGroup"
                    }
                  }
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/UnauthorizedError"
          },
          "404": {
            "description": "Group not found."
          },
          "500": {
            "$ref": "#/components/responses/InternalServerError"
          }
        }
      },
      "patch": {
        "operationId": "UpdateGroup",
        "summary": "Update a group",
        "description": "Change a group's name, description, or settings. Only the fields you send change. The body is flat, with no \"group\" wrapper. Example: { \"name\": \"Phase 2\", \"isOptional\": true, \"selectionType\": \"single\" }.\nA group has no price of its own; its total comes from its line items. To change a line item's price, use UpdateItem (PATCH /api/items/{id}). quantity sets a group-level multiplier applied to the group's line items.\nProposals that are pending signature, won, or lost cannot be edited (409 PROPOSAL_LOCKED), and neither can a locked customer version.\n",
        "tags": [
          "Groups"
        ],
        "parameters": [
          {
            "in": "path",
            "name": "id",
            "required": true,
            "schema": {
              "type": "string"
            },
            "description": "Public ID of the group, as returned by the API (for example zw5FD5HGcfE3).",
            "example": "zw5FD5HGcfE3"
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/UpdateGroupApiInput"
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Group updated. data.id is the group's Public ID. Settings such as selectionType and displayCost are returned inside data.attrs.\n",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "data": {
                      "type": "object",
                      "properties": {
                        "id": {
                          "type": "string",
                          "description": "Public ID of the group.",
                          "example": "zw5FD5HGcfE3"
                        },
                        "name": {
                          "type": "string"
                        },
                        "kind": {
                          "type": "string",
                          "example": "group"
                        },
                        "order_key": {
                          "type": "number"
                        },
                        "optional": {
                          "type": "boolean"
                        },
                        "selected": {
                          "type": "boolean"
                        },
                        "attrs": {
                          "type": "object",
                          "description": "Stored group settings."
                        },
                        "created_at": {
                          "type": "string",
                          "format": "date-time"
                        },
                        "updated_at": {
                          "type": "string",
                          "format": "date-time"
                        }
                      }
                    }
                  }
                }
              }
            }
          },
          "400": {
            "description": "Validation error (for example an empty name or an invalid selectionType)."
          },
          "401": {
            "$ref": "#/components/responses/UnauthorizedError"
          },
          "404": {
            "description": "Group not found."
          },
          "409": {
            "description": "The proposal is locked (PROPOSAL_LOCKED: pending signature, won, lost, or a locked customer version), or a library group with this name already exists (DUPLICATE_NAME).\n",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "error": {
                      "type": "object",
                      "properties": {
                        "code": {
                          "type": "string",
                          "example": "DUPLICATE_NAME"
                        },
                        "message": {
                          "type": "string",
                          "example": "A group with the name \"Group Name\" already exists in the library."
                        },
                        "existingGroupId": {
                          "type": "string",
                          "description": "Public ID of the existing library group."
                        }
                      }
                    }
                  }
                }
              }
            }
          },
          "500": {
            "$ref": "#/components/responses/InternalServerError"
          }
        }
      },
      "delete": {
        "operationId": "DeleteGroup",
        "summary": "Delete a group",
        "description": "Delete a group and every line item in it. They are soft-deleted: they disappear from the proposal and from API responses, and the proposal totals are recalculated. To remove a group but keep the option to bring it back, use ArchiveGroup and UnarchiveGroup instead. Requires permission to delete proposals.\n",
        "tags": [
          "Groups"
        ],
        "parameters": [
          {
            "in": "path",
            "name": "id",
            "required": true,
            "schema": {
              "type": "string"
            },
            "description": "Public ID of the group, as returned by the API (for example zw5FD5HGcfE3).",
            "example": "zw5FD5HGcfE3"
          }
        ],
        "responses": {
          "204": {
            "description": "Group deleted. No response body."
          },
          "401": {
            "$ref": "#/components/responses/UnauthorizedError"
          },
          "403": {
            "description": "Not authorized to delete from this proposal."
          },
          "404": {
            "description": "Group not found."
          },
          "500": {
            "$ref": "#/components/responses/InternalServerError"
          }
        }
      }
    },
    "/api/groups/{id}/unarchive": {
      "post": {
        "operationId": "UnarchiveGroup",
        "summary": "Unarchive a group",
        "description": "Restore an archived group so it shows in the proposal again, with its line items and settings. Proposals that are pending signature, won, or lost cannot be edited (409).\n",
        "tags": [
          "Groups"
        ],
        "parameters": [
          {
            "in": "path",
            "name": "id",
            "required": true,
            "schema": {
              "type": "string"
            },
            "description": "Public ID of the archived group, as returned by the API (for example zw5FD5HGcfE3).",
            "example": "zw5FD5HGcfE3"
          }
        ],
        "responses": {
          "204": {
            "description": "Group restored. No response body."
          },
          "401": {
            "$ref": "#/components/responses/UnauthorizedError"
          },
          "404": {
            "description": "Group not found."
          },
          "409": {
            "description": "The proposal is locked (pending signature, won, lost, or a locked customer version)."
          },
          "500": {
            "$ref": "#/components/responses/InternalServerError"
          }
        }
      }
    },
    "/api/groups": {
      "post": {
        "operationId": "CreateGroup",
        "summary": "Create a group",
        "description": "Create an empty group (a set of line items shown together in a pricing table). Send exactly one parent:\n- parent_proposal: Public ID of a proposal. The group goes on the\n  proposal's first Scope of Work page.\n\n- parent_page: Public ID of a Scope of Work page.\n- in_library: true saves the group to your organization's library.\nThe group's fields go inside \"group\"; only group.name is required. The new group is added at the end of the page. Example: { \"parent_proposal\": \"zw5FD5HGcfE3\", \"group\": { \"name\": \"Phase 1\", \"isOptional\": false } }.\nA group has no price of its own. Add line items with CreateItem (parent_group set to the returned id) or with POST /api/proposals/{id}/items (type line_item, target_group_id). To place a group next to an existing one, POST /api/proposals/{id}/items with type group is usually simpler. Proposals that are pending signature, won, or lost cannot be edited (409).\n",
        "tags": [
          "Groups"
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "description": "Send exactly one of parent_proposal, parent_page, or in_library.",
                "properties": {
                  "parent_proposal": {
                    "type": "string",
                    "description": "Public ID of the proposal. The group is added to its first Scope of Work page.",
                    "example": "zw5FD5HGcfE3"
                  },
                  "parent_page": {
                    "type": "string",
                    "description": "Public ID of a Scope of Work page to add the group to."
                  },
                  "in_library": {
                    "type": "boolean",
                    "description": "Create the group in your organization's library."
                  },
                  "group": {
                    "$ref": "#/components/schemas/CreateGroupContent"
                  }
                },
                "required": [
                  "group"
                ]
              }
            }
          }
        },
        "responses": {
          "201": {
            "description": "Group created.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "data": {
                      "type": "object",
                      "properties": {
                        "id": {
                          "type": "string",
                          "description": "Public ID of the new group.",
                          "example": "zw5FD5HGcfE3"
                        },
                        "name": {
                          "type": "string",
                          "description": "Group name."
                        },
                        "kind": {
                          "type": "string",
                          "example": "group"
                        },
                        "parent_id": {
                          "type": "string",
                          "nullable": true,
                          "description": "Public ID of the Scope of Work page the group is on (null for library groups)."
                        }
                      }
                    }
                  }
                }
              }
            }
          },
          "400": {
            "$ref": "#/components/responses/ValidationError"
          },
          "401": {
            "$ref": "#/components/responses/UnauthorizedError"
          },
          "403": {
            "$ref": "#/components/responses/ForbiddenError"
          },
          "404": {
            "description": "Proposal, Scope of Work page, or organization not found."
          },
          "409": {
            "description": "The proposal is locked (pending signature, won, lost, or a locked customer version), or a library group with this name already exists (only when in_library is true).\n"
          },
          "500": {
            "$ref": "#/components/responses/InternalServerError"
          }
        }
      }
    },
    "/api/items/{id}/archive": {
      "post": {
        "operationId": "ArchiveItem",
        "summary": "Archive a line item, side-by-side, or modification",
        "description": "Same URL for a line item, a side-by-side, or a modification. Pass that object's Public ID (the id the API returned), not the editor Item ID field (externalItemId).\nLine item: POST /api/items/zw5FD5HGcfE3/archive. Side-by-side: POST /api/items/Hs3kW8nYb4Rd/archive (the side-by-side's Public ID). Modification: POST /api/items/Qm7rT2vXk9Lp/archive (the modification's Public ID).\nArchiving removes the object from the proposal in a way you can undo. Archived objects are hidden from the proposal and left out of list responses. Bring one back with UnarchiveItem. Proposals that are pending signature, won, or lost cannot be edited (409).\n",
        "tags": [
          "Line Items",
          "Side-by-Sides",
          "Modifications"
        ],
        "parameters": [
          {
            "in": "path",
            "name": "id",
            "required": true,
            "schema": {
              "type": "string"
            },
            "description": "Public ID of the line item, side-by-side, or modification, as returned by the API (for example zw5FD5HGcfE3). Not the editor \"Item ID\" (externalItemId).\n",
            "example": "zw5FD5HGcfE3"
          }
        ],
        "responses": {
          "204": {
            "description": "Item archived. No response body."
          },
          "401": {
            "$ref": "#/components/responses/UnauthorizedError"
          },
          "404": {
            "description": "Item not found."
          },
          "409": {
            "description": "The proposal is locked (pending signature, won, lost, or a locked customer version)."
          },
          "500": {
            "$ref": "#/components/responses/InternalServerError"
          }
        }
      }
    },
    "/api/items/{id}/duplicate": {
      "post": {
        "operationId": "DuplicateItem",
        "summary": "Duplicate a line item, side-by-side, or modification",
        "description": "Same URL for a line item, a side-by-side, or a modification. Pass the Public ID of the object to copy (the id the API returned), not the editor Item ID field (externalItemId).\nLine item: POST /api/items/zw5FD5HGcfE3/duplicate with an empty body places the copy right after the original in the same group, named \"<original name> - Copy\", with its modifications copied too.\nSide-by-side: POST /api/items/Hs3kW8nYb4Rd/duplicate with an empty body adds \"Copy of <original name>\" at the end of the same Scope of Work page, with the same columns and features. The response includes attrs (columns and features).\nModification: POST /api/items/Qm7rT2vXk9Lp/duplicate with an empty body places the copy right after the original under the same line item, named \"<original name> - Copy\". A modification can only be copied within its own proposal; to move it to another proposal, copy its parent line item.\nTo copy a line item or side-by-side into another proposal, send proposalId (Public ID of the destination proposal) and parent_item_id (Public ID of the destination group for a line item, or of a Scope of Work page for a side-by-side). A copy into another proposal keeps the original name unless you send name. The destination cannot be pending signature, won, or lost.\nLine items only: toLibrary true saves the line item to your library, and toItem overwrites an existing line item with this one's content.\nTo change the copy's price afterwards, PATCH /api/items/{id} (line item or side-by-side) or PATCH /api/items/{id}/modifications/{modificationId} (modification) with the id returned here.\n",
        "tags": [
          "Line Items",
          "Side-by-Sides",
          "Modifications"
        ],
        "parameters": [
          {
            "in": "path",
            "name": "id",
            "required": true,
            "schema": {
              "type": "string"
            },
            "description": "Public ID of the line item, side-by-side, or modification to copy, as returned by the API (for example zw5FD5HGcfE3). Not the editor \"Item ID\" (externalItemId).\n",
            "example": "zw5FD5HGcfE3"
          }
        ],
        "requestBody": {
          "required": false,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "properties": {
                  "name": {
                    "type": "string",
                    "description": "Name for the copy. Default in the same proposal is \"<original name> - Copy\" (line items and modifications) or \"Copy of <original name>\" (side-by-sides); the original name when copying into another proposal.\n",
                    "example": "Frontend Development - Copy"
                  },
                  "includeModifiers": {
                    "type": "boolean",
                    "description": "Also copy the line item's modifications.",
                    "default": true
                  },
                  "orderKey": {
                    "type": "number",
                    "description": "Position for the copy. Default is right after the original."
                  },
                  "proposalId": {
                    "type": "string",
                    "description": "Public ID of the destination proposal, to copy into another proposal.",
                    "example": "aB3dE5fG7hJ9"
                  },
                  "parent_item_id": {
                    "type": "string",
                    "description": "With proposalId. Public ID of the destination group (for a line item) or Scope of Work page (for a side-by-side).\n"
                  },
                  "toLibrary": {
                    "type": "boolean",
                    "description": "Save a copy to your organization's library instead.",
                    "default": false
                  },
                  "tags": {
                    "type": "array",
                    "items": {
                      "type": "string"
                    },
                    "description": "With toLibrary. Tags for the library copy."
                  },
                  "toItem": {
                    "type": "string",
                    "description": "Public ID of an existing item to overwrite with this item's content (the \"update from\" action). No new item is created.\n"
                  }
                }
              }
            }
          }
        },
        "responses": {
          "201": {
            "description": "Copy created. item.id is the Public ID of the copy.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "item": {
                      "$ref": "#/components/schemas/LineItem"
                    }
                  }
                }
              }
            }
          },
          "400": {
            "description": "Validation error, for example parent_item_id missing when copying to another proposal, a modification sent with a different proposalId, or the destination proposal is pending signature, won, or lost.\n"
          },
          "401": {
            "$ref": "#/components/responses/UnauthorizedError"
          },
          "404": {
            "description": "Line item, destination proposal, or destination parent not found."
          },
          "409": {
            "description": "The source proposal is locked (pending signature, won, lost, or a locked customer version)."
          },
          "500": {
            "$ref": "#/components/responses/InternalServerError"
          }
        }
      }
    },
    "/api/items/{id}/items": {
      "get": {
        "operationId": "ListModifications",
        "summary": "List modifications on a line item",
        "description": "Return the modifications (add-ons, upsells, options, discounts, markups) and dividers under one line item, in order. Each entry includes its Public ID (id) and its current price; for fixed prices, price.value is in dollars, not cents. Archived and deleted modifications are not included. Use a returned id with UpdateModification to change a modification's price.\n",
        "tags": [
          "Modifications"
        ],
        "parameters": [
          {
            "in": "path",
            "name": "id",
            "required": true,
            "schema": {
              "type": "string"
            },
            "description": "Public ID of the parent line item, as returned by the API (for example zw5FD5HGcfE3). Not the editor \"Item ID\" (externalItemId).\n",
            "example": "zw5FD5HGcfE3"
          },
          {
            "in": "query",
            "name": "page",
            "schema": {
              "type": "integer",
              "minimum": 1,
              "default": 1
            },
            "description": "Page number for pagination"
          },
          {
            "in": "query",
            "name": "limit",
            "schema": {
              "type": "integer",
              "minimum": 1,
              "maximum": 100,
              "default": 20
            },
            "description": "Number of modifications per page (max 100)."
          },
          {
            "in": "query",
            "name": "search",
            "schema": {
              "type": "string"
            },
            "description": "Search term to filter modifications by name"
          },
          {
            "in": "query",
            "name": "sort",
            "schema": {
              "type": "string",
              "enum": [
                "order_key",
                "name",
                "created_at",
                "updated_at"
              ],
              "default": "order_key"
            },
            "description": "Field to sort by"
          },
          {
            "in": "query",
            "name": "order",
            "schema": {
              "type": "string",
              "enum": [
                "asc",
                "desc"
              ],
              "default": "asc"
            },
            "description": "Sort order"
          }
        ],
        "responses": {
          "200": {
            "description": "Modifications retrieved successfully",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "data": {
                      "type": "array",
                      "description": "Array of modifications",
                      "items": {
                        "$ref": "#/components/schemas/Modification"
                      }
                    },
                    "pagination": {
                      "type": "object",
                      "description": "Pagination information",
                      "properties": {
                        "page": {
                          "type": "integer",
                          "description": "Current page number (1-indexed)",
                          "example": 1
                        },
                        "limit": {
                          "type": "integer",
                          "description": "Number of items per page",
                          "example": 20
                        },
                        "total": {
                          "type": "integer",
                          "description": "Total number of modifications",
                          "example": 5
                        },
                        "pages": {
                          "type": "integer",
                          "description": "Total number of pages",
                          "example": 1
                        },
                        "offset": {
                          "type": "integer",
                          "description": "Offset for pagination",
                          "example": 0
                        }
                      }
                    }
                  },
                  "required": [
                    "data",
                    "pagination"
                  ]
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/UnauthorizedError"
          },
          "404": {
            "description": "Line item not found."
          },
          "500": {
            "$ref": "#/components/responses/InternalServerError"
          }
        }
      }
    },
    "/api/items/{id}/modification": {
      "post": {
        "operationId": "CreateModification",
        "summary": "Create a modification on a line item",
        "description": "Add a modification (an add-on, upsell, option, discount, or markup) to an existing line item. Only name is required.\nPricing uses the same price object as line items. If you omit price, the modification is a fixed $0 one-time price. On create, the price fields you send are merged over the defaults (model fixed, value 0, frequency once). For fixed, value is the unit price in dollars (major currency units), not cents. A negative fixed value is a discount. For percent, value is a whole-number percent (10 = 10%) and price.percentageApplyTo says what it is a percent of (line, group, or scope). Example $250 add-on: { \"name\": \"Rush delivery\", \"price\": { \"model\": \"fixed\", \"value\": 250, \"frequency\": \"once\" }, \"isOptional\": true }. Example 10% discount on the parent line item: { \"name\": \"Loyalty discount\", \"price\": { \"model\": \"percent\", \"value\": -10, \"frequency\": \"once\", \"percentageApplyTo\": \"line\" } }.\nA modification's quantity multiplies with its parent line item's quantity. Leave price.quantity out (or at 1) for an add-on that applies once per parent unit.\nPass kind mod_divider to add a divider instead: a label with no price whose selectionType controls the modifications listed below it.\nTo change the price later, use UpdateModification. Proposals that are pending signature, won, or lost cannot be edited (409).\n",
        "tags": [
          "Modifications"
        ],
        "parameters": [
          {
            "in": "path",
            "name": "id",
            "required": true,
            "schema": {
              "type": "string"
            },
            "description": "Public ID of the parent line item, as returned by the API (for example zw5FD5HGcfE3). Not the editor \"Item ID\" (externalItemId).\n",
            "example": "zw5FD5HGcfE3"
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "required": [
                  "name"
                ],
                "properties": {
                  "name": {
                    "type": "string",
                    "description": "Modification name (1 to 255 characters).",
                    "example": "Rush delivery"
                  },
                  "description": {
                    "type": "string",
                    "description": "HTML description shown under the name.",
                    "example": "<p>Delivered within 48 hours.</p>"
                  },
                  "price": {
                    "type": "object",
                    "description": "Price of the modification. Defaults to { \"model\": \"fixed\", \"value\": 0, \"frequency\": \"once\" }; fields you send override those defaults.\n",
                    "properties": {
                      "model": {
                        "type": "string",
                        "enum": [
                          "fixed",
                          "percent",
                          "text",
                          "none",
                          "hourly"
                        ],
                        "example": "fixed"
                      },
                      "value": {
                        "type": "number",
                        "description": "For fixed, the unit price in dollars (not cents); negative for a discount. For percent, a whole-number percent (10 = 10%).\n",
                        "example": 250
                      },
                      "frequency": {
                        "type": "string",
                        "enum": [
                          "once",
                          "monthly",
                          "annually",
                          "quarterly",
                          "weekly",
                          "biweekly",
                          "semi-monthly",
                          "semi-annually",
                          "every-2-years",
                          "every-3-years",
                          "every-4-years",
                          "every-5-years",
                          "every-6-years",
                          "every-7-years",
                          "every-8-years",
                          "every-9-years",
                          "every-10-years"
                        ],
                        "example": "once"
                      },
                      "percentageApplyTo": {
                        "type": "string",
                        "enum": [
                          "line",
                          "group",
                          "scope"
                        ],
                        "description": "For percent. What the percent is taken from: the parent line item, its group, or the whole Scope of Work.\n"
                      },
                      "quantity": {
                        "type": "object",
                        "description": "Optional quantity settings. Multiplies with the parent line item's quantity.\n",
                        "properties": {
                          "current": {
                            "type": "number"
                          },
                          "min": {
                            "type": "number"
                          },
                          "max": {
                            "type": "number"
                          },
                          "editableByRecipient": {
                            "type": "boolean"
                          },
                          "unitLabel": {
                            "type": "string"
                          }
                        }
                      }
                    }
                  },
                  "isOptional": {
                    "type": "boolean",
                    "description": "Whether the client can toggle the modification on and off.",
                    "default": false
                  },
                  "isSelected": {
                    "type": "boolean",
                    "description": "Whether an optional modification starts selected.",
                    "default": true
                  },
                  "label": {
                    "type": "string",
                    "description": "Text shown instead of a price when price.model is text (for example \"TBD\")."
                  },
                  "taxExempt": {
                    "type": "boolean",
                    "description": "Exclude from sales tax."
                  },
                  "markupExempt": {
                    "type": "boolean",
                    "description": "Exclude from proposal markup."
                  },
                  "discountExempt": {
                    "type": "boolean",
                    "description": "Exclude from proposal-wide percentage discounts."
                  },
                  "externalItemId": {
                    "type": "string",
                    "description": "Your own identifier (SKU, CRM id). Shown in the editor as \"Item ID\". Printable characters, including spaces and punctuation; max 255. Control characters and non-ASCII are not allowed.\n"
                  },
                  "order_key": {
                    "type": "number",
                    "description": "Position among the line item's modifications. Defaults to the end.",
                    "example": 1
                  },
                  "kind": {
                    "type": "string",
                    "enum": [
                      "mod",
                      "mod_divider"
                    ],
                    "description": "mod (default) for a priced modification. mod_divider for a divider with no price; price fields are ignored.\n",
                    "example": "mod"
                  },
                  "selectionType": {
                    "type": "string",
                    "enum": [
                      "single",
                      "multiple",
                      "loose"
                    ],
                    "description": "Only for kind mod_divider. How the modifications below the divider can be picked: single (one), multiple (several), or loose (each independently optional).\n",
                    "example": "multiple"
                  },
                  "requireSelection": {
                    "type": "boolean",
                    "description": "Only for kind mod_divider with selectionType loose. The client must pick at least one modification below it.\n"
                  }
                }
              }
            }
          }
        },
        "responses": {
          "201": {
            "description": "Modification created.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "data": {
                      "type": "object",
                      "description": "The new modification, including its price.",
                      "properties": {
                        "id": {
                          "type": "string",
                          "description": "Public ID of the new modification.",
                          "example": "zw5FD5HGcfE3"
                        },
                        "name": {
                          "type": "string"
                        },
                        "kind": {
                          "type": "string",
                          "example": "mod"
                        }
                      }
                    }
                  }
                }
              }
            }
          },
          "400": {
            "description": "Validation error (for example an empty name or an invalid price.model)."
          },
          "401": {
            "$ref": "#/components/responses/UnauthorizedError"
          },
          "404": {
            "description": "Line item not found."
          },
          "409": {
            "description": "The proposal is locked (pending signature, won, lost, or a locked customer version)."
          },
          "500": {
            "$ref": "#/components/responses/InternalServerError"
          }
        }
      }
    },
    "/api/items/{id}/modifications/{modificationId}": {
      "get": {
        "operationId": "GetModification",
        "summary": "Get a modification",
        "description": "Return one modification of a line item, including its price. Both path values are Public IDs returned by the API (for example zw5FD5HGcfE3): {id} is the parent line item and {modificationId} is the modification. Neither is the editor field labeled \"Item ID\"; that field is externalItemId on the modification.\n",
        "tags": [
          "Modifications"
        ],
        "parameters": [
          {
            "in": "path",
            "name": "id",
            "required": true,
            "schema": {
              "type": "string"
            },
            "description": "Public ID of the parent line item (for example zw5FD5HGcfE3). Not the editor \"Item ID\" (externalItemId).\n",
            "example": "zw5FD5HGcfE3"
          },
          {
            "in": "path",
            "name": "modificationId",
            "required": true,
            "schema": {
              "type": "string"
            },
            "description": "Public ID of the modification (for example kP3mNq8rT2xY). Not the editor \"Item ID\" (externalItemId).\n",
            "example": "kP3mNq8rT2xY"
          }
        ],
        "responses": {
          "200": {
            "description": "The modification.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "data": {
                      "type": "object",
                      "properties": {
                        "id": {
                          "type": "string",
                          "description": "Public ID of the modification.",
                          "example": "kP3mNq8rT2xY"
                        },
                        "name": {
                          "type": "string",
                          "example": "Rush delivery"
                        },
                        "description": {
                          "type": "string",
                          "description": "HTML description.",
                          "example": "<p>Delivered within 48 hours.</p>"
                        },
                        "price": {
                          "type": "object",
                          "description": "Current price. For fixed, value is in dollars (not cents). For percent, value is a whole-number percent (10 = 10%).\n",
                          "properties": {
                            "model": {
                              "type": "string",
                              "enum": [
                                "fixed",
                                "percent",
                                "text",
                                "none",
                                "hourly"
                              ]
                            },
                            "value": {
                              "type": "number",
                              "example": 250
                            },
                            "maxValue": {
                              "type": "number",
                              "description": "Upper end of a price range (when isRange is true)."
                            },
                            "isRange": {
                              "type": "boolean"
                            },
                            "frequency": {
                              "type": "string",
                              "example": "once"
                            },
                            "percentageApplyTo": {
                              "type": "string",
                              "enum": [
                                "line",
                                "group",
                                "scope"
                              ]
                            },
                            "quantity": {
                              "type": "object"
                            },
                            "discount": {
                              "type": "object"
                            }
                          }
                        },
                        "isOptional": {
                          "type": "boolean"
                        },
                        "isSelected": {
                          "type": "boolean"
                        },
                        "taxExempt": {
                          "type": "boolean"
                        },
                        "markupExempt": {
                          "type": "boolean"
                        },
                        "discountExempt": {
                          "type": "boolean"
                        },
                        "label": {
                          "type": "string",
                          "description": "Text shown instead of a price when price.model is text."
                        },
                        "externalItemId": {
                          "type": "string",
                          "nullable": true,
                          "description": "Your own identifier, shown in the editor as \"Item ID\"."
                        },
                        "parent_id": {
                          "type": "string",
                          "description": "Public ID of the parent line item."
                        },
                        "created_at": {
                          "type": "string",
                          "format": "date-time"
                        },
                        "updated_at": {
                          "type": "string",
                          "format": "date-time"
                        }
                      }
                    }
                  }
                }
              }
            }
          },
          "400": {
            "description": "The item is not a modification, or it does not belong to the line item in {id}."
          },
          "401": {
            "$ref": "#/components/responses/UnauthorizedError"
          },
          "404": {
            "description": "Modification or parent line item not found.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "error": {
                      "type": "object",
                      "properties": {
                        "code": {
                          "type": "string",
                          "example": "NOT_FOUND"
                        },
                        "message": {
                          "type": "string",
                          "example": "Modification not found"
                        }
                      }
                    }
                  }
                }
              }
            }
          },
          "500": {
            "$ref": "#/components/responses/InternalServerError"
          }
        }
      },
      "patch": {
        "operationId": "UpdateModification",
        "summary": "Update a modification, including its price",
        "description": "Change an existing modification in place. You can change its price: send price with model, value, and frequency together. price replaces the existing price (range fields such as maxValue and isRange that you leave out are cleared; price.quantity changes only when you send it). For fixed, value is the unit price in dollars (major currency units), not cents. A negative fixed value makes the modification a discount. Sending only { \"price\": { \"value\": 1500 } } does not update the price; model is required. For percent, value is a whole-number percent (10 = 10%) and percentageApplyTo says what it is a percent of.\nExample that sets the price to $1,500 per month: { \"price\": { \"model\": \"fixed\", \"value\": 1500, \"frequency\": \"monthly\" } }. Example $200 discount: { \"price\": { \"model\": \"fixed\", \"value\": -200, \"frequency\": \"once\" } }.\nOther fields (name, description, isOptional, isSelected, exemptions, externalItemId) change only when you send them. The body is flat, with no \"item\" wrapper. PATCH /api/items/{modificationId} accepts the same body.\nBoth path values are Public IDs returned by the API (for example zw5FD5HGcfE3), not the editor field labeled \"Item ID\". To set that field, send externalItemId (printable characters, including spaces and punctuation; max 255; control characters and non-ASCII are not allowed; null or \"\" clears it).\nProposals that are pending signature, won, or lost cannot be edited (409 PROPOSAL_LOCKED). The same applies when the customer-visible version of a versioned proposal is locked.\n",
        "tags": [
          "Modifications"
        ],
        "parameters": [
          {
            "in": "path",
            "name": "id",
            "required": true,
            "schema": {
              "type": "string"
            },
            "description": "Public ID of the parent line item (for example zw5FD5HGcfE3). Not the editor \"Item ID\" (externalItemId).\n",
            "example": "zw5FD5HGcfE3"
          },
          {
            "in": "path",
            "name": "modificationId",
            "required": true,
            "schema": {
              "type": "string"
            },
            "description": "Public ID of the modification (for example kP3mNq8rT2xY). Not the editor \"Item ID\" (externalItemId).\n",
            "example": "kP3mNq8rT2xY"
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "properties": {
                  "name": {
                    "type": "string",
                    "description": "Modification name. Cannot be empty.",
                    "example": "Rush delivery"
                  },
                  "description": {
                    "type": "string",
                    "description": "HTML description.",
                    "example": "<p>Delivered within 48 hours.</p>"
                  },
                  "price": {
                    "type": "object",
                    "description": "New price. Replaces the existing price. Send model, value, and frequency together; value without model is ignored. quantity is kept unless you send it.\n",
                    "required": [
                      "model",
                      "frequency"
                    ],
                    "properties": {
                      "model": {
                        "type": "string",
                        "enum": [
                          "fixed",
                          "percent",
                          "text",
                          "none",
                          "hourly"
                        ],
                        "description": "Pricing model. Required to change the price.",
                        "example": "fixed"
                      },
                      "value": {
                        "type": "number",
                        "description": "For fixed, the unit price in dollars (major currency units), not cents; negative for a discount. For percent, a whole-number percent (10 = 10%). Required for fixed and percent.\n",
                        "example": 1500
                      },
                      "maxValue": {
                        "type": "number",
                        "description": "Upper end of a price range. Use with isRange true."
                      },
                      "isRange": {
                        "type": "boolean",
                        "description": "Show the price as a range from value to maxValue."
                      },
                      "frequency": {
                        "type": "string",
                        "enum": [
                          "once",
                          "monthly",
                          "quarterly",
                          "annually",
                          "weekly",
                          "biweekly",
                          "semi-monthly",
                          "semi-annually",
                          "every-2-years",
                          "every-3-years",
                          "every-4-years",
                          "every-5-years",
                          "every-6-years",
                          "every-7-years",
                          "every-8-years",
                          "every-9-years",
                          "every-10-years"
                        ],
                        "description": "Billing frequency. Not used for model none.",
                        "example": "monthly"
                      },
                      "percentageApplyTo": {
                        "type": "string",
                        "enum": [
                          "line",
                          "group",
                          "scope"
                        ],
                        "description": "For percent. What the percent is taken from: the parent line item, its group, or the whole Scope of Work.\n",
                        "example": "line"
                      },
                      "quantity": {
                        "type": "object",
                        "nullable": true,
                        "description": "Quantity settings (min, max, current, editableByRecipient, unitLabel). Multiplies with the parent line item's quantity. Pass null to remove.\n"
                      },
                      "discount": {
                        "type": "object",
                        "nullable": true,
                        "description": "Per-modification discount. For type percentage, amount is a whole-number percent (10 = 10%); for type fixed, amount is dollars. Pass null to clear.\n"
                      }
                    }
                  },
                  "isOptional": {
                    "type": "boolean",
                    "description": "Whether the client can toggle the modification on and off."
                  },
                  "isSelected": {
                    "type": "boolean",
                    "description": "Whether the modification is selected."
                  },
                  "taxExempt": {
                    "type": "boolean",
                    "description": "Exclude from sales tax."
                  },
                  "markupExempt": {
                    "type": "boolean",
                    "description": "Exclude from proposal markup."
                  },
                  "discountExempt": {
                    "type": "boolean",
                    "description": "Exclude from proposal-wide percentage discounts."
                  },
                  "label": {
                    "type": "string",
                    "description": "Text shown instead of a price when price.model is text (for example \"TBD\")."
                  },
                  "externalItemId": {
                    "type": "string",
                    "nullable": true,
                    "description": "Your own identifier (SKU, CRM id), shown in the editor as \"Item ID\". Printable characters, including spaces and punctuation; max 255. Control characters and non-ASCII are not allowed. null or \"\" clears it.\n"
                  }
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Modification updated. Returns the modification as it is now stored, including its price.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "data": {
                      "type": "object",
                      "properties": {
                        "id": {
                          "type": "string",
                          "description": "Public ID of the modification.",
                          "example": "kP3mNq8rT2xY"
                        },
                        "name": {
                          "type": "string"
                        },
                        "price": {
                          "type": "object"
                        },
                        "parent_id": {
                          "type": "string",
                          "description": "Public ID of the parent line item."
                        }
                      }
                    }
                  }
                }
              }
            }
          },
          "400": {
            "description": "Validation error (for example an invalid price.model or frequency), or the modification does not belong to the line item in {id}.\n"
          },
          "401": {
            "$ref": "#/components/responses/UnauthorizedError"
          },
          "403": {
            "description": "Not authorized to edit this proposal."
          },
          "404": {
            "description": "Modification or parent line item not found."
          },
          "409": {
            "description": "The proposal is locked (pending signature, won, lost, or a locked customer version)."
          },
          "500": {
            "$ref": "#/components/responses/InternalServerError"
          }
        }
      }
    },
    "/api/items/{id}/move": {
      "post": {
        "operationId": "MoveItem",
        "summary": "Move a line item, side-by-side, or modification",
        "description": "Same URL for a line item, a side-by-side, or a modification. Pass the Public ID of the object to move (the id the API returned), not the editor Item ID field (externalItemId). after_id decides the new position.\nLine item: POST /api/items/zw5FD5HGcfE3/move with after_id set to another line item's Public ID (placed right after it, moving into that line item's group if different), a group's Public ID (placed first in that group), or null (placed first in its current group).\nSide-by-side: POST /api/items/Hs3kW8nYb4Rd/move with after_id set to the Public ID of another side-by-side on the same Scope of Work page (placed right after it), or null (placed first on the page). Do not pass a group's Public ID for a side-by-side.\nModification: POST /api/items/Qm7rT2vXk9Lp/move with after_id set to the Public ID of another modification on the same line item (placed right after it), or null (placed first under its line item).\nThe price and other settings do not change. To set the order of many line items at once, use ReorderItems. Proposals that are pending signature, won, or lost cannot be edited (409).\n",
        "tags": [
          "Line Items",
          "Side-by-Sides",
          "Modifications"
        ],
        "parameters": [
          {
            "in": "path",
            "name": "id",
            "required": true,
            "schema": {
              "type": "string"
            },
            "description": "Public ID of the line item, side-by-side, or modification to move, as returned by the API (for example zw5FD5HGcfE3). Not the editor \"Item ID\" (externalItemId).\n",
            "example": "zw5FD5HGcfE3"
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "required": [
                  "after_id"
                ],
                "properties": {
                  "after_id": {
                    "type": "string",
                    "nullable": true,
                    "description": "Required (may be null). Public ID of the sibling to place this object after, or (for a line item) Public ID of a group to place it first in that group. null places it first under its current parent.\n",
                    "example": "ijklmnop5678"
                  }
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Object moved.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "data": {
                      "type": "object",
                      "properties": {
                        "id": {
                          "type": "string",
                          "description": "Public ID of the moved object.",
                          "example": "zw5FD5HGcfE3"
                        }
                      }
                    }
                  }
                }
              }
            }
          },
          "400": {
            "description": "after_id is missing from the body."
          },
          "401": {
            "$ref": "#/components/responses/UnauthorizedError"
          },
          "404": {
            "description": "Object (or the after_id target) not found."
          },
          "409": {
            "description": "The proposal is locked (pending signature, won, lost, or a locked customer version)."
          },
          "500": {
            "$ref": "#/components/responses/InternalServerError"
          }
        }
      }
    },
    "/api/items/{id}": {
      "get": {
        "operationId": "GetItem",
        "summary": "Get a line item, side-by-side, or modification",
        "description": "Same URL for a line item, a side-by-side, or a modification. Pass that object's Public ID (the id the API returned), not the editor Item ID field (externalItemId, your own SKU or CRM id).\nLine item: GET /api/items/zw5FD5HGcfE3 returns its price, settings, parent_id (the group's Public ID), children (its modifications), and childCount.\nModification: GET /api/items/Qm7rT2vXk9Lp (the modification's Public ID) returns the modification, with parent_id set to its line item. GetModification returns the same data.\nSide-by-side: GET /api/items/Hs3kW8nYb4Rd (the side-by-side's Public ID) returns its id, name, description, kind \"side_by_side\", and selectionType. Its columns and features are not included in this response.\n",
        "tags": [
          "Line Items",
          "Side-by-Sides",
          "Modifications"
        ],
        "parameters": [
          {
            "in": "path",
            "name": "id",
            "required": true,
            "schema": {
              "type": "string"
            },
            "description": "Public ID of the line item, side-by-side, or modification, as returned by the API (for example zw5FD5HGcfE3). Not the editor \"Item ID\" (externalItemId).\n",
            "example": "zw5FD5HGcfE3"
          }
        ],
        "responses": {
          "200": {
            "description": "The item, including parent_id, children, and childCount.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "item": {
                      "$ref": "#/components/schemas/LineItem"
                    }
                  }
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/UnauthorizedError"
          },
          "404": {
            "description": "Item not found",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "error": {
                      "type": "string",
                      "example": "Item not found"
                    }
                  }
                }
              }
            }
          },
          "500": {
            "$ref": "#/components/responses/InternalServerError"
          }
        }
      },
      "patch": {
        "operationId": "UpdateItem",
        "summary": "Update a line item, modification, or side-by-side",
        "description": "Change an existing item in place, including its price. Only the fields you send change. The body shape depends on the kind of item.\nLINE ITEM (and modification): send a flat UpdateLineItemApiInput body, with no \"item\" wrapper. To change the cost, send price. price is a full replacement, so include model, value, and frequency together. For a fixed price, value is the unit price in major currency units (dollars for USD), not cents. Example that sets the price to $379 one-time: { \"price\": { \"model\": \"fixed\", \"value\": 379, \"frequency\": \"once\" } }. For a percent price, value is a whole-number percent (15 = 15%). Sending only { \"price\": { \"value\": 379 } } does not update the price; model is required.\nSIDE-BY-SIDE: wrap the fields in \"item\": { \"item\": { \"name\", \"html\", \"attrs\", \"order_key\" } }. attrs is merged per key, so keys you leave out are kept. attrs.columns replaces the whole columns array: read the current columns with GetProposal, change them, and send the complete array back, keeping each column's id. Each column has name, price (a number in dollars), frequency, and feeType (fixed, text, or none). Example: { \"item\": { \"attrs\": { \"columns\": [ { \"id\": \"3f2c...\", \"name\": \"Basic\", \"price\": 499, \"frequency\": \"monthly\", \"feeType\": \"fixed\" } ] } } }.\nTo recolor a side-by-side, set item.attrs.theme to a built-in id (metals, ocean, forest, sunset, monochrome, pastel, neon, earth, berry, corporate) without resending columns. Or set item.attrs.design for a custom theme (container, featuresColumn, heading, and a columns palette that cycles). A design object on an individual column overrides that column. theme and design are request-only — the saved item stores the painted colors, not the theme id.\nThe {id} is the Public ID the API returns (for example zw5FD5HGcfE3), not the editor field labeled \"Item ID\" (externalItemId). To set your own id, send externalItemId (printable characters, including spaces and punctuation; max 255; control characters and non-ASCII are not allowed; null or \"\" clears it). tags replaces the tag list; omit tags to leave them unchanged. Send tags: [] to clear them.\nThe same call updates a library line item or library side-by-side when {id} is that library entry's Public ID.\nProposals that are pending signature, won, or lost cannot be edited (409 PROPOSAL_LOCKED). The same applies when the customer-visible version of a versioned proposal is locked.\n",
        "tags": [
          "Line Items",
          "Side-by-Sides"
        ],
        "parameters": [
          {
            "in": "path",
            "name": "id",
            "required": true,
            "schema": {
              "type": "string"
            },
            "description": "Public ID of the line item, modification, or side-by-side, as returned by the API (for example zw5FD5HGcfE3). Not the editor \"Item ID\" (externalItemId).\n",
            "example": "zw5FD5HGcfE3"
          }
        ],
        "requestBody": {
          "required": true,
          "description": "Line items and modifications use the flat UpdateLineItemApiInput body. Side-by-sides use the { \"item\": { ... } } wrapper.\n",
          "content": {
            "application/json": {
              "schema": {
                "oneOf": [
                  {
                    "$ref": "#/components/schemas/UpdateLineItemApiInput"
                  },
                  {
                    "type": "object",
                    "title": "UpdateSideBySideInput",
                    "description": "Body for a side-by-side. All fields sit inside \"item\".",
                    "required": [
                      "item"
                    ],
                    "properties": {
                      "item": {
                        "type": "object",
                        "properties": {
                          "name": {
                            "type": "string",
                            "description": "Display name of the side-by-side."
                          },
                          "html": {
                            "type": "string",
                            "description": "HTML description shown above the columns."
                          },
                          "order_key": {
                            "type": "number",
                            "description": "Position among siblings. Must be a positive number."
                          },
                          "attrs": {
                            "type": "object",
                            "description": "Side-by-side settings. Merged per key with the current attrs; keys you omit are kept. A provided columns (or features) array replaces the old array.\n",
                            "properties": {
                              "columns": {
                                "type": "array",
                                "description": "The complete list of columns. Replaces the existing array. Keep each existing column's id; ids are not generated on update.\n",
                                "items": {
                                  "type": "object",
                                  "properties": {
                                    "id": {
                                      "type": "string",
                                      "description": "Column id from GetProposal."
                                    },
                                    "name": {
                                      "type": "string"
                                    },
                                    "description": {
                                      "type": "string"
                                    },
                                    "price": {
                                      "type": "number",
                                      "description": "Column price in dollars (major currency units), not cents.",
                                      "example": 499
                                    },
                                    "frequency": {
                                      "type": "string",
                                      "description": "e.g. once, monthly, annually.",
                                      "example": "monthly"
                                    },
                                    "feeType": {
                                      "type": "string",
                                      "enum": [
                                        "fixed",
                                        "text",
                                        "none"
                                      ]
                                    },
                                    "isSelected": {
                                      "type": "boolean"
                                    },
                                    "includedFeatures": {
                                      "type": "array",
                                      "items": {
                                        "type": "string"
                                      },
                                      "description": "Feature ids included in this column (comparison style)."
                                    },
                                    "design": {
                                      "type": "object",
                                      "nullable": true,
                                      "description": "Colors for this column. Omit to keep the current colors when you are not applying a theme. Null clears them.\n"
                                    }
                                  }
                                }
                              },
                              "features": {
                                "type": "array",
                                "description": "Comparison-style feature rows. Replaces the existing array.",
                                "items": {
                                  "type": "object",
                                  "properties": {
                                    "id": {
                                      "type": "string"
                                    },
                                    "name": {
                                      "type": "string"
                                    },
                                    "order_key": {
                                      "type": "number"
                                    }
                                  }
                                }
                              },
                              "selectionType": {
                                "type": "string",
                                "enum": [
                                  "single",
                                  "multiple",
                                  "loose"
                                ],
                                "description": "How many columns a client may select."
                              },
                              "style": {
                                "type": "string",
                                "enum": [
                                  "standard",
                                  "comparison"
                                ]
                              },
                              "theme": {
                                "type": "string",
                                "enum": [
                                  "metals",
                                  "ocean",
                                  "forest",
                                  "sunset",
                                  "monochrome",
                                  "pastel",
                                  "neon",
                                  "earth",
                                  "berry",
                                  "corporate"
                                ],
                                "description": "Apply a built-in color theme to the current columns (or to the columns array in this request). Not stored — the painted colors are.\n"
                              },
                              "design": {
                                "type": "object",
                                "description": "Custom theme. container, featuresColumn, heading, and columns (a cycled palette). Not stored under this key.\n",
                                "properties": {
                                  "container": {
                                    "type": "object"
                                  },
                                  "featuresColumn": {
                                    "type": "object"
                                  },
                                  "heading": {
                                    "type": "object"
                                  },
                                  "columns": {
                                    "type": "array",
                                    "items": {
                                      "type": "object"
                                    }
                                  }
                                }
                              }
                            }
                          }
                        }
                      }
                    }
                  }
                ]
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Item updated. Returns the item as it is now stored.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "item": {
                      "$ref": "#/components/schemas/LineItem"
                    }
                  }
                }
              }
            }
          },
          "400": {
            "description": "Validation error (for example an invalid price.model or frequency, an empty name, or a non-positive order_key).\n"
          },
          "401": {
            "$ref": "#/components/responses/UnauthorizedError"
          },
          "403": {
            "description": "Not authorized to edit this proposal."
          },
          "404": {
            "description": "Item not found"
          },
          "409": {
            "description": "The proposal is locked (PROPOSAL_LOCKED: pending signature, won, lost, or a locked customer version), or a library item with this name already exists (DUPLICATE_NAME).\n",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "error": {
                      "type": "object",
                      "properties": {
                        "code": {
                          "type": "string",
                          "example": "DUPLICATE_NAME"
                        },
                        "message": {
                          "type": "string",
                          "example": "A line item with the name \"Item Name\" already exists in the library."
                        },
                        "existingItemId": {
                          "type": "string",
                          "description": "ID of the existing item"
                        }
                      }
                    }
                  }
                }
              }
            }
          },
          "500": {
            "$ref": "#/components/responses/InternalServerError"
          }
        }
      },
      "delete": {
        "operationId": "DeleteItem",
        "summary": "Delete a line item, side-by-side, or modification",
        "description": "Same URL for a line item, a side-by-side, or a modification. Pass that object's Public ID (the id the API returned), not the editor Item ID field (externalItemId).\nLine item: DELETE /api/items/zw5FD5HGcfE3. Side-by-side: DELETE /api/items/Hs3kW8nYb4Rd (the side-by-side's Public ID). Modification: DELETE /api/items/Qm7rT2vXk9Lp (the modification's Public ID).\nThe object is soft-deleted: it disappears from the proposal and from API responses, and the proposal totals are recalculated. To remove it but keep the option to bring it back, use ArchiveItem and UnarchiveItem instead. Requires permission to delete proposals.\n",
        "tags": [
          "Line Items",
          "Side-by-Sides",
          "Modifications"
        ],
        "parameters": [
          {
            "in": "path",
            "name": "id",
            "required": true,
            "schema": {
              "type": "string"
            },
            "description": "Public ID of the line item, side-by-side, or modification, as returned by the API (for example zw5FD5HGcfE3). Not the editor \"Item ID\" (externalItemId).\n",
            "example": "zw5FD5HGcfE3"
          }
        ],
        "responses": {
          "204": {
            "description": "Item deleted. No response body."
          },
          "401": {
            "$ref": "#/components/responses/UnauthorizedError"
          },
          "403": {
            "description": "Not authorized to delete from this proposal."
          },
          "404": {
            "description": "Item not found"
          },
          "500": {
            "$ref": "#/components/responses/InternalServerError"
          }
        }
      }
    },
    "/api/items/{id}/unarchive": {
      "post": {
        "operationId": "UnarchiveItem",
        "summary": "Unarchive a line item, side-by-side, or modification",
        "description": "Same URL for a line item, a side-by-side, or a modification. Pass the archived object's Public ID (the id the API returned), not the editor Item ID field (externalItemId).\nLine item: POST /api/items/zw5FD5HGcfE3/unarchive. Side-by-side: POST /api/items/Hs3kW8nYb4Rd/unarchive (the side-by-side's Public ID). Modification: POST /api/items/Qm7rT2vXk9Lp/unarchive (the modification's Public ID).\nThe object shows in the proposal again, with the price and settings it had when archived. Proposals that are pending signature, won, or lost cannot be edited (409).\n",
        "tags": [
          "Line Items",
          "Side-by-Sides",
          "Modifications"
        ],
        "parameters": [
          {
            "in": "path",
            "name": "id",
            "required": true,
            "schema": {
              "type": "string"
            },
            "description": "Public ID of the archived line item, side-by-side, or modification, as returned by the API (for example zw5FD5HGcfE3). Not the editor \"Item ID\" (externalItemId).\n",
            "example": "zw5FD5HGcfE3"
          }
        ],
        "responses": {
          "204": {
            "description": "Item restored. No response body."
          },
          "401": {
            "$ref": "#/components/responses/UnauthorizedError"
          },
          "404": {
            "description": "Item not found."
          },
          "409": {
            "description": "The proposal is locked (pending signature, won, lost, or a locked customer version)."
          },
          "500": {
            "$ref": "#/components/responses/InternalServerError"
          }
        }
      }
    },
    "/api/items": {
      "post": {
        "operationId": "CreateItem",
        "summary": "Create a line item in a group or the library",
        "description": "Create a new line item. Send exactly one of parent_group (the Public ID of a group in a proposal, for example zw5FD5HGcfE3) or in_library: true (saves the line item to your organization's library). The line item fields go inside \"item\". A library line item created this way shows up in the Line Items library and in proposal search.\nitem.name and item.price are required. price needs model and, for every model except none, frequency. For fixed and percent, value is also required. For fixed, value is the unit price in dollars (major currency units), not cents. For percent, value is a whole-number percent (15 = 15%). item.tags is an array of strings and sets the tags. item.externalItemId is your own part number or SKU. item.internalCost is the internal cost in dollars ({ \"amount\": 40 }), when the profit margins add-on is on. To change any of these later, including tags, PATCH /api/items/{id} with a flat body (no \"item\" wrapper). Sending tags there replaces the whole list.\nLibrary example: { \"in_library\": true, \"item\": { \"name\": \"Indoor camera\", \"description\": \"<p>4K dome camera.</p>\", \"price\": { \"model\": \"fixed\", \"value\": 499, \"frequency\": \"once\" }, \"tags\": [\"cameras\", \"indoor\"], \"externalItemId\": \"CAM-100\", \"internalCost\": { \"amount\": 210 } } }.\nProposal example: { \"parent_group\": \"zw5FD5HGcfE3\", \"item\": { \"name\": \"Website design\", \"price\": { \"model\": \"fixed\", \"value\": 2500, \"frequency\": \"once\" } } }.\nCreate one line item per request. If the library already has that name, the response is 409 and includes existingItemId; PATCH that id to update it. To place a line item next to an existing item, or to create a group or side-by-side on a proposal, POST /api/proposals/{id}/items is usually simpler. Proposals that are pending signature, won, or lost cannot be edited (409).\n",
        "tags": [
          "Line Items"
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "description": "Send exactly one of parent_group or in_library.",
                "required": [
                  "item"
                ],
                "properties": {
                  "parent_group": {
                    "type": "string",
                    "description": "Public ID of a group in a proposal. The line item is added to that group.",
                    "example": "zw5FD5HGcfE3"
                  },
                  "in_library": {
                    "type": "boolean",
                    "description": "Create the line item in your organization's library."
                  },
                  "item": {
                    "$ref": "#/components/schemas/CreateLineItemApiInput"
                  }
                }
              }
            }
          }
        },
        "responses": {
          "201": {
            "description": "Line item created. data.id is its Public ID.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "data": {
                      "$ref": "#/components/schemas/LineItem"
                    }
                  }
                }
              }
            }
          },
          "400": {
            "$ref": "#/components/responses/ValidationError"
          },
          "401": {
            "$ref": "#/components/responses/UnauthorizedError"
          },
          "403": {
            "$ref": "#/components/responses/ForbiddenError"
          },
          "404": {
            "description": "Group or organization not found"
          },
          "409": {
            "description": "The proposal is locked (pending signature, won, lost, or a locked customer version), or a library item with this name already exists (only when in_library is true). A duplicate library name returns error.code DUPLICATE_NAME and error.existingItemId. PATCH /api/items/{existingItemId} to update that library line item.\n"
          },
          "500": {
            "$ref": "#/components/responses/InternalServerError"
          }
        }
      }
    },
    "/api/library/bulk-archive": {
      "post": {
        "operationId": "BulkArchiveLibraryItems",
        "summary": "Archive library line items, groups, or pages",
        "description": "Archive more than one library entry in one call. itemIds are Public IDs returned by the API (for example zw5FD5HGcfE3), not the editor Item ID field (externalItemId). Each id must belong to your organization's library. Line items, groups, and pages are accepted. Archiving hides an entry so it can be restored; it is not a permanent delete. Up to 2000 ids per request. Requires the Manage Library permission. A partial failure still returns 200: archivedCount is how many succeeded, and errors lists the rest.\n",
        "tags": [
          "Line Items",
          "Groups",
          "Pages"
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "required": [
                  "itemIds"
                ],
                "properties": {
                  "itemIds": {
                    "type": "array",
                    "maxItems": 2000,
                    "items": {
                      "type": "string"
                    },
                    "description": "Public IDs of library line items, groups, or pages to archive. Not editor Item IDs (externalItemId).\n",
                    "example": [
                      "zw5FD5HGcfE3",
                      "kP3mNq8rT2xY"
                    ]
                  }
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Archive finished. archivedCount can be lower than itemIds.length when errors is present.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "archivedCount": {
                      "type": "integer",
                      "description": "Number of items successfully archived",
                      "example": 3
                    },
                    "message": {
                      "type": "string",
                      "description": "Success message",
                      "example": "Successfully archived 3 items"
                    },
                    "errors": {
                      "type": "array",
                      "items": {
                        "type": "string"
                      },
                      "description": "Messages for ids that were not archived. Omitted when every id succeeded."
                    }
                  }
                }
              }
            }
          },
          "400": {
            "$ref": "#/components/responses/ValidationError"
          },
          "401": {
            "$ref": "#/components/responses/UnauthorizedError"
          },
          "403": {
            "$ref": "#/components/responses/ForbiddenError"
          },
          "500": {
            "$ref": "#/components/responses/InternalServerError"
          }
        }
      }
    },
    "/api/library/items/check-duplicate": {
      "get": {
        "operationId": "CheckDuplicateItemName",
        "summary": "Check if a library line item name already exists",
        "description": "Look up a line item name in your organization's library before creating one. Send the name as a query parameter. exists is true when a library line item already uses that name, and itemId is that item's Public ID (the id the API returns, not the editor Item ID / externalItemId). CreateItem with in_library true also returns 409 DUPLICATE_NAME and existingItemId when the name is taken; this call is the check you can make first.\n",
        "tags": [
          "Line Items"
        ],
        "parameters": [
          {
            "in": "query",
            "name": "name",
            "required": true,
            "schema": {
              "type": "string"
            },
            "description": "Line item name to look up in the library."
          }
        ],
        "responses": {
          "200": {
            "description": "Check result",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "exists": {
                      "type": "boolean",
                      "description": "True when a library line item already uses this name."
                    },
                    "itemId": {
                      "type": "string",
                      "nullable": true,
                      "description": "Public ID of the existing library line item when exists is true."
                    }
                  }
                }
              }
            }
          },
          "400": {
            "description": "Missing name parameter"
          },
          "401": {
            "$ref": "#/components/responses/UnauthorizedError"
          }
        }
      }
    },
    "/api/library/items": {
      "get": {
        "operationId": "ListLibraryItems",
        "summary": "List library line items",
        "description": "List line items in your organization's library. Each item's id is its Public ID. externalItemId is the editor Item ID (your part number or SKU), and price is the library price. Use externalItemId to look up one part. Omit it to page through the library. Results stay in your organization. Up to 100 items per request; use offset with pagination.total to read the rest.\n",
        "tags": [
          "Line Items"
        ],
        "parameters": [
          {
            "in": "query",
            "name": "limit",
            "schema": {
              "type": "integer",
              "default": 50,
              "minimum": 1,
              "maximum": 100
            },
            "description": "Number of items to return"
          },
          {
            "in": "query",
            "name": "offset",
            "schema": {
              "type": "integer",
              "default": 0,
              "minimum": 0
            },
            "description": "Number of items to skip"
          },
          {
            "in": "query",
            "name": "search",
            "schema": {
              "type": "string"
            },
            "description": "Filter by name or description. Does not match Item ID; use externalItemId for that."
          },
          {
            "in": "query",
            "name": "externalItemId",
            "schema": {
              "type": "string",
              "maxLength": 255
            },
            "description": "Exact editor Item ID (your part number or SKU). Printable characters only, max 255. Example: Widget (A) + 1\n"
          },
          {
            "in": "query",
            "name": "tag",
            "schema": {
              "type": "string"
            },
            "description": "Exact library tag. Omit to include every tag."
          },
          {
            "in": "query",
            "name": "sort",
            "schema": {
              "type": "string",
              "enum": [
                "name",
                "order_key",
                "created_at",
                "updated_at"
              ],
              "default": "order_key"
            },
            "description": "Field to sort by"
          },
          {
            "in": "query",
            "name": "order",
            "schema": {
              "type": "string",
              "enum": [
                "asc",
                "desc"
              ],
              "default": "asc"
            },
            "description": "Sort order"
          }
        ],
        "responses": {
          "200": {
            "description": "Library items retrieved successfully",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "data": {
                      "type": "array",
                      "items": {
                        "$ref": "#/components/schemas/LineItem"
                      }
                    },
                    "pagination": {
                      "type": "object",
                      "properties": {
                        "limit": {
                          "type": "integer"
                        },
                        "offset": {
                          "type": "integer"
                        },
                        "total": {
                          "type": "integer"
                        }
                      }
                    }
                  }
                }
              }
            }
          },
          "400": {
            "description": "externalItemId is blank, too long, or contains a character Item ID does not allow."
          },
          "401": {
            "$ref": "#/components/responses/UnauthorizedError"
          },
          "500": {
            "$ref": "#/components/responses/InternalServerError"
          }
        }
      }
    },
    "/api/library/side-by-sides": {
      "post": {
        "operationId": "CreateLibrarySideBySide",
        "summary": "Create a library side-by-side",
        "description": "Save a new side-by-side in your organization's library. name is required. columns is the list of packages; each column price is a number in dollars (major currency units), not cents. Column and feature ids are generated when you omit them. Ids you send are kept.\nExample: { \"name\": \"Camera packages\", \"description\": \"<p>Choose a package.</p>\", \"selectionType\": \"single\", \"style\": \"standard\", \"columns\": [ { \"name\": \"Basic\", \"price\": 499, \"frequency\": \"once\" }, { \"name\": \"Pro\", \"price\": 899, \"frequency\": \"monthly\" } ] }.\nA library name that already exists returns 409 with existingSideBySideId. To change one later, PATCH /api/items/{id} with an { \"item\": { ... } } body, using the returned id. To create a side-by-side on a proposal instead, POST /api/proposals/{id}/items with type=side_by_side. tags is an optional list of library tags.\n",
        "tags": [
          "Side-by-Sides"
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "required": [
                  "name"
                ],
                "properties": {
                  "name": {
                    "type": "string",
                    "description": "Display name. Must be unique in the library."
                  },
                  "description": {
                    "type": "string",
                    "description": "HTML description shown above the columns."
                  },
                  "selectionType": {
                    "type": "string",
                    "enum": [
                      "single",
                      "multiple",
                      "loose"
                    ],
                    "description": "How many columns a client may select. Default single."
                  },
                  "style": {
                    "type": "string",
                    "enum": [
                      "standard",
                      "comparison"
                    ],
                    "description": "standard is pricing cards. comparison is a feature table (send features). Default standard."
                  },
                  "columns": {
                    "type": "array",
                    "description": "The packages or columns. Each price is a number in dollars. An omitted id is generated.\n",
                    "items": {
                      "type": "object",
                      "properties": {
                        "id": {
                          "type": "string",
                          "description": "Column id. Generated when omitted."
                        },
                        "name": {
                          "type": "string"
                        },
                        "description": {
                          "type": "string"
                        },
                        "price": {
                          "type": "number",
                          "description": "Column price in dollars (major currency units), not cents.",
                          "example": 499
                        },
                        "frequency": {
                          "type": "string",
                          "description": "e.g. once, monthly, annually.",
                          "example": "once"
                        },
                        "feeType": {
                          "type": "string",
                          "enum": [
                            "fixed",
                            "text",
                            "none"
                          ]
                        },
                        "isSelected": {
                          "type": "boolean"
                        },
                        "includedFeatures": {
                          "type": "array",
                          "items": {
                            "type": "string"
                          },
                          "description": "Feature ids included in this column (comparison style)."
                        }
                      }
                    }
                  },
                  "features": {
                    "type": "array",
                    "description": "Comparison-style feature rows. An omitted id is generated.",
                    "items": {
                      "type": "object",
                      "properties": {
                        "id": {
                          "type": "string"
                        },
                        "name": {
                          "type": "string"
                        },
                        "order_key": {
                          "type": "number"
                        }
                      }
                    }
                  },
                  "containerDesign": {
                    "type": "object",
                    "description": "Optional visual settings for the side-by-side container."
                  },
                  "featuresColumnDesign": {
                    "type": "object",
                    "description": "Optional visual settings for the features column (comparison style)."
                  },
                  "headingDesign": {
                    "type": "object",
                    "description": "Optional visual settings for comparison-style heading rows."
                  },
                  "theme": {
                    "type": "string",
                    "enum": [
                      "metals",
                      "ocean",
                      "forest",
                      "sunset",
                      "monochrome",
                      "pastel",
                      "neon",
                      "earth",
                      "berry",
                      "corporate"
                    ],
                    "description": "Built-in color theme (the design drawer's Quick Themes). Paints column colors, the container, and — when style is comparison — the features column and headings. monochrome is the Mono theme. Not stored; the painted colors are.\n"
                  },
                  "tags": {
                    "type": "array",
                    "items": {
                      "type": "string"
                    },
                    "description": "Optional library tags. Omit to create the side-by-side untagged."
                  },
                  "design": {
                    "type": "object",
                    "description": "Custom theme. Use instead of theme, or with theme to override specific settings. container, featuresColumn, heading, and columns (a palette cycled across the columns).\n",
                    "properties": {
                      "container": {
                        "type": "object"
                      },
                      "featuresColumn": {
                        "type": "object"
                      },
                      "heading": {
                        "type": "object"
                      },
                      "columns": {
                        "type": "array",
                        "items": {
                          "type": "object"
                        }
                      }
                    }
                  }
                }
              }
            }
          }
        },
        "responses": {
          "201": {
            "description": "Side-by-side created. id is its Public ID.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "id": {
                      "type": "string",
                      "description": "Public ID of the new library side-by-side.",
                      "example": "Hs3kW8nYb4Rd"
                    },
                    "name": {
                      "type": "string"
                    },
                    "description": {
                      "type": "string"
                    },
                    "columns": {
                      "type": "array",
                      "items": {
                        "type": "object"
                      }
                    },
                    "selectionType": {
                      "type": "string"
                    },
                    "style": {
                      "type": "string"
                    },
                    "features": {
                      "type": "array",
                      "items": {
                        "type": "object"
                      }
                    },
                    "created_at": {
                      "type": "string",
                      "format": "date-time"
                    },
                    "updated_at": {
                      "type": "string",
                      "format": "date-time"
                    }
                  }
                }
              }
            }
          },
          "400": {
            "description": "Name is missing."
          },
          "401": {
            "$ref": "#/components/responses/UnauthorizedError"
          },
          "404": {
            "description": "Organization not found."
          },
          "409": {
            "description": "A library side-by-side with this name already exists. error.code is DUPLICATE_NAME and error.existingSideBySideId is the Public ID to update with PATCH /api/items/{id}.\n"
          },
          "500": {
            "$ref": "#/components/responses/InternalServerError"
          }
        }
      }
    },
    "/api/pages/{id}/archive": {
      "post": {
        "operationId": "ArchivePage",
        "summary": "Archive a page",
        "description": "Remove a page from the proposal in a way you can undo. The archived page is hidden from the proposal and left out of default list responses. Archiving a Scope of Work page recalculates the proposal totals. Bring it back with UnarchivePage. Proposals that are pending signature, won, or lost cannot be edited (409).\n",
        "tags": [
          "Pages"
        ],
        "parameters": [
          {
            "in": "path",
            "name": "id",
            "required": true,
            "schema": {
              "type": "string"
            },
            "description": "Public ID of the page, as returned by the API (for example zw5FD5HGcfE3).",
            "example": "zw5FD5HGcfE3"
          }
        ],
        "responses": {
          "204": {
            "description": "Page archived. No response body."
          },
          "401": {
            "$ref": "#/components/responses/UnauthorizedError"
          },
          "404": {
            "description": "Page not found."
          },
          "409": {
            "description": "The proposal is locked (pending signature, won, lost, or a locked customer version)."
          },
          "500": {
            "$ref": "#/components/responses/InternalServerError"
          }
        }
      }
    },
    "/api/pages/{id}/duplicate": {
      "post": {
        "operationId": "DuplicatePage",
        "summary": "Duplicate a page",
        "description": "Copy a page. With an empty body, the copy is placed directly below the original in the same proposal and named \"Copy of <original name>\". For a Scope of Work page this is a deep copy: its groups, line items, modifications, and side-by-sides are copied too.\nOther modes, chosen by the body: toLibrary=true saves the page to the content library (with optional tags). toProposal (a proposal Public ID) adds a library page to that proposal; add fromProposal=true to copy a page from another proposal instead (signature pages cannot be copied this way). toPage (a page Public ID) overwrites that page with this page's content.\nProposals that are pending signature, won, or lost cannot be edited (409). Copying from another proposal into one of those proposals returns 400.\n",
        "tags": [
          "Pages"
        ],
        "parameters": [
          {
            "in": "path",
            "name": "id",
            "required": true,
            "schema": {
              "type": "string"
            },
            "description": "Public ID of the page to copy, as returned by the API (for example zw5FD5HGcfE3).",
            "example": "zw5FD5HGcfE3"
          }
        ],
        "requestBody": {
          "required": false,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "properties": {
                  "name": {
                    "type": "string",
                    "description": "Name for the copy. Defaults to \"Copy of <original name>\".",
                    "example": "Terms and Conditions (2026)"
                  },
                  "toLibrary": {
                    "type": "boolean",
                    "description": "Save the page to the content library instead of copying it within the proposal.",
                    "default": false
                  },
                  "tags": {
                    "type": "array",
                    "items": {
                      "type": "string"
                    },
                    "description": "Library tags to apply when toLibrary is true."
                  },
                  "toPage": {
                    "type": "string",
                    "description": "Public ID of a page to overwrite with this page's content."
                  },
                  "toProposal": {
                    "type": "string",
                    "description": "Public ID of the proposal to copy the page into.",
                    "example": "zw5FD5HGcfE3"
                  },
                  "fromProposal": {
                    "type": "boolean",
                    "description": "Set to true with toProposal when the source page is in a proposal rather than the library."
                  },
                  "sourceProposalName": {
                    "type": "string",
                    "description": "Name of the source proposal, used in the activity log."
                  },
                  "order_key": {
                    "type": "number",
                    "description": "Position of the copy in the destination proposal (used with toProposal)."
                  },
                  "sourceOrgId": {
                    "type": "string",
                    "description": "Only for pages from the shared template library. Leave unset for your own library."
                  },
                  "attrs": {
                    "type": "object",
                    "description": "Design settings to store with the page when saving to the library."
                  }
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Target page overwritten (when toPage is set)."
          },
          "201": {
            "description": "Page copied. Returns the new page."
          },
          "400": {
            "description": "Invalid request, a signature page was sent with fromProposal, or the destination proposal is pending signature, won, or lost."
          },
          "401": {
            "$ref": "#/components/responses/UnauthorizedError"
          },
          "403": {
            "description": "You do not have permission to edit the source or destination."
          },
          "404": {
            "description": "Page or proposal not found."
          },
          "409": {
            "description": "The proposal is locked (pending signature, won, lost, or a locked customer version)."
          },
          "500": {
            "$ref": "#/components/responses/InternalServerError"
          }
        }
      }
    },
    "/api/pages/{id}/move": {
      "post": {
        "operationId": "MovePage",
        "summary": "Move a page",
        "description": "Change a page's position in its proposal. Send after_id as the Public ID of the page it should follow, or null (or the proposal's Public ID) to move it to the top. You can send order_key instead to set an exact position. The page's content and, for a Scope of Work page, its pricing move with it. Proposals that are pending signature, won, or lost cannot be edited (409).\n",
        "tags": [
          "Pages"
        ],
        "parameters": [
          {
            "in": "path",
            "name": "id",
            "required": true,
            "schema": {
              "type": "string"
            },
            "description": "Public ID of the page to move, as returned by the API (for example zw5FD5HGcfE3).",
            "example": "zw5FD5HGcfE3"
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "description": "Provide either order_key (explicit position) or after_id (position relative to another page).",
                "properties": {
                  "order_key": {
                    "type": "number",
                    "description": "New explicit order position within the proposal.",
                    "example": 3
                  },
                  "after_id": {
                    "type": "string",
                    "nullable": true,
                    "description": "Public ID of the page this page should come immediately after. Pass null, an empty string, or the proposal's id to move it to the very top. The server computes the order_key.\n",
                    "example": "zw5FD5HGcfE3"
                  }
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Page moved successfully",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "data": {
                      "type": "object",
                      "properties": {
                        "id": {
                          "type": "string",
                          "description": "Public ID of the moved page.",
                          "example": "zw5FD5HGcfE3"
                        },
                        "order_key": {
                          "type": "number",
                          "description": "New order position"
                        }
                      }
                    }
                  }
                }
              }
            }
          },
          "400": {
            "description": "Neither order_key nor after_id was sent."
          },
          "401": {
            "$ref": "#/components/responses/UnauthorizedError"
          },
          "404": {
            "description": "Page not found."
          },
          "409": {
            "description": "The proposal is locked (pending signature, won, lost, or a locked customer version)."
          },
          "500": {
            "$ref": "#/components/responses/InternalServerError"
          }
        }
      }
    },
    "/api/pages/{id}": {
      "get": {
        "operationId": "GetPage",
        "summary": "Get a page",
        "description": "Return one page with its title (name), HTML body (content), and settings. hideSection is true when the page is hidden from recipients (the editor \"Hide this Page\" toggle). isScopeOfWork is true for a pricing page. The groups and side-by-sides on a Scope of Work page are not included; read them with ListGroups or GetProposal. The page object is returned at the top level, with no \"data\" wrapper.\n",
        "tags": [
          "Pages"
        ],
        "parameters": [
          {
            "in": "path",
            "name": "id",
            "required": true,
            "schema": {
              "type": "string"
            },
            "description": "Public ID of the page, as returned by the API (for example zw5FD5HGcfE3).",
            "example": "zw5FD5HGcfE3"
          }
        ],
        "responses": {
          "200": {
            "description": "The page.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ProposalPage"
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/UnauthorizedError"
          },
          "404": {
            "description": "Page not found."
          },
          "500": {
            "$ref": "#/components/responses/InternalServerError"
          }
        }
      },
      "patch": {
        "operationId": "UpdatePage",
        "summary": "Update a page",
        "description": "Change a page's title, HTML body, or settings. Only the fields you send change. The body is flat, with no \"page\" wrapper. name is the page title and content is the HTML body. Example: { \"name\": \"Our Approach\", \"content\": \"<p>We start with discovery.</p>\" }.\nTo hide a page from recipients (the editor \"Hide this Page\" toggle), send hideSection: true. Staff still see the page in the editor; recipients do not, and hidden Scope of Work pages are left out of grand totals. This is not ArchivePage. Send hideSection: false to show the page again. excludeFromPdf only affects PDF exports.\nOn a Scope of Work page you can also change pricing-table settings such as selectionType, requireSelection, showCents, useRoundedAmounts, hideTotalsTop, hideTotalsBottom, salesTaxPercentage, markupPercentage, and discounts. Groups, side-by-sides, and line items on the page are not edited here; children in the body is ignored. Use UpdateGroup, UpdateItem, and UpdateModification for those. To change a line item's price, use PATCH /api/items/{id}.\nProposals that are pending signature, won, or lost cannot be edited (409 PROPOSAL_LOCKED), and neither can a locked customer version.\n",
        "tags": [
          "Pages"
        ],
        "parameters": [
          {
            "in": "path",
            "name": "id",
            "required": true,
            "schema": {
              "type": "string"
            },
            "description": "Public ID of the page, as returned by the API (for example zw5FD5HGcfE3).",
            "example": "zw5FD5HGcfE3"
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/ProposalPageUpdateInput"
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Page updated. data.id is the page's Public ID; settings are returned inside data.attrs.\n",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "data": {
                      "type": "object",
                      "properties": {
                        "id": {
                          "type": "string",
                          "description": "Public ID of the page.",
                          "example": "zw5FD5HGcfE3"
                        },
                        "name": {
                          "type": "string",
                          "description": "Page title."
                        },
                        "kind": {
                          "type": "string",
                          "example": "page"
                        },
                        "proposal_id": {
                          "type": "string",
                          "description": "Public ID of the proposal."
                        },
                        "order_key": {
                          "type": "number"
                        },
                        "attrs": {
                          "type": "object",
                          "description": "Stored page settings."
                        },
                        "created_at": {
                          "type": "string",
                          "format": "date-time"
                        },
                        "updated_at": {
                          "type": "string",
                          "format": "date-time"
                        }
                      }
                    }
                  }
                }
              }
            }
          },
          "400": {
            "description": "Validation error."
          },
          "401": {
            "$ref": "#/components/responses/UnauthorizedError"
          },
          "404": {
            "description": "Page not found."
          },
          "409": {
            "description": "The proposal is locked (PROPOSAL_LOCKED: pending signature, won, lost, or a locked customer version), or a library page with this name already exists (DUPLICATE_NAME).\n",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "error": {
                      "type": "object",
                      "properties": {
                        "code": {
                          "type": "string",
                          "example": "DUPLICATE_NAME"
                        },
                        "message": {
                          "type": "string",
                          "example": "A page with the name \"Page Name\" already exists in the library."
                        },
                        "existingPageId": {
                          "type": "string",
                          "description": "Public ID of the existing library page."
                        }
                      }
                    }
                  }
                }
              }
            }
          },
          "500": {
            "$ref": "#/components/responses/InternalServerError"
          }
        }
      },
      "delete": {
        "operationId": "DeletePage",
        "summary": "Delete a page",
        "description": "Delete a page. It is soft-deleted: it disappears from the proposal and from API responses. Deleting a Scope of Work page also deletes its groups, side-by-sides, and line items. To remove a page but keep the option to bring it back, use ArchivePage and UnarchivePage instead. Requires permission to delete proposals.\n",
        "tags": [
          "Pages"
        ],
        "parameters": [
          {
            "in": "path",
            "name": "id",
            "required": true,
            "schema": {
              "type": "string"
            },
            "description": "Public ID of the page, as returned by the API (for example zw5FD5HGcfE3).",
            "example": "zw5FD5HGcfE3"
          }
        ],
        "responses": {
          "204": {
            "description": "Page deleted. No response body."
          },
          "401": {
            "$ref": "#/components/responses/UnauthorizedError"
          },
          "403": {
            "description": "Not authorized to delete from this proposal."
          },
          "404": {
            "description": "Page not found."
          },
          "500": {
            "$ref": "#/components/responses/InternalServerError"
          }
        }
      }
    },
    "/api/pages/{id}/unarchive": {
      "post": {
        "operationId": "UnarchivePage",
        "summary": "Unarchive a page",
        "description": "Restore an archived page so it shows in the proposal again. For a Scope of Work page, its groups, side-by-sides, and line items are restored too, and the proposal totals are recalculated. Proposals that are pending signature, won, or lost cannot be edited (409).\n",
        "tags": [
          "Pages"
        ],
        "parameters": [
          {
            "in": "path",
            "name": "id",
            "required": true,
            "schema": {
              "type": "string"
            },
            "description": "Public ID of the archived page, as returned by the API (for example zw5FD5HGcfE3).",
            "example": "zw5FD5HGcfE3"
          }
        ],
        "responses": {
          "204": {
            "description": "Page restored. No response body."
          },
          "401": {
            "$ref": "#/components/responses/UnauthorizedError"
          },
          "404": {
            "description": "Page not found."
          },
          "409": {
            "description": "The proposal is locked (pending signature, won, lost, or a locked customer version)."
          },
          "500": {
            "$ref": "#/components/responses/InternalServerError"
          }
        }
      }
    },
    "/api/pages": {
      "post": {
        "operationId": "CreatePage",
        "summary": "Create a page",
        "description": "Add a page to a proposal (parent_proposal, the proposal's Public ID) or to your organization's library (in_library true). Send exactly one of the two. The page's fields go inside \"page\"; only page.name (the title) is required, and page.content is the HTML body. The new page is added at the end unless you send page.order_key. Example: { \"parent_proposal\": \"zw5FD5HGcfE3\", \"page\": { \"name\": \"About Us\", \"content\": \"<p>We have served clients since 2010.</p>\" } }.\nSet page.hideSection true to create the page hidden from recipients (the editor \"Hide this Page\" toggle). Staff still see it; recipients do not. Hidden Scope of Work pages are left out of grand totals. This is not the same as archiving the page. Set page.excludeFromPdf true to leave the page out of PDF exports only.\nSet page.isScopeOfWork true to create a Scope of Work (pricing) page. It starts empty and uses your organization's pricing-table defaults. Add groups, line items, and side-by-sides to it with POST /api/proposals/{id}/items (scope_of_work_page_id set to the returned id). Proposals that are pending signature, won, or lost cannot be edited (409).\n",
        "tags": [
          "Pages"
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "description": "Send exactly one of parent_proposal or in_library.",
                "properties": {
                  "parent_proposal": {
                    "type": "string",
                    "description": "Public ID of the proposal to add the page to.",
                    "example": "zw5FD5HGcfE3"
                  },
                  "in_library": {
                    "type": "boolean",
                    "description": "Create the page in your organization's library."
                  },
                  "page": {
                    "$ref": "#/components/schemas/ProposalPageCreateInput"
                  }
                },
                "required": [
                  "page"
                ]
              }
            }
          }
        },
        "responses": {
          "201": {
            "description": "Page created. data.id is the new page's Public ID.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "data": {
                      "$ref": "#/components/schemas/ProposalPage"
                    }
                  }
                }
              }
            }
          },
          "400": {
            "$ref": "#/components/responses/ValidationError"
          },
          "401": {
            "$ref": "#/components/responses/UnauthorizedError"
          },
          "403": {
            "$ref": "#/components/responses/ForbiddenError"
          },
          "404": {
            "description": "Proposal or organization not found."
          },
          "409": {
            "description": "The proposal is locked (pending signature, won, lost, or a locked customer version), or a library page with this name already exists (only when in_library is true).\n"
          },
          "500": {
            "$ref": "#/components/responses/InternalServerError"
          }
        }
      }
    },
    "/api/proposals/{id}/analytics": {
      "get": {
        "operationId": "GetProposalAnalytics",
        "summary": "Get analytics events for a proposal",
        "description": "Retrieve analytics events for a specific proposal. Requires authentication and membership in the proposal's organization (or platform admin).",
        "tags": [
          "Proposals"
        ],
        "parameters": [
          {
            "in": "path",
            "name": "id",
            "required": true,
            "schema": {
              "type": "string"
            },
            "description": "Proposal ID",
            "example": "zw5FD5HGcfE3"
          },
          {
            "in": "query",
            "name": "event_type",
            "schema": {
              "type": "string",
              "enum": [
                "proposal_viewed",
                "proposal_sent",
                "status_changed",
                "proposal_signed",
                "pdf_viewed",
                "signature_request_sent"
              ]
            },
            "description": "Filter by event type"
          },
          {
            "in": "query",
            "name": "start_date",
            "schema": {
              "type": "string",
              "format": "date-time"
            },
            "description": "Filter events from this date onwards"
          },
          {
            "in": "query",
            "name": "end_date",
            "schema": {
              "type": "string",
              "format": "date-time"
            },
            "description": "Filter events up to this date"
          },
          {
            "in": "query",
            "name": "limit",
            "schema": {
              "type": "integer",
              "default": 50,
              "minimum": 1,
              "maximum": 200
            },
            "description": "Maximum number of events to return"
          },
          {
            "in": "query",
            "name": "offset",
            "schema": {
              "type": "integer",
              "default": 0,
              "minimum": 0
            },
            "description": "Number of events to skip for pagination"
          }
        ],
        "responses": {
          "200": {
            "description": "Analytics events retrieved successfully",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "data": {
                      "type": "array",
                      "items": {
                        "type": "object",
                        "properties": {
                          "id": {
                            "type": "string",
                            "format": "uuid"
                          },
                          "event_type": {
                            "type": "string"
                          },
                          "event_timestamp": {
                            "type": "string",
                            "format": "date-time"
                          },
                          "user": {
                            "type": "object",
                            "nullable": true,
                            "properties": {
                              "public_id": {
                                "type": "string"
                              },
                              "email": {
                                "type": "string"
                              },
                              "full_name": {
                                "type": "string",
                                "nullable": true
                              }
                            }
                          },
                          "metadata": {
                            "type": "object"
                          }
                        }
                      }
                    }
                  }
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/UnauthorizedError"
          },
          "403": {
            "$ref": "#/components/responses/ForbiddenError"
          },
          "500": {
            "$ref": "#/components/responses/InternalServerError"
          }
        }
      }
    },
    "/api/proposals/{id}/archive": {
      "post": {
        "operationId": "ArchiveProposal",
        "summary": "Archive a proposal",
        "description": "Archive a proposal by setting its archived_at timestamp. Archived proposals are excluded from default listings and become read-only.",
        "tags": [
          "Proposals"
        ],
        "parameters": [
          {
            "in": "path",
            "name": "id",
            "required": true,
            "schema": {
              "type": "string"
            },
            "description": "Proposal ID",
            "example": "zw5FD5HGcfE3"
          }
        ],
        "responses": {
          "204": {
            "description": "Proposal archived successfully"
          },
          "401": {
            "$ref": "#/components/responses/UnauthorizedError"
          },
          "403": {
            "description": "Not authorized to archive this proposal",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "error": {
                      "type": "string",
                      "example": "Not authorized to archive this proposal"
                    }
                  }
                }
              }
            }
          },
          "404": {
            "description": "Proposal not found",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "error": {
                      "type": "string",
                      "example": "Proposal not found"
                    }
                  }
                }
              }
            }
          },
          "409": {
            "description": "Proposal is already archived",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "error": {
                      "type": "string",
                      "example": "Proposal is already archived"
                    }
                  }
                }
              }
            }
          },
          "500": {
            "$ref": "#/components/responses/InternalServerError"
          }
        }
      }
    },
    "/api/proposals/{id}/bulk-archive": {
      "post": {
        "operationId": "BulkArchiveItems",
        "summary": "Archive multiple items in a proposal",
        "description": "Archive multiple items (line items, groups) from a proposal",
        "tags": [
          "Proposals"
        ],
        "parameters": [
          {
            "in": "path",
            "name": "id",
            "required": true,
            "schema": {
              "type": "string"
            },
            "description": "Proposal ID",
            "example": "zw5FD5HGcfE3"
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "required": [
                  "itemIds"
                ],
                "properties": {
                  "itemIds": {
                    "type": "array",
                    "items": {
                      "type": "string"
                    },
                    "description": "Array of item IDs to archive",
                    "example": [
                      "abcdefgh1234",
                      "ijklmnop5678"
                    ]
                  }
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Items archived successfully",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "archivedCount": {
                      "type": "integer",
                      "description": "Number of items successfully archived",
                      "example": 3
                    },
                    "message": {
                      "type": "string",
                      "description": "Success message",
                      "example": "Successfully archived 3 items"
                    },
                    "errors": {
                      "type": "array",
                      "items": {
                        "type": "string"
                      },
                      "description": "Array of error messages for failed items (only present if some items failed)"
                    }
                  }
                }
              }
            }
          },
          "400": {
            "$ref": "#/components/responses/ValidationError"
          },
          "401": {
            "$ref": "#/components/responses/UnauthorizedError"
          },
          "500": {
            "$ref": "#/components/responses/InternalServerError"
          }
        }
      }
    },
    "/api/proposals/{id}/duplicate": {
      "post": {
        "operationId": "DuplicateProposal",
        "summary": "Duplicate a proposal",
        "description": "Create a complete copy of an existing proposal with all its groups, items, and pages",
        "tags": [
          "Proposals"
        ],
        "parameters": [
          {
            "in": "path",
            "name": "id",
            "required": true,
            "schema": {
              "type": "string"
            },
            "description": "Proposal ID",
            "example": "zw5FD5HGcfE3"
          }
        ],
        "requestBody": {
          "required": false,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "properties": {
                  "name": {
                    "type": "string",
                    "description": "Name for the duplicated proposal. Default: \"Copy of {original name}\"",
                    "example": "Copy of Q1 2024 Proposal"
                  }
                }
              }
            }
          }
        },
        "responses": {
          "201": {
            "description": "Proposal duplicated successfully",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "data": {
                      "type": "object",
                      "properties": {
                        "id": {
                          "type": "string",
                          "description": "ID of the newly created proposal"
                        }
                      }
                    }
                  }
                }
              }
            }
          },
          "400": {
            "description": "Validation error"
          },
          "401": {
            "$ref": "#/components/responses/UnauthorizedError"
          },
          "403": {
            "$ref": "#/components/responses/ForbiddenError"
          },
          "404": {
            "description": "Proposal not found"
          },
          "500": {
            "$ref": "#/components/responses/InternalServerError"
          }
        }
      }
    },
    "/api/proposals/{id}/export": {
      "get": {
        "operationId": "ExportProposalJSON",
        "summary": "Export a proposal or template as JSON",
        "description": "Download a proposal or template as the same importable JSON file produced by More → Export as JSON in the app. Save the response body and import it with Create Proposal → Import from JSON — no transformation needed. The response is the export object itself (not wrapped in a data envelope). Non-portable fields such as signature state, payment sessions, and email drafts are stripped, and status is set to DRAFT, matching the in-app export.\n",
        "tags": [
          "Proposals"
        ],
        "parameters": [
          {
            "in": "path",
            "name": "id",
            "required": true,
            "schema": {
              "type": "string"
            },
            "description": "Proposal or template ID",
            "example": "zw5FD5HGcfE3"
          }
        ],
        "responses": {
          "200": {
            "description": "Importable proposal or template JSON — the same file you get from More → Export as JSON in the app. Ready to import via Create Proposal → Import from JSON.\n",
            "headers": {
              "Content-Disposition": {
                "schema": {
                  "type": "string"
                },
                "description": "Suggested download filename for the export",
                "example": "attachment; filename=\"acme_proposal_zw5FD5HGcfE3.json\""
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "description": "Proposal export object (same shape as the in-app Export as JSON download). Must include at least name and pages to be importable.\n",
                  "required": [
                    "name",
                    "pages"
                  ],
                  "properties": {
                    "id": {
                      "type": "string",
                      "description": "Source proposal or template ID"
                    },
                    "name": {
                      "type": "string",
                      "description": "Proposal or template name"
                    },
                    "status": {
                      "type": "string",
                      "description": "Always DRAFT in exports",
                      "example": "DRAFT"
                    },
                    "is_template": {
                      "type": "boolean",
                      "description": "Whether this export is a template"
                    },
                    "pages": {
                      "type": "array",
                      "description": "Pages, including scope-of-work content, groups, and line items",
                      "items": {
                        "type": "object"
                      }
                    },
                    "settings": {
                      "type": "object",
                      "description": "Proposal settings included in the export"
                    },
                    "design": {
                      "type": "object",
                      "description": "Design settings included in the export"
                    }
                  }
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/UnauthorizedError"
          },
          "403": {
            "$ref": "#/components/responses/ForbiddenError"
          },
          "404": {
            "description": "Proposal not found or not authorized",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "error": {
                      "type": "object",
                      "properties": {
                        "code": {
                          "type": "string",
                          "example": "NOT_FOUND"
                        },
                        "message": {
                          "type": "string"
                        }
                      }
                    }
                  }
                }
              }
            }
          },
          "500": {
            "$ref": "#/components/responses/InternalServerError"
          }
        }
      }
    },
    "/api/proposals/{id}/groups/reorder": {
      "post": {
        "operationId": "ReorderProposalGroups",
        "summary": "Reorder groups in a proposal",
        "description": "Set the display order of groups. Send groupIds as the Public IDs of the groups in the order you want; the first id is shown first. Groups stay on their current Scope of Work page, so list groups from the same page together. Every id must be a group in this proposal, otherwise the request fails with 400 and nothing changes. To move one group, use MoveGroup. Proposals that are pending signature, won, or lost cannot be edited (409).\n",
        "tags": [
          "Groups"
        ],
        "parameters": [
          {
            "in": "path",
            "name": "id",
            "required": true,
            "schema": {
              "type": "string"
            },
            "description": "Public ID of the proposal (for example zw5FD5HGcfE3).",
            "example": "zw5FD5HGcfE3"
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "required": [
                  "groupIds"
                ],
                "properties": {
                  "groupIds": {
                    "type": "array",
                    "items": {
                      "type": "string"
                    },
                    "description": "Public IDs of groups in this proposal, in the desired order. Must not be empty.",
                    "example": [
                      "sL6mScw17PKV",
                      "RCNMjEEJrf7e"
                    ]
                  }
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Groups reordered successfully",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "message": {
                      "type": "string",
                      "example": "Groups reordered successfully"
                    },
                    "data": {
                      "type": "object",
                      "properties": {
                        "reorderedCount": {
                          "type": "integer",
                          "description": "Number of groups that were reordered",
                          "example": 2
                        }
                      }
                    }
                  }
                }
              }
            }
          },
          "400": {
            "$ref": "#/components/responses/ValidationError"
          },
          "401": {
            "$ref": "#/components/responses/UnauthorizedError"
          },
          "403": {
            "$ref": "#/components/responses/ForbiddenError"
          },
          "404": {
            "description": "Proposal not found."
          },
          "409": {
            "description": "The proposal is locked (pending signature, won, lost, or a locked customer version)."
          },
          "500": {
            "$ref": "#/components/responses/InternalServerError"
          }
        }
      }
    },
    "/api/proposals/{id}/groups": {
      "get": {
        "operationId": "ListGroups",
        "summary": "List groups in a proposal",
        "description": "Return the groups in a proposal, across all Scope of Work pages, with their Public IDs and settings. Line items are not included; use GetGroup or ListItems with a returned group id to read line items and their prices. Archived groups are left out unless you pass archived.\n",
        "tags": [
          "Groups"
        ],
        "parameters": [
          {
            "in": "path",
            "name": "id",
            "required": true,
            "schema": {
              "type": "string"
            },
            "description": "Public ID of the proposal (for example zw5FD5HGcfE3).",
            "example": "zw5FD5HGcfE3"
          },
          {
            "in": "query",
            "name": "limit",
            "schema": {
              "type": "integer",
              "default": 50,
              "minimum": 1,
              "maximum": 100
            },
            "description": "Number of groups to return (max 100)."
          },
          {
            "in": "query",
            "name": "offset",
            "schema": {
              "type": "integer",
              "default": 0,
              "minimum": 0
            },
            "description": "Number of groups to skip."
          },
          {
            "in": "query",
            "name": "search",
            "schema": {
              "type": "string"
            },
            "description": "Search term to filter groups by name or content"
          },
          {
            "in": "query",
            "name": "sort",
            "schema": {
              "type": "string",
              "enum": [
                "name",
                "order_key",
                "created_at",
                "updated_at"
              ],
              "default": "order_key"
            },
            "description": "Field to sort by"
          },
          {
            "in": "query",
            "name": "order",
            "schema": {
              "type": "string",
              "enum": [
                "asc",
                "desc"
              ],
              "default": "asc"
            },
            "description": "Sort order"
          },
          {
            "in": "query",
            "name": "archived",
            "schema": {
              "type": "string",
              "enum": [
                false,
                true,
                "any"
              ],
              "default": false
            },
            "description": "Filter by archive status (false=non-archived, true=archived only, any=both)"
          }
        ],
        "responses": {
          "200": {
            "description": "Successful response with groups list",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ListGroupsResponse"
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/UnauthorizedError"
          },
          "403": {
            "$ref": "#/components/responses/ForbiddenError"
          },
          "404": {
            "description": "Proposal not found"
          },
          "500": {
            "$ref": "#/components/responses/InternalServerError"
          }
        }
      }
    },
    "/api/proposals/{id}/items/{itemId}/modifications": {
      "patch": {
        "operationId": "UpdateProposalItemModifications",
        "summary": "Replace all modifications on a line item",
        "description": "Save the complete list of modifications for one line item in a single call. The body is a JSON array, and the result matches it exactly:\n- An entry whose id is the Public ID of an existing modification on\n  this line item overwrites that modification.\n\n- An entry with no id (or an id that is not on this line item) is\n  created as a new modification.\n\n- Existing modifications that are not in the array are archived.\n  Sending an empty array archives all of them.\n\n- The array order becomes the display order.\nSend every entry in full. Fields you leave out are not kept from the stored modification; they fall back to defaults: price becomes fixed $0 one-time, isOptional false, isSelected true, and quantity is removed. For fixed prices, price.value is in dollars (major currency units), not cents; a negative value is a discount. For percent, price.value is a whole-number percent (10 = 10%) and percentageApplyTo defaults to line. Example: [ { \"id\": \"kP3mNq8rT2xY\", \"name\": \"Rush delivery\", \"isOptional\": true, \"price\": { \"model\": \"fixed\", \"value\": 250, \"frequency\": \"once\" } }, { \"name\": \"Extended warranty\", \"isOptional\": true, \"price\": { \"model\": \"fixed\", \"value\": 99, \"frequency\": \"annually\" } } ].\nTo change one modification without resending the rest, use UpdateModification instead. Proposals that are pending signature, won, or lost cannot be edited (409 PROPOSAL_LOCKED), and neither can a locked customer version.\n",
        "tags": [
          "Modifications"
        ],
        "parameters": [
          {
            "in": "path",
            "name": "id",
            "required": true,
            "schema": {
              "type": "string"
            },
            "description": "Public ID of the proposal (for example zw5FD5HGcfE3).",
            "example": "zw5FD5HGcfE3"
          },
          {
            "in": "path",
            "name": "itemId",
            "required": true,
            "schema": {
              "type": "string"
            },
            "description": "Public ID of the line item whose modifications you are replacing (for example kP3mNq8rT2xY). It must belong to the proposal in {id}. Not the editor \"Item ID\" (externalItemId).\n",
            "example": "kP3mNq8rT2xY"
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "array",
                "description": "The complete, ordered list of modifications. Include id to keep and overwrite an existing modification; omit it to create one.\n",
                "items": {
                  "$ref": "#/components/schemas/Modification"
                }
              }
            }
          }
        },
        "responses": {
          "204": {
            "description": "Modifications saved. No response body. Read them back with ListModifications."
          },
          "400": {
            "description": "Validation error (for example an invalid externalItemId or a field from an add-on that is turned off)."
          },
          "401": {
            "$ref": "#/components/responses/UnauthorizedError"
          },
          "403": {
            "description": "Not authorized to edit this proposal."
          },
          "404": {
            "description": "Proposal not found, or the line item is not in this proposal.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "error": {
                      "type": "string",
                      "example": "Item not found"
                    }
                  }
                }
              }
            }
          },
          "409": {
            "description": "The proposal is locked (pending signature, won, lost, or a locked customer version)."
          },
          "500": {
            "description": "Failed to save modifications.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "error": {
                      "type": "string",
                      "example": "Failed to insert modifications"
                    }
                  }
                }
              }
            }
          }
        }
      }
    },
    "/api/proposals/{id}/items": {
      "post": {
        "summary": "Create a new item in a proposal",
        "description": "Create a brand-new line item, group, or side-by-side directly inside a proposal (not copied from the library). A line item is added to a group (target_group_id, or the first group by default); a group or side-by-side is added under a Scope of Work page (scope_of_work_page_id, or the first one by default). Pass after_item_id / before_item_id to position the new item relative to an existing one — it then inherits that item's parent.\nTo create a side-by-side on a proposal, send type=side_by_side with a non-empty columns array. To save one in the library instead, POST /api/library/side-by-sides. Each column price is a number in dollars (default 0). Side-by-sides have no URL of their own; to change one later, PATCH /api/items/{id} with an { \"item\": { ... } } body. Prices are in dollars (major currency units), not cents.\nColor theme: set theme to a built-in id (metals, ocean, forest, sunset, monochrome, pastel, neon, earth, berry, corporate) or design for a custom theme. theme paints column colors (cycled), the container, and — when style is comparison — the features column and headings. design.container, design.featuresColumn, design.heading, and design.columns (a cycled palette) are the custom settings. A design object on an individual column overrides that column. Send design with theme to start from a built-in theme and override pieces of it.\n",
        "tags": [
          "Proposals",
          "Side-by-Sides"
        ],
        "security": [
          {
            "ApiKeyAuth": []
          }
        ],
        "parameters": [
          {
            "in": "path",
            "name": "id",
            "required": true,
            "schema": {
              "type": "string"
            },
            "description": "Public ID of the proposal."
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "required": [
                  "type",
                  "name"
                ],
                "properties": {
                  "type": {
                    "type": "string",
                    "enum": [
                      "line_item",
                      "group",
                      "side_by_side"
                    ],
                    "description": "The kind of item to create."
                  },
                  "name": {
                    "type": "string",
                    "description": "Display name of the item."
                  },
                  "description": {
                    "type": "string",
                    "description": "Optional rich-text/HTML description (stored as the item's content)."
                  },
                  "price": {
                    "type": "object",
                    "description": "Required for type=line_item. Pricing definition, for example { \"model\": \"fixed\", \"value\": 379, \"frequency\": \"once\" }. To change it later, PATCH /api/items/{id} with a new price.\n",
                    "properties": {
                      "model": {
                        "type": "string",
                        "enum": [
                          "fixed",
                          "percent",
                          "text",
                          "none",
                          "hourly"
                        ]
                      },
                      "value": {
                        "type": "number",
                        "description": "Required for fixed/percent models. For fixed, the unit price in dollars (major currency units), not cents (379 = $379.00). For percent, a whole-number percent (15 = 15%).\n",
                        "example": 379
                      },
                      "frequency": {
                        "type": "string",
                        "enum": [
                          "once",
                          "monthly",
                          "annually",
                          "quarterly",
                          "weekly",
                          "biweekly",
                          "semi-monthly",
                          "semi-annually",
                          "every-2-years",
                          "every-3-years",
                          "every-4-years",
                          "every-5-years",
                          "every-6-years",
                          "every-7-years",
                          "every-8-years",
                          "every-9-years",
                          "every-10-years"
                        ],
                        "description": "Required for fixed/percent/text/hourly models."
                      },
                      "quantity": {
                        "type": "object",
                        "properties": {
                          "current": {
                            "type": "number"
                          },
                          "min": {
                            "type": "number"
                          },
                          "max": {
                            "type": "number"
                          },
                          "unitLabel": {
                            "type": "string"
                          }
                        }
                      }
                    }
                  },
                  "optional": {
                    "type": "boolean",
                    "description": "Whether the item is optional (client can toggle it)."
                  },
                  "selected": {
                    "type": "boolean",
                    "description": "Whether an optional item is selected by default."
                  },
                  "tags": {
                    "type": "array",
                    "items": {
                      "type": "string"
                    },
                    "description": "Tags to attach to the item."
                  },
                  "target_group_id": {
                    "type": "string",
                    "description": "For type=line_item. Group to add into (default first group)."
                  },
                  "scope_of_work_page_id": {
                    "type": "string",
                    "description": "For type=group or side_by_side. Scope of Work page to add under (default first SOW page)."
                  },
                  "columns": {
                    "type": "array",
                    "description": "Required for type=side_by_side. The selectable packages/columns. Column ids are generated server-side if omitted.\n",
                    "items": {
                      "type": "object",
                      "required": [
                        "name"
                      ],
                      "properties": {
                        "name": {
                          "type": "string"
                        },
                        "description": {
                          "type": "string"
                        },
                        "price": {
                          "type": "number",
                          "description": "Column price in dollars (major currency units), not cents. Default 0.",
                          "example": 499
                        },
                        "frequency": {
                          "type": "string",
                          "description": "e.g. once, monthly, annually (default once)."
                        },
                        "feeType": {
                          "type": "string",
                          "enum": [
                            "fixed",
                            "text",
                            "none"
                          ]
                        },
                        "isSelected": {
                          "type": "boolean"
                        },
                        "includedFeatures": {
                          "type": "array",
                          "items": {
                            "type": "string"
                          },
                          "description": "Feature ids included in this column (comparison style)."
                        },
                        "design": {
                          "type": "object",
                          "nullable": true,
                          "description": "Colors for this column. Overrides theme and design.columns for the keys you set.\n",
                          "properties": {
                            "background": {
                              "type": "string"
                            },
                            "borderColor": {
                              "type": "string"
                            },
                            "borderWidth": {
                              "type": "number"
                            },
                            "borderRadius": {
                              "type": "number"
                            },
                            "buttonColor": {
                              "type": "string"
                            },
                            "buttonTextColor": {
                              "type": "string"
                            },
                            "titleColor": {
                              "type": "string"
                            },
                            "priceColor": {
                              "type": "string"
                            },
                            "fontColor": {
                              "type": "string"
                            },
                            "checkmarkColor": {
                              "type": "string"
                            },
                            "quantityBackgroundColor": {
                              "type": "string"
                            },
                            "quantityFontColor": {
                              "type": "string"
                            }
                          }
                        }
                      }
                    }
                  },
                  "features": {
                    "type": "array",
                    "description": "For type=side_by_side comparison style — the feature rows.",
                    "items": {
                      "type": "object",
                      "required": [
                        "name"
                      ],
                      "properties": {
                        "name": {
                          "type": "string"
                        },
                        "order_key": {
                          "type": "number"
                        }
                      }
                    }
                  },
                  "selection_type": {
                    "type": "string",
                    "enum": [
                      "single",
                      "multiple",
                      "loose"
                    ],
                    "description": "For type=side_by_side. How many columns a client may select (default loose)."
                  },
                  "style": {
                    "type": "string",
                    "enum": [
                      "standard",
                      "comparison"
                    ],
                    "description": "For type=side_by_side. Layout style (default standard)."
                  },
                  "theme": {
                    "type": "string",
                    "enum": [
                      "metals",
                      "ocean",
                      "forest",
                      "sunset",
                      "monochrome",
                      "pastel",
                      "neon",
                      "earth",
                      "berry",
                      "corporate"
                    ],
                    "description": "For type=side_by_side. Built-in color theme (the design drawer's Quick Themes). monochrome is the Mono theme.\n"
                  },
                  "design": {
                    "type": "object",
                    "description": "For type=side_by_side. Custom theme. Use instead of theme, or with theme to override specific settings.\n",
                    "properties": {
                      "container": {
                        "type": "object",
                        "properties": {
                          "background": {
                            "type": "string"
                          },
                          "fontColor": {
                            "type": "string"
                          },
                          "showHeader": {
                            "type": "boolean"
                          },
                          "sizeModifier": {
                            "type": "string",
                            "enum": [
                              "compact",
                              "default",
                              "wide"
                            ]
                          },
                          "borderColor": {
                            "type": "string"
                          },
                          "borderWidth": {
                            "type": "number"
                          },
                          "selectionOutlineColor": {
                            "type": "string"
                          }
                        }
                      },
                      "featuresColumn": {
                        "type": "object",
                        "description": "Comparison style feature-label column.",
                        "properties": {
                          "background": {
                            "type": "string"
                          },
                          "fontColor": {
                            "type": "string"
                          },
                          "borderColor": {
                            "type": "string"
                          },
                          "borderWidth": {
                            "type": "number"
                          },
                          "borderRadius": {
                            "type": "number"
                          }
                        }
                      },
                      "heading": {
                        "type": "object",
                        "description": "Comparison style heading rows.",
                        "properties": {
                          "background": {
                            "type": "string"
                          },
                          "fontColor": {
                            "type": "string"
                          },
                          "alignment": {
                            "type": "string",
                            "enum": [
                              "left",
                              "center",
                              "right"
                            ]
                          }
                        }
                      },
                      "columns": {
                        "type": "array",
                        "description": "Column color palette, cycled across the columns.",
                        "items": {
                          "type": "object"
                        }
                      }
                    }
                  },
                  "after_item_id": {
                    "type": "string",
                    "description": "Place the new item immediately after this existing item (a line item id for type=line_item; a group/side-by-side id for type=group). Inherits that item's parent. Mutually exclusive with before_item_id.\n"
                  },
                  "before_item_id": {
                    "type": "string",
                    "description": "Place the new item immediately before this existing item."
                  },
                  "order_key": {
                    "type": "number",
                    "description": "Optional explicit ordering position. Defaults to appending at the end."
                  }
                }
              }
            }
          }
        },
        "responses": {
          "201": {
            "description": "Item created.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "success": {
                      "type": "boolean"
                    },
                    "data": {
                      "type": "object",
                      "properties": {
                        "id": {
                          "type": "string"
                        },
                        "type": {
                          "type": "string"
                        },
                        "name": {
                          "type": "string"
                        },
                        "order_key": {
                          "type": "number"
                        }
                      }
                    }
                  }
                }
              }
            }
          },
          "400": {
            "description": "Invalid request (bad type, missing/invalid fields, or no valid parent)."
          },
          "403": {
            "description": "Not authorized to edit the proposal."
          },
          "404": {
            "description": "Proposal (or referenced parent/position item) not found."
          }
        }
      }
    },
    "/api/proposals/{id}/library-insertions": {
      "post": {
        "summary": "Add a library item to a proposal",
        "description": "Copies a saved library item (line item, group, page, or side-by-side) into the specified proposal. The library item and the target proposal must belong to the same organization. The copy is always a deep, autonomous copy — it does not stay linked to or synced with the library source. Library content is never modified by this operation.\n",
        "tags": [
          "Proposals"
        ],
        "security": [
          {
            "ApiKeyAuth": []
          }
        ],
        "parameters": [
          {
            "in": "path",
            "name": "id",
            "required": true,
            "schema": {
              "type": "string"
            },
            "description": "Public ID of the target proposal."
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "required": [
                  "type",
                  "library_item_id"
                ],
                "properties": {
                  "type": {
                    "type": "string",
                    "enum": [
                      "line_item",
                      "group",
                      "page",
                      "side_by_side"
                    ],
                    "description": "The kind of library item to insert."
                  },
                  "library_item_id": {
                    "type": "string",
                    "description": "Public ID of the library item to copy."
                  },
                  "target_group_id": {
                    "type": "string",
                    "description": "For type=line_item only. Public ID of the group in the target proposal to add the line item to. Defaults to the first group in the proposal.\n"
                  },
                  "scope_of_work_page_id": {
                    "type": "string",
                    "description": "For type=group or side_by_side. Public ID of the Scope of Work page to insert under. Defaults to the first Scope of Work page.\n"
                  },
                  "after_item_id": {
                    "type": "string",
                    "description": "Place the new item immediately after this existing item. The new item inherits that item's parent (group for a line item; page for a group/side-by-side) and is given a midpoint order_key. Overrides target_group_id / scope_of_work_page_id. Mutually exclusive with before_item_id.\n"
                  },
                  "before_item_id": {
                    "type": "string",
                    "description": "Place the new item immediately before this existing item. Same rules as after_item_id. Mutually exclusive with it.\n"
                  },
                  "order_key": {
                    "type": "number",
                    "description": "Optional explicit ordering position. Defaults to appending at the end."
                  }
                }
              }
            }
          }
        },
        "responses": {
          "201": {
            "description": "Library item inserted into the proposal.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "success": {
                      "type": "boolean"
                    },
                    "data": {
                      "type": "object",
                      "properties": {
                        "id": {
                          "type": "string"
                        },
                        "type": {
                          "type": "string"
                        },
                        "name": {
                          "type": "string"
                        },
                        "order_key": {
                          "type": "number"
                        }
                      }
                    }
                  }
                }
              }
            }
          },
          "400": {
            "description": "Invalid request (missing/invalid type or required target)."
          },
          "403": {
            "description": "Not authorized to edit the target proposal."
          },
          "404": {
            "description": "Proposal or library item not found (or not in the same organization)."
          }
        }
      }
    },
    "/api/proposals/{id}/pages/reorder": {
      "post": {
        "operationId": "ReorderPages",
        "summary": "Reorder pages in a proposal",
        "description": "Set the order of a proposal's pages. Send page_ids as the Public IDs of the pages in the order you want; the first id is shown first. Include every page. Pages you leave out are placed after the listed pages, sorted alphabetically by name. Every id must be a page in this proposal, otherwise the request fails with 400. To move one page, use MovePage. Proposals that are pending signature, won, or lost cannot be edited (409).\n",
        "tags": [
          "Pages"
        ],
        "parameters": [
          {
            "in": "path",
            "name": "id",
            "required": true,
            "schema": {
              "type": "string"
            },
            "description": "Public ID of the proposal (for example zw5FD5HGcfE3).",
            "example": "zw5FD5HGcfE3"
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "required": [
                  "page_ids"
                ],
                "properties": {
                  "page_ids": {
                    "type": "array",
                    "items": {
                      "type": "string"
                    },
                    "description": "Public IDs of all pages in this proposal, in the desired order. Must not be empty.",
                    "example": [
                      "qeP5CKvwBveC",
                      "N66wN1Yhese9",
                      "Vv5EYHv5aNEx"
                    ]
                  }
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Pages reordered successfully",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "message": {
                      "type": "string",
                      "example": "Pages reordered successfully"
                    },
                    "updated_pages": {
                      "type": "array",
                      "items": {
                        "type": "object",
                        "properties": {
                          "id": {
                            "type": "string",
                            "description": "Public ID of the page."
                          },
                          "name": {
                            "type": "string",
                            "description": "Page name"
                          },
                          "order_key": {
                            "type": "number",
                            "description": "New order key"
                          }
                        }
                      }
                    }
                  }
                }
              }
            }
          },
          "400": {
            "$ref": "#/components/responses/ValidationError"
          },
          "401": {
            "$ref": "#/components/responses/UnauthorizedError"
          },
          "403": {
            "$ref": "#/components/responses/ForbiddenError"
          },
          "404": {
            "description": "Proposal not found."
          },
          "409": {
            "description": "The proposal is locked (pending signature, won, lost, or a locked customer version)."
          },
          "500": {
            "$ref": "#/components/responses/InternalServerError"
          }
        }
      }
    },
    "/api/proposals/{id}/pages": {
      "get": {
        "operationId": "ListPages",
        "summary": "List pages in a proposal",
        "description": "Return a proposal's pages in order, with each page's Public ID, title (name), HTML body (content), and settings. isScopeOfWork is true for pricing pages. Use a returned id with GetPage, UpdatePage, or as scope_of_work_page_id when adding groups and side-by-sides.\n",
        "tags": [
          "Pages"
        ],
        "parameters": [
          {
            "in": "path",
            "name": "id",
            "required": true,
            "schema": {
              "type": "string"
            },
            "description": "Public ID of the proposal (for example zw5FD5HGcfE3).",
            "example": "zw5FD5HGcfE3"
          },
          {
            "in": "query",
            "name": "limit",
            "schema": {
              "type": "integer",
              "default": 50,
              "minimum": 1,
              "maximum": 100
            },
            "description": "Number of pages to return (max 100)."
          },
          {
            "in": "query",
            "name": "offset",
            "schema": {
              "type": "integer",
              "default": 0,
              "minimum": 0
            },
            "description": "Number of pages to skip."
          },
          {
            "in": "query",
            "name": "search",
            "schema": {
              "type": "string"
            },
            "description": "Search term to filter pages by name or content"
          },
          {
            "in": "query",
            "name": "sort",
            "schema": {
              "type": "string",
              "enum": [
                "name",
                "order_key",
                "created_at",
                "updated_at"
              ],
              "default": "order_key"
            },
            "description": "Field to sort by"
          },
          {
            "in": "query",
            "name": "order",
            "schema": {
              "type": "string",
              "enum": [
                "asc",
                "desc"
              ],
              "default": "asc"
            },
            "description": "Sort order"
          }
        ],
        "responses": {
          "200": {
            "description": "Successful response with pages list",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "data": {
                      "type": "array",
                      "items": {
                        "$ref": "#/components/schemas/ProposalPage"
                      }
                    },
                    "pagination": {
                      "type": "object",
                      "properties": {
                        "limit": {
                          "type": "integer"
                        },
                        "offset": {
                          "type": "integer"
                        },
                        "total": {
                          "type": "integer"
                        }
                      }
                    }
                  }
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/UnauthorizedError"
          },
          "403": {
            "$ref": "#/components/responses/ForbiddenError"
          },
          "404": {
            "description": "Proposal not found"
          },
          "500": {
            "$ref": "#/components/responses/InternalServerError"
          }
        }
      }
    },
    "/api/proposals/{id}/pdf": {
      "post": {
        "operationId": "GenerateProposalPDF",
        "summary": "Generate proposal PDF",
        "description": "Generate a PDF document for the specified proposal. Returns the PDF as a binary download.",
        "tags": [
          "Proposals"
        ],
        "parameters": [
          {
            "in": "path",
            "name": "id",
            "required": true,
            "schema": {
              "type": "string"
            },
            "description": "Proposal ID",
            "example": "zw5FD5HGcfE3"
          }
        ],
        "requestBody": {
          "required": false,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "properties": {}
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "PDF generated successfully",
            "content": {
              "application/pdf": {
                "schema": {
                  "type": "string",
                  "format": "binary"
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/UnauthorizedError"
          },
          "404": {
            "description": "Proposal not found"
          },
          "500": {
            "$ref": "#/components/responses/InternalServerError"
          }
        }
      }
    },
    "/api/proposals/{id}": {
      "get": {
        "operationId": "GetProposal",
        "summary": "Get a single proposal",
        "description": "Retrieve detailed information about a specific proposal including all items and groups. For an importable backup matching More → Export as JSON in the app, use GET /api/proposals/{id}/export instead.\n",
        "tags": [
          "Proposals"
        ],
        "parameters": [
          {
            "in": "path",
            "name": "id",
            "required": true,
            "schema": {
              "type": "string"
            },
            "description": "Proposal ID",
            "example": "zw5FD5HGcfE3"
          }
        ],
        "responses": {
          "200": {
            "description": "Proposal retrieved successfully",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ProposalDetail"
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/UnauthorizedError"
          },
          "403": {
            "$ref": "#/components/responses/ForbiddenError"
          },
          "404": {
            "description": "Proposal not found",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "error": {
                      "type": "object",
                      "properties": {
                        "code": {
                          "type": "string",
                          "example": "NOT_FOUND"
                        },
                        "message": {
                          "type": "string",
                          "example": "Proposal not found"
                        }
                      }
                    }
                  }
                }
              }
            }
          },
          "500": {
            "$ref": "#/components/responses/InternalServerError"
          }
        }
      },
      "patch": {
        "operationId": "UpdateProposal",
        "summary": "Update a proposal",
        "description": "Update proposal details such as name, description, or client information",
        "tags": [
          "Proposals"
        ],
        "parameters": [
          {
            "in": "path",
            "name": "id",
            "required": true,
            "schema": {
              "type": "string"
            },
            "description": "Proposal ID",
            "example": "zw5FD5HGcfE3"
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/UpdateProposalApiInput"
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Proposal updated successfully",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ProposalListItem"
                }
              }
            }
          },
          "400": {
            "$ref": "#/components/responses/ValidationError"
          },
          "401": {
            "$ref": "#/components/responses/UnauthorizedError"
          },
          "403": {
            "$ref": "#/components/responses/ForbiddenError"
          },
          "404": {
            "description": "Proposal not found"
          },
          "500": {
            "$ref": "#/components/responses/InternalServerError"
          }
        }
      },
      "delete": {
        "operationId": "DeleteProposal",
        "summary": "Delete a proposal",
        "description": "Delete a proposal and all its associated items. The proposal will no longer appear in listings.",
        "tags": [
          "Proposals"
        ],
        "parameters": [
          {
            "in": "path",
            "name": "id",
            "required": true,
            "schema": {
              "type": "string"
            },
            "description": "Proposal ID",
            "example": "zw5FD5HGcfE3"
          }
        ],
        "responses": {
          "204": {
            "description": "Proposal deleted successfully"
          },
          "401": {
            "$ref": "#/components/responses/UnauthorizedError"
          },
          "404": {
            "$ref": "#/components/responses/NotFoundOrNotAuthorizedError"
          },
          "500": {
            "$ref": "#/components/responses/InternalServerError"
          }
        }
      }
    },
    "/api/proposals/{id}/send-email": {
      "post": {
        "operationId": "SendProposalEmail",
        "summary": "Send a proposal by email",
        "description": "Email the proposal to one or more recipients and mark it Sent. This is the\nsame action as Send in the app. Authenticate with a Bearer API key. The key's\nuser must be allowed to send proposals.\n\nrecipients, subject, message, and fromEmail are required. recipients is an\narray of { name, email }, or a comma-separated list of email addresses.\nfromEmail is the address shown as the sender.\n\nTo send the email saved on a template: create the proposal with templateId\n(see Create Proposal). The template's subject and message are copied onto the\nnew proposal as settings.template_send_email. GET the proposal, then pass\nthose values as subject and message here, along with the client's email.\n\nThe saved email signature of the API key's user is appended to message.\nPass signature_user_id (a public user id belonging to this organization) to\nuse that member's signature instead. If that person has no signature, the\nemail still sends without one. Pass include_signature false to send message\nexactly as given.\n\nThese tokens in subject and message are replaced for each recipient:\n[recipient_contact_full_name], [recipient_contact_first_name],\n[recipient_contact_email_address], [recipient_company_name],\n[recipient_address], [recipient_website], [proposal_creator_full_name],\n[proposal_creator_email_address], [proposal_creator_phone_number], and\n[view_proposal_button label=\"View Proposal\"].\n",
        "tags": [
          "Proposals"
        ],
        "parameters": [
          {
            "in": "path",
            "name": "id",
            "required": true,
            "schema": {
              "type": "string"
            },
            "description": "Proposal public id",
            "example": "zw5FD5HGcfE3"
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/SendProposalEmailInput"
              },
              "example": {
                "recipients": [
                  {
                    "name": "Jane Smith",
                    "email": "jane@acme.example"
                  }
                ],
                "subject": "Proposal for Acme Co",
                "message": "<p>Hi [recipient_contact_first_name], your proposal is ready.</p>",
                "fromEmail": "eddie@yourcompany.example"
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Email sent. The proposal status is Sent."
          },
          "400": {
            "description": "Missing fields, an invalid email, or signature_user_id is not a member of this organization"
          },
          "401": {
            "$ref": "#/components/responses/UnauthorizedError"
          },
          "403": {
            "$ref": "#/components/responses/ForbiddenError"
          },
          "404": {
            "description": "Proposal not found"
          },
          "409": {
            "description": "This version is not the customer-visible version, or approval is still required"
          },
          "500": {
            "$ref": "#/components/responses/InternalServerError"
          }
        }
      }
    },
    "/api/proposals/{id}/star": {
      "post": {
        "operationId": "StarProposal",
        "summary": "Star a proposal",
        "description": "Star a proposal for the current user. Starred proposals appear first in listings. This operation is idempotent - starring an already-starred proposal has no effect.",
        "tags": [
          "Proposals"
        ],
        "parameters": [
          {
            "in": "path",
            "name": "id",
            "required": true,
            "schema": {
              "type": "string"
            },
            "description": "Proposal ID",
            "example": "zw5FD5HGcfE3"
          }
        ],
        "responses": {
          "200": {
            "description": "Proposal starred successfully",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "is_starred": {
                      "type": "boolean",
                      "example": true
                    }
                  }
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/UnauthorizedError"
          },
          "404": {
            "$ref": "#/components/responses/NotFoundOrNotAuthorizedError"
          },
          "500": {
            "$ref": "#/components/responses/InternalServerError"
          }
        }
      }
    },
    "/api/proposals/{id}/unarchive": {
      "post": {
        "operationId": "UnarchiveProposal",
        "summary": "Unarchive a proposal",
        "description": "Unarchive a proposal by clearing its archived_at timestamp. The proposal will return to active status and become editable again.",
        "tags": [
          "Proposals"
        ],
        "parameters": [
          {
            "in": "path",
            "name": "id",
            "required": true,
            "schema": {
              "type": "string"
            },
            "description": "Proposal ID",
            "example": "zw5FD5HGcfE3"
          }
        ],
        "responses": {
          "204": {
            "description": "Proposal unarchived successfully"
          },
          "401": {
            "$ref": "#/components/responses/UnauthorizedError"
          },
          "403": {
            "description": "Not authorized to unarchive this proposal",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "error": {
                      "type": "string",
                      "example": "Not authorized to unarchive this proposal"
                    }
                  }
                }
              }
            }
          },
          "404": {
            "description": "Proposal not found",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "error": {
                      "type": "string",
                      "example": "Proposal not found"
                    }
                  }
                }
              }
            }
          },
          "409": {
            "description": "Proposal is not archived",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "error": {
                      "type": "string",
                      "example": "Proposal is not archived"
                    }
                  }
                }
              }
            }
          },
          "500": {
            "$ref": "#/components/responses/InternalServerError"
          }
        }
      }
    },
    "/api/proposals/{id}/unstar": {
      "post": {
        "operationId": "UnstarProposal",
        "summary": "Unstar a proposal",
        "description": "Remove the star from a proposal for the current user. This operation is idempotent - unstarring a proposal that is not starred has no effect.",
        "tags": [
          "Proposals"
        ],
        "parameters": [
          {
            "in": "path",
            "name": "id",
            "required": true,
            "schema": {
              "type": "string"
            },
            "description": "Proposal ID",
            "example": "zw5FD5HGcfE3"
          }
        ],
        "responses": {
          "200": {
            "description": "Proposal unstarred successfully",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "is_starred": {
                      "type": "boolean",
                      "example": false
                    }
                  }
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/UnauthorizedError"
          },
          "404": {
            "$ref": "#/components/responses/NotFoundOrNotAuthorizedError"
          },
          "500": {
            "$ref": "#/components/responses/InternalServerError"
          }
        }
      }
    },
    "/api/proposals": {
      "get": {
        "operationId": "ListProposals",
        "summary": "List all proposals",
        "description": "Retrieve a paginated list of proposals for the authenticated user's organization.\nTemplates are excluded by default. Pass `is_template=true` to list reusable templates\ninstead of regular proposals (not both). Use a returned template's `id` with\n`POST /api/proposals` (`templateId`) to create a proposal from that template.\n",
        "tags": [
          "Proposals"
        ],
        "parameters": [
          {
            "in": "query",
            "name": "page",
            "schema": {
              "type": "integer",
              "default": 1,
              "minimum": 1
            },
            "description": "Page number for pagination"
          },
          {
            "in": "query",
            "name": "limit",
            "schema": {
              "type": "integer",
              "default": 20,
              "minimum": 1,
              "maximum": 100
            },
            "description": "Number of items per page"
          },
          {
            "in": "query",
            "name": "search",
            "schema": {
              "type": "string"
            },
            "description": "Search term to filter proposals by name or client",
            "example": "Website"
          },
          {
            "in": "query",
            "name": "sort",
            "schema": {
              "type": "string",
              "enum": [
                "created_at",
                "updated_at",
                "name",
                "status",
                "client_name",
                "sent",
                "expiring_on",
                "completed",
                "last_activity"
              ],
              "default": "created_at"
            },
            "description": "Field to sort by. Date sorts (sent, expiring_on, completed, last_activity) push proposals missing that date to the end; last_activity is the most recent recipient-facing engagement."
          },
          {
            "in": "query",
            "name": "order",
            "schema": {
              "type": "string",
              "enum": [
                "asc",
                "desc"
              ],
              "default": "desc"
            },
            "description": "Sort order"
          },
          {
            "in": "query",
            "name": "status",
            "schema": {
              "type": "string",
              "enum": [
                "draft",
                "sent",
                "won",
                "lost",
                "cancelled"
              ]
            },
            "description": "Filter by proposal status"
          },
          {
            "in": "query",
            "name": "created_by",
            "schema": {
              "type": "string"
            },
            "description": "Filter by creator's public_id"
          },
          {
            "in": "query",
            "name": "archived",
            "schema": {
              "type": "string",
              "enum": [
                false,
                true,
                "any"
              ],
              "default": false
            },
            "description": "Filter by archive status (false=non-archived, true=archived only, any=both)",
            "example": "false"
          },
          {
            "in": "query",
            "name": "is_template",
            "schema": {
              "type": "boolean",
              "default": false
            },
            "description": "When true, return only template proposals (reusable templates). When omitted or false,\ntemplates are excluded and only regular proposals are returned.\n",
            "example": true
          }
        ],
        "responses": {
          "200": {
            "description": "Successful response with proposals list",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "data": {
                      "type": "array",
                      "items": {
                        "$ref": "#/components/schemas/ProposalListItem"
                      }
                    },
                    "pagination": {
                      "$ref": "#/components/schemas/Pagination"
                    }
                  }
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/UnauthorizedError"
          },
          "500": {
            "$ref": "#/components/responses/InternalServerError"
          }
        }
      },
      "post": {
        "operationId": "CreateProposal",
        "summary": "Create a new proposal",
        "description": "Create a new proposal for the user's organization.\n\nTo start from a template, first call GET /api/proposals?is_template=true and copy\nthe template id. Then POST that id as templateId, along with name and the client\nunder settings.recipient. The new proposal is a copy of the template. Pages,\npricing, and design come from the template. settings.recipient replaces the\ntemplate's client.\n\nsettings.recipient.name is the company name. settings.recipient.contact.name,\ncontact.email, and contact.phone are the person. address and website are optional.\nOnly the settings keys you send replace the template; everything else stays.\n\nOmit templateId to create a blank proposal. Set is_template to true to create a\nreusable template instead of a proposal.\n\nA default send email saved on your template is copied onto the new proposal as\nsettings.template_send_email (subject and message, no signature). Send that\nemail with POST /api/proposals/{id}/send-email.\n\nTo link the new proposal to a CRM record, pass integrations.crm.provider and\nintegrations.crm.deal_id. provider is hubspot, pipedrive, zoho, or gohighlevel.\ndeal_id is that record's id in the CRM (a HubSpot, Pipedrive, or Zoho deal, or\na Go High Level opportunity). The organization must already have that CRM\nconnected. The record is looked up and linked. Name matching is not used.\nRecipient fields are not copied from the CRM; send settings.recipient when\nthe client should be filled in. The proposal URL is written to the field\nconfigured in Integrations, and the proposal total syncs to the deal value.\nWhen the account has Add tax from CRM turned on, a HubSpot deal's tax\nrate is written onto the new proposal's pricing pages. salesTaxPercentage\non this request wins over that rate.\n",
        "tags": [
          "Proposals"
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/CreateProposalApiInput"
              },
              "example": {
                "name": "Acme website redesign",
                "templateId": "zw5FD5HGcfE3",
                "settings": {
                  "recipient": {
                    "name": "Acme Co",
                    "address": "123 Main St, Tulsa, OK",
                    "website": "https://acme.example",
                    "contact": {
                      "name": "Jane Smith",
                      "email": "jane@acme.example",
                      "phone": "555-0100"
                    }
                  }
                }
              }
            }
          }
        },
        "responses": {
          "201": {
            "description": "Proposal created successfully",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "data": {
                      "$ref": "#/components/schemas/CreateProposalResponse"
                    }
                  }
                }
              }
            }
          },
          "400": {
            "$ref": "#/components/responses/ValidationError"
          },
          "401": {
            "$ref": "#/components/responses/UnauthorizedError"
          },
          "403": {
            "$ref": "#/components/responses/ForbiddenError"
          },
          "500": {
            "$ref": "#/components/responses/InternalServerError"
          }
        }
      }
    }
  },
  "tags": []
}