{
  "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 API V3",
    "version": "3.1.0",
    "description": "Cutting optimisation for sheet, linear and roll stock.\n\nSend a list of parts and the stock you have; get back an optimised cutting layout. Supports guillotine cutting for panel saws and true-shape nesting for CNC, honouring grain direction, edge banding, blade kerf and per-edge trim. Results export to PDF, CSV, DXF, SVG and saw formats including PTX, Biesse XML and Mayer.\n\nCalculations are asynchronous: submit to `/v3/calculate`, then poll `/v3/result/ready` and collect from `/v3/result`. Authentication is your API key in the `Authorization` header, with **no `Bearer` prefix**.\n\n**Beta surfaces:**\n- `parts[].labels` + top-level `labels.customer` — typed saw label-printer fields. Emitted by PTX (named PARTS_INF columns) and Mayer .MAY (part-level fields → MA010 text slots); Biesse XML to follow. `labels.customer` is PTX-only. Schema may change; see `/v3/export/ptx` and `/v3/export/mayer`.",
    "contact": {
      "name": "SmartCut",
      "url": "https://api.smartcut.dev"
    }
  },
  "externalDocs": {
    "description": "Specification, runnable examples and MCP configuration",
    "url": "https://github.com/jgmedialtd/smartcut-api"
  },
  "paths": {
    "/v3/calculate": {
      "post": {
        "tags": [
          "Calculations"
        ],
        "summary": "Submit calculation",
        "description": "Submit a calculation request using V3 format.\n\nThe response contains a job ID. Results are delivered to your webhook when the job completes — the recommended path, see [Result webhooks](https://smartcut.dev/docs/webhooks). Polling `/v3/result/ready` is the fallback.",
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/CalculateRequest"
              },
              "examples": {
                "guillotine": {
                  "summary": "Guillotine (sheet cutting)",
                  "description": "Two-dimensional sheet cutting with nested rectangular cuts",
                  "value": {
                    "saw": {
                      "cutType": "guillotine",
                      "cutPreference": "l",
                      "bladeWidth": 3,
                      "stockType": "sheet"
                    },
                    "stock": [
                      {
                        "l": 2400,
                        "w": 1200,
                        "t": 18,
                        "material": "Plywood",
                        "trim": {
                          "l1": 0,
                          "l2": 0,
                          "w1": 0,
                          "w2": 0
                        },
                        "grain": "l",
                        "autoAdd": false
                      }
                    ],
                    "parts": [
                      {
                        "l": 400,
                        "w": 300,
                        "t": 18,
                        "material": "Plywood",
                        "q": 5,
                        "extras": {
                          "banding": {
                            "sides": {
                              "l1": "Oak",
                              "l2": true
                            }
                          },
                          "finish": {
                            "faces": {
                              "a": "Gloss"
                            }
                          }
                        }
                      }
                    ]
                  }
                },
                "efficiency": {
                  "summary": "Efficiency (sheet optimization)",
                  "description": "Optimized sheet cutting with automatic stock selection",
                  "value": {
                    "saw": {
                      "cutType": "efficiency",
                      "cutPreference": "l",
                      "bladeWidth": 3,
                      "stockType": "sheet"
                    },
                    "stock": [
                      {
                        "l": 2440,
                        "w": 1220,
                        "t": 18,
                        "material": "MDF",
                        "q": 10,
                        "autoAdd": true,
                        "cost": 45.5
                      }
                    ],
                    "parts": [
                      {
                        "l": 800,
                        "w": 600,
                        "t": 18,
                        "material": "MDF",
                        "q": 12,
                        "grain": "l",
                        "orientationLock": ""
                      },
                      {
                        "l": 400,
                        "w": 300,
                        "t": 18,
                        "material": "MDF",
                        "q": 8,
                        "grain": "",
                        "extras": {
                          "banding": {
                            "sides": {
                              "l1": "White",
                              "l2": "White",
                              "w1": "White",
                              "w2": "White"
                            }
                          }
                        }
                      }
                    ]
                  }
                },
                "linear": {
                  "summary": "Linear (bar cutting)",
                  "description": "One-dimensional cutting for bars, tube, or trim. Linear stock is cut to length, so `cutType` and `cutPreference` are omitted — the optimiser selects the linear calculation from `stockType` alone.",
                  "value": {
                    "saw": {
                      "bladeWidth": 3,
                      "stockType": "linear"
                    },
                    "stock": [
                      {
                        "l": 3000,
                        "w": 100,
                        "t": 50,
                        "material": "Pine",
                        "q": 5,
                        "autoAdd": true,
                        "cost": 12.5
                      }
                    ],
                    "parts": [
                      {
                        "l": 1200,
                        "w": 100,
                        "t": 50,
                        "material": "Pine",
                        "q": 3
                      },
                      {
                        "l": 800,
                        "w": 100,
                        "t": 50,
                        "material": "Pine",
                        "q": 5
                      },
                      {
                        "l": 450,
                        "w": 100,
                        "t": 50,
                        "material": "Pine",
                        "q": 10
                      }
                    ]
                  }
                },
                "withLabels": {
                  "summary": "Beam saw with label-printer fields (BETA)",
                  "description": "Same as guillotine, but populates the beta `labels` surface so the PTX export drives the saw's on-board label printer / barcode machine. Unsupported fields for the chosen saw return as `labelWarnings`; the job still runs.",
                  "value": {
                    "saw": {
                      "cutType": "beam",
                      "cutPreference": "l",
                      "bladeWidth": 4.8,
                      "stockType": "sheet",
                      "vendor": "Homag",
                      "model": "homag-ptx"
                    },
                    "stock": [
                      {
                        "l": 2440,
                        "w": 1220,
                        "t": 18,
                        "material": "MDF",
                        "q": 10,
                        "autoAdd": true
                      }
                    ],
                    "parts": [
                      {
                        "name": "Side Panel",
                        "l": 600,
                        "w": 400,
                        "t": 18,
                        "material": "MDF",
                        "q": 5,
                        "code": "ART-1234",
                        "labels": {
                          "barcode1": "CNC-SIDE-A",
                          "barcode2": "CNC-SIDE-B",
                          "room": "Kitchen",
                          "order": "PO-2024-042",
                          "pallet": "PAL-7",
                          "colour": "Walnut"
                        }
                      }
                    ],
                    "labels": {
                      "customer": "Acme Joinery"
                    }
                  }
                }
              }
            }
          }
        },
        "responses": {
          "202": {
            "description": "Calculation accepted",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "jobId": {
                      "type": "number",
                      "description": "Job identifier. Identifies the job in the webhook delivery (the recommended way to receive results), or when polling `/v3/result/ready`."
                    },
                    "warnings": {
                      "type": "array",
                      "items": {
                        "type": "string"
                      },
                      "description": "Saw settings the saw-rules registry resolved differently from what was sent — the job runs; the caller is told. Each entry names the rule that fired, e.g. `[strategy.time-ignored-for-beam] …`; on a mixed job it is prefixed `Saw '<id>': `. Present only when at least one rule rewrote something. Every rule is listed at https://smartcut.dev/api-docs/saw-settings",
                      "example": [
                        "[strategy.time-ignored-for-beam] guillotineOptions.strategy 'time' is the fence-consistency knob for a manual table saw; a beam saw positions the fence per cut, so it is ignored — set to 'efficiency'."
                      ]
                    },
                    "labelWarnings": {
                      "type": "array",
                      "description": "**BETA** — Non-fatal warnings: label fields the request set that the chosen saw's post-processor format(s) will not surface. The job still runs; the unsupported fields are dropped at emit time. Shape may change before the `labels` surface stabilises.",
                      "items": {
                        "type": "object",
                        "properties": {
                          "category": {
                            "type": "string",
                            "enum": [
                              "label"
                            ]
                          },
                          "field": {
                            "type": "string",
                            "description": "Name of the label field that was unsupported."
                          },
                          "partIndex": {
                            "type": "integer",
                            "nullable": true,
                            "description": "Index in parts[] for part-level fields; null for job-level (top-level labels.customer)."
                          },
                          "partId": {
                            "type": "string",
                            "description": "Part id when available."
                          },
                          "supportedFormats": {
                            "type": "array",
                            "items": {
                              "type": "string"
                            },
                            "description": "Formats that DO surface this field. Migrate the saw choice if a value is required."
                          },
                          "sawFormats": {
                            "type": "array",
                            "items": {
                              "type": "string"
                            },
                            "description": "Formats the chosen saw actually supports."
                          },
                          "message": {
                            "type": "string"
                          }
                        }
                      }
                    }
                  },
                  "required": [
                    "jobId"
                  ]
                }
              }
            }
          },
          "400": {
            "description": "Invalid V3 format - see error details for migration help",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResponse"
                }
              }
            }
          },
          "401": {
            "description": "Authentication required"
          },
          "403": {
            "description": "Not authorized"
          },
          "429": {
            "description": "Rate limit exceeded, or too many concurrent jobs — wait before retrying"
          },
          "500": {
            "description": "Server error"
          },
          "503": {
            "description": "Service unavailable"
          }
        }
      }
    },
    "/v3/validate": {
      "post": {
        "tags": [
          "Calculations"
        ],
        "summary": "Validate inputs without calculating",
        "description": "Run the same input validation and stock-matching a calculation would, but return the\nissues immediately — no job is created, nothing is persisted, no usage is counted.\nUse it for instant pre-submit feedback (e.g. \"will not fit on any relevant stock\",\n\"no matching stock material found\", or being over a plan limit).\n\nThe request body is identical to `/v3/calculate`.",
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/CalculateRequest"
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Validation result. `valid` is true only when there are no issues.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "valid": {
                      "type": "boolean",
                      "description": "True when the inputs produced no issues."
                    },
                    "stage": {
                      "type": "string",
                      "enum": [
                        "input",
                        "match"
                      ],
                      "description": "`input` = failed the basic validation pipeline (missing dimensions, plan limits); `match` = structurally valid, the issues are stock-fit reasons from the matcher."
                    },
                    "issues": {
                      "type": "array",
                      "description": "Validation issues. At stage `match`, each issue points at the offending part via `index`/`field`.",
                      "items": {
                        "type": "object",
                        "properties": {
                          "message": {
                            "type": "string",
                            "description": "The reason — an i18n key (e.g. `errors.validation.will_not_fit`) or a message."
                          },
                          "type": {
                            "type": "string",
                            "enum": [
                              "error",
                              "warning"
                            ]
                          },
                          "category": {
                            "type": "array",
                            "items": {
                              "type": "string"
                            }
                          },
                          "field": {
                            "type": "array",
                            "items": {
                              "type": "array",
                              "items": {
                                "type": "string"
                              }
                            },
                            "description": "Field paths the issue points at, e.g. [[\"material\"],[\"t\"]]."
                          },
                          "index": {
                            "type": "array",
                            "items": {
                              "type": "integer"
                            },
                            "description": "0-based indices of the affected parts."
                          },
                          "identifier": {
                            "type": "string",
                            "description": "Human-readable source (part name or 1-based row)."
                          }
                        }
                      }
                    },
                    "warnings": {
                      "type": "array",
                      "description": "Saw settings the saw-rules registry resolved differently from what was sent — the job runs; the caller is told. Each `message` names the rule that fired, e.g. `[min-spacing.efficiency-only] …`; on a mixed job it is prefixed `Saw '<id>': `. Present only when at least one rule rewrote something, and never affects `valid`. Every rule is listed at https://smartcut.dev/api-docs/saw-settings",
                      "items": {
                        "type": "object",
                        "properties": {
                          "message": {
                            "type": "string",
                            "description": "What was resolved and why, prefixed with the rule id in square brackets."
                          },
                          "type": {
                            "type": "string",
                            "enum": [
                              "warning"
                            ]
                          }
                        },
                        "required": [
                          "message",
                          "type"
                        ]
                      }
                    }
                  },
                  "required": [
                    "valid",
                    "issues"
                  ]
                }
              }
            }
          },
          "400": {
            "description": "Invalid V3 format - see error details for migration help",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResponse"
                }
              }
            }
          },
          "429": {
            "description": "Rate limit exceeded - wait before retry"
          },
          "500": {
            "description": "Server error"
          }
        }
      }
    },
    "/v3/model/analysis": {
      "post": {
        "tags": [
          "Calculations"
        ],
        "summary": "Analyse a 3D model into parts",
        "description": "Upload a 3D model and get back a parts list. The `parts` array is `/v3/calculate`'s\n`parts` array — same field names, same units — so you can submit it verbatim.\n\n**The body is the file itself**, not a multipart form: POST the raw bytes with\n`Content-Type: application/octet-stream`. Everything else is a query parameter.\n\n**Formats.** `step` (best — exact solids and a real declared unit), `stl`, `obj`,\n`ply` and `gltf`/`glb` are read server-side. Collada (`dae`) and `3mf` are **not**:\nboth need a browser DOM to parse, so they return `415` with advice to convert. Format\nis detected from the file's magic bytes; OBJ and ASCII STL carry none, so send\n`filename` or `format` for those or the request is refused rather than guessed at.\n\n**Units.** Only STEP, 3MF and Collada declare a unit. For everything else, say what\nthe model was drawn in with `sourceUnits`, or the numbers are taken to be already in\n`units`. Getting this wrong is silent — every part stays consistent with every other\n— so it is worth being explicit.\n\n**What is in `advice`.** Everything the analysis knows that a calculation does not:\nwhether a part is a rectangle or a shape, what squaring it off would waste, why\nsomething was judged a fitting rather than sheet goods, which two parts share space\nin the assembly. Entries point back at `parts` by index.\n\n**Cost.** Billed on the same compute-time meter as a calculation, and counted against\nyour monthly call allowance. A refused format or an over-size model costs nothing.\nMaximum upload 32MB; models above 2,000,000 triangles are refused — decimate first.",
        "parameters": [
          {
            "name": "filename",
            "in": "query",
            "required": false,
            "schema": {
              "type": "string"
            },
            "description": "The file's name, e.g. `kitchen.step`. Used as a tiebreak when the bytes carry no magic — required for OBJ and ASCII STL unless `format` is sent."
          },
          {
            "name": "format",
            "in": "query",
            "required": false,
            "schema": {
              "type": "string",
              "enum": [
                "step",
                "stl",
                "obj",
                "ply",
                "gltf",
                "glb"
              ]
            },
            "description": "Force the format instead of inferring it from `filename`. Magic bytes still win where the file has them."
          },
          {
            "name": "units",
            "in": "query",
            "required": false,
            "schema": {
              "type": "string",
              "enum": [
                "mm",
                "cm",
                "m",
                "in",
                "ft"
              ],
              "default": "mm"
            },
            "description": "The unit every number in the response is in."
          },
          {
            "name": "sourceUnits",
            "in": "query",
            "required": false,
            "schema": {
              "type": "string",
              "enum": [
                "mm",
                "cm",
                "m",
                "in",
                "ft"
              ]
            },
            "description": "The unit the model was drawn in. Overrides whatever the file declares. Defaults to the file's own declaration, then to `units`."
          },
          {
            "name": "dedupe",
            "in": "query",
            "required": false,
            "schema": {
              "type": "boolean",
              "default": true
            },
            "description": "Collapse identical parts into one row with a quantity — what a cut list wants."
          },
          {
            "name": "mirrorDedupe",
            "in": "query",
            "required": false,
            "schema": {
              "type": "boolean",
              "default": false
            },
            "description": "Treat a part and its mirror image as the same part. Off by default: a mirrored panel is genuinely different once the sheet has a face, a grain direction or machining on one side."
          },
          {
            "name": "bladeWidth",
            "in": "query",
            "required": false,
            "schema": {
              "type": "number"
            },
            "description": "Your saw's kerf, in `units`. Supply it and the response can tell you when the model is too small to be measured in the unit you chose (`model.suggestedUnits`). Without it no unit suggestion is made."
          },
          {
            "name": "holes",
            "in": "query",
            "required": false,
            "schema": {
              "type": "string",
              "enum": [
                "drill",
                "cut"
              ],
              "default": "drill"
            },
            "description": "Which form a ROUND hole comes back in. `drill` (default) returns it as a `machining.holes` operation — a drilled shelf stays a plain rectangle. `cut` returns it as a polygon ring in `holes`, which is the only form a nesting model reads. Holes that are not round are shape either way; there is no drilled equivalent of a slot."
          }
        ],
        "requestBody": {
          "required": true,
          "description": "The model file, as raw bytes.",
          "content": {
            "application/octet-stream": {
              "schema": {
                "type": "string",
                "format": "binary"
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "The model was analysed. `parts` is submittable to `/v3/calculate` as-is.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ModelAnalysisResponse"
                }
              }
            }
          },
          "400": {
            "description": "The body is not a file, the format could not be determined, or the units are not ones we convert.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResponse"
                }
              }
            }
          },
          "401": {
            "description": "Authentication required"
          },
          "403": {
            "description": "Over the monthly call limit, or the account is on hold"
          },
          "413": {
            "description": "Body larger than 32MB"
          },
          "415": {
            "description": "A format this server cannot read (Collada and 3MF need a browser). `details.supported` lists the formats that do work.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResponse"
                }
              }
            }
          },
          "422": {
            "description": "Too many triangles, or the analysis did not finish inside its budget. Decimate the mesh and retry.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResponse"
                }
              }
            }
          },
          "429": {
            "description": "Rate limit exceeded - wait before retry"
          },
          "500": {
            "description": "Server error"
          }
        }
      }
    },
    "/v3/result/ready": {
      "get": {
        "tags": [
          "Results"
        ],
        "summary": "Check if calculation is complete",
        "description": "**Webhooks are the recommended way to receive results** — SmartCut posts the finished result to your URL as soon as the job completes, so there is nothing to poll and no delay. See [Result webhooks](https://smartcut.dev/docs/webhooks) to set one up.\n\nUse this endpoint as a fallback when your client cannot receive inbound HTTP. Poll every 2-3 seconds, stop on `200` (then fetch `/v3/result`) or on `404`/`410`, and back off on `429`.",
        "parameters": [
          {
            "name": "id",
            "in": "query",
            "required": true,
            "schema": {
              "type": "string"
            },
            "description": "Job identifier from /v3/calculate"
          }
        ],
        "responses": {
          "200": {
            "description": "Result is ready - fetch via /v3/result"
          },
          "404": {
            "description": "Job not found or not ready yet"
          },
          "410": {
            "description": "Job expired or failed"
          },
          "429": {
            "description": "Rate limit exceeded - wait before retry"
          }
        }
      }
    },
    "/v3/result": {
      "get": {
        "tags": [
          "Results"
        ],
        "summary": "Retrieve calculation result",
        "description": "Fetch the complete optimization result in V3 format.\n\nIf you use a webhook (recommended), this exact payload is delivered to your URL and you do not need to call this endpoint at all.\n\n**V3 Response Format:**\n- All fields use l1/l2/w1/w2 coordinates\n- Extras in structured format (extras.banding.sides, etc.)\n- Complete metadata with all analysis fields\n- Stock, parts, cuts, segments, offcuts all included",
        "parameters": [
          {
            "name": "id",
            "in": "query",
            "required": true,
            "schema": {
              "type": "string"
            },
            "description": "Job identifier"
          }
        ],
        "responses": {
          "200": {
            "description": "Result retrieved successfully",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ResultResponse"
                }
              }
            }
          },
          "404": {
            "description": "Result not found"
          },
          "410": {
            "description": "Result expired"
          },
          "500": {
            "description": "Server error"
          }
        }
      }
    },
    "/v3/export/pdf": {
      "get": {
        "tags": [
          "Exports"
        ],
        "summary": "Export result as PDF",
        "description": "Generate a PDF file with optimization layouts, cut lists, and part details. V3 format always uses CSV version 2.",
        "parameters": [
          {
            "name": "id",
            "in": "query",
            "required": true,
            "schema": {
              "type": "string"
            },
            "description": "Job ID to export"
          },
          {
            "name": "option",
            "in": "query",
            "required": false,
            "schema": {
              "type": "string",
              "enum": [
                "layout",
                "parts",
                "machining"
              ]
            },
            "description": "Export variant: layout (default, full optimization), parts (parts list only), machining (machining details)"
          },
          {
            "name": "numberFormat",
            "in": "query",
            "required": false,
            "schema": {
              "type": "string",
              "enum": [
                "decimal",
                "fraction"
              ]
            },
            "description": "Number format (also accepts unitType or units as aliases). Default: decimal"
          },
          {
            "name": "credit",
            "in": "query",
            "required": false,
            "schema": {
              "type": "string"
            },
            "description": "Custom credit text (requires whiteLabel plan option)"
          },
          {
            "name": "cropLength",
            "in": "query",
            "required": false,
            "schema": {
              "type": "string",
              "enum": [
                "y",
                "n"
              ]
            },
            "description": "Crop stock length to fit parts. Default: n"
          },
          {
            "name": "tables",
            "in": "query",
            "required": false,
            "schema": {
              "type": "string",
              "enum": [
                "y",
                "n"
              ]
            },
            "description": "Show tables in output. Default: y"
          },
          {
            "name": "showDiagram",
            "in": "query",
            "required": false,
            "schema": {
              "type": "string",
              "enum": [
                "y",
                "n"
              ]
            },
            "description": "Show the visual layout diagram (stock + parts drawing). Set to n for a table-only PDF. Default: y"
          },
          {
            "name": "showCoordinates",
            "in": "query",
            "required": false,
            "schema": {
              "type": "string",
              "enum": [
                "y",
                "n"
              ]
            },
            "description": "Include X/Y coordinate columns in the parts table. Default: y"
          },
          {
            "name": "showNotes",
            "in": "query",
            "required": false,
            "schema": {
              "type": "string",
              "enum": [
                "y",
                "n"
              ]
            },
            "description": "Include a Notes column in the parts table, populated from each part's notes field. Default: n"
          },
          {
            "name": "partAnnotations",
            "in": "query",
            "required": false,
            "schema": {
              "type": "string",
              "enum": [
                "y",
                "n"
              ]
            },
            "description": "Add a clickable PDF text annotation centred on each part in the layout diagram. The popup contains the full part details (name, code, dimensions, material, position, rotation, trim, banding, finish, planing, notes, stock). Useful for inspecting parts whose information is too long to fit on the diagram. Default: n"
          }
        ],
        "responses": {
          "200": {
            "description": "S3 URL to the generated PDF file",
            "content": {
              "application/json": {
                "schema": {
                  "type": "string",
                  "example": "https://smartcut-storage.s3.eu-west-2.amazonaws.com/12345.pdf"
                }
              }
            }
          },
          "400": {
            "description": "Invalid parameters or job not found"
          },
          "403": {
            "description": "Feature not available on your plan"
          },
          "500": {
            "description": "Server error"
          }
        }
      }
    },
    "/v3/export/csv": {
      "get": {
        "tags": [
          "Exports"
        ],
        "summary": "Export result as CSV",
        "description": "Generate a CSV file with part and stock lists. V3 format always uses CSV version 2.",
        "parameters": [
          {
            "name": "id",
            "in": "query",
            "required": true,
            "schema": {
              "type": "string"
            },
            "description": "Job ID to export"
          }
        ],
        "responses": {
          "200": {
            "description": "S3 URL to the generated CSV file",
            "content": {
              "application/json": {
                "schema": {
                  "type": "string",
                  "example": "https://smartcut-storage.s3.eu-west-2.amazonaws.com/12345.csv"
                }
              }
            }
          },
          "400": {
            "description": "Invalid parameters or job not found"
          },
          "500": {
            "description": "Server error"
          }
        }
      }
    },
    "/v3/export/ptx": {
      "get": {
        "tags": [
          "Exports"
        ],
        "summary": "Export result as PTX",
        "description": "Generate a PTX file (v1.17) for beam saw machines.\n\n**Label-printer mapping (typed) — BETA:**\n\nUse the typed `labels` field on each part (and the top-level `labels` on the request) to populate PTX label columns. These are the fields the saw's own label printer / barcode machine reads. **The `labels` surface is in beta — field names and behaviour may change before stable.**\n\n| Source field | PTX column | Section |\n|---|---|---|\n| `code` (top-level on part) | PRODUCT | PARTS_INF |\n| `code` → `name` → part id (first non-empty) | CODE | PARTS_REQ |\n| `labels.barcode1` | BARCODE1 | PARTS_INF |\n| `labels.barcode2` | BARCODE2 | PARTS_INF |\n| `labels.order` | ORDER | PARTS_INF |\n| `labels.pallet` | PALLET | PARTS_INF |\n| `labels.drawing` | DRAWING | PARTS_INF |\n| `labels.productInfo` | PROD_INFO | PARTS_INF |\n| `labels.productNumber` | PROD_NUM | PARTS_INF |\n| `labels.room` | ROOM | PARTS_INF |\n| `labels.colour` | COLOUR | PARTS_INF |\n| `labels.customer` (request top-level) | CUSTOMER | JOBS |\n\nEach label field is limited to 50 characters (100 for `customer`). Sending a label field whose chosen saw does not support it returns a `labelWarnings` array on the calculate response — the request still succeeds and the field is dropped at emit time.\n\nThese fields are not API-only — the embedded storefront calculator writes to the same typed `labels` object, three ways: per-part values supplied by the host on `parts[].labels` at init (identical to this contract), per-part values collected from the customer at order time via its `labelFields` option (which only chooses which columns to show), and the job-level `customer` via its `options.labels` — see the [checkout widget docs](https://smartcut.dev/docs/ecommerce-checkout).\n\n**Legacy `customData` keys (deprecated, removed in a future release):**\n\nFor one release, the PTX emitter still reads these `customData` keys: `customData.barcode1`, `customData.barcode2` (BARCODE1/BARCODE2), `customData.clientName` (JOBS.CUSTOMER) and `customData.barcode` (PARTS_REQ.CODE). Migrate to `labels` to keep the contract typed and avoid silent typos.\n\n`barcode1`, `barcode2` and `clientName` are **fallbacks** — the typed field wins when both are present. **`customData.barcode` is the exception: it OVERRIDES `code`** for PARTS_REQ.CODE, so a part carrying both emits the legacy value in CODE and `code` in PARTS_INF.PRODUCT — two columns disagreeing about the same part. Set one or the other, not both.\n\nAll `customData` entries are still written verbatim to the **PARTS_UDI** section (INFO1..INFO60, up to 60 fields, 200 chars each). Keys are collected across all parts so columns are consistent.\n\nExample part input:\n```json\n{\n  \"name\": \"Side Panel\",\n  \"l\": 600, \"w\": 400,\n  \"code\": \"ART-1234\",\n  \"labels\": {\n    \"barcode1\": \"CNC-SIDE-A\",\n    \"barcode2\": \"CNC-SIDE-B\",\n    \"room\": \"Kitchen\",\n    \"order\": \"PO-2024-042\"\n  },\n  \"customData\": {\n    \"projectCode\": \"PRJ-042\",\n    \"projectName\": \"Kitchen Smith\"\n  }\n}\n```",
        "parameters": [
          {
            "name": "id",
            "in": "query",
            "required": true,
            "schema": {
              "type": "string"
            },
            "description": "Job ID to export"
          }
        ],
        "responses": {
          "200": {
            "description": "S3 URL to the generated PTX file",
            "content": {
              "application/json": {
                "schema": {
                  "type": "string",
                  "example": "https://smartcut-storage.s3.eu-west-2.amazonaws.com/12345.ptx"
                }
              }
            }
          },
          "400": {
            "description": "Invalid parameters or job not found"
          },
          "403": {
            "description": "Unauthorized"
          },
          "500": {
            "description": "Server error"
          }
        }
      }
    },
    "/v3/export/dxf": {
      "get": {
        "tags": [
          "Exports"
        ],
        "summary": "Export result as DXF",
        "description": "Generate DXF files for CNC machines. Returns a ZIP file containing DXF files for each sheet.",
        "parameters": [
          {
            "name": "id",
            "in": "query",
            "required": true,
            "schema": {
              "type": "string"
            },
            "description": "Job ID to export"
          },
          {
            "name": "option",
            "in": "query",
            "required": false,
            "schema": {
              "type": "string",
              "enum": [
                "layout",
                "machining"
              ]
            },
            "description": "Export variant: layout (default, cutting layouts), machining (machining details)"
          },
          {
            "name": "units",
            "in": "query",
            "required": false,
            "schema": {
              "type": "string"
            },
            "description": "Unit system (e.g., mm, in). Default: mm"
          }
        ],
        "responses": {
          "200": {
            "description": "S3 URL to the generated DXF ZIP file",
            "content": {
              "application/json": {
                "schema": {
                  "type": "string",
                  "example": "https://smartcut-storage.s3.eu-west-2.amazonaws.com/12345.zip"
                }
              }
            }
          },
          "400": {
            "description": "Invalid parameters or job not found"
          },
          "500": {
            "description": "Server error"
          }
        }
      }
    },
    "/v3/export/svg": {
      "get": {
        "tags": [
          "Exports"
        ],
        "summary": "Export result as SVG",
        "description": "Generate SVG files for visualization or cutting. Returns a ZIP file containing SVG files for each sheet.",
        "parameters": [
          {
            "name": "id",
            "in": "query",
            "required": true,
            "schema": {
              "type": "string"
            },
            "description": "Job ID to export"
          },
          {
            "name": "units",
            "in": "query",
            "required": false,
            "schema": {
              "type": "string"
            },
            "description": "Unit system (e.g., mm, in). Default: mm"
          }
        ],
        "responses": {
          "200": {
            "description": "S3 URL to the generated SVG ZIP file",
            "content": {
              "application/json": {
                "schema": {
                  "type": "string",
                  "example": "https://smartcut-storage.s3.eu-west-2.amazonaws.com/12345.zip"
                }
              }
            }
          },
          "400": {
            "description": "Invalid parameters or job not found"
          },
          "500": {
            "description": "Server error"
          }
        }
      }
    },
    "/v3/export/mayer": {
      "get": {
        "tags": [
          "Exports"
        ],
        "summary": "Export result as Mayer .may",
        "description": "Generate a `.may` job file for Otto Mayer / Felder beam saws running the PC7000 control (Erler).\n\nOn `/v3/calculate` identify the saw as `{ \"vendor\": \"Mayer\", \"model\": \"PC7000\" }` and use the **beam** cut type — the Mayer profile sets `cutType: \"beam\"` by default, so leave `saw.cutType` out or send `\"beam\"`. This matters: the `.may` format positions every part by a cursor, not by coordinates, and only a layout planned under the Mayer profile is built within what the format can express. A job calculated with another cut type, or without the Mayer saw identity, may export a file the saw cannot cut as shown.\n\nThe file is ISO-8859-1 (Latin-1) with `§§` record delimiters, the PC7000 reader's default:\n- MA000: job header\n- MA010: one record per part — dimensions, quantity and the label text slots below\n- MA200: layout header per board (stock dimensions, trims)\n- MA300 / MA310 / MA320 / MA325 / MA330 / MA340 / MA350 / MA360: strip, column and piece placement records\n- MA999: footer\n\n**Label-printer mapping (typed) — BETA:**\n\nEach part's `labels` fields are written to the MA010 record's generic text slots. The PC7000 prints whatever its label template binds to a slot (`Text0`…`Text10`, zero-indexed), so confirm the slot order against the site's template:\n\n| Source field | MA010 text slot | Template binding |\n|---|---|---|\n| `labels.barcode1` | 1 | Text0 |\n| `labels.barcode2` | 2 | Text1 |\n| `labels.order` | 3 | Text2 |\n| `labels.room` | 4 | Text3 |\n| `labels.drawing` | 5 | Text4 |\n| `labels.pallet` | 6 | Text5 |\n| `labels.colour` | 7 | Text6 |\n| `labels.productInfo` | 8 | Text7 |\n| `labels.productNumber` | 9 | Text8 |\n\nSlots 10–11 are left empty. Each field is limited to 50 characters. `labels.customer` (request top-level) has no MA-record home and is not written — it is PTX-only. Sending a label field the chosen saw does not support returns a `labelWarnings` array on the calculate response; the job still runs.\n\nIf the operator re-optimises the job on the saw, the PC7000 keeps only text slots 1–5 (`barcode1`, `barcode2`, `order`, `room`, `drawing`); the rest survive only when the patterns are cut as sent.\n\nThe MA000 header's unit and currency fields are fixed by the Mayer profile (`MM`, `EUR`); there is no per-request option. If your machine expects other values, contact support.",
        "parameters": [
          {
            "name": "id",
            "in": "query",
            "required": true,
            "schema": {
              "type": "string"
            },
            "description": "Job ID to export"
          }
        ],
        "responses": {
          "200": {
            "description": "S3 URL to the generated .may file",
            "content": {
              "application/json": {
                "schema": {
                  "type": "string",
                  "example": "https://smartcut-storage.s3.eu-west-2.amazonaws.com/12345.may"
                }
              }
            }
          },
          "400": {
            "description": "Invalid parameters or job not found"
          },
          "500": {
            "description": "Server error"
          }
        }
      }
    },
    "/v3/export/biesse": {
      "get": {
        "tags": [
          "Exports"
        ],
        "summary": "Export result as Biesse XML CutList",
        "description": "**BETA** - Generate a Biesse OptiPlanner CutList XML for Biesse beam saws.\n\nThe XML describes the beam-saw cutting pattern (rip + cross cuts, with optional head cuts)\nfor each used stock board, plus the part list. Beam/guillotine cut types only — efficiency-pack\nlayouts have arbitrary piece positions that cannot be expressed as a beam-saw pattern.\n\nOptional saw-profile / cuttability validation is available on the legacy `/machine/biesse`\nroute via `?validate=true`.\n\nEmitter settings (decimal precision) are taken from the saw profile automatically — configure them on the saw, not per request.",
        "parameters": [
          {
            "name": "id",
            "in": "query",
            "required": true,
            "schema": {
              "type": "string"
            },
            "description": "Job ID to export"
          }
        ],
        "responses": {
          "200": {
            "description": "S3 URL to the generated Biesse .xml file",
            "content": {
              "application/json": {
                "schema": {
                  "type": "string",
                  "example": "https://smartcut-storage.s3.eu-west-2.amazonaws.com/12345.xml"
                }
              }
            }
          },
          "400": {
            "description": "Invalid parameters or job not found"
          },
          "500": {
            "description": "Server error"
          }
        }
      }
    },
    "/v3/export/cadmatic": {
      "get": {
        "tags": [
          "Exports"
        ],
        "summary": "Export result as a Homag CADmatic .saw file",
        "description": "**BETA** - Generate a Homag CADmatic `.saw` transfer file for Homag beam saws.\n\nThe file carries the board list, one row per part (the part code is its name, or its part id\nwhen it has none; different parts sharing a name are written `NAME-1`, `NAME-2`, …) and the\nbeam-saw patterns with their stack depth. Beam cut types only. The file is written in\nWindows-1252, the code page CADmatic reads — characters outside it become `?`.\n\nEvery file is checked before it is returned (placements on their boards, no overlaps, checksum,\none part per physical part cut); a file that fails is refused with the reason rather than shipped.\nThe CADmatic dialect (CAD4 / CAD5) comes from the saw profile or the saw's software version.",
        "parameters": [
          {
            "name": "id",
            "in": "query",
            "required": true,
            "schema": {
              "type": "string"
            },
            "description": "Job ID to export"
          }
        ],
        "responses": {
          "200": {
            "description": "S3 URL to the generated .saw file",
            "content": {
              "application/json": {
                "schema": {
                  "type": "string",
                  "example": "https://smartcut-storage.s3.eu-west-2.amazonaws.com/12345.saw"
                }
              }
            }
          },
          "400": {
            "description": "Invalid parameters or job not found"
          },
          "500": {
            "description": "Server error, or the file failed its pre-export check"
          }
        }
      }
    },
    "/v3/saws": {
      "get": {
        "tags": [
          "Discovery"
        ],
        "summary": "List saws and the export formats each offers",
        "description": "Discovery: the saws whose saw-control export format(s) are live in production, and the\n`/v3/export/<format>` endpoint for each. Use this to know which export endpoints are live and\nwhich saw identity (vendor/model or sawProfileId) to send. The list is derived automatically\nfrom the post-processor factory (a format appears once it reaches the in-production stage), so\nquery it at runtime rather than hard-coding — it grows as more beam-saw formats are promoted.\n\n**A `sawProfileId` we no longer recognise is never rejected.** It is logged once and the request\nfalls through to the `saw` vendor/model identity you sent; if that resolves nothing either, no saw\nprofile is applied and the job is optimised saw-agnostically (exports then use the format's\ncanonical profile). So a stale id keeps working as a fall-through rather than 400-ing — but it also\nmeans a typo is not an error, and you get the identity's saw, not the id's. Send the vendor/model\nidentity alongside the id, and re-query this endpoint to pick ids up after a rename.\n\nWhat each recognised saw brings to a job (its constraints, defaults and the settings it derives),\nand how the `saw` object's settings interact, is browsable — and testable against your own JSON — at\nthe [Saw settings explorer](https://smartcut.dev/api-docs/saw-settings).",
        "responses": {
          "200": {
            "description": "Saws offered + their export endpoints",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "saws": {
                      "type": "array",
                      "items": {
                        "type": "object",
                        "properties": {
                          "id": {
                            "type": "string",
                            "description": "Saw profile id (e.g. biesse-optiplanner)"
                          },
                          "name": {
                            "type": "string"
                          },
                          "vendor": {
                            "type": "string"
                          },
                          "formats": {
                            "type": "array",
                            "items": {
                              "type": "string"
                            },
                            "description": "Export format id(s)"
                          },
                          "exports": {
                            "type": "array",
                            "items": {
                              "type": "object",
                              "properties": {
                                "format": {
                                  "type": "string"
                                },
                                "endpoint": {
                                  "type": "string",
                                  "description": "v3 export route, e.g. /v3/export/biesse"
                                }
                              }
                            }
                          }
                        }
                      }
                    }
                  }
                }
              }
            }
          }
        }
      }
    },
    "/v3/labels": {
      "post": {
        "tags": [
          "Labels"
        ],
        "summary": "Generate printable part labels",
        "description": "Generate customizable PDF labels for parts with options for layout, formatting, and content",
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "properties": {
                  "id": {
                    "type": "string",
                    "description": "Job ID to generate labels for"
                  },
                  "units": {
                    "type": "string",
                    "enum": [
                      "pt",
                      "px",
                      "in",
                      "mm",
                      "cm",
                      "ex",
                      "em",
                      "pc"
                    ],
                    "default": "mm",
                    "description": "Measurement units for labels"
                  },
                  "format": {
                    "type": "string",
                    "default": "a4",
                    "description": "Paper size (e.g., a4, letter)"
                  },
                  "orientation": {
                    "type": "string",
                    "enum": [
                      "portrait",
                      "landscape"
                    ],
                    "default": "portrait",
                    "description": "Page orientation"
                  },
                  "cols": {
                    "type": "number",
                    "default": 4,
                    "description": "Number of columns per page. Used when labelW/labelH are not set; each label becomes usableWidth/cols wide."
                  },
                  "rows": {
                    "type": "number",
                    "default": 4,
                    "description": "Number of rows per page. Used when labelW/labelH are not set; each label becomes usableHeight/rows tall."
                  },
                  "labelW": {
                    "type": "number",
                    "description": "Explicit label width (in units). When set, labels render at exactly this width (top-left aligned, leftover space on the right) and cols is only used to wrap to the next row."
                  },
                  "labelH": {
                    "type": "number",
                    "description": "Explicit label height (in units). When set, labels render at exactly this height (top-left aligned, leftover space on the bottom) and rows is only used to wrap to the next page."
                  },
                  "pageMarginH": {
                    "type": "number",
                    "default": 14,
                    "description": "Horizontal page margin"
                  },
                  "pageMarginW": {
                    "type": "number",
                    "default": 14,
                    "description": "Vertical page margin"
                  },
                  "padding": {
                    "type": "number",
                    "default": 0,
                    "description": "Padding around each label"
                  },
                  "fontSize": {
                    "type": "number",
                    "default": 10,
                    "description": "Base font size"
                  },
                  "largeFontSize": {
                    "type": "number",
                    "default": 12,
                    "description": "Large text font size"
                  },
                  "banding": {
                    "type": "boolean",
                    "default": true,
                    "description": "Show edge banding diagram (visual rectangle with colored borders for banded sides). When false, labels use a plain text layout."
                  },
                  "notes": {
                    "type": "boolean",
                    "default": false,
                    "description": "Include each part's notes field as text on its label (wrapped to label width, max 4 lines)."
                  },
                  "showId": {
                    "type": "boolean",
                    "default": true,
                    "description": "Show part IDs"
                  },
                  "stockId": {
                    "type": "boolean",
                    "default": true,
                    "description": "Show stock IDs"
                  },
                  "boxes": {
                    "type": "boolean",
                    "default": true,
                    "description": "Draw boxes around labels"
                  },
                  "includeOffcuts": {
                    "type": "boolean",
                    "default": true,
                    "description": "Include offcut labels"
                  },
                  "newStockNewPage": {
                    "type": "boolean",
                    "default": false,
                    "description": "Start new page for each stock item"
                  },
                  "materials": {
                    "type": "boolean",
                    "default": true,
                    "description": "Show material names"
                  },
                  "companyName": {
                    "type": "string",
                    "description": "Company name to display on labels"
                  },
                  "numberFormat": {
                    "type": "string",
                    "enum": [
                      "decimal",
                      "fraction"
                    ],
                    "description": "Number format for dimensions"
                  },
                  "decimalPlaces": {
                    "type": "number",
                    "description": "Number of decimal places (for decimal format)"
                  },
                  "fractionRoundTo": {
                    "type": "number",
                    "description": "Fraction denominator to round to (for fraction format)"
                  },
                  "qrCode": {
                    "type": "boolean",
                    "default": true,
                    "description": "Include QR codes on labels linking to part preview page"
                  }
                },
                "required": [
                  "id",
                  "companyName",
                  "numberFormat",
                  "decimalPlaces",
                  "fractionRoundTo"
                ]
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Labels PDF generated successfully - returns URL to download the PDF",
            "content": {
              "application/json": {
                "schema": {
                  "type": "string",
                  "description": "URL to download the generated labels PDF"
                }
              }
            }
          },
          "400": {
            "description": "Invalid request (missing ID, no parts found, etc.)"
          },
          "500": {
            "description": "Error generating labels"
          }
        }
      }
    },
    "/webhooks/result": {
      "post": {
        "tags": [
          "Webhooks"
        ],
        "summary": "Calculation result delivery",
        "description": "**This is not an endpoint you call — it documents what SmartCut POSTs to your webhook URL.**\n\nWhen a calculation completes, SmartCut sends a `POST` request to your configured webhook URL.\nThe request body is identical to the `ResultResponse` returned by `GET /v3/result`.\n\n**Example handler (Node.js / Express):**\n```javascript\napp.post('/your-webhook-url', express.json(), (req, res) => {\n  const result = req.body // ResultResponse\n  console.log(`Job ${result.jobId} complete`)\n  // Process result...\n  res.sendStatus(200)\n})\n```",
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/ResultResponse"
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Your endpoint acknowledged the delivery"
          }
        }
      }
    },
    "/v3/calls": {
      "get": {
        "tags": [
          "Stats"
        ],
        "summary": "Get API usage statistics",
        "responses": {
          "200": {
            "description": "Usage statistics",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "used": {
                      "type": "number"
                    },
                    "limit": {
                      "type": "number"
                    },
                    "remaining": {
                      "type": "number"
                    }
                  }
                }
              }
            }
          }
        }
      }
    }
  },
  "components": {
    "securitySchemes": {
      "ApiKeyAuth": {
        "type": "apiKey",
        "in": "header",
        "name": "Authorization",
        "description": "Pass your API key in the `Authorization` header. Obtain your key from https://smartcut.dev/account\n\n**Example:**\n```javascript\nconst response = await fetch('https://api.smartcut.dev/v3/calculate', {\n  method: 'POST',\n  headers: {\n    'Authorization': 'YOUR_API_KEY',\n    'Content-Type': 'application/json'\n  },\n  body: JSON.stringify({\n    // Your request data\n  })\n})\n```\n\n> **Note:** Do not include a `Bearer` prefix — pass the key directly as the header value."
      }
    },
    "schemas": {
      "ModelAnalysisResponse": {
        "type": "object",
        "description": "The result of analysing a 3D model. `parts` is submittable to `/v3/calculate` unchanged.",
        "properties": {
          "version": {
            "type": "string",
            "enum": [
              "3"
            ]
          },
          "model": {
            "type": "object",
            "description": "What was read, and in what units.",
            "properties": {
              "format": {
                "type": "string",
                "nullable": true,
                "description": "Detected format: `step`, `stl`, `obj`, `ply` or `gltf`."
              },
              "units": {
                "type": "string",
                "description": "The unit every number in this response is in."
              },
              "sourceUnits": {
                "type": "string",
                "description": "The unit the geometry was read as — your `sourceUnits`, else the file's own declaration, else `units`."
              },
              "declaredUnits": {
                "type": "string",
                "nullable": true,
                "description": "The unit the FILE declared, where the format carries one (STEP, 3MF, Collada). Null otherwise."
              },
              "suggestedUnits": {
                "type": "string",
                "description": "A different `sourceUnits` that would make the model plausible against the `bladeWidth` you supplied. Only present when you supplied one and the current reading looks wrong."
              },
              "stats": {
                "type": "object",
                "properties": {
                  "parts": {
                    "type": "integer",
                    "description": "Rows in `parts`."
                  },
                  "rectangular": {
                    "type": "integer"
                  },
                  "irregular": {
                    "type": "integer"
                  },
                  "unrecognised": {
                    "type": "integer"
                  },
                  "fixtures": {
                    "type": "integer",
                    "description": "Parts judged to be fittings rather than sheet goods."
                  },
                  "merged": {
                    "type": "integer",
                    "description": "Identical parts collapsed into a quantity."
                  },
                  "triangles": {
                    "type": "integer"
                  },
                  "components": {
                    "type": "integer",
                    "description": "Separate solids found in the file."
                  },
                  "debris": {
                    "type": "integer",
                    "description": "Fragments too small to be parts, ignored."
                  },
                  "modelDiagonal": {
                    "type": "number",
                    "description": "Bounding-box diagonal of the whole model, in `units`."
                  },
                  "modelSize": {
                    "type": "object",
                    "properties": {
                      "x": {
                        "type": "number"
                      },
                      "y": {
                        "type": "number"
                      },
                      "z": {
                        "type": "number"
                      }
                    }
                  }
                }
              }
            },
            "required": [
              "format",
              "units",
              "sourceUnits",
              "declaredUnits",
              "stats"
            ]
          },
          "parts": {
            "type": "array",
            "description": "Parts, in `/v3/calculate`'s own vocabulary. Post this array back as that endpoint's `parts`.",
            "items": {
              "$ref": "#/components/schemas/ModelPart"
            }
          },
          "advice": {
            "type": "array",
            "description": "Everything the analysis knows that a calculation does not. Points at `parts` by index.",
            "items": {
              "$ref": "#/components/schemas/ModelAdvice"
            }
          },
          "warnings": {
            "type": "array",
            "items": {
              "type": "string"
            },
            "description": "Notes from the file reader itself, e.g. that an STL carries no part names."
          }
        },
        "required": [
          "version",
          "model",
          "parts",
          "advice",
          "warnings"
        ]
      },
      "ModelPart": {
        "type": "object",
        "description": "A part as the analyser found it. Every field except `role` is a `/v3/calculate` part field; `role` is ignored there, so the object can be submitted as-is.",
        "properties": {
          "name": {
            "type": "string",
            "description": "Part name, from the model where it had one."
          },
          "l": {
            "type": "number",
            "description": "Length — the long side."
          },
          "w": {
            "type": "number",
            "description": "Width — the short side."
          },
          "t": {
            "type": "number",
            "description": "Thickness."
          },
          "q": {
            "type": "integer",
            "description": "Quantity. Above 1 when identical copies were merged into one row."
          },
          "material": {
            "type": "string",
            "description": "Material name read from the model, or empty when the file carried none."
          },
          "outline": {
            "type": "array",
            "description": "Closed polygon outline. Present for shaped parts — send it to a nesting model to keep the shape.",
            "items": {
              "type": "object",
              "properties": {
                "x": {
                  "type": "number"
                },
                "y": {
                  "type": "number"
                }
              }
            }
          },
          "holes": {
            "type": "array",
            "description": "Inner rings that are SHAPE — slots, cut-outs — in the outline's own frame. A ROUND ring is drilling rather than shape and comes back in `machining.holes` instead; nothing appears in both, so a part is never drilled and cut for the same hole.",
            "items": {
              "type": "array",
              "items": {
                "type": "object",
                "properties": {
                  "x": {
                    "type": "number"
                  },
                  "y": {
                    "type": "number"
                  }
                }
              }
            }
          },
          "machining": {
            "type": "object",
            "description": "Machining read off the model, in `/v3/calculate`'s own `machining` shape — post it back untouched.",
            "properties": {
              "holes": {
                "type": "array",
                "description": "Round holes found in the part. Through holes, so `depth` is absent; measurements are in the response's `units`.",
                "items": {
                  "type": "object",
                  "properties": {
                    "x": {
                      "type": "number",
                      "description": "X of the hole centre, from the left edge."
                    },
                    "y": {
                      "type": "number",
                      "description": "Y of the hole centre, from the bottom edge."
                    },
                    "diameter": {
                      "type": "number",
                      "description": "Hole diameter."
                    },
                    "depth": {
                      "type": "number",
                      "description": "Hole depth. Absent for a through hole, which is what the analysis always finds."
                    },
                    "face": {
                      "type": "number",
                      "enum": [
                        0,
                        1
                      ],
                      "description": "Face index (0=front, 1=back)."
                    },
                    "type": {
                      "type": "string",
                      "enum": [
                        "regular",
                        "hinge",
                        "shelf"
                      ],
                      "description": "Hole type."
                    }
                  },
                  "required": [
                    "x",
                    "y",
                    "diameter"
                  ]
                }
              }
            }
          },
          "role": {
            "type": "string",
            "enum": [
              "rect",
              "nesting",
              "fixture"
            ],
            "description": "What the part is FOR: a plain rectangle, a shape worth nesting, or a fitting that was never cut from sheet. Drop `fixture` rows before cutting. Ignored by `/v3/calculate`."
          }
        },
        "required": [
          "name",
          "l",
          "w",
          "t",
          "q",
          "material",
          "role"
        ]
      },
      "ModelAdvice": {
        "type": "object",
        "description": "One thing worth knowing about the model or a part in it.",
        "properties": {
          "code": {
            "type": "string",
            "description": "Stable machine-readable code, e.g. `open-mesh`, `fixture-detected`, `parts-overlap`, `squared-off-waste`, `dimensions-approximate`."
          },
          "severity": {
            "type": "string",
            "enum": [
              "info",
              "warning",
              "error"
            ],
            "description": "`error` means the part cannot be cut as measured."
          },
          "message": {
            "type": "string"
          },
          "partIndex": {
            "type": "integer",
            "description": "Index into `parts`. Absent for advice about the file as a whole."
          },
          "otherPartIndex": {
            "type": "integer",
            "description": "The second part, for advice about a PAIR (overlaps)."
          },
          "penetrationFraction": {
            "type": "number",
            "description": "How deeply two parts overlap, as a fraction of the smaller part's smallest dimension."
          },
          "wastePct": {
            "type": "number",
            "description": "Fraction of the bounding rectangle that is waste, 0..1 — what nesting this part would save."
          },
          "confidence": {
            "type": "number",
            "description": "How sure the role verdict was, 0..1."
          }
        },
        "required": [
          "code",
          "severity",
          "message"
        ]
      },
      "CalculateRequest": {
        "type": "object",
        "description": "Calculation request",
        "properties": {
          "saw": {
            "description": "Saw configuration. Either a single saw — every stock is cut on it, exactly as before — or a NON-EMPTY ARRAY of identified saws for a MIXED job. Array form: the FIRST element is the job's default saw, and `materials` bind stock/parts to the others via `sawId`. Which settings apply to which saw, and what the platform does when two of them disagree, is one published registry — browse it (and try your own saw against it) at https://smartcut.dev/api-docs/saw-settings; anything it resolves comes back on the response `warnings`.",
            "oneOf": [
              {
                "$ref": "#/components/schemas/Saw"
              },
              {
                "type": "array",
                "minItems": 1,
                "items": {
                  "$ref": "#/components/schemas/IdentifiedSaw"
                }
              }
            ]
          },
          "stock": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/Stock"
            },
            "description": "Stock items"
          },
          "parts": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/Part"
            },
            "description": "Parts to cut"
          },
          "materials": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/Material"
            },
            "description": "Materials with saw associations — the groups a MIXED job is partitioned and optimised by. Stock/parts attach to a material via their `materialId` (or by material name + thickness); each material's group runs on its `sawId` / inline `saw`, or on the job's default saw when neither is set. Omit for a classic single-saw job."
          },
          "groups": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/Group"
            },
            "description": "User-defined part groups with XY positions"
          },
          "model": {
            "type": "string",
            "enum": [
              "rectangle-guillotine-fast",
              "rectangle-guillotine-max-1",
              "rectangle-guillotine-max-2",
              "rectangle-guillotine-max-3",
              "irregular-nesting-fast",
              "irregular-nesting-max-1",
              "irregular-nesting-max-2"
            ],
            "default": "rectangle-guillotine-fast",
            "description": "Optimisation model. Optional; defaults to `rectangle-guillotine-fast`.\n\n| Model | Budget | Use it when |\n| --- | --- | --- |\n| `rectangle-guillotine-fast` | Seconds | Default. Interactive use and live quoting |\n| `rectangle-guillotine-max-1` | ~60s | A cheap second look (beam/guillotine-only, needs a recognised saw profile on beam, uncapped jobs only (no maxPhase from the request or the saw profile), ≤500 parts) |\n| `rectangle-guillotine-max-2` | ~150s | The Max to reach for first (beam/guillotine-only, needs a recognised saw profile on beam, uncapped jobs only (no maxPhase from the request or the saw profile), ≤500 parts) |\n| `rectangle-guillotine-max-3` | ~420s | Batch or overnight, where board cost dominates (beam/guillotine-only, needs a recognised saw profile on beam, uncapped jobs only (no maxPhase from the request or the saw profile), ≤500 parts) |\n| `irregular-nesting-fast` | Seconds | Default for `nesting` jobs. Interactive use and live quoting |\n| `irregular-nesting-max-1` | +5s search | The nesting Max to reach for first — most of the gain; adds ~5s of search to the base solve (nesting-only) |\n| `irregular-nesting-max-2` | +30s search | Nesting jobs above ~50 parts, where material cost dominates (nesting-only) |\n\n**Max** spends up to the budget above searching for a better layout, and can save boards on larger jobs. Compute time is billed per millisecond, so a Max run costs in line with the time it uses.\n\n**Nesting (`saw.cutType: \"nesting\"`) has its own tiers**, which pack parts tighter: `irregular-nesting-max-1` adds about 5s of search and `irregular-nesting-max-2` about 30s, best on jobs above ~50 parts. The budget is extra search on top of the base solve, so a large job takes one full solve plus roughly the budget.\n\nEach Max tier has the requirements shown in its row. A request that doesn't meet them runs `rectangle-guillotine-fast` instead, and naming a tier for the wrong cut type is never an error. Full catalogue: `GET /v3/spec`.\n\n**On a beam saw, Max needs a recognised saw profile AND an uncapped job.** A beam job with no recognised profile (`sawProfileId`, or `saw.vendor` + `saw.model`) runs `rectangle-guillotine-fast` (\"Max needs a recognised beam saw profile …\"). Any phase cap — `saw.guillotineOptions.maxPhase` above 0, or the default a saw profile sets (the Mayer and Biesse profiles cap at 3) — also runs `rectangle-guillotine-fast` (\"MAX needs an uncapped job; this saw/request sets maxPhase N\"). The result's `modelDowngrade.reason` names which. Sending `maxPhase: 0` does not remove a saw profile's cap: 0 means \"not set\", and the profile fills it."
          },
          "webhook": {
            "type": "string",
            "format": "uri",
            "description": "Webhook URL for async result delivery"
          },
          "labels": {
            "type": "object",
            "description": "Typed saw label-printer fields: `parts[].labels` + top-level `labels.customer`. Emitted by PTX and Mayer .MAY (part-level fields); `labels.customer` is PTX-only; Biesse XML to follow. Schema may change; see `/v3/export/ptx` and `/v3/export/mayer`.",
            "properties": {
              "customer": {
                "type": "string",
                "maxLength": 100,
                "description": "Customer name printed on saw labels (JOBS.CUSTOMER in PTX)"
              }
            }
          }
        },
        "required": [
          "saw",
          "stock",
          "parts"
        ]
      },
      "Saw": {
        "type": "object",
        "description": "Saw configuration. Settings that do not apply to each other are RESOLVED rather than rejected — the saw-rules registry clamps, defaults or zeroes them and the response `warnings` name the rule that fired. Browse every rule, and try your own saw against them, at https://smartcut.dev/api-docs/saw-settings",
        "properties": {
          "vendor": {
            "type": "string",
            "description": "Saw vendor (e.g. Biesse, Mayer, SCM). Part of the identity pair (`vendor` + `model`) used to look up this saw's profile, which supplies its algorithm constraints and preferences. Optional — without an identity (or a `sawProfileId`) no profile is applied."
          },
          "model": {
            "type": "string",
            "description": "Saw model name (e.g. PC7000, OptiPlanner, Pratika). Paired with `vendor` to resolve the saw profile — `GET /v3/saws` lists the recognised pairs."
          },
          "softwareVersion": {
            "type": "string",
            "description": "Saw control software version (e.g. \"CADmatic 5\") — selects the CADmatic output dialect where recognised (5 → CAD5/PTNR, 3|4 → CAD4/PTN3), otherwise provenance only."
          },
          "cutType": {
            "type": "string",
            "enum": [
              "efficiency",
              "guillotine",
              "beam",
              "nesting"
            ],
            "description": "Type of cutting algorithm. `guillotine` and `beam` produce edge-to-edge saw cuts; `efficiency` packs without the guillotine constraint; `nesting` is IRREGULAR nesting for CNC routers and similar — give each part an `outline` (and optional `holes`) and it is packed as its true shape rather than its bounding rectangle."
          },
          "cutPreference": {
            "type": "string",
            "enum": [
              "l",
              "w",
              "flex"
            ],
            "description": "Preferred cutting direction. Only applicable when cutType is 'guillotine' or 'beam'. Set to 'flex' to try both 'l' and 'w' on a per-sheet basis and keep the better orientation per sheet — charged at 2× the standard credit rate."
          },
          "bladeWidth": {
            "type": "number",
            "minimum": 0,
            "description": "Width of the saw blade"
          },
          "stockType": {
            "type": "string",
            "enum": [
              "sheet",
              "linear",
              "roll"
            ],
            "description": "Type of stock material"
          },
          "minOffcut": {
            "type": "object",
            "nullable": true,
            "description": "Per-job override of this saw's minimum reusable offcut size, in the same units as every other dimension you send. A saw profile (`sawProfileId`) may already declare its own floor — sending this REPLACES it entirely for the job; the profile's value is only a default. Every leftover still ships in `offcuts`; a floor only decides each one's `retained` flag (whether this saw's operator would re-rack it), never geometry or board count. Compared orientation-free: `{ l: 150, w: 100 }` reads \"no side under 100, longer side at least 150\" — 160×95 and 95×160 give the same verdict. `area` is an independent third check. Absent → the resolved saw profile's own floor, or none.",
            "properties": {
              "l": {
                "type": "number",
                "minimum": 0,
                "description": "Minimum long side of a reusable offcut"
              },
              "w": {
                "type": "number",
                "minimum": 0,
                "description": "Minimum short side of a reusable offcut"
              },
              "area": {
                "type": "number",
                "minimum": 0,
                "description": "Minimum area of a reusable offcut, independent of side length"
              }
            }
          },
          "guillotineOptions": {
            "type": "object",
            "description": "Guillotine-specific options. Only applicable when cutType is 'guillotine' or 'beam'",
            "properties": {
              "strategy": {
                "type": "string",
                "enum": [
                  "efficiency",
                  "time"
                ],
                "default": "efficiency",
                "description": "Cutting strategy"
              },
              "maxPhase": {
                "type": "number",
                "minimum": 0,
                "maximum": 10,
                "default": 0,
                "description": "Maximum cutting phase — the number of turns a board may take: 0 = unlimited, 1 = two-stage cutting (rips, one turn, crosscuts — every strip takes only parts whose cross dimension matches the strip), 2 = three-stage, … Enforced exactly as sent for every cut type, except where a saw profile (sawProfileId) states the saw's own limit: then 0 takes the profile's default, and on a saw whose control counts its own turns a higher value is lowered to the saw's ceiling. Mayer (mayer-may, PC7000 control): default 3, maximum 4 — 4 is opt-in, because the PC7000 has no automatic turn table and each extra turn is one more piece the operator turns and feeds by hand; it saves stock on some jobs (7 of 713 in our Mayer test set, one sheet each). A Max `model` runs only when the resulting cap is 0 (the Mayer and Biesse profiles default to 3, so 0 there still means 3) — any cap runs the Fast model; on a beam saw it also needs a recognised saw profile."
              },
              "adaptiveMaxPhase": {
                "type": "boolean",
                "description": "Let the optimiser raise maxPhase one turn at a time, up to the saw control's own turn ceiling (Mayer PC7000: 4), when a deeper cap gives FEWER sheets, or the SAME sheets in less predicted cutting time (equal time: the larger offcut wins). It never returns more sheets than the maxPhase you set, and on a saw with no cutting-time model it raises the cap only to save a sheet. Applies only to a saw profile whose control counts its own turns and has a ceiling deeper than the job's cap (today: mayer-may); elsewhere it does nothing. The caps tried, and which one was used and why, are reported in metadata.phaseStep. Omit it to use your account default (off)."
              },
              "timeObjective": {
                "type": "boolean",
                "description": "Plan for less cutting time as well as material. The job is also planned material-first, and the faster plan is used only when it needs no more sheets and, at equal sheets, is predicted to cut faster. This roughly doubles the compute for the job, and compute is billed. Applies only to a saw profile with a cutting-time model (today: mayer-may); elsewhere it does nothing. The outcome is reported in optimisation.productionTime.guard. Omit it to use your account default (off)."
              },
              "duplicateStrips": {
                "type": "boolean",
                "description": "Convenience option: where the job holds another full copy of a strip's parts, place the identical strip next to it on the same sheet, so a saw that stacks identical strips (for example a Mayer PC7000 control) can cut the two together. It groups identical strips for stacking and may use more material than the default plan. Applies to guillotine and beam saws. Omit it to use your account default (off)."
              },
              "headCuts": {
                "type": "boolean",
                "default": false,
                "description": "Enable head cuts"
              },
              "limitStripDimensions": {
                "type": "boolean",
                "default": false,
                "description": "Prefer narrower strips, derived from the job's own parts (mean part depth, floored at a fifth of the board). Carries no dimension of its own — use maxStripWidth to state an operator limit. With both set the TIGHTER of the two wins: on a job of small parts the derived cap is usually well below any operator limit, and maxStripWidth then has no effect at all. To make your own number the cap, send it with this flag off."
              },
              "verticalPanelSaw": {
                "type": "boolean",
                "default": false,
                "description": "Arrange the layout for a vertical panel saw: narrowest strips at the top, residual waste at the bottom, cut from the top down. Works on any saw; no profile required."
              },
              "maxStripWidth": {
                "type": "number",
                "minimum": 0,
                "nullable": true,
                "default": null,
                "description": "Widest assembled strip an operator can safely handle, in the same units as every other dimension you send. The optimiser steers away from strips wider than this — it opens strips on parts within the limit wherever it can. Set this alone (limitStripDimensions off) and your number IS the cap. With limitStripDimensions also on, the tighter of the two wins — the parts-derived cap is often tighter, and then this value has no effect. It is a STRONG PREFERENCE, not a guarantee — a part whose own smaller side exceeds the limit cannot be made to fit one, and layouts are never left unplaced to satisfy it. Hard rejection of an over-wide strip requires a saw profile (sawProfileId), whose oracle reports strip-width-exceeded. Null = no limit."
              }
            }
          },
          "efficiencyOptions": {
            "type": "object",
            "description": "Efficiency-specific options. Only applicable when cutType is 'efficiency'",
            "properties": {
              "primaryCompression": {
                "type": "string",
                "enum": [
                  "l",
                  "w"
                ],
                "default": "w",
                "description": "Primary compression direction (l=length/horizontal, w=width/vertical)"
              }
            }
          },
          "stackHeight": {
            "type": "number",
            "default": 100,
            "description": "The saw's maximum cutting height, in the same units as every other dimension you send. Sets how many sheets one pattern may be cut through at once: `⌊stackHeight ÷ sheet thickness⌋`. Applies to any saw, not only beam saws. Only read when `options.stackingMode` is not `none`."
          },
          "options": {
            "type": "object",
            "description": "General optimisation options: which stock is chosen, whether sheets with the same pattern are cut as a stack, and spacing between parts.",
            "properties": {
              "stockSelection": {
                "type": "string",
                "enum": [
                  "efficiency",
                  "smallest"
                ],
                "default": "efficiency",
                "description": "How the optimiser chooses between the stock sizes you send.\n\n- `efficiency`: pick the stock that gives the best material utilisation.\n- `smallest`: use the smallest stock (by area) that the parts fit on first."
              },
              "offcutPriority": {
                "type": "number",
                "minimum": 0,
                "maximum": 1,
                "default": 0.5,
                "description": "Offcut priority — how stock rows marked `offcut: true` are used against full sheets. 0 = never use offcut stock (offcut rows are left out of the job), 0.5 = offcuts compete with full sheets on score (default), 1 = always use offcuts first (an offcut is taken whenever anything fits it). Between 0.5 and 1 an offcut is taken when its fill is at least (1 − k) of the best full sheet's fill, k = (p − 0.5) × 2; values between 0 and 0.5 currently behave as 0.5. Has no effect unless some stock rows are marked `offcut: true`, and is ignored for a material whose stock rows are ALL offcuts (there is no full sheet to choose instead)."
              },
              "stackingMode": {
                "type": "string",
                "enum": [
                  "dimensions",
                  "identical",
                  "none"
                ],
                "default": "dimensions",
                "description": "Whether sheets that share a cutting pattern are cut together as a **stack**: several sheets laid on top of each other and cut once. In the result, a stack is one `stock` entry whose `stack` is the number of sheets, and each part cut from it carries `stacked: { sheet, of }`.\n\n- `identical`: stack only when every position holds the same part (same size and material) on every sheet.\n- `dimensions`: stack when every position holds a part of the same size. The parts may be different parts, so each sheet of the stack can yield different parts.\n- `none`: never stack. Every sheet is cut individually.\n\nStack depth is capped at `⌊saw.stackHeight ÷ thickness⌋`, and at `options.maxStackSheets` when set. Thickness is the stock's `t`, or else the thickest part on the sheet. When neither is known, the height does not limit the stack. Stacking is also off when `guillotineOptions.strategy` is `time`.\n\n**Grain across a stack.** Every sheet of a stack is cut to one pattern, so each position has one place and one orientation on every sheet. A part fills a position on another sheet only if it may take that orientation: a part locked to the grain (`orientationLock`) joins a stack only where the pattern's orientation keeps its grain on that stock, so grain is never broken to make a stack. Under `identical` the part on every sheet must also carry the same `orientationLock`.\n\nThe result lists every stack in `stacks`: its depth, and each sheet with its number in the job."
              },
              "maxStackSheets": {
                "type": "integer",
                "minimum": 1,
                "nullable": true,
                "description": "The most sheets one stack may hold, whatever their thickness. Applied on top of the height cap `⌊stackHeight ÷ thickness⌋` — the smaller of the two wins. Absent or null = no count limit (the height cap alone)."
              },
              "minSpacing": {
                "type": "number",
                "minimum": 0,
                "default": 0,
                "description": "Minimum gap between parts (not between a part and the stock edge), in the same units as every other dimension you send. The blade width is the floor: the gap used is the larger of `bladeWidth` and this value, not their sum. Only applies when `cutType` is `efficiency`. Other cut types space parts by the blade width, so the value is set to 0 and a warning is returned."
              },
              "minSpacingDimension": {
                "type": "object",
                "description": "A different minimum spacing for small parts. Parts whose length or width is at or below `dimension` use this `minSpacing` instead of the general one, still floored at the blade width. Only applies when `cutType` is `efficiency`.",
                "properties": {
                  "dimension": {
                    "type": "number",
                    "minimum": 0,
                    "description": "Parts with length or width equal to or smaller than this value will use the spacing below"
                  },
                  "minSpacing": {
                    "type": "number",
                    "minimum": 0,
                    "description": "Minimum spacing for parts meeting the dimension threshold"
                  }
                }
              }
            }
          }
        },
        "required": [
          "bladeWidth",
          "stockType"
        ]
      },
      "IdentifiedSaw": {
        "description": "A saw with an id — the array form of `saw` and the `ResultResponse.saws` echo. Materials reference it via `sawId`.",
        "allOf": [
          {
            "$ref": "#/components/schemas/Saw"
          },
          {
            "type": "object",
            "properties": {
              "id": {
                "type": "string",
                "minLength": 1,
                "description": "Identifier materials reference via `sawId`. The FIRST element of the `saw` array is the job's default saw."
              }
            },
            "required": [
              "id"
            ]
          }
        ]
      },
      "Material": {
        "type": "object",
        "description": "A material with a saw association — the unit a MIXED job is partitioned and optimised by. Associate it to a saw with `sawId` OR an inline `saw` (never both); with neither, it runs on the job's default saw.",
        "properties": {
          "id": {
            "type": "string",
            "description": "Material identifier stock/parts reference via their `materialId`. Defaults to `name`."
          },
          "name": {
            "type": "string",
            "description": "Material name. Stock/parts without a `materialId` attach by matching their `material` (and thickness, when `thicknesses` is set)."
          },
          "type": {
            "type": "string",
            "enum": [
              "sheet",
              "linear",
              "roll"
            ],
            "default": "sheet",
            "description": "Stock type of this material — must equal the associated saw's `stockType`."
          },
          "sawId": {
            "type": "string",
            "description": "Reference to an identified saw in the `saw` array. Mutually exclusive with inline `saw`."
          },
          "saw": {
            "$ref": "#/components/schemas/Saw",
            "description": "Inline saw for this material (hoisted to an identified saw with id = material id). Mutually exclusive with `sawId`."
          },
          "thicknesses": {
            "type": "array",
            "items": {
              "type": "number",
              "minimum": 0
            },
            "description": "Thicknesses this material is available in — used for name-based association."
          },
          "grain": {
            "type": "string",
            "enum": [
              "l",
              "w",
              ""
            ],
            "description": "Grain direction of the material."
          }
        },
        "required": [
          "name"
        ]
      },
      "Stock": {
        "type": "object",
        "description": "Stock item",
        "properties": {
          "name": {
            "type": "string",
            "description": "Stock name"
          },
          "l": {
            "type": "number",
            "description": "Length (long side dimension)",
            "minimum": 0
          },
          "w": {
            "type": "number",
            "description": "Width (short side dimension)",
            "minimum": 0
          },
          "t": {
            "description": "Thickness (z-axis dimension)",
            "type": "number",
            "minimum": 0,
            "nullable": true
          },
          "material": {
            "description": "Material name",
            "type": "string",
            "maxLength": 200
          },
          "materialId": {
            "type": "string",
            "description": "Id of the request `materials` entry this stock belongs to (mixed jobs). Without it, association falls back to matching `material` name + thickness."
          },
          "q": {
            "type": "number",
            "description": "Quantity",
            "default": 1,
            "minimum": 1
          },
          "grain": {
            "description": "Grain direction of the material",
            "default": "",
            "type": "string",
            "enum": [
              "l",
              "w",
              ""
            ]
          },
          "trim": {
            "$ref": "#/components/schemas/Trim",
            "description": "Trim allowances using l1/l2/w1/w2 coordinates"
          },
          "cost": {
            "type": "number",
            "nullable": true,
            "description": "Cost per unit. Carried through to the result for pricing and reports; it does not currently influence which stock the optimiser chooses (equal choices fall to the order the stock is listed in).",
            "default": 0,
            "minimum": 0
          },
          "notes": {
            "description": "Description",
            "type": "string",
            "maxLength": 200
          },
          "code": {
            "type": "string",
            "description": "Material or product code from supplier. Mapped to BOARDS CODE in PTX export."
          },
          "customData": {
            "type": "object",
            "additionalProperties": true,
            "description": "Custom user-defined data."
          },
          "autoAdd": {
            "type": "boolean",
            "description": "Automatically add stock as needed",
            "default": false
          },
          "infiniteLength": {
            "type": "boolean",
            "description": "Whether stock has infinite length (e.g., for rolls)",
            "default": false
          },
          "allowExactFitShapes": {
            "description": "Allow parts that exactly fit the stock dimensions even when trim is applied",
            "type": "boolean"
          },
          "offcut": {
            "type": "boolean",
            "default": false,
            "description": "Marks this stock row as an offcut (a remnant from earlier jobs) rather than a full sheet. `saw.options.offcutPriority` decides how offcut rows are used against full sheets: 0 leaves them out of the job, 0.5 (default) lets them compete on score, 1 takes an offcut whenever anything fits it. Without any row marked `offcut: true`, `offcutPriority` has no effect; when a material's rows are all offcuts, they are all used whatever the priority."
          }
        },
        "required": [
          "l",
          "w"
        ]
      },
      "Part": {
        "type": "object",
        "description": "Part to cut",
        "properties": {
          "name": {
            "type": "string",
            "description": "Part name"
          },
          "l": {
            "type": "number",
            "description": "Length (long side dimension)",
            "minimum": 0
          },
          "w": {
            "type": "number",
            "description": "Width (short side dimension)",
            "minimum": 0
          },
          "t": {
            "description": "Thickness (z-axis dimension)",
            "type": "number",
            "minimum": 0,
            "nullable": true
          },
          "material": {
            "description": "Material name",
            "type": "string",
            "maxLength": 200
          },
          "materialId": {
            "type": "string",
            "description": "Id of the request `materials` entry this part belongs to (mixed jobs). Without it, association falls back to matching `material` name + thickness."
          },
          "q": {
            "type": "number",
            "description": "Quantity",
            "default": 1,
            "minimum": 1
          },
          "trim": {
            "$ref": "#/components/schemas/Trim",
            "description": "Trim allowances using l1/l2/w1/w2 coordinates"
          },
          "notes": {
            "type": "string",
            "maxLength": 200,
            "description": "Part notes"
          },
          "outline": {
            "type": "array",
            "nullable": true,
            "description": "Polygon outline for IRREGULAR NESTING (`saw.cutType: \"nesting\"`). Ordered vertices in part-local coordinates; the ring is closed implicitly, so do not repeat the first point. Minimum 3 vertices. When present the part is nested as its true shape; when absent the part is nested as the `l`×`w` rectangle. Ignored by every other cutType.",
            "items": {
              "type": "object",
              "properties": {
                "x": {
                  "type": "number",
                  "description": "Vertex X in part-local coordinates"
                },
                "y": {
                  "type": "number",
                  "description": "Vertex Y in part-local coordinates"
                }
              },
              "required": [
                "x",
                "y"
              ]
            }
          },
          "holes": {
            "type": "array",
            "nullable": true,
            "description": "Interior voids of an irregular-nesting part — an array of polygon rings, same vertex convention as `outline`. Each ring is cut but never packed into. Only meaningful alongside `outline`; distinct from `machining.holes`, which are drilled operations rather than geometry.",
            "items": {
              "type": "array",
              "items": {
                "type": "object",
                "properties": {
                  "x": {
                    "type": "number",
                    "description": "Vertex X in part-local coordinates"
                  },
                  "y": {
                    "type": "number",
                    "description": "Vertex Y in part-local coordinates"
                  }
                },
                "required": [
                  "x",
                  "y"
                ]
              }
            }
          },
          "code": {
            "type": "string",
            "description": "Internal product or material code. Mapped to PRODUCT in PTX export."
          },
          "labels": {
            "type": "object",
            "description": "Typed per-part saw label-printer fields (with top-level `labels.customer` for the job). Emitted by PTX (named PARTS_INF columns) and Mayer .MAY (MA010 text slots 1-9); Biesse XML to follow. Schema may change; see `/v3/export/ptx` and `/v3/export/mayer`.",
            "properties": {
              "barcode1": {
                "type": "string",
                "maxLength": 50,
                "description": "CNC program side 1 (PARTS_INF.BARCODE1 in PTX)"
              },
              "barcode2": {
                "type": "string",
                "maxLength": 50,
                "description": "CNC program side 2 (PARTS_INF.BARCODE2 in PTX)"
              },
              "room": {
                "type": "string",
                "maxLength": 50,
                "description": "Room or location label (PARTS_INF.ROOM in PTX)"
              },
              "pallet": {
                "type": "string",
                "maxLength": 50,
                "description": "Pallet identifier (PARTS_INF.PALLET in PTX)"
              },
              "drawing": {
                "type": "string",
                "maxLength": 50,
                "description": "Drawing reference (PARTS_INF.DRAWING in PTX)"
              },
              "order": {
                "type": "string",
                "maxLength": 50,
                "description": "Order number (PARTS_INF.ORDER in PTX)"
              },
              "colour": {
                "type": "string",
                "maxLength": 50,
                "description": "Colour label (PARTS_INF.COLOUR in PTX)"
              },
              "productInfo": {
                "type": "string",
                "maxLength": 50,
                "description": "Product info (PARTS_INF.PROD_INFO in PTX)"
              },
              "productNumber": {
                "type": "string",
                "maxLength": 50,
                "description": "Production number (PARTS_INF.PROD_NUM in PTX)"
              }
            }
          },
          "customData": {
            "type": "object",
            "additionalProperties": true,
            "description": "Custom user-defined data. All entries are written to PARTS_UDI (INFO1..INFO60) in PTX export. Note: `barcode`/`barcode1`/`barcode2`/`clientName` here are legacy fallbacks — prefer the typed `labels` field above."
          },
          "extras": {
            "$ref": "#/components/schemas/Extras",
            "description": "Extras structure (required for banding, finish, etc.)"
          },
          "machining": {
            "$ref": "#/components/schemas/Machining"
          },
          "orientationLock": {
            "type": "string",
            "enum": [
              "",
              "l",
              "w"
            ],
            "description": "Orientation lock for rotation constraints",
            "nullable": true
          },
          "upDirection": {
            "description": "Up direction indicator",
            "type": "string",
            "enum": [
              "l",
              "w"
            ],
            "nullable": true
          },
          "stockLock": {
            "type": "array",
            "items": {
              "type": "string"
            },
            "default": [],
            "description": "Parent IDs of stock(s) this part is locked to"
          }
        },
        "required": [
          "l",
          "w"
        ]
      },
      "Extras": {
        "type": "object",
        "description": "Extras container (V3 format)",
        "properties": {
          "banding": {
            "type": "object",
            "properties": {
              "sides": {
                "$ref": "#/components/schemas/SideValues"
              }
            },
            "description": "Edge banding"
          },
          "finish": {
            "type": "object",
            "properties": {
              "faces": {
                "type": "object",
                "properties": {
                  "a": {
                    "type": "string",
                    "description": "Front face"
                  },
                  "b": {
                    "type": "string",
                    "description": "Back face"
                  }
                }
              },
              "sides": {
                "$ref": "#/components/schemas/SideValues"
              }
            },
            "description": "Surface finish"
          },
          "planing": {
            "type": "object",
            "properties": {
              "sides": {
                "$ref": "#/components/schemas/SideValues"
              },
              "faces": {
                "type": "object",
                "properties": {
                  "a": {
                    "type": "boolean"
                  },
                  "b": {
                    "type": "boolean"
                  }
                }
              }
            },
            "description": "Planing operations"
          },
          "imageUpload": {
            "type": "object",
            "description": "Image upload data"
          }
        }
      },
      "Trim": {
        "type": "object",
        "description": "Trim allowances",
        "properties": {
          "l1": {
            "type": "number",
            "minimum": 0,
            "default": 0,
            "description": "Long side 1 trim value"
          },
          "l2": {
            "type": "number",
            "minimum": 0,
            "default": 0,
            "description": "Long side 2 trim value"
          },
          "w1": {
            "type": "number",
            "minimum": 0,
            "default": 0,
            "description": "Short side 1 trim value"
          },
          "w2": {
            "type": "number",
            "minimum": 0,
            "default": 0,
            "description": "Short side 2 trim value"
          }
        }
      },
      "StockAnalysis": {
        "type": "object",
        "description": "Analysis metrics for stock item",
        "properties": {
          "partArea": {
            "type": "number",
            "description": "Total area of parts on this stock"
          },
          "areaEfficiency": {
            "type": "number",
            "description": "Percentage efficiency of this stock"
          },
          "finishArea": {
            "type": "number",
            "description": "Total finish area for parts on this stock"
          },
          "totalParts": {
            "type": "integer",
            "description": "Number of parts on this stock"
          },
          "numberOfCuts": {
            "type": "integer",
            "description": "Total number of cuts on this stock"
          },
          "stackedNumberOfCuts": {
            "type": "integer",
            "description": "Total cuts assuming stacked stock are cut once"
          },
          "cutLength": {
            "type": "number",
            "description": "Total length of cuts on this stock"
          },
          "stackedCutLength": {
            "type": "number",
            "description": "Total cut length assuming stacked stock are cut once"
          },
          "bandingLength": {
            "type": "number",
            "description": "Total banding length for parts on this stock"
          },
          "rollLength": {
            "type": "number",
            "description": "Roll length used (for roll stock)"
          },
          "maxPhase": {
            "type": "integer",
            "minimum": 0,
            "nullable": true,
            "description": "Deepest saw turn any cut on this board needs (root cuts at 0, +1 per direction flip) — the quantity guillotineOptions.maxPhase caps. null when the board carries no turn-stamped cuts"
          }
        }
      },
      "SideValues": {
        "type": "object",
        "description": "Side values",
        "properties": {
          "l1": {
            "oneOf": [
              {
                "type": "boolean"
              },
              {
                "type": "string"
              }
            ],
            "description": "Long side 1"
          },
          "l2": {
            "oneOf": [
              {
                "type": "boolean"
              },
              {
                "type": "string"
              }
            ],
            "description": "Long side 2"
          },
          "w1": {
            "oneOf": [
              {
                "type": "boolean"
              },
              {
                "type": "string"
              }
            ],
            "description": "Short side 1"
          },
          "w2": {
            "oneOf": [
              {
                "type": "boolean"
              },
              {
                "type": "string"
              }
            ],
            "description": "Short side 2"
          }
        }
      },
      "Group": {
        "type": "object",
        "description": "User-defined part group with XY coordinate-based positioning. The group is placed as one rigid unit at the authored positions; it never rotates as a whole. Member orientation is NOT part of the group: it defers entirely to each part's `orientationLock` — a position has no rotation of its own, and `rot` on results is derived from the final dimensions.",
        "properties": {
          "positions": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/GroupPosition"
            },
            "minItems": 2,
            "description": "Part positions within the group, authored against each part's LOCKED footprint: a part with `orientationLock: \"w\"` occupies its rotated dimensions (short side along x), a part locked \"l\" — or with no lock — occupies its landscape dimensions (long side along x). Include the kerf in the pitch when parts should touch (e.g. 200-wide members with a 3.2 blade sit at x = 0, 203.2, 406.4). Validation checks overlaps in this same frame and names the offending pair."
          },
          "q": {
            "type": "number",
            "minimum": 1,
            "description": "Quantity of groups"
          }
        },
        "required": [
          "positions",
          "q"
        ]
      },
      "GroupPosition": {
        "type": "object",
        "description": "Position of a part within a user group. Coordinates place the part's locked footprint (see the positions description on the group) — there is no per-position rotation field by design: orientation belongs to the part via `orientationLock`.",
        "properties": {
          "part": {
            "type": "number",
            "minimum": 0,
            "description": "Index of the part in the parts array"
          },
          "x": {
            "type": "number",
            "minimum": 0,
            "description": "X position within group (0 = left edge, increases rightward)"
          },
          "y": {
            "type": "number",
            "minimum": 0,
            "description": "Y position within group (0 = bottom edge, increases upward)"
          }
        },
        "required": [
          "part",
          "x",
          "y"
        ]
      },
      "ResultResponse": {
        "type": "object",
        "description": "Complete result.\n\n**Coordinates.** Every position (`parts[].x/y`, `cuts[].x1…y2`, `offcuts[].x/y`) is measured on its stock sheet from the origin corner, with the sheet's long side (`l`) along x and its short side (`w`) along y. y = 0 is the origin edge and **y increases upward**, so \"top\" means high y. On a vertical panel saw the origin edge is the bottom rail the sheet stands on; with `guillotineOptions.verticalPanelSaw` the narrow strips are placed at the top of the sheet (high y) and the waste at the bottom, and the cut `order` works from the top down.\n\n**Stacks.** Sheets that share a cutting pattern may be cut together as a stack (`saw.options.stackingMode`). A stack is listed ONCE in `stock`, with `stack` = its number of sheets; `stacks` lists every stack with each sheet's position (1 = top) and its number across the job, and each part cut from a stack carries `stacked: { sheet, of, stack }`. For a label, print \"sheet `stacked.sheet` of `stacked.of`, stack `stacked.stack`\". What the control cuts together WITHIN a sheet (strips cut in one pass) is per sheet in `metadata.productionTime.sheets[].stacking`, for saws with a cutting-time model.",
        "properties": {
          "jobId": {
            "type": "number"
          },
          "calcTime": {
            "type": "number",
            "description": "Calculation time in milliseconds"
          },
          "saw": {
            "$ref": "#/components/schemas/Saw",
            "description": "Saw configuration used. On a mixed (multi-saw) job this is the job's DEFAULT saw — attribute each stock via its `sawId` and the `saws` echo."
          },
          "saws": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/IdentifiedSaw"
            },
            "description": "The identified saws a mixed job ran with (the request's `saw` array plus any hoisted inline material saws). Present ONLY on mixed jobs."
          },
          "materials": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/Material"
            },
            "description": "The materials a mixed job was partitioned by. Present ONLY on mixed jobs."
          },
          "stock": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/ResultStock"
            },
            "description": "Stock with layouts"
          },
          "parts": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/ResultPart"
            },
            "description": "Parts with placement"
          },
          "stacks": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/ResultStack"
            },
            "description": "What the saw cuts together: one entry per used `stock` entry, in `stock` order, with its depth and every sheet's position in the stack and number in the job. The depths sum to the number of sheets used. Present ONLY when at least one stack holds two or more sheets."
          },
          "stripRepeats": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/ResultStripRepeat"
            },
            "description": "Identical strips across the job's sheets and within one, for an operator who stacks strips by hand (e.g. on a vertical panel saw). Each group names its member strips by `stock[].id` and strip number; each part of a member strip carries `stripGroup`. Present ONLY when at least one strip repeats."
          },
          "cuts": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/Cut"
            },
            "description": "Cut instructions"
          },
          "offcuts": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/Offcut"
            },
            "description": "Every remaining offcut piece. Filter on `retained` for the pieces the saw would take back as stock."
          },
          "unusableParts": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/UnusablePart"
            },
            "description": "Parts rejected BEFORE optimisation because no available stock can ever hold them (larger than every stock size, or no stock with a matching material/thickness). This is NOT the full list of parts left out of the layout — it is the \"can never fit\" subset. For a completeness check (\"did every part place?\") use metadata.unplacedParts, or compare the count of placed parts to the count requested. unusableParts also appears within metadata.unplacedParts."
          },
          "metadata": {
            "$ref": "#/components/schemas/Metadata",
            "description": "Complete analysis (all internal fields)"
          }
        },
        "required": [
          "jobId",
          "stock",
          "parts",
          "metadata"
        ]
      },
      "ResultStock": {
        "type": "object",
        "description": "Result stock",
        "properties": {
          "id": {
            "type": "string",
            "description": "Unique identifier for containers (required for Stock and Segment)",
            "default": ""
          },
          "name": {
            "type": "string",
            "description": "User input name",
            "default": ""
          },
          "l": {
            "type": "number",
            "description": "Dimension in the stock length direction"
          },
          "w": {
            "type": "number",
            "description": "Dimension in the stock width direction"
          },
          "t": {
            "description": "Thickness (z-axis dimension)"
          },
          "material": {
            "description": "Material name"
          },
          "grain": {
            "type": "string",
            "enum": [
              "l",
              "w",
              ""
            ],
            "nullable": true,
            "description": "Grain direction"
          },
          "trim": {
            "$ref": "#/components/schemas/Trim"
          },
          "cost": {
            "type": "number",
            "description": "Cost per unit"
          },
          "notes": {
            "type": "string",
            "description": "Stock notes"
          },
          "code": {
            "type": "string",
            "description": "Internal product or material code"
          },
          "customData": {
            "type": "object",
            "additionalProperties": true,
            "description": "Custom user-defined data"
          },
          "type": {
            "type": "string",
            "enum": [
              "sheet",
              "linear",
              "roll"
            ],
            "description": "Stock type"
          },
          "used": {
            "type": "boolean",
            "description": "Whether this stock was used in the optimization"
          },
          "analysis": {
            "$ref": "#/components/schemas/StockAnalysis",
            "description": "Detailed analysis metrics for this stock item. null when the stock carries no analysis (an unused stock item)."
          },
          "stack": {
            "type": "number",
            "description": "Stack depth: the number of sheets cut to this pattern together (1 = not stacked). Sheets 2…n are not listed separately — see the response `stacks`."
          },
          "sawId": {
            "type": "string",
            "description": "Id of the saw this stock was optimised on. Present ONLY on mixed jobs — see the response `saws` echo."
          },
          "materialId": {
            "type": "string",
            "description": "Id of the material this stock belongs to. Present ONLY on mixed jobs — see the response `materials` echo."
          }
        }
      },
      "ResultPart": {
        "type": "object",
        "description": "Result part with coordinates",
        "properties": {
          "id": {
            "type": "string",
            "description": "Unique identifier for referencing items",
            "default": ""
          },
          "name": {
            "type": "string",
            "description": "User input name",
            "default": ""
          },
          "l": {
            "type": "number",
            "description": "Dimension in the stock length direction"
          },
          "w": {
            "type": "number",
            "description": "Dimension in the stock width direction"
          },
          "t": {
            "description": "Thickness (z-axis dimension)"
          },
          "material": {
            "description": "Material name"
          },
          "q": {
            "type": "number",
            "description": "Quantity"
          },
          "trim": {
            "$ref": "#/components/schemas/Trim"
          },
          "notes": {
            "type": "string",
            "description": "Part notes"
          },
          "code": {
            "type": "string",
            "description": "Internal product or material code"
          },
          "customData": {
            "type": "object",
            "additionalProperties": true,
            "description": "Custom user-defined data"
          },
          "extras": {
            "$ref": "#/components/schemas/Extras"
          },
          "machining": {
            "$ref": "#/components/schemas/Machining"
          },
          "x": {
            "type": "number",
            "nullable": true,
            "description": "X coordinate position",
            "default": null
          },
          "y": {
            "type": "number",
            "nullable": true,
            "description": "Y coordinate position",
            "default": null
          },
          "rot": {
            "type": "integer",
            "enum": [
              0,
              1
            ],
            "description": "Rotation of the placed part: 0 = as entered, 1 = rotated 90°. (Matches the wire format — the engine emits 0/1, not true/false.)"
          },
          "added": {
            "type": "boolean",
            "description": "Whether this part was auto-added during optimization"
          },
          "stock": {
            "type": "object",
            "properties": {
              "id": {
                "type": "string",
                "description": "ID of the stock this part is placed on. For a part cut on a stack, the id of the stack's top sheet — the entry listed in `stock`."
              },
              "code": {
                "type": "string",
                "description": "Product/material code of the stock (if set)"
              }
            }
          },
          "stacked": {
            "type": "object",
            "description": "Present only when the part is cut on a stack of sheets. Which sheet of the stack it was cut from; `stock.id` names the stack.",
            "properties": {
              "sheet": {
                "type": "integer",
                "minimum": 1,
                "description": "Sheet the part was cut from — 1 is the top sheet of the stack"
              },
              "of": {
                "type": "integer",
                "minimum": 1,
                "description": "Number of sheets in the stack"
              },
              "stack": {
                "type": "integer",
                "minimum": 1,
                "description": "The stack's number — its `stacks[].stack`"
              }
            },
            "required": [
              "sheet",
              "of"
            ]
          },
          "stripGroup": {
            "type": "integer",
            "minimum": 1,
            "description": "Present only when the strip this part is cut from repeats: its `stripRepeats[].group`."
          }
        }
      },
      "ResultStack": {
        "type": "object",
        "description": "One stack: a cutting pattern (one `stock` entry) cut through `depth` sheets at once. A single sheet is a stack of depth 1.",
        "properties": {
          "stack": {
            "type": "integer",
            "minimum": 1,
            "description": "Stack number, 1…n in `stock` order"
          },
          "stockId": {
            "type": "string",
            "description": "The `stock[].id` of the pattern this stack is cut to"
          },
          "depth": {
            "type": "integer",
            "minimum": 1,
            "description": "Sheets in the stack, cut together"
          },
          "sheets": {
            "type": "array",
            "description": "Every sheet of the stack, top first",
            "items": {
              "type": "object",
              "properties": {
                "sheet": {
                  "type": "integer",
                  "minimum": 1,
                  "description": "Position in the stack: 1 is the top sheet"
                },
                "number": {
                  "type": "integer",
                  "minimum": 1,
                  "description": "The sheet's number across the whole job, 1…n"
                }
              },
              "required": [
                "sheet",
                "number"
              ]
            }
          }
        },
        "required": [
          "stack",
          "stockId",
          "depth",
          "sheets"
        ]
      },
      "ResultStripRepeat": {
        "type": "object",
        "description": "One group of identical strips (RM-311). A strip is the band between two of a sheet's opening rips (`cuts[]` with `phase` 0). Two strips are identical when they have the same width and, position by position along the strip, the same part (or a part declared identical to it) at the same place with the same size, every dimension within half the saw's `bladeWidth`. Members may be on different sheets or on the same sheet.\n\n**Stacked sheets.** A strip on a stack of n sheets is already cut n high: it is ONE member with `sheets: n`, and `sheets` on the group counts the physical strips. A group needs two or more member strips, so a lone strip on a stack is not listed. Sheets whose opening cuts run both ways (a head cut) are not analysed.",
        "properties": {
          "group": {
            "type": "integer",
            "minimum": 1,
            "description": "Group number, 1…n in the order the first strip of each group is cut; the `stripGroup` its parts carry"
          },
          "width": {
            "type": "number",
            "description": "The strip's width: the extent of its parts across the opening rips"
          },
          "length": {
            "type": "number",
            "description": "The extent of the strip's parts along the rips"
          },
          "strips": {
            "type": "integer",
            "minimum": 2,
            "description": "Member strips in the layout (each listed in `members`)"
          },
          "sheets": {
            "type": "integer",
            "minimum": 2,
            "description": "Physical strips cut: the members' `sheets` summed (a strip on a stack of n sheets is n strips)"
          },
          "members": {
            "type": "array",
            "description": "Every strip of the group, in stock order then strip number",
            "items": {
              "type": "object",
              "properties": {
                "stockId": {
                  "type": "string",
                  "description": "The `stock[].id` of the sheet (or stack) the strip is on"
                },
                "stack": {
                  "type": "integer",
                  "minimum": 1,
                  "description": "The `stacks[].stack` number, when the response lists stacks"
                },
                "strip": {
                  "type": "integer",
                  "minimum": 1,
                  "description": "The strip's number on its sheet in cutting order, 1…n: strips are numbered in the order the rip that frees each one is cut (the cuts' `order`), counting strips that hold parts. With `guillotineOptions.verticalPanelSaw` (cut from the top down) strip 1 is the top strip"
                },
                "at": {
                  "type": "number",
                  "description": "The strip's offset from the origin edge across the rips (y = 0 for rips along x, x = 0 for rips along y)"
                },
                "sheets": {
                  "type": "integer",
                  "minimum": 1,
                  "description": "Sheets this strip is cut through: its stack depth (1 = a single sheet)"
                }
              },
              "required": [
                "stockId",
                "strip",
                "at",
                "sheets"
              ]
            }
          }
        },
        "required": [
          "group",
          "width",
          "length",
          "strips",
          "sheets",
          "members"
        ]
      },
      "Machining": {
        "type": "object",
        "nullable": true,
        "description": "Machining operations on a part — the same shape in the request and the result.",
        "properties": {
          "holes": {
            "type": "array",
            "items": {
              "type": "object",
              "properties": {
                "x": {
                  "type": "number",
                  "description": "X position of the hole centre, from the left edge"
                },
                "y": {
                  "type": "number",
                  "description": "Y position of the hole centre, from the bottom edge"
                },
                "diameter": {
                  "type": "number",
                  "description": "Hole diameter"
                },
                "depth": {
                  "type": "number",
                  "description": "Hole depth. Absent for a through hole"
                },
                "face": {
                  "type": "number",
                  "enum": [
                    0,
                    1
                  ],
                  "description": "Face index (0=front, 1=back)"
                },
                "type": {
                  "type": "string",
                  "enum": [
                    "regular",
                    "hinge",
                    "shelf"
                  ],
                  "description": "Hole type"
                },
                "edge": {
                  "type": "string",
                  "enum": [
                    "l1",
                    "l2",
                    "w1",
                    "w2"
                  ],
                  "nullable": true,
                  "description": "Edge the hole is bored into. null for a face hole"
                }
              },
              "required": [
                "x",
                "y",
                "diameter"
              ]
            },
            "description": "Drilled holes"
          },
          "hingeHoles": {
            "type": "array",
            "items": {
              "type": "object",
              "properties": {
                "position": {
                  "type": "number",
                  "description": "Position along the side"
                },
                "side": {
                  "type": "string",
                  "enum": [
                    "l1",
                    "l2",
                    "w1",
                    "w2"
                  ],
                  "description": "Side the hinge is placed on"
                },
                "face": {
                  "type": "number",
                  "enum": [
                    0,
                    1
                  ],
                  "description": "Face index (0=front, 1=back)"
                },
                "numHoles": {
                  "type": "number",
                  "description": "Number of holes in the hinge"
                },
                "diameter": {
                  "type": "number",
                  "description": "Hole diameter"
                },
                "depth": {
                  "type": "number",
                  "description": "Hole depth"
                },
                "hingeLength": {
                  "type": "number",
                  "description": "Length of the hinge"
                },
                "distanceFromEdge": {
                  "type": "number",
                  "description": "Distance from the edge to the hole centre"
                },
                "outerSpacing": {
                  "type": "number",
                  "description": "Spacing from the hinge ends to the outer holes"
                }
              },
              "required": [
                "position",
                "side"
              ]
            },
            "description": "Hinge hole sets"
          },
          "corners": {
            "type": "array",
            "items": {
              "type": "object",
              "properties": {
                "index": {
                  "type": "integer",
                  "enum": [
                    0,
                    1,
                    2,
                    3
                  ],
                  "description": "Corner (0=bottom-left, 1=bottom-right, 2=top-right, 3=top-left)"
                },
                "type": {
                  "type": "string",
                  "enum": [
                    "radius",
                    "bevel"
                  ],
                  "nullable": true,
                  "description": "Corner modification"
                },
                "size": {
                  "type": "number",
                  "nullable": true,
                  "description": "Size of the corner modification"
                }
              }
            },
            "description": "Corner modifications"
          },
          "grooves": {
            "type": "array",
            "items": {
              "type": "object",
              "properties": {
                "direction": {
                  "type": "string",
                  "enum": [
                    "l",
                    "w"
                  ],
                  "description": "Groove direction (l=along the length, w=along the width)"
                },
                "position": {
                  "type": "number",
                  "description": "Distance from the near edge, perpendicular to the groove"
                },
                "start": {
                  "type": "number",
                  "description": "Start offset along the groove"
                },
                "length": {
                  "type": "number",
                  "description": "Groove length. Absent for the full side"
                },
                "width": {
                  "type": "number",
                  "description": "Groove width"
                },
                "depth": {
                  "type": "number",
                  "description": "Groove depth"
                },
                "face": {
                  "type": "number",
                  "enum": [
                    0,
                    1
                  ],
                  "description": "Face index (0=front, 1=back)"
                }
              }
            },
            "description": "Grooves"
          }
        }
      },
      "UnusablePart": {
        "type": "object",
        "description": "A part that cannot be placed on ANY available stock (too large for every stock size, or no stock with a matching material/thickness) — rejected up front before optimisation. Excludes placement fields; includes the validation issues explaining the rejection. Distinct from a part that was attempted but did not fit — see metadata.unplacedParts.",
        "properties": {
          "id": {
            "type": "string",
            "description": "Unique identifier for referencing items",
            "default": ""
          },
          "name": {
            "type": "string",
            "description": "User input name",
            "default": ""
          },
          "l": {
            "type": "number",
            "description": "Dimension in the stock length direction"
          },
          "w": {
            "type": "number",
            "description": "Dimension in the stock width direction"
          },
          "t": {
            "description": "Thickness (z-axis dimension)"
          },
          "material": {
            "description": "Material name"
          },
          "trim": {
            "$ref": "#/components/schemas/Trim"
          },
          "notes": {
            "type": "string",
            "description": "Part notes"
          },
          "code": {
            "type": "string",
            "description": "Internal product or material code"
          },
          "customData": {
            "type": "object",
            "additionalProperties": true,
            "description": "Custom user-defined data"
          },
          "extras": {
            "$ref": "#/components/schemas/Extras"
          },
          "machining": {
            "$ref": "#/components/schemas/Machining"
          },
          "issues": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/Issue"
            },
            "description": "Validation issues explaining why the part could not be placed"
          }
        }
      },
      "Issue": {
        "type": "object",
        "description": "Validation issue for a part",
        "properties": {
          "message": {
            "type": "string",
            "description": "Human-readable error message"
          },
          "type": {
            "type": "string",
            "enum": [
              "error",
              "warning"
            ],
            "description": "Issue severity"
          },
          "category": {
            "type": "array",
            "items": {
              "type": "string",
              "enum": [
                "saw",
                "stock",
                "part",
                "group",
                "machining",
                "extras"
              ]
            },
            "description": "Issue category"
          },
          "field": {
            "type": "array",
            "items": {
              "type": "array",
              "items": {
                "type": "string"
              }
            },
            "description": "Path to the field with the issue"
          }
        }
      },
      "Cut": {
        "type": "object",
        "description": "Cut instruction with coordinates",
        "properties": {
          "x1": {
            "type": "number",
            "description": "Start X coordinate of the cut"
          },
          "x2": {
            "type": "number",
            "description": "End X coordinate of the cut"
          },
          "y1": {
            "type": "number",
            "description": "Start Y coordinate of the cut"
          },
          "y2": {
            "type": "number",
            "description": "End Y coordinate of the cut"
          },
          "dimension": {
            "type": "string",
            "enum": [
              "l",
              "w"
            ],
            "description": "Cut dimension (l=horizontal, w=vertical)"
          },
          "position": {
            "type": "string",
            "enum": [
              "top",
              "bottom",
              "left",
              "right",
              "merged"
            ],
            "description": "Cut position relative to segment"
          },
          "isTrim": {
            "type": "boolean",
            "description": "Whether this is a trim cut"
          },
          "order": {
            "type": "number",
            "nullable": true,
            "description": "Position of this cut in its board's cutting sequence. Numbering restarts on every board: to cut a board, take the cuts whose `stock.id` is that board and run them in ascending `order`. Stock trim cuts carry negative values and come first. With `guillotineOptions.verticalPanelSaw` the sequence works from the top of the board down."
          },
          "phase": {
            "type": "integer",
            "minimum": 0,
            "nullable": true,
            "description": "The saw turn this cut is made on, read from the cut's geometry: 0 = the board's first cutting direction (the opening rips), 1 = after the material is turned once, and so on. A head cut counts like any other cut: the section it opens is turned before it is ripped, so that section's first cuts are one turn later. The quantity `guillotineOptions.maxPhase` caps; `stock[].analysis.maxPhase` is the deepest on each board. Null on cuts that carry no turn (efficiency, nesting and roll layouts)."
          },
          "stock": {
            "type": "object",
            "properties": {
              "id": {
                "type": "string",
                "description": "ID of the stock this cut belongs to"
              }
            }
          }
        }
      },
      "Offcut": {
        "type": "object",
        "description": "Remaining offcut piece from optimization",
        "properties": {
          "id": {
            "type": "string",
            "description": "Stable identifier, unique within the job. A retained offcut is numbered `9001`, `9002`, … in board order then position; this is the number a Mayer `.may` export prints on the matching `Rest` record (its MA010 row and MA319/MA419), so a result offcut and a file Rest match by id. Any other (waste) offcut is `W1`, `W2`, …."
          },
          "l": {
            "type": "number",
            "description": "Length dimension"
          },
          "w": {
            "type": "number",
            "description": "Width dimension"
          },
          "t": {
            "type": "number",
            "description": "Thickness"
          },
          "x": {
            "type": "number",
            "description": "X coordinate position on stock"
          },
          "y": {
            "type": "number",
            "description": "Y coordinate position on stock"
          },
          "q": {
            "type": "number",
            "description": "Quantity"
          },
          "material": {
            "type": "string",
            "description": "Material name (inherited from parent stock)"
          },
          "grain": {
            "type": "string",
            "enum": [
              "l",
              "w",
              ""
            ],
            "nullable": true,
            "description": "Grain direction (inherited from parent stock)"
          },
          "retained": {
            "type": "boolean",
            "default": true,
            "description": "Whether the target saw would take this piece back as stock. `false` means it is below the saw's declared minimum reusable offcut, so it cannot be racked: treat it as waste. It is still listed because it is real empty space on the board. Always `true` for a saw that declares no minimum."
          },
          "stock": {
            "type": "object",
            "properties": {
              "id": {
                "type": "string",
                "description": "ID of the stock this offcut came from"
              }
            }
          }
        }
      },
      "Metadata": {
        "type": "object",
        "description": "Complete result metadata with all analysis fields",
        "properties": {
          "totalEfficiency": {
            "type": "number",
            "default": 0
          },
          "totalStockArea": {
            "type": "number",
            "default": 0
          },
          "totalStockUsedArea": {
            "type": "number",
            "default": 0
          },
          "totalStockUnusedArea": {
            "type": "number",
            "default": 0
          },
          "totalPartsArea": {
            "type": "number",
            "default": 0
          },
          "totalPartArea": {
            "type": "number",
            "description": "Total area of the placed parts, including every sheet of a stack. Same value as totalPartsArea"
          },
          "totalUnusedPartsArea": {
            "type": "number",
            "default": 0
          },
          "totalOffcutsArea": {
            "type": "number",
            "default": 0
          },
          "totalOffcuts": {
            "type": "number",
            "default": 0
          },
          "totalStockCost": {
            "type": "number",
            "default": 0
          },
          "stockAreaCost": {
            "type": "number",
            "default": 0
          },
          "bandingLengthByType": {
            "type": "object",
            "additionalProperties": {
              "type": "number"
            },
            "default": {},
            "description": "Banding per material type"
          },
          "totalBandingLength": {
            "type": "number",
            "default": 0
          },
          "finishAreaByType": {
            "type": "object",
            "additionalProperties": {
              "type": "number"
            },
            "default": {},
            "description": "Finish area per type"
          },
          "totalFinishArea": {
            "type": "number",
            "default": 0
          },
          "planingAreaByType": {
            "type": "object",
            "additionalProperties": {
              "type": "number"
            },
            "default": {},
            "description": "Planing area per type"
          },
          "totalPlaningArea": {
            "type": "number",
            "default": 0
          },
          "totalStockWeight": {
            "type": "number",
            "description": "Total weight of the stock used, from each stock's weight. 0 when no stock declares a weight"
          },
          "totalPartWeight": {
            "type": "number",
            "description": "Total weight of the placed parts, estimated from each stock's weight by volume. 0 when no stock declares a weight and thickness"
          },
          "totalCutLength": {
            "type": "number",
            "default": 0
          },
          "totalStackedCutLength": {
            "type": "number",
            "default": 0
          },
          "totalCuts": {
            "type": "number",
            "default": 0
          },
          "totalStackedCuts": {
            "type": "number",
            "default": 0
          },
          "maxPhase": {
            "type": "integer",
            "minimum": 0,
            "nullable": true,
            "description": "Deepest saw turn any board in the job needs — the max of stock[].analysis.maxPhase (root cuts at 0, +1 per direction flip; the quantity guillotineOptions.maxPhase caps). null when no board carries turn-stamped cuts"
          },
          "phaseStep": {
            "type": "object",
            "description": "Present when the optimiser tried a deeper turn cap than the job's (saw.guillotineOptions.adaptiveMaxPhase). Absent when none was tried.",
            "properties": {
              "from": {
                "type": "integer",
                "description": "The cap the job ran at (yours, or the saw profile's default)"
              },
              "to": {
                "type": "integer",
                "description": "The cap used when kept is true; otherwise the deepest cap tried"
              },
              "kept": {
                "type": "boolean",
                "description": "Whether a deeper cap's layout was returned"
              },
              "sheetsSaved": {
                "type": "number",
                "description": "Sheets the returned deeper layout saves (0 when none was kept)"
              },
              "secondsAdded": {
                "type": "number",
                "description": "Predicted cutting seconds the returned deeper layout adds (negative = saves). Absent without a cutting-time model"
              },
              "reason": {
                "type": "string",
                "description": "Why the returned cap won, or why none did: fewer-sheets, same-sheets-faster, same-sheets-better-offcut; more-sheets, more-stock-area, slower, same-time-no-better-offcut, no-time-estimate, hard-gate, no-result. New values may be added"
              },
              "tried": {
                "type": "array",
                "description": "Every deeper cap built, shallowest first",
                "items": {
                  "type": "object",
                  "properties": {
                    "cap": {
                      "type": "integer"
                    },
                    "sheets": {
                      "type": "number",
                      "nullable": true,
                      "description": "Sheets that cap's layout needs (null = no layout)"
                    },
                    "seconds": {
                      "type": "number",
                      "description": "Its predicted cutting seconds; absent without a cutting-time model"
                    },
                    "kept": {
                      "type": "boolean",
                      "description": "Whether it beat the best layout so far"
                    },
                    "reason": {
                      "type": "string"
                    }
                  }
                }
              },
              "boards": {
                "type": "array",
                "items": {
                  "type": "string"
                },
                "description": "Stock ids of the boards cut deeper than `from`"
              },
              "note": {
                "type": "string",
                "description": "The trade in one line"
              }
            }
          },
          "productionTime": {
            "type": "object",
            "description": "The predicted PRODUCTION TIME of the job on the saw, in seconds, priced from the plates its control cuts for the file the saw gets. Present only for a saw with a cutting-time model (today: Mayer `.may`, the PC7000 control); absent otherwise.",
            "properties": {
              "model": {
                "type": "string",
                "description": "The machine time model it was priced with"
              },
              "total": {
                "type": "number",
                "description": "Seconds for the whole job: every board's stack cut once, operator feed included"
              },
              "machine": {
                "type": "number",
                "description": "The machine's share of total"
              },
              "operator": {
                "type": "number",
                "description": "The operator's share of total (bringing each sheet and strip to the saw)"
              },
              "sheets": {
                "type": "array",
                "description": "Per board (per pattern)",
                "items": {
                  "type": "object",
                  "properties": {
                    "stockId": {
                      "type": "string",
                      "description": "The board — `stock[].id`"
                    },
                    "seconds": {
                      "type": "number",
                      "description": "One pass of the board's stack"
                    },
                    "stackDepth": {
                      "type": "integer",
                      "description": "Sheets cut together as one stack"
                    },
                    "stripStacks": {
                      "type": "integer",
                      "description": "Strips (or columns of a strip) the control cuts together with an identical one — a pass cutting k counts k − 1"
                    },
                    "plates": {
                      "type": "integer",
                      "description": "Plates the control cuts: the sheet, then each strip or column brought back to the saw"
                    },
                    "stacking": {
                      "type": "object",
                      "description": "What the saw cuts together on this board. Absent where it could not be traced.",
                      "properties": {
                        "sheetStack": {
                          "type": "object",
                          "properties": {
                            "depth": {
                              "type": "integer",
                              "description": "Sheets cut together (1 = cut alone)"
                            },
                            "secondsSaved": {
                              "type": "number",
                              "description": "Seconds the stack saves against cutting its sheets one at a time"
                            }
                          }
                        },
                        "strips": {
                          "type": "array",
                          "description": "The board's strips in written order, each as the band it is ripped as (board frame, like `parts`)",
                          "items": {
                            "type": "object",
                            "properties": {
                              "autoId": {
                                "type": "string",
                                "description": "The strip's key"
                              },
                              "x": {
                                "type": "number"
                              },
                              "y": {
                                "type": "number"
                              },
                              "l": {
                                "type": "number"
                              },
                              "w": {
                                "type": "number"
                              }
                            }
                          }
                        },
                        "passes": {
                          "type": "array",
                          "description": "Every saw pass after the sheet, in cutting order — each strip or column brought back to the saw",
                          "items": {
                            "type": "object",
                            "properties": {
                              "level": {
                                "type": "integer",
                                "description": "1 = a strip; 2 = a column of a strip"
                              },
                              "strips": {
                                "type": "array",
                                "items": {
                                  "type": "string"
                                },
                                "description": "Per unit cut in the pass, the key of its strip"
                              },
                              "rects": {
                                "type": "array",
                                "items": {
                                  "type": "object",
                                  "properties": {
                                    "x": {
                                      "type": "number"
                                    },
                                    "y": {
                                      "type": "number"
                                    },
                                    "l": {
                                      "type": "number"
                                    },
                                    "w": {
                                      "type": "number"
                                    }
                                  }
                                },
                                "description": "Per unit cut in the pass, its rectangle"
                              }
                            }
                          }
                        },
                        "stripStacks": {
                          "type": "array",
                          "description": "The passes that cut several identical strips (or columns) together",
                          "items": {
                            "type": "object",
                            "properties": {
                              "pass": {
                                "type": "integer",
                                "description": "Index into passes"
                              },
                              "strips": {
                                "type": "array",
                                "items": {
                                  "type": "string"
                                },
                                "description": "The keys of the strips cut together"
                              },
                              "depth": {
                                "type": "integer",
                                "description": "How many are cut together"
                              },
                              "secondsSaved": {
                                "type": "number",
                                "description": "Seconds saved against cutting them one at a time"
                              }
                            }
                          }
                        },
                        "sawPasses": {
                          "type": "object",
                          "properties": {
                            "before": {
                              "type": "integer",
                              "description": "Saw passes after the sheet if every strip and column were cut alone"
                            },
                            "after": {
                              "type": "integer",
                              "description": "Saw passes after the sheet as the control cuts it"
                            }
                          }
                        }
                      }
                    }
                  }
                }
              }
            }
          },
          "totalBladeArea": {
            "type": "number",
            "default": 0
          },
          "totalTrimArea": {
            "type": "number",
            "default": 0
          },
          "hasMachining": {
            "type": "boolean",
            "default": false
          },
          "numHoles": {
            "type": "number",
            "default": 0
          },
          "numCorners": {
            "type": "number",
            "default": 0
          },
          "totalPartsProduced": {
            "type": "number"
          },
          "totalUsedStock": {
            "type": "number"
          },
          "totalRollLength": {
            "type": "number"
          },
          "materialSummary": {
            "type": "object",
            "additionalProperties": {
              "type": "object",
              "additionalProperties": {
                "type": "object",
                "properties": {
                  "used": {
                    "type": "number"
                  },
                  "stacks": {
                    "type": "number"
                  },
                  "bandingLengthByType": {
                    "type": "object",
                    "additionalProperties": {
                      "type": "number"
                    }
                  }
                }
              }
            },
            "description": "Nested summary by material → thickness"
          },
          "usedStockTally": {
            "type": "object",
            "additionalProperties": {
              "type": "number"
            }
          },
          "addedPartTally": {
            "type": "object",
            "additionalProperties": {
              "type": "number"
            }
          },
          "unplacedParts": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/UnplacedPart"
            },
            "description": "Every part NOT placed in the returned layout — the authoritative completeness list. Includes BOTH parts attempted but not fitted (e.g. stock ran out with auto-add off) AND the up-front-rejected top-level unusableParts. An empty array means every requested part was placed. Use this (or compare placed vs. requested counts) to decide success — not the top-level unusableParts, which is only the \"can never fit\" subset. Quantities are collapsed per part with a count."
          },
          "warnings": {
            "type": "array",
            "items": {
              "type": "string"
            },
            "description": "Human-readable warnings about this result, for example a sheet that cannot be fully cut on the selected saw. Empty on a sound job."
          }
        }
      },
      "UnplacedPart": {
        "type": "object",
        "description": "A requested part that did not appear in the returned layout, with its quantity. Covers both capacity misses (would fit, but no stock was left) and structurally unusable parts (also listed in the top-level unusableParts).",
        "properties": {
          "id": {
            "type": "string",
            "description": "Part id"
          },
          "name": {
            "type": "string",
            "description": "Part name"
          },
          "count": {
            "type": "number",
            "description": "How many of this part were left unplaced"
          },
          "l": {
            "type": "number",
            "description": "Dimension in the stock length direction"
          },
          "w": {
            "type": "number",
            "description": "Dimension in the stock width direction"
          },
          "t": {
            "description": "Thickness (z-axis dimension)"
          },
          "material": {
            "description": "Material name"
          }
        }
      },
      "ErrorResponse": {
        "type": "object",
        "description": "Error response with migration help",
        "properties": {
          "error": {
            "type": "string",
            "description": "Error message"
          },
          "details": {
            "type": "object",
            "description": "Detailed information"
          },
          "version": {
            "type": "string",
            "enum": [
              "V3"
            ]
          },
          "help": {
            "type": "string",
            "description": "Migration guidance"
          },
          "example": {
            "type": "object",
            "description": "Correct format example"
          },
          "docs": {
            "type": "string",
            "format": "uri",
            "description": "Migration guide URL"
          }
        },
        "required": [
          "error",
          "version"
        ]
      }
    }
  },
  "security": [
    {
      "ApiKeyAuth": []
    }
  ],
  "servers": [
    {
      "url": "https://api.smartcut.dev",
      "description": "Production"
    },
    {
      "url": "http://localhost:5000",
      "description": "Local development"
    }
  ],
  "tags": [
    {
      "name": "Calculations",
      "description": "Submit cutting optimization calculations"
    },
    {
      "name": "Results",
      "description": "Check status and retrieve calculation results"
    },
    {
      "name": "Exports",
      "description": "Export a finished result as a file — PDF, CSV, or a saw-ready cutting file"
    },
    {
      "name": "Discovery",
      "description": "Ask the server what it offers — optimisation models, saws and the export formats each one supports. Read these at runtime to drive pickers and client-side validation instead of hard-coding the options"
    },
    {
      "name": "Labels",
      "description": "Generate printable labels for parts"
    },
    {
      "name": "Stats",
      "description": "View API usage statistics and call history"
    },
    {
      "name": "Webhooks",
      "description": "**The recommended way to receive results.** SmartCut delivers the finished calculation to a URL of your choice, so you do not have to poll for it.\n\nFull guide, including signature verification and retry behaviour: https://smartcut.dev/docs/webhooks\n\n**Setup options:**\n- **Per-request:** Include a `webhook` field in your `/v3/calculate` request body with your target URL.\n- **Account default:** Set a default webhook URL at https://smartcut.dev/account — used for all requests that do not include a per-request URL.\n\nWhen the calculation completes, SmartCut sends an HTTP `POST` to your URL with a `Content-Type: application/json` body matching the `ResultResponse` schema.\n\n**Your endpoint should:**\n- Return a `2xx` status within 10 seconds.\n- Be accessible over HTTPS (or HTTP for local development).\n\n**Delivery.** Every delivery is signed — verify the `X-SmartCut-Signature` header (HMAC-SHA256 over the timestamped body) before trusting a payload; reveal or rotate your signing secret at https://smartcut.dev/account. Failed deliveries are retried with backoff for roughly a day, reusing the `X-SmartCut-Delivery` id so you can treat it as an idempotency key. An account-default endpoint that keeps failing is eventually disabled and must be re-enabled by hand; per-request URLs are retried but not tracked this way."
    }
  ]
}