{
  "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. PTX-only today; Biesse XML and Mayer .MAY to follow. Schema may change; see `/v3/export/ptx` for the current PTX column mapping.",
    "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 (beam saw format)",
        "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| `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\n**Legacy `customData` keys (deprecated, removed in a future release):**\n\nFor one release, the PTX emitter still falls back to these `customData` keys when the equivalent `labels` field is not set: `customData.barcode` (PARTS_REQ.CODE fallback after `code`/`name`), `customData.barcode1`, `customData.barcode2`, `customData.clientName` (JOBS.CUSTOMER). Migrate to `labels` to keep the contract typed and avoid silent typos.\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 (Beta)",
        "description": "**BETA** - Generate a .may file for OTTO MAYER / Burmay Optimizer panel saws.\n\nThe .may format uses ISO-8859-1 encoding with §§ delimiters and includes:\n- MA000: Job header\n- MA010: Part definitions with dimensions and quantities\n- MA200: Layout headers with stock dimensions and trims\n- MA300/310/320/330: Strip and piece placement records\n- MA999: Footer\n\nRequires guillotine cut type for accurate strip detection.\n\nEmitter settings (units, currency) 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 .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 (Beta)",
        "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/saws": {
      "get": {
        "tags": [
          "Exports"
        ],
        "summary": "List saws + export formats offered",
        "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/spec": {
      "get": {
        "tags": [
          "Exports"
        ],
        "summary": "Optimisation spec — selectable models + saws + their gating",
        "description": "The machine-readable catalogue of the v3 optimisation surface: the selectable `model`s (with\ntheir declarative gating — which cut types / saw profile / part limit each needs), the saw\ncatalogue (id, vendor, cutType, capabilities + formats), and the cut-type vocabulary. Read this\nat runtime to drive pickers + client-side validation instead of hard-coding the options — the\nserver gate and these docs are generated from the SAME source (`buildOptimisationSpec`), so it\nnever drifts. Public + cacheable (max-age 300s).",
        "responses": {
          "200": {
            "description": "The optimisation spec (models, saws, cut types)",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "version": {
                      "type": "integer",
                      "enum": [
                        3
                      ]
                    },
                    "defaultModel": {
                      "type": "string",
                      "description": "Model id used when none is requested"
                    },
                    "models": {
                      "type": "array",
                      "description": "Selectable models + their declarative `requires` gating",
                      "items": {
                        "type": "object"
                      }
                    },
                    "saws": {
                      "type": "array",
                      "description": "Saw catalogue (id, vendor, cutType, capabilities, formats)",
                      "items": {
                        "type": "object"
                      }
                    },
                    "cutTypes": {
                      "type": "array",
                      "items": {
                        "type": "string"
                      }
                    }
                  }
                }
              }
            }
          }
        }
      }
    },
    "/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-only, needs a recognised saw profile, ≤500 parts) |\n| `rectangle-guillotine-max-2` | ~150s | The Max to reach for first (beam-only, needs a recognised saw profile, ≤500 parts) |\n| `rectangle-guillotine-max-3` | ~420s | Batch or overnight, where board cost dominates (beam-only, needs a recognised 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 is EXPERIMENTAL — a trade, not an upgrade:** it spends the budget above (a per-job ceiling, not a guarantee) hunting a better layout, and on many jobs returns the same layout as Fast — in our published showcase the rectangle tiers tie Fast on half the jobs and save one board on the rest.\n\n**A Max tier costs you more.** Compute time is billed per millisecond, so naming one bills whatever of its budget the job actually spends — a `~30s` tier can cost many times a Fast run of the same job, and a `~420s` tier far more again. Pick the cheapest tier that pays for itself against your material.\n\n**Nesting (`saw.cutType: \"nesting\"`) has its own tiers.** They pack the parts tighter rather than dropping boards: measured across our nesting benchmark, `irregular-nesting-max-1` (+5s) closes roughly 10 density points of the gap to the best packing we can find, and `irregular-nesting-max-2` (+30s) adds a little more on jobs above ~50 parts. 30s is the ceiling we publish — past it the curve is flat and you would be paying for compute that buys almost nothing. The nesting budget is ADDITIONAL SEARCH on top of the base solve, not a total-runtime cap — a search round in flight always finishes, so a large sheet takes as long as one complete solve plus roughly the budget (an 80-part job measured ~20s on the +5s tier).\n\nEach Max tier is gated — see its row above. A request that misses a requirement safely runs `rectangle-guillotine-fast` instead — naming a nesting tier on a beam job, or a rectangle tier on a nesting job, is never an error. Full catalogue: `GET /v3/spec`."
          },
          "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`. PTX-only today; Biesse XML and Mayer .MAY to follow. Schema may change; see `/v3/export/ptx` for the current PTX column mapping.",
            "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 triple (`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. OptiPlanner, Pratika). Paired with `vendor` to resolve the saw profile."
          },
          "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"
          },
          "algorithm": {
            "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"
            ],
            "description": "EXPERIMENTAL — default optimisation model for jobs on this saw. Same values as the request-level `model`, which overrides it per job. Absent → `rectangle-guillotine-fast`. Naming a Max model here opts this saw in, subject to that model's own requirements (see `model`); otherwise the job runs Fast."
          },
          "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. guillotineOptions.maxPhase 1 (2-stage cutting) is not supported for beam saws — normalised to 2 (three-stage); 0 = unlimited"
              },
              "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. The two combine: with both set, the derived cap is tightened by maxStripWidth."
              },
              "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. Independent of limitStripDimensions and verticalPanelSaw: set this alone and it applies. 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": "Maximum stack height for beam saw"
          },
          "options": {
            "type": "object",
            "description": "Additional saw options",
            "properties": {
              "stockSelection": {
                "type": "string",
                "enum": [
                  "efficiency",
                  "smallest"
                ],
                "default": "efficiency",
                "description": "Stock selection method"
              },
              "stackingMode": {
                "type": "string",
                "enum": [
                  "dimensions",
                  "identical",
                  "none"
                ],
                "default": "identical",
                "description": "Part stacking mode"
              },
              "minSpacing": {
                "type": "number",
                "minimum": 0,
                "default": 0,
                "description": "Minimum spacing between parts"
              },
              "minSpacingDimension": {
                "type": "object",
                "description": "Dimension-specific minimum spacing for smaller parts",
                "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",
            "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": {
            "type": "boolean",
            "description": "Allow parts that exactly fit the stock dimensions even when trim is applied",
            "default": false
          }
        },
        "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). PTX-only today; Biesse XML and Mayer .MAY to follow. Schema may change; see `/v3/export/ptx` for the current PTX column mapping.",
            "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": {
            "type": "object",
            "nullable": true,
            "description": "Machining operations (holes, hingeHoles, corners)",
            "properties": {
              "holes": {
                "type": "array",
                "items": {
                  "type": "object",
                  "properties": {
                    "x": {
                      "type": "number",
                      "description": "X coordinate from left edge"
                    },
                    "y": {
                      "type": "number",
                      "description": "Y coordinate from bottom edge"
                    },
                    "diameter": {
                      "type": "number",
                      "minimum": 0.1,
                      "description": "Hole diameter in mm"
                    },
                    "depth": {
                      "type": "number",
                      "minimum": 0,
                      "description": "Hole depth (omit for through hole)"
                    },
                    "face": {
                      "type": "number",
                      "enum": [
                        0,
                        1
                      ],
                      "default": 0,
                      "description": "Face index (0=front, 1=back)"
                    },
                    "type": {
                      "type": "string",
                      "enum": [
                        "regular",
                        "hinge",
                        "shelf"
                      ],
                      "default": "regular",
                      "description": "Hole type"
                    }
                  },
                  "required": [
                    "x",
                    "y",
                    "diameter"
                  ]
                },
                "description": "Array of 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": "Which side to place hinge holes"
                    },
                    "face": {
                      "type": "number",
                      "enum": [
                        0,
                        1
                      ],
                      "default": 0,
                      "description": "Face index (0=front, 1=back)"
                    },
                    "numHoles": {
                      "type": "number",
                      "minimum": 1,
                      "description": "Number of holes in the hinge"
                    },
                    "diameter": {
                      "type": "number",
                      "minimum": 0.1,
                      "description": "Hole diameter in mm"
                    },
                    "depth": {
                      "type": "number",
                      "minimum": 0,
                      "description": "Hole depth in mm"
                    },
                    "hingeLength": {
                      "type": "number",
                      "minimum": 0,
                      "description": "Total hinge length"
                    },
                    "distanceFromEdge": {
                      "type": "number",
                      "minimum": 0,
                      "description": "Distance from perpendicular edge"
                    },
                    "outerSpacing": {
                      "type": "number",
                      "minimum": 0,
                      "description": "Spacing between outer holes"
                    }
                  },
                  "required": [
                    "position",
                    "side",
                    "numHoles",
                    "diameter",
                    "depth",
                    "hingeLength",
                    "distanceFromEdge",
                    "outerSpacing"
                  ]
                },
                "description": "Array of hinge hole sets"
              },
              "corners": {
                "type": "object",
                "description": "Corner operations mapped by location (side.a = bottom-left, side.b = top-left, side.c = top-right, side.d = bottom-right)",
                "properties": {
                  "side.a": {
                    "type": "object",
                    "properties": {
                      "type": {
                        "type": "string",
                        "enum": [
                          "bevel",
                          "radius"
                        ],
                        "nullable": true,
                        "description": "Corner modification type"
                      },
                      "size": {
                        "type": "number",
                        "minimum": 0,
                        "description": "Corner size in mm"
                      }
                    }
                  },
                  "side.b": {
                    "type": "object",
                    "properties": {
                      "type": {
                        "type": "string",
                        "enum": [
                          "bevel",
                          "radius"
                        ],
                        "nullable": true,
                        "description": "Corner modification type"
                      },
                      "size": {
                        "type": "number",
                        "minimum": 0,
                        "description": "Corner size in mm"
                      }
                    }
                  },
                  "side.c": {
                    "type": "object",
                    "properties": {
                      "type": {
                        "type": "string",
                        "enum": [
                          "bevel",
                          "radius"
                        ],
                        "nullable": true,
                        "description": "Corner modification type"
                      },
                      "size": {
                        "type": "number",
                        "minimum": 0,
                        "description": "Corner size in mm"
                      }
                    }
                  },
                  "side.d": {
                    "type": "object",
                    "properties": {
                      "type": {
                        "type": "string",
                        "enum": [
                          "bevel",
                          "radius"
                        ],
                        "nullable": true,
                        "description": "Corner modification type"
                      },
                      "size": {
                        "type": "number",
                        "minimum": 0,
                        "description": "Corner size in mm"
                      }
                    }
                  }
                },
                "additionalProperties": false
              }
            }
          },
          "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"
          }
        }
      },
      "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",
        "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"
          },
          "cuts": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/Cut"
            },
            "description": "Cut instructions"
          },
          "offcuts": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/Offcut"
            },
            "description": "Remaining offcut pieces"
          },
          "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": {
            "type": "object",
            "description": "Detailed analysis metrics for this stock item"
          },
          "stack": {
            "type": "number",
            "description": "Stack number"
          },
          "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": {
            "type": "object",
            "nullable": true,
            "description": "Machining operations (holes, hingeHoles, corners)"
          },
          "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"
              },
              "code": {
                "type": "string",
                "description": "Product/material code of the stock (if set)"
              }
            }
          }
        }
      },
      "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": {
            "type": "object",
            "nullable": true,
            "description": "Machining operations (holes, hingeHoles, corners)"
          },
          "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": "Cut order for guillotine cuts"
          },
          "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": "Unique identifier for the offcut"
          },
          "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)"
          },
          "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
          },
          "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
          },
          "totalCutLength": {
            "type": "number",
            "default": 0
          },
          "totalStackedCutLength": {
            "type": "number",
            "default": 0
          },
          "totalCuts": {
            "type": "number",
            "default": 0
          },
          "totalStackedCuts": {
            "type": "number",
            "default": 0
          },
          "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."
          }
        }
      },
      "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 results in various formats (PDF, CSV, etc.)"
    },
    {
      "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."
    }
  ]
}