{
  "openapi": "3.1.0",
  "info": {
    "title": "CutOptim Engine API",
    "version": "1",
    "summary": "Deterministic 2D panel and 1D linear cutting optimization over HTTP.",
    "description": "The cutting-optimization engine behind CutOptim, callable directly. Send parts and stock,\nget back the full layout, the guillotine cut plan, offcuts and yield.\n\n## Units\n\nThe API is **unit-agnostic**. Whatever unit you send is the unit you get back. Every length\nin the request — part `w`/`h`/`length`, stock `w`/`h`/`length`, `kerf`, `tolerance`,\n`trim.*`, `minOffcut` — and every length in the response — `x`, `y`, `w`, `h`, `pos`,\n`remaining`, `offcuts`, `metrics.cutLength`, `cutPlan[].pos`, `cutPlan[].length` — are in\nthat one unit. Nothing is converted server-side, and no field name asserts a unit. Mixing\nunits within one request produces a valid-looking but physically wrong plan; the server\ncannot detect it.\n\n## Coordinate system (2D)\n\nThe origin is the **top-left corner of the sheet**. `x` grows to the right along the sheet\nwidth `w`; `y` grows downward along the sheet height `h`. A part's `x`/`y` is its top-left\ncorner, and `w`/`h` are its placed dimensions — already swapped when `rotated` is `true`, so\nthe rectangle `x, y, w, h` is always the footprint on the board with no further maths.\n`options.trim.left` shifts every placement right; `options.trim.top` shifts every placement\ndown. `trim.right` / `trim.bottom` shrink the usable area without moving the origin. Sheet\n`w`/`h` are always the FULL stock dimensions, trim included.\n\n## Determinism\n\nThe algorithms contain no randomness and read no clock, so the same request always returns\nthe same response. That makes results cacheable — and it makes any algorithm improvement a\nbreaking change for a caching client, which is why every response carries `engineVersion`.\nPin behaviour by sending `engine` explicitly and watching `engineVersion`; each engine\nversions independently. The `/v1/` path segment moves only if a response SHAPE changes.\n\n## Authentication\n\nAll endpoints except `GET /v1/health` require `Authorization: Bearer <key>`. Keys are\ncreated in the CutOptim dashboard. The quota is a hard monthly cap enforced server-side by a\nsingle atomic database statement, so exceeding it is structurally impossible and there is no\noverage bill.",
    "contact": {
      "name": "CutOptim",
      "url": "https://cutoptim.com/contact"
    },
    "termsOfService": "https://cutoptim.com/terms"
  },
  "externalDocs": {
    "description": "Human-readable API reference",
    "url": "https://cutoptim.com/engine/reference"
  },
  "servers": [
    {
      "url": "https://api.cutoptim.com",
      "description": "Production"
    }
  ],
  "security": [
    {
      "bearerAuth": []
    }
  ],
  "tags": [
    {
      "name": "optimize",
      "description": "Run an optimization."
    },
    {
      "name": "account",
      "description": "Quota and liveness."
    }
  ],
  "paths": {
    "/v1/optimize/2d": {
      "post": {
        "operationId": "optimize2d",
        "tags": [
          "optimize"
        ],
        "summary": "2D panel optimization",
        "description": "Nest rectangular parts into rectangular sheets. Returns the placement of every part, the remaining offcuts, the guillotine cut plan and the yield metrics.",
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/Optimize2DRequest"
              },
              "example": {
                "parts": [
                  {
                    "name": "Door",
                    "w": 600,
                    "h": 400,
                    "qty": 4,
                    "rotatable": true
                  }
                ],
                "stock": [
                  {
                    "w": 2440,
                    "h": 1220,
                    "qty": 10,
                    "price": 42
                  }
                ],
                "options": {
                  "kerf": 3,
                  "trim": {
                    "left": 10,
                    "top": 10
                  }
                },
                "engine": "heuristic",
                "include": [
                  "cutPlan",
                  "offcuts"
                ]
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "A plan. Note that an UNSATISFIABLE job is still a 200: parts that did not fit are listed in `unplaced` with a note in `warnings`, because a plan you can act on beats a status code.",
            "headers": {
              "X-RateLimit-Limit": {
                "description": "The key's monthly quota (api_keys.monthly_quota, frozen when the key was minted).",
                "schema": {
                  "type": "integer"
                }
              },
              "X-RateLimit-Remaining": {
                "description": "Calls left in the current UTC calendar month AFTER this one. Sent only on a successful optimize — the counter is reserved as part of authorizing the request.",
                "schema": {
                  "type": "integer",
                  "minimum": 0
                }
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Optimize2DResponse"
                },
                "example": {
                  "engine": "heuristic",
                  "engineVersion": "1.0.0+10e0c941",
                  "contractVersion": "1",
                  "deterministic": true,
                  "sheets": [
                    {
                      "w": 2440,
                      "h": 1220,
                      "price": 42,
                      "parts": [
                        {
                          "name": "Door",
                          "x": 10,
                          "y": 10,
                          "w": 400,
                          "h": 600,
                          "rotated": true
                        },
                        {
                          "name": "Door",
                          "x": 413,
                          "y": 10,
                          "w": 400,
                          "h": 600,
                          "rotated": true
                        },
                        {
                          "name": "Door",
                          "x": 816,
                          "y": 10,
                          "w": 400,
                          "h": 600,
                          "rotated": true
                        },
                        {
                          "name": "Door",
                          "x": 1219,
                          "y": 10,
                          "w": 400,
                          "h": 600,
                          "rotated": true
                        }
                      ],
                      "offcuts": [
                        {
                          "x": 1622,
                          "y": 10,
                          "w": 818,
                          "h": 600
                        },
                        {
                          "x": 10,
                          "y": 613,
                          "w": 2430,
                          "h": 607
                        }
                      ]
                    }
                  ],
                  "metrics": {
                    "sheetCount": 1,
                    "yieldPct": 32.25,
                    "placed": 4,
                    "total": 4,
                    "cutLines": 5,
                    "sawPasses": 5,
                    "cutLength": 4830,
                    "totalPrice": 42
                  },
                  "unplaced": [],
                  "warnings": [],
                  "guillotineValid": true,
                  "timing": {
                    "solveMs": 3.13
                  },
                  "cutPlan": [
                    {
                      "sheet": 0,
                      "step": 1,
                      "axis": "h",
                      "pos": 610,
                      "length": 2430,
                      "stage": 1
                    },
                    {
                      "sheet": 0,
                      "step": 2,
                      "axis": "v",
                      "pos": 410,
                      "length": 600,
                      "stage": 2
                    },
                    {
                      "sheet": 0,
                      "step": 3,
                      "axis": "v",
                      "pos": 813,
                      "length": 600,
                      "stage": 2
                    },
                    {
                      "sheet": 0,
                      "step": 4,
                      "axis": "v",
                      "pos": 1216,
                      "length": 600,
                      "stage": 2
                    },
                    {
                      "sheet": 0,
                      "step": 5,
                      "axis": "v",
                      "pos": 1619,
                      "length": 600,
                      "stage": 2
                    }
                  ]
                }
              }
            }
          },
          "400": {
            "description": "Schema error. `details.path` names the offending field. Validation runs BEFORE the quota is reserved, so a rejected request never costs a call.",
            "content": {
              "application/json": {
                "schema": {
                  "allOf": [
                    {
                      "$ref": "#/components/schemas/Error"
                    },
                    {
                      "properties": {
                        "error": {
                          "enum": [
                            "invalid_request"
                          ]
                        }
                      }
                    }
                  ]
                },
                "example": {
                  "error": "invalid_request",
                  "message": "parts[0].h: must be greater than 0",
                  "details": {
                    "path": "parts[0].h"
                  }
                }
              }
            }
          },
          "401": {
            "description": "Missing or unknown API key.",
            "content": {
              "application/json": {
                "schema": {
                  "allOf": [
                    {
                      "$ref": "#/components/schemas/Error"
                    },
                    {
                      "properties": {
                        "error": {
                          "enum": [
                            "unauthorized"
                          ]
                        }
                      }
                    }
                  ]
                },
                "example": {
                  "error": "unauthorized",
                  "message": "missing API key"
                }
              }
            }
          },
          "402": {
            "description": "The monthly quota is exhausted. Nothing was solved and nothing was charged. The cap is hard — there is no overage billing.",
            "headers": {
              "Retry-After": {
                "description": "Seconds until the quota resets — i.e. until 00:00 UTC on the 1st of next month.",
                "schema": {
                  "type": "integer",
                  "minimum": 1
                }
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "allOf": [
                    {
                      "$ref": "#/components/schemas/Error"
                    },
                    {
                      "properties": {
                        "error": {
                          "enum": [
                            "quota_exceeded"
                          ]
                        }
                      }
                    }
                  ]
                },
                "example": {
                  "error": "quota_exceeded",
                  "message": "monthly quota exhausted"
                }
              }
            }
          },
          "403": {
            "description": "The key exists but has been revoked.",
            "content": {
              "application/json": {
                "schema": {
                  "allOf": [
                    {
                      "$ref": "#/components/schemas/Error"
                    },
                    {
                      "properties": {
                        "error": {
                          "enum": [
                            "key_revoked"
                          ]
                        }
                      }
                    }
                  ]
                },
                "example": {
                  "error": "key_revoked",
                  "message": "this API key has been revoked"
                }
              }
            }
          },
          "404": {
            "description": "No such route. Also returned when the path is right but the method is wrong — these two endpoints are POST-only and a GET is not answered with 405. Note the different envelope: `error` is `not_found`, which is not one of the `Error` codes.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/NotFoundError"
                },
                "example": {
                  "error": "not_found",
                  "message": "no route for GET /v1/optimize/2d"
                }
              }
            }
          },
          "413": {
            "description": "An input cap was exceeded: more than 2000 parts (total quantity, counted AFTER qty expansion), more than 50 stock rows, more than 5000 total stock quantity, or a request body over 1000000 bytes.",
            "content": {
              "application/json": {
                "schema": {
                  "allOf": [
                    {
                      "$ref": "#/components/schemas/Error"
                    },
                    {
                      "properties": {
                        "error": {
                          "enum": [
                            "too_large"
                          ]
                        }
                      }
                    }
                  ]
                },
                "example": {
                  "error": "too_large",
                  "message": "parts total quantity 3000 exceeds 2000",
                  "details": {
                    "path": "parts",
                    "max": 2000,
                    "got": 3000
                  }
                }
              }
            }
          },
          "429": {
            "description": "The engine is momentarily at capacity (solves are serialised). The reserved call is refunded, so a busy signal never consumes quota. Retry shortly.",
            "headers": {
              "Retry-After": {
                "description": "Seconds to wait before retrying.",
                "schema": {
                  "type": "integer",
                  "minimum": 1
                }
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "allOf": [
                    {
                      "$ref": "#/components/schemas/Error"
                    },
                    {
                      "properties": {
                        "error": {
                          "enum": [
                            "busy"
                          ]
                        }
                      }
                    }
                  ]
                },
                "example": {
                  "error": "busy",
                  "message": "the engine is at capacity; retry shortly"
                }
              }
            }
          },
          "500": {
            "description": "Unexpected server error, or the authorization backend is unreachable. Authorization fails CLOSED: if the key/quota store cannot be read the request is refused rather than served free.",
            "content": {
              "application/json": {
                "schema": {
                  "allOf": [
                    {
                      "$ref": "#/components/schemas/Error"
                    },
                    {
                      "properties": {
                        "error": {
                          "enum": [
                            "internal"
                          ]
                        }
                      }
                    }
                  ]
                },
                "example": {
                  "error": "internal",
                  "message": "authorization failed"
                }
              }
            }
          },
          "504": {
            "description": "The solve exceeded the hard wall-clock bound. This status is produced by the reverse proxy, not by the engine, so the body is the proxy's and is NOT the JSON error envelope. It should be unreachable within the documented input caps."
          }
        }
      }
    },
    "/v1/optimize/1d": {
      "post": {
        "operationId": "optimize1d",
        "tags": [
          "optimize"
        ],
        "summary": "1D / linear optimization (bars, profiles, tube)",
        "description": "Cut parts of a given length from stock bars. Returns the rods used, the position of every part along each rod, the leftover length and the yield metrics.",
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/Optimize1DRequest"
              },
              "example": {
                "parts": [
                  {
                    "name": "Rail",
                    "length": 1200,
                    "qty": 6
                  }
                ],
                "stock": [
                  {
                    "length": 3000,
                    "qty": 5,
                    "price": 12.5
                  }
                ],
                "options": {
                  "kerf": 3,
                  "trim": {
                    "start": 10,
                    "end": 0
                  }
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "A plan. `rods` replaces `sheets`, and there is no `guillotineValid` — a linear cut is always producible.",
            "headers": {
              "X-RateLimit-Limit": {
                "description": "The key's monthly quota (api_keys.monthly_quota, frozen when the key was minted).",
                "schema": {
                  "type": "integer"
                }
              },
              "X-RateLimit-Remaining": {
                "description": "Calls left in the current UTC calendar month AFTER this one. Sent only on a successful optimize — the counter is reserved as part of authorizing the request.",
                "schema": {
                  "type": "integer",
                  "minimum": 0
                }
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Optimize1DResponse"
                },
                "example": {
                  "engine": "heuristic",
                  "engineVersion": "1.0.0+10e0c941",
                  "contractVersion": "1",
                  "deterministic": true,
                  "rods": [
                    {
                      "length": 3000,
                      "price": 12.5,
                      "remaining": 587,
                      "parts": [
                        {
                          "name": "Rail",
                          "pos": 10,
                          "length": 1200
                        },
                        {
                          "name": "Rail",
                          "pos": 1213,
                          "length": 1200
                        }
                      ],
                      "offcuts": [
                        587
                      ]
                    },
                    {
                      "length": 3000,
                      "price": 12.5,
                      "remaining": 587,
                      "parts": [
                        {
                          "name": "Rail",
                          "pos": 10,
                          "length": 1200
                        },
                        {
                          "name": "Rail",
                          "pos": 1213,
                          "length": 1200
                        }
                      ],
                      "offcuts": [
                        587
                      ]
                    },
                    {
                      "length": 3000,
                      "price": 12.5,
                      "remaining": 587,
                      "parts": [
                        {
                          "name": "Rail",
                          "pos": 10,
                          "length": 1200
                        },
                        {
                          "name": "Rail",
                          "pos": 1213,
                          "length": 1200
                        }
                      ],
                      "offcuts": [
                        587
                      ]
                    }
                  ],
                  "metrics": {
                    "rodCount": 3,
                    "yieldPct": 80,
                    "placed": 6,
                    "total": 6,
                    "cuts": 6,
                    "totalPrice": 37.5,
                    "toleranceAcceptedCount": 0
                  },
                  "unplaced": [],
                  "warnings": [],
                  "timing": {
                    "solveMs": 2.19
                  },
                  "cutPlan": [
                    {
                      "sheet": 0,
                      "step": 1,
                      "axis": "v",
                      "pos": 1210,
                      "length": 0,
                      "stage": 1
                    },
                    {
                      "sheet": 0,
                      "step": 2,
                      "axis": "v",
                      "pos": 2413,
                      "length": 0,
                      "stage": 1
                    },
                    {
                      "sheet": 1,
                      "step": 1,
                      "axis": "v",
                      "pos": 1210,
                      "length": 0,
                      "stage": 1
                    },
                    {
                      "sheet": 1,
                      "step": 2,
                      "axis": "v",
                      "pos": 2413,
                      "length": 0,
                      "stage": 1
                    },
                    {
                      "sheet": 2,
                      "step": 1,
                      "axis": "v",
                      "pos": 1210,
                      "length": 0,
                      "stage": 1
                    },
                    {
                      "sheet": 2,
                      "step": 2,
                      "axis": "v",
                      "pos": 2413,
                      "length": 0,
                      "stage": 1
                    }
                  ]
                }
              }
            }
          },
          "400": {
            "description": "Schema error. `details.path` names the offending field. Validation runs BEFORE the quota is reserved, so a rejected request never costs a call.",
            "content": {
              "application/json": {
                "schema": {
                  "allOf": [
                    {
                      "$ref": "#/components/schemas/Error"
                    },
                    {
                      "properties": {
                        "error": {
                          "enum": [
                            "invalid_request"
                          ]
                        }
                      }
                    }
                  ]
                },
                "example": {
                  "error": "invalid_request",
                  "message": "parts[0].h: must be greater than 0",
                  "details": {
                    "path": "parts[0].h"
                  }
                }
              }
            }
          },
          "401": {
            "description": "Missing or unknown API key.",
            "content": {
              "application/json": {
                "schema": {
                  "allOf": [
                    {
                      "$ref": "#/components/schemas/Error"
                    },
                    {
                      "properties": {
                        "error": {
                          "enum": [
                            "unauthorized"
                          ]
                        }
                      }
                    }
                  ]
                },
                "example": {
                  "error": "unauthorized",
                  "message": "missing API key"
                }
              }
            }
          },
          "402": {
            "description": "The monthly quota is exhausted. Nothing was solved and nothing was charged. The cap is hard — there is no overage billing.",
            "headers": {
              "Retry-After": {
                "description": "Seconds until the quota resets — i.e. until 00:00 UTC on the 1st of next month.",
                "schema": {
                  "type": "integer",
                  "minimum": 1
                }
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "allOf": [
                    {
                      "$ref": "#/components/schemas/Error"
                    },
                    {
                      "properties": {
                        "error": {
                          "enum": [
                            "quota_exceeded"
                          ]
                        }
                      }
                    }
                  ]
                },
                "example": {
                  "error": "quota_exceeded",
                  "message": "monthly quota exhausted"
                }
              }
            }
          },
          "403": {
            "description": "The key exists but has been revoked.",
            "content": {
              "application/json": {
                "schema": {
                  "allOf": [
                    {
                      "$ref": "#/components/schemas/Error"
                    },
                    {
                      "properties": {
                        "error": {
                          "enum": [
                            "key_revoked"
                          ]
                        }
                      }
                    }
                  ]
                },
                "example": {
                  "error": "key_revoked",
                  "message": "this API key has been revoked"
                }
              }
            }
          },
          "404": {
            "description": "No such route. Also returned when the path is right but the method is wrong — these two endpoints are POST-only and a GET is not answered with 405. Note the different envelope: `error` is `not_found`, which is not one of the `Error` codes.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/NotFoundError"
                },
                "example": {
                  "error": "not_found",
                  "message": "no route for GET /v1/optimize/2d"
                }
              }
            }
          },
          "413": {
            "description": "An input cap was exceeded: more than 2000 parts (total quantity, counted AFTER qty expansion), more than 50 stock rows, more than 5000 total stock quantity, or a request body over 1000000 bytes.",
            "content": {
              "application/json": {
                "schema": {
                  "allOf": [
                    {
                      "$ref": "#/components/schemas/Error"
                    },
                    {
                      "properties": {
                        "error": {
                          "enum": [
                            "too_large"
                          ]
                        }
                      }
                    }
                  ]
                },
                "example": {
                  "error": "too_large",
                  "message": "parts total quantity 3000 exceeds 2000",
                  "details": {
                    "path": "parts",
                    "max": 2000,
                    "got": 3000
                  }
                }
              }
            }
          },
          "429": {
            "description": "The engine is momentarily at capacity (solves are serialised). The reserved call is refunded, so a busy signal never consumes quota. Retry shortly.",
            "headers": {
              "Retry-After": {
                "description": "Seconds to wait before retrying.",
                "schema": {
                  "type": "integer",
                  "minimum": 1
                }
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "allOf": [
                    {
                      "$ref": "#/components/schemas/Error"
                    },
                    {
                      "properties": {
                        "error": {
                          "enum": [
                            "busy"
                          ]
                        }
                      }
                    }
                  ]
                },
                "example": {
                  "error": "busy",
                  "message": "the engine is at capacity; retry shortly"
                }
              }
            }
          },
          "500": {
            "description": "Unexpected server error, or the authorization backend is unreachable. Authorization fails CLOSED: if the key/quota store cannot be read the request is refused rather than served free.",
            "content": {
              "application/json": {
                "schema": {
                  "allOf": [
                    {
                      "$ref": "#/components/schemas/Error"
                    },
                    {
                      "properties": {
                        "error": {
                          "enum": [
                            "internal"
                          ]
                        }
                      }
                    }
                  ]
                },
                "example": {
                  "error": "internal",
                  "message": "authorization failed"
                }
              }
            }
          },
          "504": {
            "description": "The solve exceeded the hard wall-clock bound. This status is produced by the reverse proxy, not by the engine, so the body is the proxy's and is NOT the JSON error envelope. It should be unreachable within the documented input caps."
          }
        }
      }
    },
    "/v1/usage": {
      "get": {
        "operationId": "getUsage",
        "tags": [
          "account"
        ],
        "summary": "The calling key's current-month usage and quota",
        "description": "Read-only. Does not consume quota and does not send rate-limit headers. The period is the UTC calendar month.",
        "responses": {
          "200": {
            "description": "Current quota state for the key that made the call.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/UsageResponse"
                },
                "example": {
                  "plan": "studio",
                  "used": 137,
                  "limit": 10000,
                  "remaining": 9863,
                  "periodEnd": "2026-08-01",
                  "keyPrefix": "co_live_ab12",
                  "contractVersion": "1"
                }
              }
            }
          },
          "401": {
            "description": "Missing or unknown API key.",
            "content": {
              "application/json": {
                "schema": {
                  "allOf": [
                    {
                      "$ref": "#/components/schemas/Error"
                    },
                    {
                      "properties": {
                        "error": {
                          "enum": [
                            "unauthorized"
                          ]
                        }
                      }
                    }
                  ]
                },
                "example": {
                  "error": "unauthorized",
                  "message": "missing API key"
                }
              }
            }
          },
          "403": {
            "description": "The key exists but has been revoked.",
            "content": {
              "application/json": {
                "schema": {
                  "allOf": [
                    {
                      "$ref": "#/components/schemas/Error"
                    },
                    {
                      "properties": {
                        "error": {
                          "enum": [
                            "key_revoked"
                          ]
                        }
                      }
                    }
                  ]
                },
                "example": {
                  "error": "key_revoked",
                  "message": "this API key has been revoked"
                }
              }
            }
          },
          "404": {
            "description": "No such route. Also returned when the path is right but the method is wrong — these two endpoints are POST-only and a GET is not answered with 405. Note the different envelope: `error` is `not_found`, which is not one of the `Error` codes.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/NotFoundError"
                },
                "example": {
                  "error": "not_found",
                  "message": "no route for GET /v1/optimize/2d"
                }
              }
            }
          },
          "500": {
            "description": "The usage backend is unreachable.",
            "content": {
              "application/json": {
                "schema": {
                  "allOf": [
                    {
                      "$ref": "#/components/schemas/Error"
                    },
                    {
                      "properties": {
                        "error": {
                          "enum": [
                            "internal"
                          ]
                        }
                      }
                    }
                  ]
                },
                "example": {
                  "error": "internal",
                  "message": "usage backend unavailable"
                }
              }
            }
          }
        }
      }
    },
    "/v1/health": {
      "get": {
        "operationId": "getHealth",
        "tags": [
          "account"
        ],
        "summary": "Liveness",
        "description": "Open: no API key, no quota, no database. Deliberately touches nothing stateful, so a database outage cannot make the service look dead to an orchestrator.",
        "security": [],
        "responses": {
          "200": {
            "description": "The engine is up. `engines` lists the ids `engine` will accept.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/HealthResponse"
                },
                "example": {
                  "status": "healthy",
                  "service": "cutoptim-engine",
                  "contractVersion": "1",
                  "engineVersion": "1.0.0+10e0c941",
                  "engines": [
                    "heuristic",
                    "balanced"
                  ],
                  "uptimeSec": 16
                }
              }
            }
          }
        }
      }
    }
  },
  "components": {
    "securitySchemes": {
      "bearerAuth": {
        "type": "http",
        "scheme": "bearer",
        "description": "An API key from the CutOptim dashboard, sent as `Authorization: Bearer <key>`. Only the SHA-256 hash of the key is stored server-side, so a lost key cannot be recovered — mint a new one and revoke the old."
      }
    },
    "schemas": {
      "Error": {
        "type": "object",
        "description": "The error envelope for every failure the engine itself produces.",
        "required": [
          "error",
          "message"
        ],
        "properties": {
          "error": {
            "type": "string",
            "enum": [
              "invalid_request",
              "unauthorized",
              "quota_exceeded",
              "key_revoked",
              "too_large",
              "busy",
              "internal"
            ],
            "description": "Stable machine-readable code. Branch on this, never on `message`."
          },
          "message": {
            "type": "string",
            "description": "Human-readable explanation. Wording may change between releases."
          },
          "details": {
            "type": "object",
            "description": "Present on some errors. For `invalid_request` it carries `path`, the dotted/indexed location of the offending field (e.g. `parts[0].h`). For `too_large` it carries `path`, `max` and sometimes `got`.",
            "properties": {
              "path": {
                "type": "string"
              },
              "max": {
                "type": "integer"
              },
              "got": {
                "type": "integer"
              }
            }
          }
        }
      },
      "NotFoundError": {
        "type": "object",
        "description": "Routing failure. Uses a code (`not_found`) that is intentionally outside the `Error` enum, because it is produced by the router rather than by the API contract.",
        "required": [
          "error",
          "message"
        ],
        "properties": {
          "error": {
            "type": "string",
            "const": "not_found"
          },
          "message": {
            "type": "string"
          }
        }
      },
      "Part2D": {
        "type": "object",
        "required": [
          "w",
          "h"
        ],
        "properties": {
          "name": {
            "type": "string",
            "maxLength": 200,
            "description": "Optional label, echoed on every placement. Defaults to `Part <1-based row index>`."
          },
          "w": {
            "type": "number",
            "exclusiveMinimum": 0,
            "description": "Part width. Unit-agnostic: pick one unit (mm, inch, …) for every length in the request and every length in the response is in that same unit. Nothing is converted server-side."
          },
          "h": {
            "type": "number",
            "exclusiveMinimum": 0,
            "description": "Part height. Unit-agnostic: pick one unit (mm, inch, …) for every length in the request and every length in the response is in that same unit. Nothing is converted server-side."
          },
          "qty": {
            "type": "integer",
            "minimum": 1,
            "default": 1,
            "description": "How many identical copies. Expanded server-side into individual pieces; the sum across all rows must not exceed 2000."
          },
          "rotatable": {
            "type": "boolean",
            "default": true,
            "description": "May the part be turned 90°? Set false for grain-direction or print-direction parts."
          },
          "grainGroup": {
            "type": "string",
            "maxLength": 200,
            "description": "Parts sharing a group are kept on one sheet (grain matching). If a group is too large for one sheet the members are split and a string in `warnings` names the group."
          },
          "priority": {
            "type": "boolean",
            "description": "Must-cut. Wins board space over non-priority parts when stock is capped — only meaningful together with `options.respectStock`."
          }
        }
      },
      "Stock2D": {
        "type": "object",
        "required": [
          "w",
          "h"
        ],
        "properties": {
          "w": {
            "type": "number",
            "exclusiveMinimum": 0,
            "description": "Sheet width. Unit-agnostic: pick one unit (mm, inch, …) for every length in the request and every length in the response is in that same unit. Nothing is converted server-side."
          },
          "h": {
            "type": "number",
            "exclusiveMinimum": 0,
            "description": "Sheet height. Unit-agnostic: pick one unit (mm, inch, …) for every length in the request and every length in the response is in that same unit. Nothing is converted server-side."
          },
          "qty": {
            "type": "integer",
            "minimum": 1,
            "default": 1,
            "description": "How many sheets of this format are available. A HARD cap only when `options.respectStock` is true; otherwise the format is treated as unlimited. The sum across all rows must not exceed 5000."
          },
          "price": {
            "type": "number",
            "minimum": 0,
            "description": "Price per sheet. Drives `metrics.totalPrice` and `options.minimizeCost`."
          }
        }
      },
      "Trim2D": {
        "type": "object",
        "description": "Per-side edge trim removed before nesting. Asymmetric on purpose: a damaged long edge may need more than the others, and a symmetric trim would then give a wrong plan, not merely a worse one. `left` shifts placements right, `top` shifts them down.",
        "properties": {
          "left": {
            "type": "number",
            "minimum": 0,
            "description": "Unit-agnostic: pick one unit (mm, inch, …) for every length in the request and every length in the response is in that same unit. Nothing is converted server-side."
          },
          "right": {
            "type": "number",
            "minimum": 0,
            "description": "Unit-agnostic: pick one unit (mm, inch, …) for every length in the request and every length in the response is in that same unit. Nothing is converted server-side."
          },
          "top": {
            "type": "number",
            "minimum": 0,
            "description": "Unit-agnostic: pick one unit (mm, inch, …) for every length in the request and every length in the response is in that same unit. Nothing is converted server-side."
          },
          "bottom": {
            "type": "number",
            "minimum": 0,
            "description": "Unit-agnostic: pick one unit (mm, inch, …) for every length in the request and every length in the response is in that same unit. Nothing is converted server-side."
          }
        }
      },
      "Options2D": {
        "type": "object",
        "properties": {
          "kerf": {
            "type": "number",
            "minimum": 0,
            "description": "Blade width. Unit-agnostic: pick one unit (mm, inch, …) for every length in the request and every length in the response is in that same unit. Nothing is converted server-side.",
            "default": 0
          },
          "tolerance": {
            "type": "number",
            "minimum": 0,
            "description": "Accept a cut that overshoots by up to this much, i.e. allow parts to encroach on the kerf. Turns \"two 501 mm parts do not fit a 1000 mm board\" into one board.",
            "default": 0
          },
          "trim": {
            "$ref": "#/components/schemas/Trim2D"
          },
          "firstCut": {
            "type": "string",
            "enum": [
              "auto",
              "horizontal",
              "vertical"
            ],
            "default": "auto",
            "description": "Force the orientation of the first cut across the board. `auto` searches both and also enables two extra strategy families that are not direction-aware."
          },
          "minimizeCost": {
            "type": "boolean",
            "default": false,
            "description": "Rank candidate layouts by total stock price instead of sheet count. Needs `price` on the stock rows to mean anything."
          },
          "respectStock": {
            "type": "boolean",
            "default": false,
            "description": "Treat each stock row's `qty` as a hard cap. Parts that then do not fit come back in `unplaced` instead of on an extra invented sheet."
          },
          "minOffcut": {
            "type": "number",
            "minimum": 0,
            "description": "Output filter only: report a leftover rectangle only if its SHORT side is at least this. Does not change the layout.",
            "default": 0
          },
          "maxCutStages": {
            "type": [
              "integer",
              "null"
            ],
            "minimum": 1,
            "description": "Panel-saw stage limit — a count of machine PHASES (axis changes), not raw cut-tree depth. A real kitchen job is commonly 9 cuts deep but only 3 stages. An unsatisfiable limit still returns the best plan plus a string in `warnings`; it is never an error."
          },
          "minimizeRotations": {
            "type": "boolean",
            "default": false,
            "description": "Prefer layouts that turn fewer parts, at a possible small yield cost."
          }
        }
      },
      "Optimize2DRequest": {
        "type": "object",
        "required": [
          "parts",
          "stock"
        ],
        "properties": {
          "parts": {
            "type": "array",
            "minItems": 1,
            "items": {
              "$ref": "#/components/schemas/Part2D"
            }
          },
          "stock": {
            "type": "array",
            "minItems": 1,
            "maxItems": 50,
            "items": {
              "$ref": "#/components/schemas/Stock2D"
            }
          },
          "options": {
            "$ref": "#/components/schemas/Options2D"
          },
          "engine": {
            "type": "string",
            "enum": [
              "heuristic",
              "balanced"
            ],
            "default": "heuristic",
            "description": "Which optimizer to run. `heuristic` is the guillotine multi-strategy packer: highest yield, every layout edge-to-edge saw-cuttable, always a full `cutPlan`. `balanced` is a MaxRects free-nesting packer: much faster on large jobs for a small yield cost, but its layouts are often not guillotine (`guillotineValid: false`, `cutPlan: null`), and it does not model `tolerance`, `minimizeCost`, `grainGroup`, `maxCutStages` or `minimizeRotations` — set one of those and a string in `warnings` tells you it was ignored. The default never changes silently."
          },
          "include": {
            "type": "array",
            "items": {
              "type": "string",
              "enum": [
                "cutPlan",
                "offcuts"
              ]
            },
            "description": "Narrows the response. Omit the field entirely to get both. Listing only one drops the other from the payload (`cutPlan` is absent, `sheets[].offcuts` / `rods[].offcuts` are absent). An empty array drops both."
          }
        }
      },
      "Part1D": {
        "type": "object",
        "required": [
          "length"
        ],
        "description": "A linear part. There is no `rotatable` or `grainGroup` in 1D.",
        "properties": {
          "name": {
            "type": "string",
            "maxLength": 200,
            "description": "Optional label. Defaults to `Part <1-based row index>`."
          },
          "length": {
            "type": "number",
            "exclusiveMinimum": 0,
            "description": "Cut length. Unit-agnostic: pick one unit (mm, inch, …) for every length in the request and every length in the response is in that same unit. Nothing is converted server-side."
          },
          "qty": {
            "type": "integer",
            "minimum": 1,
            "default": 1,
            "description": "How many copies. The sum across all rows must not exceed 2000."
          },
          "priority": {
            "type": "boolean",
            "description": "Must-cut; wins stock when `options.respectStock` caps it."
          }
        }
      },
      "Stock1D": {
        "type": "object",
        "required": [
          "length"
        ],
        "properties": {
          "length": {
            "type": "number",
            "exclusiveMinimum": 0,
            "description": "Bar length. Unit-agnostic: pick one unit (mm, inch, …) for every length in the request and every length in the response is in that same unit. Nothing is converted server-side."
          },
          "qty": {
            "type": "integer",
            "minimum": 1,
            "default": 1,
            "description": "Bars available. A hard cap only with `options.respectStock`."
          },
          "price": {
            "type": "number",
            "minimum": 0,
            "description": "Price per bar."
          }
        }
      },
      "Trim1D": {
        "type": "object",
        "description": "A bar has two ends, not four edges. `start` is the leading trim (it shifts the first part along the bar, so the first `pos` equals `start`); `end` is the trailing trim.",
        "properties": {
          "start": {
            "type": "number",
            "minimum": 0,
            "description": "Unit-agnostic: pick one unit (mm, inch, …) for every length in the request and every length in the response is in that same unit. Nothing is converted server-side."
          },
          "end": {
            "type": "number",
            "minimum": 0,
            "description": "Unit-agnostic: pick one unit (mm, inch, …) for every length in the request and every length in the response is in that same unit. Nothing is converted server-side."
          }
        }
      },
      "Options1D": {
        "type": "object",
        "description": "The 1D option set is deliberately smaller: there is no `firstCut`, `maxCutStages` or `minimizeRotations`, because none of them means anything on a bar.",
        "properties": {
          "kerf": {
            "type": "number",
            "minimum": 0,
            "description": "Blade width. Unit-agnostic: pick one unit (mm, inch, …) for every length in the request and every length in the response is in that same unit. Nothing is converted server-side.",
            "default": 0
          },
          "tolerance": {
            "type": "number",
            "minimum": 0,
            "description": "Accept cuts overshooting by up to this much. Pieces that only fitted thanks to it are counted in `metrics.toleranceAcceptedCount` and noted in `warnings`.",
            "default": 0
          },
          "trim": {
            "$ref": "#/components/schemas/Trim1D"
          },
          "minimizeCost": {
            "type": "boolean",
            "default": false,
            "description": "Rank by total bar price."
          },
          "respectStock": {
            "type": "boolean",
            "default": false,
            "description": "Treat `qty` as a hard cap."
          },
          "minOffcut": {
            "type": "number",
            "minimum": 0,
            "description": "Output filter: report a leftover only if it is at least this long.",
            "default": 0
          }
        }
      },
      "Optimize1DRequest": {
        "type": "object",
        "required": [
          "parts",
          "stock"
        ],
        "properties": {
          "parts": {
            "type": "array",
            "minItems": 1,
            "items": {
              "$ref": "#/components/schemas/Part1D"
            }
          },
          "stock": {
            "type": "array",
            "minItems": 1,
            "maxItems": 50,
            "items": {
              "$ref": "#/components/schemas/Stock1D"
            }
          },
          "options": {
            "$ref": "#/components/schemas/Options1D"
          },
          "engine": {
            "type": "string",
            "enum": [
              "heuristic",
              "balanced"
            ],
            "default": "heuristic",
            "description": "In 1D `balanced` resolves to the same packer as `heuristic` — the linear packer is already a best-of-multiple-strategies search, so there is no separate balanced mode. The value is accepted and echoed back so a single client can send one engine id for both endpoints."
          },
          "include": {
            "type": "array",
            "items": {
              "type": "string",
              "enum": [
                "cutPlan",
                "offcuts"
              ]
            },
            "description": "Narrows the response. Omit the field entirely to get both. Listing only one drops the other from the payload (`cutPlan` is absent, `sheets[].offcuts` / `rods[].offcuts` are absent). An empty array drops both."
          }
        }
      },
      "CutStep": {
        "type": "object",
        "description": "One blade movement, in physically executable order (parent before children: you cannot crosscut a strip before you have ripped it off).",
        "required": [
          "sheet",
          "step",
          "axis",
          "pos",
          "length",
          "stage"
        ],
        "properties": {
          "sheet": {
            "type": "integer",
            "minimum": 0,
            "description": "0-based index into `sheets` (2D) or `rods` (1D). Named `sheet` in both."
          },
          "step": {
            "type": "integer",
            "minimum": 1,
            "description": "1-based order within THIS sheet/rod. It RESTARTS at 1 on every sheet — it is not a running number across the whole job."
          },
          "axis": {
            "type": "string",
            "enum": [
              "h",
              "v"
            ],
            "description": "`h` = a horizontal cut line: the blade travels along x and separates top from bottom. `v` = a vertical cut line: the blade travels along y and separates left from right. In 1D `axis` is always `v`."
          },
          "pos": {
            "type": "number",
            "description": "The blade's LOW-COORDINATE edge, not its centre line: the `y` value for `h`, the `x` value for `v`. The kerf occupies `pos` to `pos + kerf`, i.e. it advances in the direction the coordinate GROWS — downward for `h`, rightward for `v` (see the coordinate system in `info.description`). The material on the low side of the line — above it for `h`, to its left for `v` — is the piece that cut frees. In 1D `pos` is the offset along the bar at which that part is freed, measured from the bar end that `trim.start` trims. Unit-agnostic: pick one unit (mm, inch, …) for every length in the request and every length in the response is in that same unit. Nothing is converted server-side."
          },
          "length": {
            "type": "number",
            "minimum": 0,
            "description": "How far the blade travels for this cut — the extent of the REGION it crosses, not the whole board (the industry convention). Always `0` in 1D: a bar crosscut has no travel distance to report. Unit-agnostic: pick one unit (mm, inch, …) for every length in the request and every length in the response is in that same unit. Nothing is converted server-side."
          },
          "stage": {
            "type": "integer",
            "minimum": 1,
            "description": "1-based machine pass. Starts at 1 and increments ONLY when the axis flips relative to the parent cut, so ripping a board into six strips is one stage and crosscutting them is the next. This is the panel-saw sense of \"3-stage cutting\", not tree depth, and it is what `options.maxCutStages` constrains."
          }
        }
      },
      "PlacedPart2D": {
        "type": "object",
        "description": "A part on a board. `x`/`y` is its top-left corner measured from the top-left corner of the sheet, `x` growing right and `y` growing downward.",
        "required": [
          "name",
          "x",
          "y",
          "w",
          "h",
          "rotated"
        ],
        "properties": {
          "name": {
            "type": "string",
            "description": "The requested `name`, or the generated default."
          },
          "x": {
            "type": "number",
            "description": "Distance from the sheet's left edge. Unit-agnostic: pick one unit (mm, inch, …) for every length in the request and every length in the response is in that same unit. Nothing is converted server-side."
          },
          "y": {
            "type": "number",
            "description": "Distance from the sheet's top edge. Unit-agnostic: pick one unit (mm, inch, …) for every length in the request and every length in the response is in that same unit. Nothing is converted server-side."
          },
          "w": {
            "type": "number",
            "description": "PLACED width — already swapped with the requested height when `rotated` is true. No client-side swap needed."
          },
          "h": {
            "type": "number",
            "description": "PLACED height. See `w`."
          },
          "rotated": {
            "type": "boolean",
            "description": "True if the part was turned 90° from the requested `w`×`h`. Informational: `w`/`h` here already reflect it."
          }
        }
      },
      "Rect": {
        "type": "object",
        "description": "A leftover rectangle, in the same coordinate system as the placements.",
        "required": [
          "x",
          "y",
          "w",
          "h"
        ],
        "properties": {
          "x": {
            "type": "number"
          },
          "y": {
            "type": "number"
          },
          "w": {
            "type": "number"
          },
          "h": {
            "type": "number"
          }
        }
      },
      "Sheet": {
        "type": "object",
        "required": [
          "w",
          "h",
          "price",
          "parts"
        ],
        "properties": {
          "w": {
            "type": "number",
            "description": "FULL sheet width as supplied in `stock`, trim included — not the usable width."
          },
          "h": {
            "type": "number",
            "description": "FULL sheet height, trim included."
          },
          "price": {
            "type": [
              "number",
              "null"
            ],
            "description": "The `price` of the stock row this sheet came from, or `null` if none was given."
          },
          "parts": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/PlacedPart2D"
            }
          },
          "offcuts": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/Rect"
            },
            "description": "Usable leftovers, filtered by `options.minOffcut`. ABSENT (not empty) when `include` excludes `offcuts`."
          }
        }
      },
      "Metrics2D": {
        "type": "object",
        "required": [
          "sheetCount",
          "yieldPct",
          "placed",
          "total",
          "cutLines",
          "sawPasses",
          "cutLength",
          "totalPrice"
        ],
        "properties": {
          "sheetCount": {
            "type": "integer",
            "description": "Sheets used. Equals `sheets.length`."
          },
          "yieldPct": {
            "type": "number",
            "description": "Placed part area ÷ total FULL sheet area × 100, rounded to 2 decimals. Trim and kerf count as waste, which is why a trimmed job scores lower than a bare area sum."
          },
          "placed": {
            "type": "integer",
            "description": "Pieces placed, after `qty` expansion."
          },
          "total": {
            "type": "integer",
            "description": "Pieces requested, after `qty` expansion."
          },
          "cutLines": {
            "type": "integer",
            "description": "Collinear cuts merged: same axis, same coordinate, same stage count once. Physically \"how many fence settings the operator makes\". Deliberately NOT claimed to equal any competitor's count — only measured to land in the same range."
          },
          "sawPasses": {
            "type": "integer",
            "description": "Every cut, one per strip crossed — how many separate passes the saw makes."
          },
          "cutLength": {
            "type": "number",
            "description": "Total distance sawn, summed over every cut, rounded to 3 decimals. Convention-independent: merging collinear cuts does not shorten the material. Unit-agnostic: pick one unit (mm, inch, …) for every length in the request and every length in the response is in that same unit. Nothing is converted server-side."
          },
          "totalPrice": {
            "type": "number",
            "description": "Sum of the used sheets’ prices, rounded to 2 decimals. `0` when no stock row carried a `price`."
          }
        }
      },
      "Unplaced2D": {
        "type": "object",
        "description": "Parts that did not fit, aggregated back by name + dimensions.",
        "required": [
          "name",
          "w",
          "h",
          "qty"
        ],
        "properties": {
          "name": {
            "type": "string"
          },
          "w": {
            "type": "number"
          },
          "h": {
            "type": "number"
          },
          "qty": {
            "type": "integer",
            "minimum": 1,
            "description": "How many of this part are unplaced."
          }
        }
      },
      "Timing": {
        "type": "object",
        "required": [
          "solveMs"
        ],
        "properties": {
          "solveMs": {
            "type": "number",
            "description": "Wall-clock milliseconds spent inside the packer, rounded to 2 decimals. Excludes parsing, authorization and queueing."
          }
        }
      },
      "Optimize2DResponse": {
        "type": "object",
        "required": [
          "engine",
          "engineVersion",
          "contractVersion",
          "deterministic",
          "sheets",
          "metrics",
          "unplaced",
          "warnings",
          "guillotineValid",
          "timing"
        ],
        "properties": {
          "engine": {
            "type": "string",
            "enum": [
              "heuristic",
              "balanced"
            ],
            "description": "Which engine actually ran."
          },
          "engineVersion": {
            "type": "string",
            "description": "Algorithm identity: the package version plus a content hash of the algorithm source. Changes automatically whenever the packer changes, and independently per engine. Pin or compare this if you cache."
          },
          "contractVersion": {
            "type": "string",
            "description": "Shape version, matching the `/v1/` in the path. Currently `\"1\"`."
          },
          "deterministic": {
            "type": "boolean",
            "const": true,
            "description": "Always true. Present so a client can assert the guarantee it relies on."
          },
          "sheets": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/Sheet"
            },
            "description": "One entry per sheet used."
          },
          "metrics": {
            "$ref": "#/components/schemas/Metrics2D"
          },
          "cutPlan": {
            "type": [
              "array",
              "null"
            ],
            "items": {
              "$ref": "#/components/schemas/CutStep"
            },
            "description": "The sawing plan, ordered. `null` when `guillotineValid` is false — a non-guillotine layout has no edge-to-edge cut sequence, so there is nothing honest to return. ABSENT entirely when `include` excludes `cutPlan`. In 2D `step` restarts per sheet."
          },
          "unplaced": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/Unplaced2D"
            },
            "description": "Empty when everything fitted."
          },
          "warnings": {
            "type": "array",
            "items": {
              "type": "string"
            },
            "description": "Human-readable notes about the plan: unplaced parts, an unsatisfiable `maxCutStages`, a grain group split across sheets, a non-guillotine layout, or options the chosen engine does not model. Free text — do not parse it; branch on `unplaced`, `guillotineValid` and `metrics` instead."
          },
          "guillotineValid": {
            "type": "boolean",
            "description": "True when the layout can be produced with edge-to-edge cuts, i.e. on a panel saw. False is information, not an error: the nesting is denser but a panel saw cannot make it. `heuristic` is always true; `balanced` often is not."
          },
          "timing": {
            "$ref": "#/components/schemas/Timing"
          }
        }
      },
      "PlacedPart1D": {
        "type": "object",
        "required": [
          "name",
          "pos",
          "length"
        ],
        "properties": {
          "name": {
            "type": "string"
          },
          "pos": {
            "type": "number",
            "description": "Offset of the part’s NEAR end from the bar end that `trim.start` trims. The first part therefore starts at `trim.start`, and each subsequent `pos` adds one kerf. Unit-agnostic: pick one unit (mm, inch, …) for every length in the request and every length in the response is in that same unit. Nothing is converted server-side."
          },
          "length": {
            "type": "number",
            "description": "The part length, as requested."
          }
        }
      },
      "Rod": {
        "type": "object",
        "required": [
          "length",
          "price",
          "remaining",
          "parts"
        ],
        "properties": {
          "length": {
            "type": "number",
            "description": "FULL bar length as supplied in `stock`."
          },
          "price": {
            "type": [
              "number",
              "null"
            ],
            "description": "Price of the stock row, or `null`."
          },
          "remaining": {
            "type": "number",
            "description": "Unused length left on this bar, rounded to 3 decimals. Reported even when it is below `options.minOffcut` — `minOffcut` filters `offcuts`, not this."
          },
          "parts": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/PlacedPart1D"
            },
            "description": "In cutting order along the bar."
          },
          "offcuts": {
            "type": "array",
            "items": {
              "type": "number"
            },
            "description": "At most one entry: `[remaining]` when it is greater than 0 and at least `options.minOffcut`, otherwise `[]`. ABSENT when `include` excludes `offcuts`."
          }
        }
      },
      "Metrics1D": {
        "type": "object",
        "required": [
          "rodCount",
          "yieldPct",
          "placed",
          "total",
          "cuts",
          "totalPrice",
          "toleranceAcceptedCount"
        ],
        "properties": {
          "rodCount": {
            "type": "integer",
            "description": "Bars used. Equals `rods.length`."
          },
          "yieldPct": {
            "type": "number",
            "description": "Placed length ÷ total FULL bar length × 100, rounded to 2 decimals. Trim and kerf count as waste."
          },
          "placed": {
            "type": "integer"
          },
          "total": {
            "type": "integer"
          },
          "cuts": {
            "type": "integer",
            "description": "Total crosscuts across all used bars — one per placed piece, since each piece is freed by a cut at its far end. There is no length to report for a bar cut, which is why 1D has no `cutLength`."
          },
          "totalPrice": {
            "type": "number",
            "description": "Sum of used bar prices, rounded to 2 decimals."
          },
          "toleranceAcceptedCount": {
            "type": "integer",
            "description": "How many pieces fitted only because `options.tolerance` allowed an overshoot."
          }
        }
      },
      "Unplaced1D": {
        "type": "object",
        "required": [
          "name",
          "length",
          "qty"
        ],
        "properties": {
          "name": {
            "type": "string"
          },
          "length": {
            "type": "number"
          },
          "qty": {
            "type": "integer",
            "minimum": 1
          }
        }
      },
      "Optimize1DResponse": {
        "type": "object",
        "required": [
          "engine",
          "engineVersion",
          "contractVersion",
          "deterministic",
          "rods",
          "metrics",
          "unplaced",
          "warnings",
          "timing"
        ],
        "properties": {
          "engine": {
            "type": "string",
            "enum": [
              "heuristic",
              "balanced"
            ]
          },
          "engineVersion": {
            "type": "string"
          },
          "contractVersion": {
            "type": "string"
          },
          "deterministic": {
            "type": "boolean",
            "const": true
          },
          "rods": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/Rod"
            }
          },
          "metrics": {
            "$ref": "#/components/schemas/Metrics1D"
          },
          "cutPlan": {
            "type": [
              "array",
              "null"
            ],
            "items": {
              "$ref": "#/components/schemas/CutStep"
            },
            "description": "One step per placed piece. `sheet` is the ROD index and `step` restarts at 1 on each rod; `axis` is always `v`, `length` always 0 and `stage` always 1. ABSENT when `include` excludes `cutPlan`. Unlike 2D it is never `null` in practice, because a linear plan is always producible."
          },
          "unplaced": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/Unplaced1D"
            }
          },
          "warnings": {
            "type": "array",
            "items": {
              "type": "string"
            }
          },
          "timing": {
            "$ref": "#/components/schemas/Timing"
          }
        }
      },
      "UsageResponse": {
        "type": "object",
        "required": [
          "plan",
          "used",
          "limit",
          "remaining",
          "periodEnd",
          "keyPrefix",
          "contractVersion"
        ],
        "properties": {
          "plan": {
            "type": "string",
            "description": "The tier slug frozen onto the key when it was minted. Re-pricing the product later does not rewrite a live key."
          },
          "used": {
            "type": "integer",
            "minimum": 0,
            "description": "Calls counted in the current UTC calendar month."
          },
          "limit": {
            "type": "integer",
            "description": "The monthly cap frozen onto the key."
          },
          "remaining": {
            "type": "integer",
            "minimum": 0,
            "description": "`limit` − `used`, never negative."
          },
          "periodEnd": {
            "type": "string",
            "format": "date",
            "description": "First day of next month as `YYYY-MM-DD` — a DATE, not a timestamp. The counter resets at 00:00 UTC on that day."
          },
          "keyPrefix": {
            "type": "string",
            "description": "Non-secret display prefix of the calling key, so you can tell which key you queried. The key itself is never returned by any endpoint."
          },
          "contractVersion": {
            "type": "string"
          }
        }
      },
      "HealthResponse": {
        "type": "object",
        "required": [
          "status",
          "service",
          "contractVersion",
          "engineVersion",
          "engines",
          "uptimeSec"
        ],
        "properties": {
          "status": {
            "type": "string",
            "const": "healthy"
          },
          "service": {
            "type": "string",
            "const": "cutoptim-engine"
          },
          "contractVersion": {
            "type": "string"
          },
          "engineVersion": {
            "type": "string",
            "description": "The DEFAULT engine's version (`heuristic`), not a per-engine list."
          },
          "engines": {
            "type": "array",
            "items": {
              "type": "string",
              "enum": [
                "heuristic",
                "balanced"
              ]
            },
            "description": "Engine ids this deployment accepts in `engine`."
          },
          "uptimeSec": {
            "type": "integer",
            "minimum": 0,
            "description": "Whole seconds since process start."
          }
        }
      }
    }
  }
}