{
  "openapi": "3.0.3",
  "info": {
    "termsOfService": "https://smartcut.dev/terms",
    "license": {
      "name": "MIT (specification document only — API use governed by the Terms)",
      "url": "https://github.com/jgmedialtd/smartcut-api/blob/main/LICENSE"
    },
    "x-logo": {
      "url": "https://smartcut.dev/images/logo.svg",
      "altText": "SmartCut"
    },
    "title": "SmartCut Store API — inventory & orders",
    "version": "Ecommerce",
    "description": "Store endpoints for managing inventory and tracking order progress.\n\n## Inventory\n\nInventory is a two-level hierarchy. A **material** is a product line\n(\"18mm Oak MFC\") identified by a `code` that is unique within your store.\nA **stock** item is one cuttable sheet or length beneath it, carrying its own\ndimensions and quantity.\n\nStock is addressed under its material — `POST /ecommerce/api/materials/{id}/stock`\n— so the parent link comes from the URL and is never something you send.\n\n**Inheritance:** stock inherits `cost`, `grain`, `category`, `extras` and the other\nshared fields from its material, so you only set what differs. Reads always return\nthe effective values. Setting an inherited field on a stock item pins it, and it will\nno longer follow later edits to the material; set it to `null` to resume inheriting.\n\n**Quantities:** use `POST /ecommerce/api/stock/{id}/adjust` with a relative `delta`\nrather than `PATCH`-ing `q`. The adjust endpoint is atomic, so concurrent callers\ncannot oversell, and it never deletes a row that reaches zero.\n\n## Order workflow\n\n**Order status progression:** `pending → cut → complete → dispatched`\n\n**Part tracking:** Each basket item contains an array of input parts.\nUse `numberCut` to track how many of each part type have been cut,\nand `numberComplete` to track how many have been fully completed.\nParts are identified by `partIndex` (their position in the basket item's parts array).\n\n## Errors\n\nValidation failures return `400` with a machine-readable `code` and an `errors`\narray naming each offending field. Unrecognised fields are rejected rather than\nignored, so a misspelled key is never silently dropped.\n\n**Authentication:** All endpoints require an API key and an active ecommerce subscription.",
    "contact": {
      "name": "SmartCut",
      "url": "https://smartcut.dev"
    }
  },
  "externalDocs": {
    "description": "Specification, runnable examples and MCP configuration",
    "url": "https://github.com/jgmedialtd/smartcut-api"
  },
  "paths": {
    "/ecommerce/api/materials": {
      "get": {
        "tags": [
          "Materials"
        ],
        "summary": "List materials",
        "description": "Paginated list of the product lines in your store.",
        "parameters": [
          {
            "name": "page",
            "in": "query",
            "required": false,
            "schema": {
              "type": "integer",
              "minimum": 1,
              "default": 1
            },
            "description": "Page number."
          },
          {
            "name": "limit",
            "in": "query",
            "required": false,
            "schema": {
              "type": "integer",
              "minimum": 1,
              "maximum": 100,
              "default": 50
            },
            "description": "Rows per page (max 100)."
          },
          {
            "name": "search",
            "in": "query",
            "required": false,
            "schema": {
              "type": "string"
            },
            "description": "Free-text match on name, code, description, category and tags."
          }
        ],
        "responses": {
          "200": {
            "description": "A page of results",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "data": {
                      "type": "array",
                      "items": {
                        "$ref": "#/components/schemas/Material"
                      }
                    },
                    "page": {
                      "type": "integer"
                    },
                    "limit": {
                      "type": "integer"
                    },
                    "total": {
                      "type": "integer"
                    },
                    "totalPages": {
                      "type": "integer"
                    }
                  }
                }
              }
            }
          },
          "401": {
            "description": "Authentication required"
          },
          "403": {
            "description": "No e-commerce access on this account, or the account is blocked"
          },
          "429": {
            "description": "Rate limited — retry after the interval in the Retry-After header"
          }
        }
      },
      "post": {
        "tags": [
          "Materials"
        ],
        "summary": "Create materials",
        "description": "Create one material, or a batch by sending `{ \"materials\": [ ... ] }`.\n\n`code` is required and must be unique within your store.\n\nBatches are **all or nothing**: if any row fails validation nothing is written,\nand the response lists errors against the row index that caused them.",
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "oneOf": [
                  {
                    "$ref": "#/components/schemas/Material"
                  },
                  {
                    "type": "object",
                    "properties": {
                      "materials": {
                        "type": "array",
                        "items": {
                          "$ref": "#/components/schemas/Material"
                        }
                      }
                    }
                  }
                ]
              },
              "examples": {
                "single": {
                  "summary": "One material",
                  "value": {
                    "code": "OAK-18",
                    "name": "18mm Oak MFC",
                    "type": "sheet",
                    "l": 2440,
                    "w": 1220,
                    "t": [
                      18
                    ],
                    "cost": 42.5
                  }
                },
                "batch": {
                  "summary": "A batch",
                  "value": {
                    "materials": [
                      {
                        "code": "OAK-18",
                        "name": "18mm Oak MFC",
                        "t": [
                          18
                        ],
                        "cost": 42.5
                      },
                      {
                        "code": "MDF-12",
                        "name": "12mm MDF",
                        "t": [
                          12
                        ],
                        "cost": 18
                      }
                    ]
                  }
                }
              }
            }
          }
        },
        "responses": {
          "201": {
            "description": "Created",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "material": {
                      "$ref": "#/components/schemas/Material"
                    }
                  }
                }
              }
            }
          },
          "400": {
            "description": "Validation failed",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ValidationError"
                }
              }
            }
          },
          "401": {
            "description": "Authentication required"
          },
          "403": {
            "description": "No e-commerce access on this account, or the account is blocked"
          },
          "404": {
            "description": "Not found in this store. An id belonging to a different store also returns 404 — never 403 — so this response cannot be used to discover whether it exists elsewhere."
          },
          "429": {
            "description": "Rate limited — retry after the interval in the Retry-After header"
          },
          "500": {
            "description": "Server error"
          }
        }
      }
    },
    "/ecommerce/api/materials/by-code/{code}": {
      "get": {
        "tags": [
          "Materials"
        ],
        "summary": "Get a material by code",
        "parameters": [
          {
            "name": "code",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string"
            },
            "description": "The material's code, unique within your store. URL-encode it if it contains spaces or slashes."
          }
        ],
        "responses": {
          "200": {
            "description": "The material",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "material": {
                      "$ref": "#/components/schemas/Material"
                    }
                  }
                }
              }
            }
          },
          "400": {
            "description": "Validation failed",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ValidationError"
                }
              }
            }
          },
          "401": {
            "description": "Authentication required"
          },
          "403": {
            "description": "No e-commerce access on this account, or the account is blocked"
          },
          "404": {
            "description": "Not found in this store. An id belonging to a different store also returns 404 — never 403 — so this response cannot be used to discover whether it exists elsewhere."
          },
          "429": {
            "description": "Rate limited — retry after the interval in the Retry-After header"
          },
          "500": {
            "description": "Server error"
          }
        }
      },
      "put": {
        "tags": [
          "Materials"
        ],
        "summary": "Create or update a material by code",
        "description": "Idempotent upsert keyed on your own product code — create if absent, update if present.\n\nUse this for scheduled syncs from an ERP: the same request can be replayed safely,\nand you never need to track SmartCut ids or check existence first.\n\nResponds `201` when it created the material and `200` when it updated one; the\n`created` flag says which.",
        "parameters": [
          {
            "name": "code",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string"
            },
            "description": "The material's code, unique within your store. URL-encode it if it contains spaces or slashes."
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/Material"
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Updated",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "material": {
                      "$ref": "#/components/schemas/Material"
                    },
                    "created": {
                      "type": "boolean",
                      "example": false
                    }
                  }
                }
              }
            }
          },
          "201": {
            "description": "Created",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "material": {
                      "$ref": "#/components/schemas/Material"
                    },
                    "created": {
                      "type": "boolean",
                      "example": true
                    }
                  }
                }
              }
            }
          },
          "400": {
            "description": "Validation failed",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ValidationError"
                }
              }
            }
          },
          "401": {
            "description": "Authentication required"
          },
          "403": {
            "description": "No e-commerce access on this account, or the account is blocked"
          },
          "404": {
            "description": "Not found in this store. An id belonging to a different store also returns 404 — never 403 — so this response cannot be used to discover whether it exists elsewhere."
          },
          "429": {
            "description": "Rate limited — retry after the interval in the Retry-After header"
          },
          "500": {
            "description": "Server error"
          }
        }
      }
    },
    "/ecommerce/api/materials/{id}": {
      "get": {
        "tags": [
          "Materials"
        ],
        "summary": "Get a material",
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string"
            },
            "description": "Material id."
          }
        ],
        "responses": {
          "200": {
            "description": "The material",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "material": {
                      "$ref": "#/components/schemas/Material"
                    }
                  }
                }
              }
            }
          },
          "400": {
            "description": "Validation failed",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ValidationError"
                }
              }
            }
          },
          "401": {
            "description": "Authentication required"
          },
          "403": {
            "description": "No e-commerce access on this account, or the account is blocked"
          },
          "404": {
            "description": "Not found in this store. An id belonging to a different store also returns 404 — never 403 — so this response cannot be used to discover whether it exists elsewhere."
          },
          "429": {
            "description": "Rate limited — retry after the interval in the Retry-After header"
          },
          "500": {
            "description": "Server error"
          }
        }
      },
      "patch": {
        "tags": [
          "Materials"
        ],
        "summary": "Update a material",
        "description": "Partial update — send only the fields you are changing.\n\nChanging an inheritable field fans out to every stock item beneath this material\nthat has not pinned that field.\n\nUnrecognised fields are rejected rather than ignored, so a typo fails loudly\ninstead of returning success without changing anything.",
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string"
            },
            "description": "Material id."
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/Material"
              },
              "examples": {
                "price": {
                  "summary": "Change the price",
                  "value": {
                    "cost": 44.95
                  }
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Updated",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "material": {
                      "$ref": "#/components/schemas/Material"
                    },
                    "warnings": {
                      "type": "array",
                      "items": {
                        "type": "object"
                      },
                      "description": "Pre-existing problems on fields you did not change. Non-blocking."
                    }
                  }
                }
              }
            }
          },
          "400": {
            "description": "Validation failed",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ValidationError"
                }
              }
            }
          },
          "401": {
            "description": "Authentication required"
          },
          "403": {
            "description": "No e-commerce access on this account, or the account is blocked"
          },
          "404": {
            "description": "Not found in this store. An id belonging to a different store also returns 404 — never 403 — so this response cannot be used to discover whether it exists elsewhere."
          },
          "429": {
            "description": "Rate limited — retry after the interval in the Retry-After header"
          },
          "500": {
            "description": "Server error"
          }
        }
      },
      "delete": {
        "tags": [
          "Materials"
        ],
        "summary": "Delete a material",
        "description": "Deletes the material **and every stock item beneath it**. The response reports how many stock rows were removed.",
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string"
            },
            "description": "Material id."
          }
        ],
        "responses": {
          "200": {
            "description": "Deleted",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "deleted": {
                      "type": "boolean"
                    },
                    "id": {
                      "type": "string"
                    },
                    "deletedStockCount": {
                      "type": "integer",
                      "description": "How many stock items the cascade removed."
                    }
                  }
                }
              }
            }
          },
          "400": {
            "description": "Validation failed",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ValidationError"
                }
              }
            }
          },
          "401": {
            "description": "Authentication required"
          },
          "403": {
            "description": "No e-commerce access on this account, or the account is blocked"
          },
          "404": {
            "description": "Not found in this store. An id belonging to a different store also returns 404 — never 403 — so this response cannot be used to discover whether it exists elsewhere."
          },
          "429": {
            "description": "Rate limited — retry after the interval in the Retry-After header"
          },
          "500": {
            "description": "Server error"
          }
        }
      }
    },
    "/ecommerce/api/materials/{id}/stock": {
      "get": {
        "tags": [
          "Stock"
        ],
        "summary": "List stock under a material",
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string"
            },
            "description": "Material id."
          },
          {
            "name": "page",
            "in": "query",
            "required": false,
            "schema": {
              "type": "integer",
              "minimum": 1,
              "default": 1
            },
            "description": "Page number."
          },
          {
            "name": "limit",
            "in": "query",
            "required": false,
            "schema": {
              "type": "integer",
              "minimum": 1,
              "maximum": 100,
              "default": 50
            },
            "description": "Rows per page (max 100)."
          },
          {
            "name": "search",
            "in": "query",
            "required": false,
            "schema": {
              "type": "string"
            },
            "description": "Free-text match on name, code, description, category and tags."
          },
          {
            "name": "ecommerce",
            "in": "query",
            "required": false,
            "schema": {
              "type": "boolean"
            },
            "description": "Restrict to storefront-enabled stock."
          },
          {
            "name": "isOffcut",
            "in": "query",
            "required": false,
            "schema": {
              "type": "boolean"
            },
            "description": "Restrict to offcuts, or exclude them."
          }
        ],
        "responses": {
          "200": {
            "description": "A page of results",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "data": {
                      "type": "array",
                      "items": {
                        "$ref": "#/components/schemas/Stock"
                      }
                    },
                    "page": {
                      "type": "integer"
                    },
                    "limit": {
                      "type": "integer"
                    },
                    "total": {
                      "type": "integer"
                    },
                    "totalPages": {
                      "type": "integer"
                    }
                  }
                }
              }
            }
          },
          "401": {
            "description": "Authentication required"
          },
          "403": {
            "description": "No e-commerce access on this account, or the account is blocked"
          },
          "429": {
            "description": "Rate limited — retry after the interval in the Retry-After header"
          }
        }
      },
      "post": {
        "tags": [
          "Stock"
        ],
        "summary": "Create stock under a material",
        "description": "Create one stock item, or a batch by sending `{ \"stock\": [ ... ] }`.\n\nThe parent material comes from the URL — do not send a material reference in the body.\n\nAny inherited field you set here that differs from the material is pinned automatically,\nso later edits to the material will not overwrite it. Omit a field to keep inheriting it.\n\nSet `ecommerce: true` for stock you sell; those rows require a positive `cost`.",
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string"
            },
            "description": "Material id."
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "oneOf": [
                  {
                    "$ref": "#/components/schemas/Stock"
                  },
                  {
                    "type": "object",
                    "properties": {
                      "stock": {
                        "type": "array",
                        "items": {
                          "$ref": "#/components/schemas/Stock"
                        }
                      }
                    }
                  }
                ]
              },
              "examples": {
                "single": {
                  "summary": "One sheet",
                  "value": {
                    "l": 2440,
                    "w": 1220,
                    "t": 18,
                    "q": 40,
                    "cost": 42.5,
                    "ecommerce": true
                  }
                },
                "batch": {
                  "summary": "Two thicknesses",
                  "value": {
                    "stock": [
                      {
                        "l": 2440,
                        "w": 1220,
                        "t": 18,
                        "q": 40,
                        "cost": 42.5,
                        "ecommerce": true
                      },
                      {
                        "l": 2440,
                        "w": 1220,
                        "t": 12,
                        "q": 25,
                        "cost": 33,
                        "ecommerce": true
                      }
                    ]
                  }
                }
              }
            }
          }
        },
        "responses": {
          "201": {
            "description": "Created",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "stock": {
                      "$ref": "#/components/schemas/Stock"
                    }
                  }
                }
              }
            }
          },
          "400": {
            "description": "Validation failed",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ValidationError"
                }
              }
            }
          },
          "401": {
            "description": "Authentication required"
          },
          "403": {
            "description": "No e-commerce access on this account, or the account is blocked"
          },
          "404": {
            "description": "Not found in this store. An id belonging to a different store also returns 404 — never 403 — so this response cannot be used to discover whether it exists elsewhere."
          },
          "429": {
            "description": "Rate limited — retry after the interval in the Retry-After header"
          },
          "500": {
            "description": "Server error"
          }
        }
      }
    },
    "/ecommerce/api/stock": {
      "get": {
        "tags": [
          "Stock"
        ],
        "summary": "List stock",
        "description": "Every stock item in your store. Pass `code` to scope to one material without needing its id.",
        "parameters": [
          {
            "name": "page",
            "in": "query",
            "required": false,
            "schema": {
              "type": "integer",
              "minimum": 1,
              "default": 1
            },
            "description": "Page number."
          },
          {
            "name": "limit",
            "in": "query",
            "required": false,
            "schema": {
              "type": "integer",
              "minimum": 1,
              "maximum": 100,
              "default": 50
            },
            "description": "Rows per page (max 100)."
          },
          {
            "name": "search",
            "in": "query",
            "required": false,
            "schema": {
              "type": "string"
            },
            "description": "Free-text match on name, code, description, category and tags."
          },
          {
            "name": "code",
            "in": "query",
            "required": false,
            "schema": {
              "type": "string"
            },
            "description": "Restrict to stock under the material with this code. An unknown code returns an empty page."
          },
          {
            "name": "ecommerce",
            "in": "query",
            "required": false,
            "schema": {
              "type": "boolean"
            },
            "description": "Restrict to storefront-enabled stock."
          },
          {
            "name": "isOffcut",
            "in": "query",
            "required": false,
            "schema": {
              "type": "boolean"
            },
            "description": "Restrict to offcuts, or exclude them."
          }
        ],
        "responses": {
          "200": {
            "description": "A page of results",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "data": {
                      "type": "array",
                      "items": {
                        "$ref": "#/components/schemas/Stock"
                      }
                    },
                    "page": {
                      "type": "integer"
                    },
                    "limit": {
                      "type": "integer"
                    },
                    "total": {
                      "type": "integer"
                    },
                    "totalPages": {
                      "type": "integer"
                    }
                  }
                }
              }
            }
          },
          "401": {
            "description": "Authentication required"
          },
          "403": {
            "description": "No e-commerce access on this account, or the account is blocked"
          },
          "429": {
            "description": "Rate limited — retry after the interval in the Retry-After header"
          }
        }
      }
    },
    "/ecommerce/api/stock/{id}": {
      "get": {
        "tags": [
          "Stock"
        ],
        "summary": "Get a stock item",
        "description": "Inherited fields are resolved, so the response shows the values actually in effect.",
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string"
            },
            "description": "Stock item id."
          }
        ],
        "responses": {
          "200": {
            "description": "The stock item",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "stock": {
                      "$ref": "#/components/schemas/Stock"
                    }
                  }
                }
              }
            }
          },
          "400": {
            "description": "Validation failed",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ValidationError"
                }
              }
            }
          },
          "401": {
            "description": "Authentication required"
          },
          "403": {
            "description": "No e-commerce access on this account, or the account is blocked"
          },
          "404": {
            "description": "Not found in this store. An id belonging to a different store also returns 404 — never 403 — so this response cannot be used to discover whether it exists elsewhere."
          },
          "429": {
            "description": "Rate limited — retry after the interval in the Retry-After header"
          },
          "500": {
            "description": "Server error"
          }
        }
      },
      "patch": {
        "tags": [
          "Stock"
        ],
        "summary": "Update a stock item",
        "description": "Partial update — send only the fields you are changing.\n\nSetting an inherited field pins it to this item, so later edits to the parent\nmaterial will not overwrite it. Set it to `null` to drop the pin and resume\ninheriting the material's value.\n\nTo change quantity, prefer `POST /ecommerce/api/stock/{id}/adjust` — it is atomic.",
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string"
            },
            "description": "Stock item id."
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/Stock"
              },
              "examples": {
                "pin": {
                  "summary": "Pin a different price on this sheet",
                  "value": {
                    "cost": 49.99
                  }
                },
                "reinherit": {
                  "summary": "Go back to the material's price",
                  "value": {
                    "cost": null
                  }
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Updated",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "stock": {
                      "$ref": "#/components/schemas/Stock"
                    },
                    "warnings": {
                      "type": "array",
                      "items": {
                        "type": "object"
                      },
                      "description": "Pre-existing problems on fields you did not change. Non-blocking."
                    }
                  }
                }
              }
            }
          },
          "400": {
            "description": "Validation failed",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ValidationError"
                }
              }
            }
          },
          "401": {
            "description": "Authentication required"
          },
          "403": {
            "description": "No e-commerce access on this account, or the account is blocked"
          },
          "404": {
            "description": "Not found in this store. An id belonging to a different store also returns 404 — never 403 — so this response cannot be used to discover whether it exists elsewhere."
          },
          "429": {
            "description": "Rate limited — retry after the interval in the Retry-After header"
          },
          "500": {
            "description": "Server error"
          }
        }
      },
      "delete": {
        "tags": [
          "Stock"
        ],
        "summary": "Delete a stock item",
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string"
            },
            "description": "Stock item id."
          }
        ],
        "responses": {
          "200": {
            "description": "Deleted",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "deleted": {
                      "type": "boolean"
                    },
                    "id": {
                      "type": "string"
                    }
                  }
                }
              }
            }
          },
          "400": {
            "description": "Validation failed",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ValidationError"
                }
              }
            }
          },
          "401": {
            "description": "Authentication required"
          },
          "403": {
            "description": "No e-commerce access on this account, or the account is blocked"
          },
          "404": {
            "description": "Not found in this store. An id belonging to a different store also returns 404 — never 403 — so this response cannot be used to discover whether it exists elsewhere."
          },
          "429": {
            "description": "Rate limited — retry after the interval in the Retry-After header"
          },
          "500": {
            "description": "Server error"
          }
        }
      }
    },
    "/ecommerce/api/stock/{id}/adjust": {
      "post": {
        "tags": [
          "Stock"
        ],
        "summary": "Adjust stock quantity",
        "description": "Apply a **relative** change to quantity — negative to remove, positive to add.\n\nThis is a single atomic operation, so two systems adjusting the same item at the\nsame time cannot oversell it. Prefer it over `PATCH`-ing `q`, which reads and\nwrites separately and can lose a concurrent update.\n\nThe row is **never deleted** when quantity reaches zero — it stays at `0` so your\nnext sync still finds it.\n\nA delta that would take quantity below zero is refused with `409` and reports how\nmany are actually available, rather than silently clamping.\n\nItems marked `unlimitedQuantity` return `200` with `adjusted: false`.",
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string"
            },
            "description": "Stock item id."
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "required": [
                  "delta"
                ],
                "properties": {
                  "delta": {
                    "type": "integer",
                    "description": "Whole number. Negative removes stock, positive adds it. Must not be zero."
                  },
                  "reason": {
                    "type": "string",
                    "description": "Optional free-text note describing why."
                  }
                }
              },
              "examples": {
                "out": {
                  "summary": "Five sheets picked",
                  "value": {
                    "delta": -5,
                    "reason": "goods out"
                  }
                },
                "in": {
                  "summary": "Delivery received",
                  "value": {
                    "delta": 20,
                    "reason": "PO 4471"
                  }
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Adjusted",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "stock": {
                      "$ref": "#/components/schemas/Stock"
                    },
                    "adjusted": {
                      "type": "boolean",
                      "description": "False when the item is unlimited and quantity is not tracked."
                    },
                    "previousQuantity": {
                      "type": "integer"
                    },
                    "quantity": {
                      "type": "integer"
                    },
                    "reason": {
                      "type": "string",
                      "description": "Present when adjusted is false, explaining why."
                    }
                  }
                }
              }
            }
          },
          "400": {
            "description": "Validation failed",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ValidationError"
                }
              }
            }
          },
          "401": {
            "description": "Authentication required"
          },
          "403": {
            "description": "No e-commerce access on this account, or the account is blocked"
          },
          "404": {
            "description": "Not found in this store. An id belonging to a different store also returns 404 — never 403 — so this response cannot be used to discover whether it exists elsewhere."
          },
          "409": {
            "description": "Not enough stock",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "error": {
                      "type": "string",
                      "example": "Cannot remove 10 — only 4 in stock."
                    },
                    "code": {
                      "type": "string",
                      "enum": [
                        "INSUFFICIENT_QUANTITY"
                      ]
                    },
                    "available": {
                      "type": "integer"
                    },
                    "requested": {
                      "type": "integer"
                    }
                  }
                }
              }
            }
          },
          "429": {
            "description": "Rate limited — retry after the interval in the Retry-After header"
          },
          "500": {
            "description": "Server error"
          }
        }
      }
    },
    "/ecommerce/orders/{id}": {
      "patch": {
        "tags": [
          "Orders"
        ],
        "summary": "Update order status",
        "description": "Update the status of an order.\n\n**Status progression:** `pending → cut → complete → dispatched`\n\n**Transitions:**\n- `pending` → `cut`: Marks all parts as fully cut. Use `forceOverwrite: true` to bypass partial progress warning.\n- `cut` → `complete`: Marks the order as fully completed.\n- `complete` → `dispatched`: Marks the order as dispatched.\n- Any non-dispatched state → `cancelled`: Cancels the order.\n- `cut` → `pending`: Reverts to pending. Use `resetCuts: true` to also reset all part cut counts.\n\nReturns `409 Conflict` if marking as `cut` when parts have partial progress (some but not all cut).\nPass `forceOverwrite: true` to skip this check.",
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string"
            },
            "description": "Order ID"
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/UpdateOrderStatusRequest"
              },
              "examples": {
                "markCut": {
                  "summary": "Mark order as cut",
                  "value": {
                    "status": "cut",
                    "updateInventory": true,
                    "forceOverwrite": false
                  }
                },
                "markComplete": {
                  "summary": "Mark order as complete",
                  "value": {
                    "status": "complete"
                  }
                },
                "markDispatched": {
                  "summary": "Mark order as dispatched",
                  "value": {
                    "status": "dispatched"
                  }
                },
                "cancel": {
                  "summary": "Cancel order",
                  "value": {
                    "status": "cancelled"
                  }
                },
                "revertToPending": {
                  "summary": "Revert to pending and reset cut counts",
                  "value": {
                    "status": "pending",
                    "resetCuts": true
                  }
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Order status updated",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "success": {
                      "type": "boolean"
                    },
                    "data": {
                      "$ref": "#/components/schemas/Order"
                    }
                  }
                }
              }
            }
          },
          "400": {
            "description": "Invalid status value"
          },
          "401": {
            "description": "Authentication required"
          },
          "403": {
            "description": "Ecommerce subscription required"
          },
          "404": {
            "description": "Order not found"
          },
          "409": {
            "description": "Order has parts with partial cut progress",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/PartialProgressError"
                }
              }
            }
          },
          "500": {
            "description": "Server error"
          }
        }
      }
    },
    "/ecommerce/orders/parts/mark-cut": {
      "patch": {
        "tags": [
          "Parts"
        ],
        "summary": "Mark parts as cut",
        "description": "Increment the `numberCut` count for one or more parts across one or more orders.\n\nEach update specifies an order, a basket item within that order, the part index,\nand how many additional instances of that part have been cut.\nThe count is capped at the part's total quantity (`q`).\n\nIf all parts across all items in an order reach their full quantity,\nthe order status is automatically set to `cut`.",
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/MarkPartsRequest"
              },
              "examples": {
                "single": {
                  "summary": "Mark one part cut",
                  "value": {
                    "updates": [
                      {
                        "orderId": "66abc123",
                        "itemId": "item-uuid",
                        "partIndex": 0,
                        "count": 1
                      }
                    ]
                  }
                },
                "batch": {
                  "summary": "Mark multiple parts cut",
                  "value": {
                    "updates": [
                      {
                        "orderId": "66abc123",
                        "itemId": "item-uuid",
                        "partIndex": 0,
                        "count": 3
                      },
                      {
                        "orderId": "66abc123",
                        "itemId": "item-uuid",
                        "partIndex": 1,
                        "count": 2
                      }
                    ]
                  }
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Parts updated",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/MarkPartsResponse"
                }
              }
            }
          },
          "400": {
            "description": "Invalid request body"
          },
          "401": {
            "description": "Authentication required"
          },
          "403": {
            "description": "Ecommerce subscription required"
          },
          "500": {
            "description": "Server error"
          }
        }
      }
    },
    "/ecommerce/orders/parts/mark-complete": {
      "patch": {
        "tags": [
          "Parts"
        ],
        "summary": "Mark parts as complete",
        "description": "Increment the `numberComplete` count for one or more parts across one or more orders.\n\nUse this after parts have been cut and passed any finishing or QC steps.\nThe count is capped at the part's total quantity (`q`).\n\nIf all parts across all items in an order reach their full quantity,\nthe order status is automatically set to `complete`.\n\n**Note:** The order must be in `cut` status for auto-promotion to `complete` to trigger.",
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/MarkPartsRequest"
              },
              "examples": {
                "single": {
                  "summary": "Mark one part complete",
                  "value": {
                    "updates": [
                      {
                        "orderId": "66abc123",
                        "itemId": "item-uuid",
                        "partIndex": 0,
                        "count": 1
                      }
                    ]
                  }
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Parts updated",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/MarkPartsResponse"
                }
              }
            }
          },
          "400": {
            "description": "Invalid request body"
          },
          "401": {
            "description": "Authentication required"
          },
          "403": {
            "description": "Ecommerce subscription required"
          },
          "500": {
            "description": "Server error"
          }
        }
      }
    }
  },
  "components": {
    "securitySchemes": {
      "ApiKeyAuth": {
        "type": "apiKey",
        "in": "header",
        "name": "Authorization",
        "description": "Example: `Authorization: <your-api-key>`. Obtain your key from https://smartcut.dev/account"
      }
    },
    "schemas": {
      "Material": {
        "type": "object",
        "description": "A product line. `code` is unique within your store and is the key used by the by-code endpoints.",
        "properties": {
          "id": {
            "type": "string",
            "description": "Resource id, assigned by SmartCut.",
            "readOnly": true
          },
          "brand": {
            "type": "string",
            "description": "Manufacturer / brand name. Distinct from Variant — Brand is the company that made the material; Variant is a sub-grouping within that brand."
          },
          "category": {
            "type": "string",
            "description": "Category for grouping in the store, e.g. Water Resistant, Fire Rated, Exterior."
          },
          "code": {
            "type": "string",
            "description": "Unique identifier (manufacturer SKU) for the material — required on every row, must be unique within your shop. Rows sharing a code are merged into one material; if you really have two materials, give them distinct codes. Stock items inherit this from their parent material; you can't set a stock-level code separately."
          },
          "color": {
            "type": "string",
            "description": "Colour name, e.g. White, Natural, Oak Brown."
          },
          "colorHex": {
            "type": "string",
            "description": "Hex colour code, e.g. #8B4513, #FFFFFF."
          },
          "cost": {
            "type": "number",
            "minimum": 0,
            "description": "Unit price per sheet. Numeric value."
          },
          "density": {
            "type": "number",
            "minimum": 0,
            "description": "Density in kg/m³. Drives the computed weight (`density × l × w × t`) shown in the cart, on offcuts, and used by shipping. Typical: chipboard ~640, MDF ~720, plywood ~600, HPL ~1400, honeycomb-core lightweight ~340."
          },
          "description": {
            "type": "string",
            "description": "Detailed description of the material item."
          },
          "discount": {
            "type": "number",
            "minimum": 0,
            "maximum": 100,
            "description": "Discount percentage (0-100). e.g. 15 means 15% off."
          },
          "ecommerce": {
            "type": "boolean",
            "description": "Available"
          },
          "extras": {
            "type": "object",
            "description": "Extras"
          },
          "finish": {
            "type": "string",
            "description": "Surface finish description, e.g. Natural, High Gloss, Sanded."
          },
          "fullSizeOnly": {
            "type": "boolean",
            "description": "Disable cut-to-size. When y, customers must purchase whole sheets only."
          },
          "grain": {
            "type": "string",
            "enum": [
              "",
              "l",
              "w"
            ],
            "description": "Grain direction. Use l (along length), w (along width), or leave empty for none."
          },
          "imageUrl": {
            "type": "string",
            "description": "Full URL to a product image. Must be a valid URL, e.g. https://example.com/image.jpg."
          },
          "l": {
            "type": "number",
            "minimum": 0,
            "description": "Stock length in mm. Required."
          },
          "name": {
            "type": "string",
            "description": "Display name for the material item shown to customers. Required. When using a finish-family variant, put the per-decor identity here (\"Dust Grey\", \"Taupe\") — auto import keeps each decor as a separate material."
          },
          "pricingFormula": {
            "type": "string",
            "description": "Pricing Formula"
          },
          "supplierCode": {
            "type": "string",
            "description": "Optional supplier / manufacturer code (e.g. the decor code \"F037 ST76\"), which may differ from your own Code. Used to match the material to an image in the decor library — when present it takes priority over Code for image matching. Not shown to customers."
          },
          "t": {
            "type": "array",
            "items": {
              "oneOf": [
                {
                  "type": "number"
                },
                {
                  "type": "string"
                }
              ]
            },
            "description": "Available thicknesses in mm. A bonded board is one entry written \"base,double\" (e.g. \"18,36\"); creating the material expands it into a stock item per half."
          },
          "tags": {
            "type": "array",
            "items": {
              "type": "string"
            },
            "description": "Searchable tags. Pipe-separated, e.g. plywood|birch|hardwood."
          },
          "trim": {
            "oneOf": [
              {
                "type": "array",
                "items": {
                  "type": "string"
                }
              },
              {
                "type": "object",
                "properties": {
                  "l1": {
                    "type": "number"
                  },
                  "l2": {
                    "type": "number"
                  },
                  "w1": {
                    "type": "number"
                  },
                  "w2": {
                    "type": "number"
                  }
                }
              }
            ],
            "description": "Trim"
          },
          "type": {
            "type": "string",
            "enum": [
              "sheet",
              "linear",
              "roll"
            ],
            "description": "Type of stock. Must be one of: sheet, linear, roll. Defaults to sheet."
          },
          "urls": {
            "description": "urls"
          },
          "variant": {
            "type": "string",
            "description": "Sub-grouping within a brand or material (product line, range, sub-collection). Free-form. Two valid patterns: (a) per-decor identity (\"F037 ST76 Taormina Travertine\") where variant ≈ name; (b) finish family (\"Acrylic Gloss\", \"Super Matt\") shared across many decors, with name carrying the per-decor identity. Auto import preserves both correctly."
          },
          "w": {
            "type": "number",
            "minimum": 0,
            "description": "Stock width in mm. Required."
          },
          "weight": {
            "type": "number",
            "minimum": 0,
            "description": "Weight per unit. Optional — when density is set, weight is computed from `density × l × w × t`. Override here only when the compute can't express the per-row weight (packed bundles, mixed materials)."
          }
        }
      },
      "Stock": {
        "type": "object",
        "description": "One cuttable sheet or length beneath a material. Inherited fields are resolved on read.",
        "properties": {
          "id": {
            "type": "string",
            "description": "Resource id, assigned by SmartCut.",
            "readOnly": true
          },
          "allowExactFitShapes": {
            "description": "allowExactFitShapes"
          },
          "brand": {
            "type": "string",
            "description": "Manufacturer / brand name. Distinct from Variant — Brand is the company that made the material; Variant is a sub-grouping within that brand."
          },
          "category": {
            "type": "string",
            "description": "Category for grouping in the store, e.g. Water Resistant, Fire Rated, Exterior."
          },
          "color": {
            "type": "string",
            "description": "Colour name, e.g. White, Natural, Oak Brown."
          },
          "colorHex": {
            "type": "string",
            "description": "Hex colour code, e.g. #8B4513, #FFFFFF."
          },
          "cost": {
            "type": "number",
            "minimum": 0,
            "description": "Unit price per sheet. Numeric value."
          },
          "density": {
            "type": "number",
            "minimum": 0,
            "description": "Density in kg/m³. Drives the computed weight (`density × l × w × t`) shown in the cart, on offcuts, and used by shipping. Typical: chipboard ~640, MDF ~720, plywood ~600, HPL ~1400, honeycomb-core lightweight ~340."
          },
          "description": {
            "type": "string",
            "description": "Detailed description of the material item."
          },
          "discount": {
            "type": "number",
            "minimum": 0,
            "maximum": 100,
            "description": "Discount percentage (0-100). e.g. 15 means 15% off."
          },
          "ecommerce": {
            "type": "boolean",
            "description": "Available"
          },
          "extras": {
            "type": "object",
            "description": "Extras"
          },
          "finish": {
            "type": "string",
            "description": "Surface finish description, e.g. Natural, High Gloss, Sanded."
          },
          "fullSizeOnly": {
            "type": "boolean",
            "description": "Disable cut-to-size. When y, customers must purchase whole sheets only."
          },
          "grain": {
            "type": "string",
            "enum": [
              "",
              "l",
              "w"
            ],
            "description": "Grain direction. Use l (along length), w (along width), or leave empty for none."
          },
          "imageUrl": {
            "type": "string",
            "description": "Full URL to a product image. Must be a valid URL, e.g. https://example.com/image.jpg."
          },
          "isOffcut": {
            "description": "isOffcut"
          },
          "l": {
            "type": "number",
            "minimum": 0,
            "description": "Stock length in mm. Required."
          },
          "name": {
            "type": "string",
            "description": "Display name for the material item shown to customers. Required. When using a finish-family variant, put the per-decor identity here (\"Dust Grey\", \"Taupe\") — auto import keeps each decor as a separate material."
          },
          "overrides": {
            "description": "overrides"
          },
          "pricingFormula": {
            "type": "string",
            "description": "Pricing Formula"
          },
          "q": {
            "type": "integer",
            "minimum": 0,
            "description": "Quantity in stock. Use the adjust endpoint for relative changes; it is atomic and safe against concurrent callers."
          },
          "t": {
            "type": "number",
            "description": "Thickness in mm, as a single number. Bonded pairs are a material-level concept — set them on the material's thickness list and each half becomes its own stock item."
          },
          "tags": {
            "type": "array",
            "items": {
              "type": "string"
            },
            "description": "Searchable tags. Pipe-separated, e.g. plywood|birch|hardwood."
          },
          "trim": {
            "oneOf": [
              {
                "type": "array",
                "items": {
                  "type": "string"
                }
              },
              {
                "type": "object",
                "properties": {
                  "l1": {
                    "type": "number"
                  },
                  "l2": {
                    "type": "number"
                  },
                  "w1": {
                    "type": "number"
                  },
                  "w2": {
                    "type": "number"
                  }
                }
              }
            ],
            "description": "Trim"
          },
          "unlimitedQuantity": {
            "type": "boolean",
            "description": "Whether stock is unlimited. Use y or n. When y, the quantity value is ignored."
          },
          "urls": {
            "description": "urls"
          },
          "variant": {
            "type": "string",
            "description": "Sub-grouping within a brand or material (product line, range, sub-collection). Free-form. Two valid patterns: (a) per-decor identity (\"F037 ST76 Taormina Travertine\") where variant ≈ name; (b) finish family (\"Acrylic Gloss\", \"Super Matt\") shared across many decors, with name carrying the per-decor identity. Auto import preserves both correctly."
          },
          "w": {
            "type": "number",
            "minimum": 0,
            "description": "Stock width in mm. Required."
          },
          "weight": {
            "type": "number",
            "minimum": 0,
            "description": "Weight per unit. Optional — when density is set, weight is computed from `density × l × w × t`. Override here only when the compute can't express the per-row weight (packed bundles, mixed materials)."
          }
        }
      },
      "ValidationError": {
        "type": "object",
        "description": "A rejected request. Every failure names the field it came from.",
        "properties": {
          "error": {
            "type": "string",
            "example": "Validation failed"
          },
          "code": {
            "type": "string",
            "enum": [
              "VALIDATION_FAILED",
              "NOT_FOUND",
              "FORBIDDEN",
              "INSUFFICIENT_QUANTITY",
              "SUBSCRIPTION_REQUIRED",
              "ACCOUNT_BLOCKED",
              "INTERNAL_ERROR"
            ],
            "description": "Machine-readable classification. Branch on this, not on the message text."
          },
          "errors": {
            "type": "array",
            "description": "Field-level detail. For a batch request each entry is `{ index, issues }` instead.",
            "items": {
              "type": "object",
              "properties": {
                "field": {
                  "type": "string",
                  "description": "Dotted path of the offending field."
                },
                "code": {
                  "type": "string",
                  "enum": [
                    "UNKNOWN_FIELD",
                    "FORBIDDEN_FIELD",
                    "READONLY_FIELD",
                    "REQUIRED_FIELD",
                    "INVALID_VALUE",
                    "DUPLICATE_CODE"
                  ],
                  "description": "UNKNOWN_FIELD lists the accepted set in its message. FORBIDDEN_FIELD is an internal field. READONLY_FIELD is server-managed."
                },
                "message": {
                  "type": "string",
                  "description": "Human-readable explanation, safe to surface to an operator."
                }
              }
            }
          }
        }
      },
      "UpdateOrderStatusRequest": {
        "type": "object",
        "description": "Request body for updating order status",
        "properties": {
          "status": {
            "type": "string",
            "enum": [
              "pending",
              "cut",
              "complete",
              "dispatched",
              "cancelled"
            ],
            "description": "New order status"
          },
          "updateInventory": {
            "type": "boolean",
            "default": false,
            "description": "Decrement inventory stock when marking as cut"
          },
          "forceOverwrite": {
            "type": "boolean",
            "default": false,
            "description": "Skip partial progress check when marking as cut"
          },
          "resetCuts": {
            "type": "boolean",
            "default": false,
            "description": "Reset all part cut counts to zero when reverting to pending"
          }
        },
        "required": [
          "status"
        ]
      },
      "Order": {
        "type": "object",
        "description": "Order document",
        "properties": {
          "_id": {
            "type": "string",
            "description": "Order ID"
          },
          "status": {
            "type": "string",
            "enum": [
              "pending",
              "cut",
              "complete",
              "dispatched",
              "cancelled"
            ],
            "description": "Order status"
          },
          "paymentStatus": {
            "type": "string",
            "enum": [
              "unpaid",
              "paid",
              "refunded"
            ],
            "description": "Payment status"
          },
          "customer": {
            "type": "object",
            "properties": {
              "name": {
                "type": "string"
              },
              "email": {
                "type": "string"
              },
              "phone": {
                "type": "string"
              }
            }
          },
          "items": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/OrderBasketItem"
            },
            "description": "Basket items in this order"
          },
          "createdAt": {
            "type": "string",
            "format": "date-time"
          },
          "updatedAt": {
            "type": "string",
            "format": "date-time"
          }
        }
      },
      "OrderBasketItem": {
        "type": "object",
        "description": "A basket item within an order, containing a calculation result and part tracking arrays",
        "properties": {
          "id": {
            "type": "string",
            "description": "Basket item UUID"
          },
          "jobId": {
            "type": "string",
            "description": "Calculation job ID"
          },
          "partsCount": {
            "type": "number",
            "description": "Total number of part definitions in this item"
          },
          "numberCut": {
            "type": "array",
            "items": {
              "type": "number"
            },
            "description": "Number of each input part that have been cut. Indexed by partIndex."
          },
          "numberComplete": {
            "type": "array",
            "items": {
              "type": "number"
            },
            "description": "Number of each input part that have been completed. Indexed by partIndex."
          }
        }
      },
      "MarkPartsRequest": {
        "type": "object",
        "description": "Request body for marking parts cut or complete",
        "properties": {
          "updates": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/PartUpdate"
            },
            "minItems": 1,
            "description": "Array of part update operations"
          }
        },
        "required": [
          "updates"
        ]
      },
      "PartUpdate": {
        "type": "object",
        "description": "A single part update operation",
        "properties": {
          "orderId": {
            "type": "string",
            "description": "Order ID"
          },
          "itemId": {
            "type": "string",
            "description": "Basket item ID within the order"
          },
          "partIndex": {
            "type": "number",
            "minimum": 0,
            "description": "Index of the part in the basket item's input parts array"
          },
          "count": {
            "type": "number",
            "minimum": 1,
            "description": "Number of additional instances to mark as cut/complete"
          }
        },
        "required": [
          "orderId",
          "itemId",
          "partIndex",
          "count"
        ]
      },
      "MarkPartsResponse": {
        "type": "object",
        "description": "Response from a mark-cut or mark-complete operation",
        "properties": {
          "success": {
            "type": "boolean"
          },
          "data": {
            "type": "object",
            "properties": {
              "results": {
                "type": "array",
                "items": {
                  "type": "object",
                  "properties": {
                    "orderId": {
                      "type": "string"
                    },
                    "success": {
                      "type": "boolean"
                    },
                    "error": {
                      "type": "string",
                      "description": "Error message if success is false"
                    }
                  }
                },
                "description": "Per-order result of the update operation"
              },
              "autoMarkedOrders": {
                "type": "array",
                "items": {
                  "type": "string"
                },
                "description": "IDs of orders automatically promoted to the next status (cut or complete) because all parts reached full quantity"
              }
            }
          }
        }
      },
      "PartialProgressError": {
        "type": "object",
        "description": "Error returned when an order has parts with partial cut progress",
        "properties": {
          "error": {
            "type": "string"
          },
          "code": {
            "type": "string",
            "enum": [
              "PARTIAL_PROGRESS"
            ]
          },
          "data": {
            "type": "object",
            "properties": {
              "partsWithProgress": {
                "type": "array",
                "items": {
                  "type": "object",
                  "properties": {
                    "itemName": {
                      "type": "string"
                    },
                    "partIndex": {
                      "type": "number"
                    },
                    "current": {
                      "type": "number",
                      "description": "Number currently cut"
                    },
                    "total": {
                      "type": "number",
                      "description": "Total quantity"
                    }
                  }
                }
              }
            }
          }
        }
      }
    }
  },
  "security": [
    {
      "ApiKeyAuth": []
    }
  ],
  "servers": [
    {
      "url": "https://api.smartcut.dev",
      "description": "Production"
    },
    {
      "url": "http://localhost:5000",
      "description": "Local development"
    }
  ],
  "tags": [
    {
      "name": "Materials",
      "description": "Product lines — the top level of the inventory hierarchy"
    },
    {
      "name": "Stock",
      "description": "Cuttable sheets and lengths beneath a material, with quantities"
    },
    {
      "name": "Orders",
      "description": "Update order status through the cutting workflow"
    },
    {
      "name": "Parts",
      "description": "Track individual part progress within orders"
    }
  ]
}