Saltar al contenido principal
← CutOptim Engine API

Referencia de la API

URL base https://api.cutoptim.com · versión del contrato v1

Crea una clave en tu panel y luego envíala como Authorization: Bearer <key>.

Especificación OpenAPI: /engine/openapi.json

Endpoints

POST/v1/optimize/2doptimización de paneles 2D
POST/v1/optimize/1doptimización 1D / lineal (barras, perfiles, tubo)
POST/v1/optimize/woodoptimización de madera — 1D con emparejamiento por sección transversal
POST/v1/optimize/nestanidado de forma real — polígonos irregulares en tableros fijos, con zonas de exclusión (láser / plasma / chorro de agua)
POST/v1/validate/2dvalidar una solicitud sin resolver — gratis, sin clave, sin cuota (2d / 1d / wood / nest)
POST/v1/validate/1dvalidar una solicitud sin resolver — gratis, sin clave, sin cuota (2d / 1d / wood / nest)
POST/v1/validate/woodvalidar una solicitud sin resolver — gratis, sin clave, sin cuota (2d / 1d / wood / nest)
POST/v1/validate/nestvalidar una solicitud sin resolver — gratis, sin clave, sin cuota (2d / 1d / wood / nest)
GET/v1/jobs/{id}sondear un trabajo asíncrono del motor max — devuelve su status y, una vez terminado, el plan (sin cuota; solo tus propios trabajos)
POST/v1/import/nestlee los contornos de las piezas de un archivo SVG o DXF — requiere clave, no consume cuota
GET/v1/usageel uso y la cuota del mes en curso de la CUENTA (todas las claves comparten una)
GET/v1/healthliveness — sin clave, sin límite de tasa, sin base de datos

POST /v1/validate/{2d,1d,wood,nest} — response

validbooleanAlways true on a 200 — a malformed body is a 400 with the exact bad field path instead. The body is the SAME one the matching optimize endpoint takes.
modestring"2d" | "1d" | "wood" | "nest" — the endpoint you called.
contractVersionstringShape version. Currently "1".
engineEnabledbooleanNEST ONLY — whether the nesting engine is built into this deployment. Absent for the rectangular modes.
partsobject{ rows, total } — part rows sent, and the total quantity after qty expansion. Check it against the part cap before you spend a call.
stockobject{ rows, total } — the same for stock.
warningsstring[]Approximate, SOLVE-FREE feasibility notes — e.g. a part that fits no stock (2D and nest: bounding box; wood: no matching section long enough). Empty ⇒ every part fits something. Ignores trim, kerf and material: a shape check, not a solve.

Unidades

La API es agnóstica respecto a la unidad. Elige una unidad — milímetros, pulgadas, la que sea — úsala para cada número que envíes, y cada número que recibas estará en esa misma unidad. Nada se convierte en el servidor, y ningún nombre de campo impone una unidad.

Eso abarca las dimensiones de las piezas y del material, kerf, tolerance, trim y minOffcut a la entrada, y cada coordenada, posición, longitud restante, recorte y longitud de corte a la salida. Mezclar unidades dentro de una misma solicitud produce un plan que valida y es físicamente incorrecto, y el servidor no puede detectarlo.

Sistema de coordenadas

El origen es la esquina superior izquierda del tablero: x crece hacia la derecha a lo largo del ancho del tablero, y crece hacia abajo a lo largo de la altura del tablero. La x y la y de una pieza son su esquina superior izquierda, y su w y h son las dimensiones tal como se coloca — ya intercambiadas cuando rotated es true — de modo que el rectángulo x, y, w, h es la huella sobre el tablero sin más cálculos. Los rectángulos de los recortes usan el mismo sistema.

El refilado desplaza las colocaciones: trim.left desplaza cada pieza a la derecha y trim.top desplaza cada pieza hacia abajo, porque las piezas se anidan dentro del área útil y luego se vuelven a desplazar sobre el tablero completo. trim.right y trim.bottom reducen el área útil sin mover el origen. La w y la h de un tablero son siempre las dimensiones completas del material, refilado incluido — que es también por lo que el refilado cuenta como merma en yieldPct.

2D — solicitud

POST /v1/optimize/2d
Authorization: Bearer co_live_…
Content-Type: application/json

{
  "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 },
    "minimizeCost": true,
    "effort": "balanced"
  },
  "engine": "heuristic",
  "include": ["cutPlan", "offcuts"]
}

Campos de la pieza

w, hobligatorio — dimensiones de la pieza
qtypredeterminado 1 — expandido en el servidor; cuenta para el límite de 2,000
nameetiqueta opcional, devuelta en cada colocación
rotatablepredeterminado true — ¿puede girarse la pieza 90°?
grainGrouplos miembros de un grupo se mantienen en un mismo tablero (emparejamiento de veta)
priorityde corte obligatorio: gana espacio en el tablero cuando el material está limitado (con respectStock)
edgeBandingcanteado por lado: nombra una referencia de tipo en cualquiera de top / right / bottom / left (una cadena libre, tu propio código) — la respuesta suma los metros por referencia. Solo metadatos, nunca mueve una pieza. Solo 2D
materialetiqueta de material (una cadena libre, tu propio código): las piezas y el material del mismo tipo se empaquetan solo juntos. A diferencia de edgeBanding, cambia la disposición. Ausente = un único grupo sin especificar. Funciona en todos los modos

Campos del material

w y h son obligatorios. qty es 1 por defecto y solo actúa como límite estricto con respectStock. price es por tablero y determina totalPrice y el modo coste. priority (booleano) agota este material primero; material (una cadena libre) lo restringe a piezas del mismo material.

Opciones

kerfancho de la hoja (predeterminado 0)
toleranceaceptar cortes que se pasen como máximo por este valor
trimrefilado por lado: left, right, top, bottom
firstCut'auto' | 'horizontal' | 'vertical' (predeterminado 'auto')
minimizeCostclasificar los candidatos por el menor precio total del material; con varios tamaños de material con precio los combina (2D) o elige la longitud más barata (1D) para minimizar la factura, aunque eso use más material
respectStocktratar la qty de cada fila de material como un límite estricto
minOffcutinformar solo de los recortes cuyo lado corto sea al menos este valor
maxCutStageslímite de fases de la seccionadora — un número de fases, no la profundidad bruta del árbol
minimizeRotationspreferir distribuciones que giren menos piezas
effort'fast' | 'balanced' (predeterminado 'balanced'). Profundidad de búsqueda: 'balanced' ejecuta el best-of multiestrategia completo; 'fast' omite la única búsqueda costosa de combinaciones de área por tablero — notablemente más rápido en trabajos grandes a cambio de unos pocos puntos de aprovechamiento, sigue siendo de guillotina y nunca más denso que 'balanced'. En trabajos pequeños suele ser idéntico. Solo motor heuristic

include hace dos cosas. ACOTAR — "cutPlan" y "offcuts" están activos por defecto; un array presente conserva solo los tokens de acotado que lista (un array vacío elimina ambos). EXPORTACIÓN ADITIVA — "svg", "csv" y "dxf" añaden cada uno esa exportación a la respuesta como una CADENA: svg un dibujo de distribución 2D autónomo (solo 2D — una solicitud 1d/wood devuelve en su lugar una advertencia), dxf un dibujo R12/AC1009 en las capas STOCK/PARTS/LABELS, csv una lista de corte. Los tokens de exportación no afectan al acotado, así que include:["svg"] añade svg y — al no nombrar ningún token de acotado — elimina cutPlan/offcuts; usa ["cutPlan","offcuts","svg"] para conservar todo y añadir svg.

parts[].meta · stock[].meta — passthrough (2d · 1d · wood · nest)

parts[].metaobjectOpaque JSON of your own — an ERP article number, an order-line id, a customer ref. Echoed VERBATIM on every placed piece of that part. The optimizer never reads it, so it can never change a layout.
stock[].metaobjectThe same on a stock row: echoed on every sheet / rod / nested sheet cut from it, so the plan reconciles with your system without a lookup table.

La opción effort equilibra el tiempo de cálculo frente al aprovechamiento. Aquí está ese compromiso, medido en un trabajo exigente — cada cifra proviene del packer real.

Interruptor effort: aprovechamiento del material vs tiempo de cálculoInterruptor effort: aprovechamiento del material vs tiempo de cálculo. fast: 350 tableros · 76.2% · ≈1.9 s. balanced: 330 tableros · 80.8% · ≈4.8 s. max: reservado — más denso exige búsqueda más lenta. En la mayoría de los trabajos (más pequeños) ambos son idénticos; la diferencia solo se abre en trabajos grandes como este. balanced es el valor predeterminado y nunca es más denso de lo que fast puede alcanzar.Interruptor effort: aprovechamiento del material vs tiempo de cálculoUn trabajo exigente — unas 1550 piezas en un tablero de 2,07 × 5,6 m. Cada cifra medida en el packer real.74%76%78%80%82%84%02 s4 s6 stiempo de cálculo · más rápido →aprovechamiento del material · más denso ↑⇄ el interruptor effort+4,6 pts aprov. · −20 tableros−5,7% material · ≈2,5× más lentofast350 tableros · 76.2% · ≈1.9 s★ balanced · predeterminado330 tableros · 80.8% · ≈4.8 smaxreservadomás denso exigebúsqueda máslenta
En la mayoría de los trabajos (más pequeños) ambos son idénticos; la diferencia solo se abre en trabajos grandes como este. balanced es el valor predeterminado y nunca es más denso de lo que fast puede alcanzar.

Y en cualquier caso es rápido: incluso los trabajos de producción más grandes — 2000 piezas o más — se resuelven en segundos con el motor predeterminado, holgadamente dentro del presupuesto de tiempo de la API.

2D — respuesta

{
  "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": 600, "h": 400, "rotated": false },
        { "name": "Door", "x": 613, "y": 10, "w": 600, "h": 400, "rotated": false },
        { "name": "Door", "x": 1216, "y": 10, "w": 600, "h": 400, "rotated": false },
        { "name": "Door", "x": 1819, "y": 10, "w": 600, "h": 400, "rotated": false }
      ],
      "offcuts": [
        { "x": 2422, "y": 10, "w": 18, "h": 400 },
        { "x": 10, "y": 413, "w": 2430, "h": 807 }
      ]
    }
  ],
  "metrics": {
    "sheetCount": 1,
    "yieldPct": 32.25,
    "placed": 4,
    "total": 4,
    "cutLines": 5,
    "sawPasses": 5,
    "cutLength": 4030,
    "totalPrice": 42
  },
  "unplaced": [],
  "warnings": [],
  "guillotineValid": true,
  "timing": { "solveMs": 3.13 },
  "cutPlan": [
    { "sheet": 0, "step": 1, "axis": "h", "pos": 410, "length": 2430, "stage": 1 },
    { "sheet": 0, "step": 2, "axis": "v", "pos": 610, "length": 400, "stage": 2 },
    { "sheet": 0, "step": 3, "axis": "v", "pos": 1213, "length": 400, "stage": 2 },
    { "sheet": 0, "step": 4, "axis": "v", "pos": 1816, "length": 400, "stage": 2 },
    { "sheet": 0, "step": 5, "axis": "v", "pos": 2419, "length": 400, "stage": 2 }
  ]
}
  • cutLines vs sawPasses — cutLines fusiona los cortes colineales (un único ajuste de tope); sawPasses cuenta cada pasada. Dos medidas honestas del mismo plan, no la pretensión de coincidir con el recuento de ningún competidor.
  • guillotineValid / cutPlan — Cuando una distribución no puede cortarse de canto a canto, guillotineValid es false y cutPlan es null. Eso es información real — no puede fabricarse en una seccionadora — no un error.
  • unplaced + warnings — Un trabajo insatisfactible devuelve 200 con las piezas listadas en unplaced y una nota en warnings. Un plan sobre el que puedes actuar vale más que un código de estado.
  • edgeBanding — Cuando alguna pieza lleva edgeBanding, la respuesta añade un bloque edgeBanding: los metros lineales que consume cada referencia de tipo, por pieza y como total del pedido. Es geometría exacta, sin margen de merma — el taller añade el suyo — y asume entrada en milímetros (÷1000 para metros). La clave está ausente por completo en un trabajo sin canteado.
  • materials / unmatchedMaterials — Cuando alguna pieza o elemento de material lleva material, la respuesta añade materials (un resumen por material — material, sheetCount/rodCount, yieldPct, placed, total, totalPrice) y unmatchedMaterials (demanda cuyo material no tiene existencias coincidentes). En madera, el material va por cada sección de sección transversal. Ambas claves están ausentes en un trabajo sin etiquetas de material, que permanece idéntico byte a byte.

Campos de la respuesta

Los nombres de los campos y los tipos son el contrato, por lo que las tablas siguientes permanecen en inglés en todos los idiomas — un nombre de campo traducido documentaría una API que no existe.

POST /v1/optimize/2d — top level

enginestringWhich engine ran: "heuristic", "balanced" or "max".
engineVersionstringAlgorithm identity — package version + a content hash of the algorithm source. Moves automatically on any packer change, and independently per engine.
contractVersionstringShape version, matching the /v1/ in the path. Currently "1".
deterministicbooleanAlways true. Present so a client can assert the guarantee it relies on.
sheetsarrayOne entry per sheet used, in cutting order.
metricsobjectAggregate numbers for the whole job.
cutPlanarray | nullThe sawing plan. null when guillotineValid is false; absent when excluded via include.
unplacedarrayParts that did not fit, aggregated by name + size. Empty when everything fitted.
warningsstring[]Free-text notes about the plan. Do not parse — branch on unplaced, guillotineValid and metrics.
guillotineValidbooleanTrue when the layout is producible with edge-to-edge cuts, i.e. on a panel saw.
edgeBandingobjectLinear metres of edge banding, grouped by type reference. ABSENT unless a part requested banding via parts[].edgeBanding. 2D only.
materialsarrayPer-material rollup. ABSENT unless a part or stock row carried material — a material-free job stays byte-identical. (OPEN-256)
unmatchedMaterialsarrayDemand whose material has no matching stock at all. ABSENT when it does not happen. A missing-material report, not a did-not-fit one.
svgstringInline SVG of the 2D layout (self-contained, no external refs). Present ONLY when include contains "svg". 2D only. (OPEN-223)
csvstringInline CSV cut list. Present ONLY when include contains "csv".
dxfstringInline DXF (R12/AC1009) on layers STOCK/PARTS/LABELS. Present ONLY when include contains "dxf".
timing.solveMsnumberMilliseconds inside the packer, 2 decimals. Excludes parsing, auth and queueing.

sheets[]

w, hnumberFULL stock dimensions, trim included — not the usable area.
pricenumber | nullPrice of the stock row this sheet came from, or null if none was given.
partsarrayPlacements on this sheet.
offcutsarrayUsable leftover rectangles, filtered by options.minOffcut. Absent (not empty) when excluded via include.
metaobjectPresent only when the stock row carried meta — echoed verbatim from stock[].meta. (OPEN-224)

sheets[].parts[]

namestringThe requested name, or the generated default "Part <row>".
x, ynumberTop-left corner of the part, from the top-left corner of the sheet.
w, hnumberDimensions AS PLACED — already swapped when rotated is true. No client-side swap needed.
rotatedbooleanTrue if the part was turned 90° from the requested w×h. Informational only.
metaobjectPresent only when the part carried meta — echoed verbatim from parts[].meta on every placed piece. (OPEN-224)

sheets[].offcuts[]

x, y, w, hnumberA leftover rectangle, in the same coordinate system as the placements.

metrics (2D)

sheetCountintegerSheets used. Equals sheets.length.
yieldPctnumberPlaced part area ÷ total FULL sheet area × 100, 2 decimals. Trim and kerf count as waste.
placedintegerPieces placed, after qty expansion.
totalintegerPieces requested, after qty expansion.
cutLinesintegerCollinear cuts merged: same axis, same coordinate, same stage counted once — one fence setting.
sawPassesintegerEvery cut, one per strip crossed — how many separate passes the saw makes.
cutLengthnumberTotal distance sawn, 3 decimals. Same under either counting convention. In your unit.
totalPricenumberSum of the used sheets’ prices, 2 decimals. 0 when no stock row carried a price.

unplaced[] (2D)

namestringThe part name.
w, hnumberAs requested.
qtyintegerHow many of this part could not be placed.

edgeBanding (2D — present only when a part is banded)

totalMetersnumberOrder-wide total across every banded edge, 3 decimals. ⚠️ Assumes mm input: top/bottom edges run the part width, left/right the height, ×qty, ÷1000. In another unit it is your raw edge length ÷ 1000.
byType[]{ reference, meters }Order total split by the caller-supplied type reference, sorted by reference.
byPart[]{ name, meters, byType }One entry per requested part row that has any banded edge; its byType splits that part’s metres by reference.

cutPlan[]

sheetinteger0-based index into sheets (2D) or rods (1D). Named sheet in both.
stepinteger1-based order within THIS sheet — it restarts at 1 on every sheet.
axis"h" | "v""h": blade travels along x, separating top from bottom. "v": along y, separating left from right.
posnumberThe blade’s LOW-COORDINATE edge — the y value for "h", the x value for "v" — not its centre line. The kerf occupies pos to pos + kerf.
lengthnumberDistance the blade travels on this cut: the extent of the region crossed. Always 0 in 1D.
stageinteger1-based machine pass. Increments only when the axis flips relative to the parent cut.

2D · edgeBanding

Cuando alguna pieza lleva edgeBanding, la respuesta añade un bloque edgeBanding: los metros lineales que consume cada referencia de tipo, por pieza y como total del pedido. Es geometría exacta, sin margen de merma — el taller añade el suyo — y asume entrada en milímetros (÷1000 para metros). La clave está ausente por completo en un trabajo sin canteado.

POST /v1/optimize/2d

{
  "parts": [
    {
      "name": "Door",
      "w": 600,
      "h": 400,
      "qty": 2,
      "edgeBanding": {
        "top": "ABS oak 22",
        "bottom": "ABS oak 22",
        "left": "ABS white 22",
        "right": "ABS white 22"
      }
    },
    { "name": "Shelf", "w": 800, "h": 300, "edgeBanding": { "top": "ABS oak 22" } }
  ],
  "stock": [{ "w": 2440, "h": 1220, "qty": 10, "price": 42 }],
  "options": { "kerf": 3 }
}
{
  "edgeBanding": {
    "totalMeters": 4.8,
    "byType": [
      { "reference": "ABS oak 22", "meters": 3.2 },
      { "reference": "ABS white 22", "meters": 1.6 }
    ],
    "byPart": [
      {
        "name": "Door",
        "meters": 4,
        "byType": [
          { "reference": "ABS oak 22", "meters": 2.4 },
          { "reference": "ABS white 22", "meters": 1.6 }
        ]
      },
      {
        "name": "Shelf",
        "meters": 0.8,
        "byType": [{ "reference": "ABS oak 22", "meters": 0.8 }]
      }
    ]
  }
}

material

Cuando alguna pieza o elemento de material lleva material, la respuesta añade materials (un resumen por material — material, sheetCount/rodCount, yieldPct, placed, total, totalPrice) y unmatchedMaterials (demanda cuyo material no tiene existencias coincidentes). En madera, el material va por cada sección de sección transversal. Ambas claves están ausentes en un trabajo sin etiquetas de material, que permanece idéntico byte a byte.

POST /v1/optimize/2d

{
  "parts": [
    { "name": "Door", "w": 600, "h": 400, "qty": 4, "material": "MDF 18" },
    { "name": "Shelf", "w": 800, "h": 300, "qty": 6, "material": "Oak 18" },
    { "name": "Back panel", "w": 1000, "h": 500, "qty": 2, "material": "Ply 6" }
  ],
  "stock": [
    { "w": 2440, "h": 1220, "qty": 10, "price": 42, "material": "MDF 18" },
    { "w": 2440, "h": 1220, "qty": 10, "price": 68, "material": "Oak 18", "priority": true }
  ],
  "options": { "kerf": 3 },
  "engine": "heuristic"
}
{
  "materials": [
    {
      "material": "MDF 18",
      "sheetCount": 1,
      "yieldPct": 32.25,
      "placed": 4,
      "total": 4,
      "totalPrice": 42
    },
    {
      "material": "Oak 18",
      "sheetCount": 1,
      "yieldPct": 48.37,
      "placed": 6,
      "total": 6,
      "totalPrice": 68
    }
  ],
  "unmatchedMaterials": [
    {
      "material": "Ply 6",
      "parts": [{ "name": "Back panel", "w": 1000, "h": 500, "qty": 2 }]
    }
  ]
}

materials[] — per-material rollup (2d · 1d · nest)

materialstringThe tag exactly as you sent it. Free text, matched exactly.
sheetCount | rodCountintegerStock consumed for this material — sheetCount on 2D and nest, rodCount on 1D.
yieldPct | densitynumberThis material’s own fill — yieldPct on the rectangular modes, density on nest (a polygon fill, not comparable to yieldPct).
placed, totalintegerPieces placed and requested for this material, after qty expansion.
totalPricenumberSum of the prices of the stock used for this material.

unmatchedMaterials[] — demand with no matching stock

materialstringThe tag that has no stock of its own anywhere in the request.
partsarrayThe demand rows in that material, in the mode’s unplaced shape: name, w, h, qty on 2D; name, length, qty on 1D; name, qty on nest.

cutPlan — qué significa físicamente un step

Un step es un movimiento de la hoja, y la lista está en el orden en que realmente puedes serrar: un corte padre antes de los cortes dentro de la pieza que produjo, porque no puedes tronzar una tira antes de haberla despiezado. axis "h" significa que la hoja se desplaza a lo largo de x y separa arriba de abajo; axis "v" significa que se desplaza a lo largo de y y separa izquierda de derecha. pos es el borde de COORDENADA MÁS BAJA de la hoja — el valor y para "h", el valor x para "v" — no su línea central: el kerf ocupa de pos a pos + kerf, así que la hoja come en la dirección en la que crece la coordenada, que es hacia abajo para "h" y hacia la derecha para "v". El material del lado bajo de la línea — por encima de ella para "h", a su izquierda para "v" — es la pieza que este corte libera. length es cuánto recorre la hoja en ese único corte: la extensión de la región que atraviesa, no el ancho de todo el tablero.

stage es una pasada de máquina. Empieza en 1 y solo se incrementa cuando el axis se invierte respecto al corte padre, de modo que despiezar un tablero en seis tiras es una fase y tronzarlas es la siguiente. Ese es el sentido, propio de la seccionadora, del «corte en tres fases», no la profundidad del árbol de corte, y es lo que restringe maxCutStages. sheet es un índice de base 0 en sheets, y step vuelve a empezar en 1 en cada tablero en lugar de correr a lo largo de todo el trabajo.

cutPlan es null — no ausente, no vacío — siempre que guillotineValid sea false: una distribución que no puede cortarse de canto a canto no tiene secuencia de corte que devolver. Desaparece por completo de la carga útil solo si lo dejaste fuera de include.

1D — lineal

POST /v1/optimize/1d

{
  "parts": [{ "name": "Rail", "length": 1200, "qty": 6 }],
  "stock": [{ "length": 3000, "qty": 5, "price": 12.5 }],
  "options": { "kerf": 3, "trim": { "start": 10, "end": 0 } }
}

Las piezas llevan length (además de qty, name, priority); el material lleva length, qty y price. Tanto las piezas como el material también aceptan una etiqueta material opcional (el material también priority): material restringe una pieza a material del mismo tipo, y la respuesta añade entonces materials y unmatchedMaterials igual que en 2D. Las opciones son kerf, tolerance, trim.start / trim.end, minimizeCost, respectStock y minOffcut. La respuesta devuelve rods en lugar de sheets, cada una con sus parts, su longitud restante y sus offcuts.

1D — respuesta

{
  "engine": "heuristic",
  "engineVersion": "1.0.0+10e0c941",
  "contractVersion": "1",
  "deterministic": true,
  "rods": [
    {
      "length": 3000,
      "price": 12.5,
      "remaining": 584,
      "parts": [
        { "name": "Rail", "pos": 10, "length": 1200 },
        { "name": "Rail", "pos": 1213, "length": 1200 }
      ],
      "offcuts": [584]
    },
    {
      "length": 3000,
      "price": 12.5,
      "remaining": 584,
      "parts": [
        { "name": "Rail", "pos": 10, "length": 1200 },
        { "name": "Rail", "pos": 1213, "length": 1200 }
      ],
      "offcuts": [584]
    },
    {
      "length": 3000,
      "price": 12.5,
      "remaining": 584,
      "parts": [
        { "name": "Rail", "pos": 10, "length": 1200 },
        { "name": "Rail", "pos": 1213, "length": 1200 }
      ],
      "offcuts": [584]
    }
  ],
  "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 }
  ]
}

rods sustituye a sheets y no hay guillotineValid, porque un corte lineal siempre es fabricable. La pos de cada pieza es el desplazamiento de su extremo cercano desde el extremo de la barra que refila trim.start, de modo que la primera pieza empieza exactamente en trim.start y cada pos siguiente suma un kerf. remaining es el recorte APROVECHABLE: el kerf del corte que lo libera de la última pieza ya está descontado, de modo que es la longitud reaprovechable, no el hueco en bruto. Se informa en cada barra incluso cuando está por debajo de minOffcut — minOffcut solo filtra el array offcuts, que contiene como máximo una entrada. En el plan de corte, sheet es el índice de la barra, axis es siempre "v", stage es siempre 1 y length es siempre 0: un tronzado de barra no tiene distancia de recorrido que informar, que es también por lo que las métricas 1D llevan cuts pero no cutLength.

Madera — sección transversal

POST /v1/optimize/wood

{
  "parts": [
    { "name": "Rafter", "sw": 50, "sh": 100, "length": 2400, "qty": 2 },
    { "name": "Noggin", "sw": 50, "sh": 100, "length": 600, "qty": 4 },
    { "name": "Beam", "sw": 50, "sh": 150, "length": 3000, "qty": 2 }
  ],
  "stock": [
    { "name": "C24 50x100", "sw": 50, "sh": 100, "length": 4000, "qty": 5, "price": 12.5 },
    { "name": "C24 50x150", "sw": 50, "sh": 150, "length": 4000, "qty": 3, "price": 18 }
  ],
  "options": { "kerf": 3 }
}

La madera tiene una identidad que una barra lisa no tiene: una pieza de 50×150 no puede salir de material de 50×100, por mucha longitud que quede. Por eso las piezas y el material llevan sw y sh, los dos lados de la sección transversal, en cualquier orden — 50×100 y 100×50 son la misma barra volteada y se emparejan como una sola sección. El trabajo se divide por sección transversal, cada sección se empareja con su propio material y se resuelve por separado, y una sola llamada devuelve todo. Las piezas y el material también aceptan una etiqueta material opcional (el material también priority): con ella, un roble 50×100 y un pino 50×100 pasan a ser dos secciones separadas, y cada sección lleva su material. Las opciones son las mismas que en 1D.

Madera — respuesta

{
  "engine": "heuristic",
  "engineVersion": "1.0.0+7c1f3a62",
  "contractVersion": "1",
  "deterministic": true,
  "sections": [
    {
      "section": "50x100",
      "sw": 50,
      "sh": 100,
      "stockName": "C24 50x100",
      "rods": [
        {
          "length": 4000,
          "price": 12.5,
          "remaining": 391,
          "parts": [
            { "name": "Rafter", "pos": 0, "length": 2400 },
            { "name": "Noggin", "pos": 2403, "length": 600 },
            { "name": "Noggin", "pos": 3006, "length": 600 }
          ],
          "offcuts": [391]
        },
        {
          "length": 4000,
          "price": 12.5,
          "remaining": 391,
          "parts": [
            { "name": "Rafter", "pos": 0, "length": 2400 },
            { "name": "Noggin", "pos": 2403, "length": 600 },
            { "name": "Noggin", "pos": 3006, "length": 600 }
          ],
          "offcuts": [391]
        }
      ],
      "metrics": {
        "rodCount": 2,
        "yieldPct": 90,
        "placed": 6,
        "total": 6,
        "cuts": 6,
        "totalPrice": 25
      },
      "unplaced": []
    },
    {
      "section": "50x150",
      "sw": 50,
      "sh": 150,
      "stockName": "C24 50x150",
      "rods": [
        {
          "length": 4000,
          "price": 18,
          "remaining": 997,
          "parts": [{ "name": "Beam", "pos": 0, "length": 3000 }],
          "offcuts": [997]
        },
        {
          "length": 4000,
          "price": 18,
          "remaining": 997,
          "parts": [{ "name": "Beam", "pos": 0, "length": 3000 }],
          "offcuts": [997]
        }
      ],
      "metrics": {
        "rodCount": 2,
        "yieldPct": 75,
        "placed": 2,
        "total": 2,
        "cuts": 2,
        "totalPrice": 36
      },
      "unplaced": []
    }
  ],
  "unmatched": [],
  "metrics": {
    "sectionCount": 2,
    "rodCount": 4,
    "yieldPct": 82.5,
    "placed": 8,
    "total": 8,
    "cuts": 8,
    "totalPrice": 61,
    "toleranceAcceptedCount": 0
  },
  "unplaced": [],
  "warnings": [],
  "timing": { "solveMs": 2.96 },
  "cutPlan": [
    {
      "section": "50x100",
      "sheet": 0,
      "step": 1,
      "axis": "v",
      "pos": 2400,
      "length": 0,
      "stage": 1
    },
    {
      "section": "50x100",
      "sheet": 0,
      "step": 2,
      "axis": "v",
      "pos": 3003,
      "length": 0,
      "stage": 1
    },
    {
      "section": "50x100",
      "sheet": 0,
      "step": 3,
      "axis": "v",
      "pos": 3606,
      "length": 0,
      "stage": 1
    },
    {
      "section": "50x100",
      "sheet": 1,
      "step": 1,
      "axis": "v",
      "pos": 2400,
      "length": 0,
      "stage": 1
    },
    {
      "section": "50x100",
      "sheet": 1,
      "step": 2,
      "axis": "v",
      "pos": 3003,
      "length": 0,
      "stage": 1
    },
    {
      "section": "50x100",
      "sheet": 1,
      "step": 3,
      "axis": "v",
      "pos": 3606,
      "length": 0,
      "stage": 1
    },
    {
      "section": "50x150",
      "sheet": 0,
      "step": 1,
      "axis": "v",
      "pos": 3000,
      "length": 0,
      "stage": 1
    },
    {
      "section": "50x150",
      "sheet": 1,
      "step": 1,
      "axis": "v",
      "pos": 3000,
      "length": 0,
      "stage": 1
    }
  ]
}

sections sustituye a rods en el nivel superior: cada entrada es una sección transversal con sus propias rods (idénticas en forma a las de 1D) y sus propias metrics, de modo que las cifras por material están ahí sin recalcularlas. unmatched no tiene equivalente en 1D — es demanda para cuya sección transversal no aportaste material alguno, lo que es un problema distinto de unplaced (piezas que tenían material y no cupieron) y tiene una solución distinta, así que ambos nunca se mezclan. metrics.total cuenta cada pieza que pediste, incluidas las unmatched. En el plan de corte cada step también nombra su section, y sheet es el índice de la barra DENTRO de esa sección en lugar de un contador de todo el trabajo.

POST /v1/optimize/wood — top level (what differs from 1D)

sectionsarrayReplaces rods at the top level: one entry per cross-section, each matched to its own stock and solved on its own.
unmatchedarrayDemand whose cross-section has NO stock at all. Deliberately separate from unplaced (had stock, did not fit) — the fix differs, so the two are never mixed.
unplacedarrayDid-not-fit demand, aggregated across the sections that DID have stock. Same shape as 1D.
metricsobjectJob-wide totals across every section (below).
cutPlanarray | nullAs 1D, except every step also carries section, and sheet is the rod index WITHIN that section, not a job-wide counter.
csv, dxfstringInline export, present only when include names the token. svg is 2D-only — a wood request asking for it gets a warning instead.

sections[]

sectionstringNormalised cross-section key, e.g. "50x100" — short side first, so 50×100 and 100×50 are one section.
sw, shnumberThe two cross-section sides: sw the SHORT one, sh the long one, whatever order they arrived in.
stockNamestring | nullThe name of the stock row this section was matched to, or null when that row carried none.
materialstringPresent only when the section carries a material — an oak 50×100 and a pine 50×100 are two sections. (OPEN-256)
rodsarrayIdentical in shape to the 1D rods[] above, meta and offcuts included.
metricsobjectThis section’s own totals: rodCount, yieldPct, placed, total, cuts, totalPrice — so the per-section figures need no recomputation.
unplacedarrayThis section’s parts that had stock and still did not fit.

metrics (wood)

sectionCountintegerCross-sections solved. Equals sections.length.
rodCountintegerBars used across every section.
yieldPctnumberPlaced length ÷ total FULL bar length × 100 over the whole job, 2 decimals.
placedintegerPieces placed, after qty expansion.
totalintegerPieces REQUESTED, after qty expansion — unmatched ones included.
cutsintegerCrosscuts across every used bar.
totalPricenumberSum of the used bars’ prices, 2 decimals.
toleranceAcceptedCountintegerPieces that fitted only because options.tolerance allowed an overshoot.

unmatched[]

sectionstringThe cross-section key nothing in stock matched.
sw, shnumberThat cross-section’s two sides.
materialstringPresent when the cross-section DOES exist in stock but only in a different material. (OPEN-256)
partsarrayThe demand rows in that section, in the 1D unplaced shape: name, length, qty.
qtyintegerTotal pieces in this section that had no stock at all.

Anidado de forma real

POST /v1/optimize/nest

{
  "parts": [
    {
      "name": "bracket",
      "polygon": [[0, 0], [300, 0], [300, 100], [100, 100], [100, 300], [0, 300]],
      "qty": 4,
      "allowedRotations": [0, 90, 180, 270]
    },
    {
      "name": "gusset",
      "polygon": [[0, 0], [280, 0], [0, 280]],
      "qty": 4,
      "allowedRotations": [0, 90, 180, 270]
    }
  ],
  "stock": [
    {
      "w": 2440,
      "h": 1220,
      "price": 12.5,
      "exclusions": [{ "polygon": [[0, 0], [300, 0], [0, 300]], "quality": 0 }]
    }
  ]
}

Los tres modos anteriores empaquetan rectángulos. POST /v1/optimize/nest empaqueta POLÍGONOS ARBITRARIOS: una pieza es un contorno (polygon, con holes interiores opcionales), no un ancho×alto, de modo que las piezas encajan unas en las cavidades cóncavas de las otras y se recupera el aire del hueco que un bounding box desperdicia — en un trabajo representativo, 6 tableros donde las mismas piezas por su bounding box necesitan 9. Es una clase de algoritmo distinta (un motor geométrico de colisiones, no el empaquetador de guillotina), para corte por láser, plasma y chorro de agua. Vienen con él dos cosas que la API rectangular no puede expresar: zonas de exclusión por tablero (stock[].exclusions — un defecto, la huella de una brida, un área preimpresa; una zona de quality 0 es una región vetada para cualquier pieza) y holes de forma real. La partición por material y el traspaso de meta funcionan igual que en todas partes. El ejemplo de abajo es una llamada real capturada — ocho piezas en un solo tablero con una esquina dañada excluida.

POST /v1/optimize/nest — request (top level)

partsarrayOne or more NestPart (see below). Required.
stockarrayOne or more NestStock sheet types (see below). Required.
optionsobjectSolve options (see below). Optional.
enginestring"lbf" (default, single-pass, instant) or "sparrow" (advertised for a future higher-density build; currently served by lbf with a warning).
includestring[]Additive inline export of the ACHIEVED nest: "svg" a self-contained styled drawing (one titled band per sheet, parts as filled polygons with holes, exclusion zones hatched), "dxf" an R12/AC1009 document on layers STOCK/PARTS/HOLES/ZONES/LABELS. Unit-agnostic, exactly like the request coordinates. The geometry in sheets[] is returned either way.

parts[] (NestPart)

polygonnumber[][]The part outline: an ordered ring of ≥3 [x, y] vertices. Given closed (first == last) or open; winding order is not required — it is oriented internally. Required UNLESS the row carries source.
sourceobjectOPEN-262 — read the outline out of an SVG or DXF FILE instead of listing coordinates (fields below). EXACTLY ONE of polygon / source: both, or neither, is a 400.
holesnumber[][][]Optional interior holes — a part with a cut-out. Each hole is a ring like polygon. NOT accepted alongside source: the file already carries its own holes, and two sources of truth for one geometry is not a thing we resolve silently.
qtyintegerCopies to place. Default 1.
allowedRotationsnumber[] | "continuous"Allowed rotations in DEGREES (e.g. [0,90,180,270]). Omit or "continuous" for free rotation.
minQualityintegerThe part may only be placed where sheet quality ≥ this. Default 1 → it avoids every quality-0 exclusion zone. Raise it to keep the part off inferior-but-not-forbidden zones too.
prioritybooleanMust-cut: wins sheet space when stock is capped (options.respectStock).
materialstringOPEN-256 — a part of material X nests only on material-X sheets; the job is partitioned by material.
namestringOptional label, echoed on every placement. Defaults to "Part <1-based row index>".
metaobjectOPEN-224 — opaque JSON (your ERP ids), echoed verbatim on every placed copy. Never affects the layout.

stock[] (NestStock)

w, hnumberRectangular sheet size. Give w & h OR polygon, not both.
polygonnumber[][]Arbitrary sheet outline (an off-cut remnant, a non-rectangular board); overrides w/h.
qtyintegerDefault 1. A HARD cap only when options.respectStock is true.
pricenumberPer-sheet price, for cost mode + totalPrice.
exclusionsobject[]OPEN-233 — per-sheet excluded / inferior zones. Each: { polygon: number[][], quality?: integer }. quality 0 (the default) = a hard no-go region for any part; a higher quality only excludes parts whose minQuality demands at least that. This is what the rectangular API cannot express.
materialstringOPEN-256 — this sheet serves only material-matching parts.
metaobjectOPEN-224 — echoed on every sheet cut from this stock row.

options (NestOptions)

minSeparationnumberMinimum clearance between parts and between a part and any hazard (sheet edge / exclusion zone). Use for kerf / beam / torch width. Default 0.
seedintegerDeterminism: a fixed seed → a reproducible layout. Omit and the server pins a fixed default so the response stays reproducible.
minimizeCostbooleanRank plans by total sheet price rather than sheet count.
respectStockbooleanTreat each stock qty as a hard cap.
simplifyTolerancenumberPolygon simplification tolerance (max area deviation as a fraction). Speeds up dense DXF outlines with hundreds of vertices. 0 disables.
timeBudgetMsintegerWall-clock budget for the metaheuristic (engine "sparrow"). Ignored by "lbf" (single-pass).

Nest — respuesta

{
  "engine": "lbf",
  "engineVersion": "1.0.0+nest-d4046d5-07546022",
  "contractVersion": "1",
  "deterministic": true,
  "sheets": [
    {
      "w": 2440,
      "h": 1220,
      "price": 12.5,
      "density": 0.1217,
      "parts": [
        { "name": "bracket", "sheet": 0, "x": 300.003, "y": 400.226, "rotation": -180 },
        { "name": "bracket", "sheet": 0, "x": 300.133, "y": 700.617, "rotation": -180 },
        { "name": "bracket", "sheet": 0, "x": 300.061, "y": 1000.969, "rotation": -180 },
        { "name": "bracket", "sheet": 0, "x": 400.095, "y": 1102.332, "rotation": -180 },
        { "name": "gusset", "sheet": 0, "x": 530.102, "y": 50.205, "rotation": 90 },
        { "name": "gusset", "sheet": 0, "x": 300.058, "y": 380.171, "rotation": -90 },
        { "name": "gusset", "sheet": 0, "x": 580.15, "y": 380.193, "rotation": 90 },
        { "name": "gusset", "sheet": 0, "x": 300.204, "y": 660.982, "rotation": -90 }
      ],
      "exclusions": [{ "polygon": [[0, 0], [300, 0], [0, 300]], "quality": 0 }]
    }
  ],
  "metrics": { "sheetCount": 1, "density": 0.1217, "placed": 8, "total": 8, "totalPrice": 12.5 },
  "unplaced": [],
  "warnings": [],
  "timing": { "solveMs": 44.19 }
}

Cada entrada de sheets es un tablero usado; una pieza colocada lleva la transformación rígida (rotation en grados, luego la traslación x/y), NO un polígono reemitido — gira tu contorno de entrada por rotation alrededor de su origen y suma (x, y) para reconstruir la colocación exactamente. rotation puede ser negativo; la reconstrucción es exacta con independencia del signo. ⚠️ density es el área del POLÍGONO colocado sobre el área del tablero usado — el llenado honesto, con las cavidades cóncavas contadas como vacías — y NO es comparable con el yieldPct de un empaquetador rectangular (que cuenta cada bounding box como macizo, por lo que da una cifra más alta para un resultado peor); la métrica comparable entre ambos es sheetCount sobre las mismas piezas. La distribución es determinista: fija options.seed para reproducirla. exclusions se devuelve en cada tablero para el renderizado.

POST /v1/optimize/nest — top level

enginestringWhich nesting engine ran: "lbf" or "sparrow".
engineVersionstringThe nesting engine's algorithm identity (jagua-rs revision + build hash). Versions independently of the rectangular engines.
contractVersionstringNest contract version, currently "1". Versions independently of the /v1/ rectangular contract — it is a different path and engine family.
deterministicbooleanAlways true — guaranteed by the pinned seed.
sheetsarrayOne entry per used sheet.
metricsobjectJob totals (see below).
unplacedarrayDemand that could not be placed — a plan-plus-warning, not an error.
warningsstring[]e.g. an engine substitution ("sparrow" served by "lbf"), unplaced parts, or a material with no matching stock.
materialsarrayOPEN-256 per-material rollup — present only when parts/stock carry material.
unmatchedMaterialsarrayParts whose material has no matching stock — present only when it happens.
importedobjectOPEN-262 — what the request’s source files contributed (fields above). ABSENT for a coordinates-only job, which is what keeps such a request byte-identical to before the feature existed.
svg, dxfstringThe achieved nest as an inline drawing — present ONLY when include contains that token. svg is self-contained (no external refs); dxf is R12/AC1009.
timingobject{ solveMs: number } — the solve time; environment-dependent.

sheets[] (nest)

w, hnumberPresent for rectangular sheets.
polygonnumber[][]Present for arbitrary-outline sheets instead of w/h.
pricenumber | nullThe stock row's price, or null.
densitynumberThis sheet's fill = placed polygon area / sheet area.
partsarrayPlacements on this sheet (see below).
exclusionsobject[]The zones that applied to this sheet, echoed for rendering.
materialstringPresent when the sheet carried a material (OPEN-256).
metaobjectEchoed from the stock row's meta (OPEN-224).

sheets[].parts[] (nest — placed)

namestringThe requested name, or the generated default.
sheetinteger0-based index into sheets.
x, ynumberTranslation, applied AFTER rotation about the part's origin.
rotationnumber⚠️ Degrees, applied about the part's own origin FIRST. A placed part carries this rigid transform, NOT a re-emitted polygon: rotate your input outline by rotation, then add (x, y) to reconstruct the placement exactly. May be negative; the reconstruction is exact regardless of sign.
materialstringThe material this copy nested from (OPEN-256).
metaobjectThe part's opaque passthrough (OPEN-224).

metrics (nest)

sheetCountintegerSheets used. Equals sheets.length. ⚠️ The honest, cross-comparable metric between nesting and the rectangular modes.
densitynumber⚠️ Placed POLYGON area / used sheet area — the honest fill (concave pockets count as empty). NOT comparable to a rectangular packer's yieldPct, which counts each bounding box as solid and so reads higher for a worse result.
placedintegerPart copies placed, after qty expansion.
totalintegerPart copies requested, after qty expansion.
totalPricenumberSum of used sheet prices.

unplaced[] (nest)

namestringThe part name.
qtyintegerHow many copies could not be placed.

rods[]

lengthnumberFULL bar length as supplied in stock.
pricenumber | nullPrice of the stock row, or null.
remainingnumberUSABLE off-cut left on this bar, 3 decimals — the kerf of the cut that frees it from the last piece is already deducted (OPEN-257), so it is the reclaimable length, not the raw gap. Reported even when below minOffcut.
partsarrayPlacements, in cutting order along the bar.
offcutsnumber[]At most one entry: [remaining] (the kerf-corrected usable off-cut, OPEN-257) when it is > 0 and ≥ minOffcut, else []. Absent when excluded via include.
metaobjectPresent only when the stock row carried meta — echoed from stock[].meta (OPEN-224). Applies to 1D and wood rods.

rods[].parts[]

namestringThe requested name, or the generated default.
posnumberOffset of the part’s NEAR end from the bar end that trim.start trims.
lengthnumberThe part length, as requested.
metaobjectPresent only when the part carried meta — echoed from parts[].meta (OPEN-224).

metrics (1D)

rodCountintegerBars used. Equals rods.length.
yieldPctnumberPlaced length ÷ total FULL bar length × 100, 2 decimals. Trim and kerf count as waste.
placedintegerPieces placed, after qty expansion.
totalintegerPieces requested, after qty expansion.
cutsintegerTotal crosscuts across all used bars — one per placed piece.
totalPricenumberSum of used bar prices, 2 decimals.
toleranceAcceptedCountintegerPieces that fitted only because options.tolerance allowed an overshoot.

unplaced[] (1D)

namestringThe part name.
lengthnumberAs requested.
qtyintegerHow many could not be placed.

Piezas desde un archivo (SVG · DXF)

Una pieza no tiene por qué llegar como coordenadas. Pon un documento SVG o DXF en parts[].source y el servidor extrae de él el contorno — y sus agujeros — con el mismo lector que usa la app de CutOptim cuando sueltas un dibujo sobre su modo Nesting. El archivo sustituye SOLO la geometría: qty, material, allowedRotations, minQuality, priority y meta se comportan exactamente igual que en una pieza polygon, así que una biblioteca de piezas que ya existe como archivos CAD no necesita un aplanador de curvas y arcos propio. Un source describe UNA pieza; un dibujo con varios componentes separados devuelve un 400 que te remite al endpoint de importación de abajo. La respuesta lleva entonces un bloque imported: cuántas filas vinieron de un archivo, cuántos vértices produjeron y qué unidades declararon esos archivos — informadas, nunca aplicadas, porque esta API no convierte nada.

No se almacena nada. Los bytes existen solo como cuerpo de la solicitud, se procesan en memoria y desaparecen cuando se escribe la respuesta: sin disco, sin base de datos, sin archivo temporal, sin línea de registro. Después no hay nada que borrar y nada queda retenido — la misma ausencia de estado que mantiene cualquier otro endpoint.

POST /v1/optimize/nest

{
  "parts": [
    {
      "source": {
        "content": "<svg xmlns=\"http://www.w3.org/2000/svg\" viewBox=\"0 0 400 200\">\n  <path d=\"M0 0 H120 V80 H0 Z\"/>\n  <path d=\"M40 30 H80 V50 H40 Z\"/>\n</svg>",
        "filename": "washer-plate.svg"
      },
      "qty": 4,
      "meta": { "erpId": "ART-8891" }
    }
  ],
  "stock": [{ "w": 2440, "h": 1220 }]
}

Los nombres de los campos y los tipos son el contrato, por lo que las tablas siguientes permanecen en inglés en todos los idiomas — un nombre de campo traducido documentaría una API que no existe.

{
  "imported": { "parts": 1, "vertices": 8, "units": [] },
  "parts_after_import": [
    {
      "name": "washer-plate",
      "polygon": [[0, 0], [120, 0], [120, 80], [0, 80]],
      "holes": [[[40, 30], [80, 30], [80, 50], [40, 50]]],
      "qty": 4,
      "meta": { "erpId": "ART-8891" }
    }
  ]
}

parts[].source — geometry from an SVG / DXF file

contentstringThe file TEXT — not base64, not a URL, not a multipart upload. Both formats are text documents, so they travel inside the JSON body like any other field. At most 4,000,000 characters per file and 8,000,000 across one request; over either is a 413.
format"svg" | "dxf"Omit and the server sniffs the content (an <svg tag ⇒ svg, otherwise dxf). An explicit value ALWAYS wins — including over a filename whose extension disagrees, which is the case worth setting it for.
filenamestringUsed for format detection AND as the part name when the row has no name of its own (extension stripped). It never touches a filesystem — there is no file on our side to name.
flattenTolerancenumberCurve and arc flattening tolerance, in the FILE’s own coordinate unit. Default 0.2. Larger = fewer vertices; this is the lever when a dense outline trips the 2,000-vertex per-ring cap.

imported — what the files contributed (response, absent without a source)

partsintegerPart ROWS whose geometry came from a file.
verticesintegerTotal vertices those files produced after flattening, outlines and holes together — the number to watch against the per-ring cap.
unitsstring[]⚠️ Units the files DECLARED (a DXF $INSUNITS), not units we applied. Empty when none declared one. More than one entry also raises a warning: mixing a millimetre drawing with an inch one produces a plan that validates and is physically wrong, and the server must not "fix" that by converting — no field in this API asserts a unit.

POST /v1/import/nest — un archivo, todos los contornos

Cuando un solo dibujo contiene varias piezas distintas, impórtalo primero: este endpoint devuelve cada contorno cerrado que contiene, empezando por el mayor, exactamente en la forma que espera una fila de parts[]. Pega los que necesites, añade tus propios qty y material y envía eso a /v1/optimize/nest. También es la manera de ver qué hay en un archivo antes de gastar un cálculo en él. Requiere una clave — aplanar geometría arbitraria es trabajo real de CPU, y la CPU anónima es un mal negocio — pero no reserva nada: tu cuota queda intacta y no vuelven cabeceras de rate limit, igual que al consultar un job.

POST /v1/import/nest
Authorization: Bearer co_live_…

{
  "content": "<svg xmlns=\"http://www.w3.org/2000/svg\" viewBox=\"0 0 400 200\">\n  <path d=\"M0 0 H120 V80 H0 Z\"/>\n  <path d=\"M40 30 H80 V50 H40 Z\"/>\n</svg>",
  "filename": "washer-plate.svg"
}
{
  "format": "svg",
  "parts": [
    {
      "name": "washer-plate",
      "polygon": [[0, 0], [120, 0], [120, 80], [0, 80]],
      "holes": [[[40, 30], [80, 30], [80, 50], [40, 50]]]
    }
  ],
  "vertices": 8,
  "warnings": [],
  "engineEnabled": true,
  "contractVersion": "1"
}

POST /v1/import/nest — request

contentstringThe file text. Required. Same 4,000,000-character cap as parts[].source.
format"svg" | "dxf"Omit to sniff; an explicit value wins over the filename extension.
filenamestringUsed for detection and to NAME the results: a file with one outline keeps the bare name, several are numbered "<name> 1", "<name> 2", …
flattenTolerancenumberAs on parts[].source. Default 0.2.

POST /v1/import/nest — response

format"svg" | "dxf"The parser that actually ran — the useful bit when you let the server sniff.
parts[]{ name, polygon, holes? }One entry per closed outline, LARGEST FIRST. Each is already in the shape a parts[] row wants: paste it in and add your own qty / material / allowedRotations. Deterministic — the same bytes always give the same vertices in the same order.
verticesintegerTotal vertices after flattening. Compare it against the 2,000-per-ring cap before you build a large request.
sourceUnit"mm" | "in"A DXF $INSUNITS declaration, when the file carries one. REPORTED, NEVER APPLIED. Absent for SVG (the format has no unit) and for a DXF that declares none.
warningsstring[]What the parser could not honour — an SVG transform= attribute (CAD part exports are flat, so we do not apply them), or geometry that never closed into a contour. Empty means the file was read whole.
engineEnabledbooleanWhether THIS deployment can also SOLVE a nest. Importing is pure parsing and works everywhere; where the nesting engine is not built in, /v1/optimize/nest answers 503 and this flag says so up front. Same field /v1/validate/nest carries.
contractVersionstringNest contract version. Currently "1".

Motores

  • heuristic (predeterminado) — el empaquetador de guillotina multiestrategia. Máximo aprovechamiento, cada distribución cortable en sierra, siempre un cutPlan completo.
  • balanced — un empaquetador MaxRects de anidado libre. Mucho más rápido en trabajos grandes (medido ~25× con 2000 piezas) a cambio de un pequeño coste de aprovechamiento, y sus distribuciones a menudo no son de guillotina (guillotineValid: false, cutPlan: null). No modela tolerance, minimizeCost, grainGroup, maxCutStages ni minimizeRotations — si estableces una, una advertencia te indica que se ignoró.
  • max — el nivel asíncrono de búsqueda en árbol (solo 2D): alcanza el óptimo probado en muchos más trabajos a costa de segundos-a-un-minuto por cálculo. Sigue siendo determinista y de guillotina. No devuelve un plan directamente — ver Trabajos asíncronos más abajo. Modela UN SOLO formato de stock a tamaño completo de tablero, con disponibilidad ilimitada y un patrón de guillotina fijo de 3 fases: una segunda fila de stock, trim, respectStock, material o grainGroup se rechazan con 400 antes de reservar una llamada; tolerance, minimizeCost, maxCutStages, minimizeRotations, firstCut y effort pasan pero se ignoran con un aviso, y un resultado max no informa de recortes. Envía esos trabajos al motor heuristic.

Trabajos asíncronos (engine = max)

Un cálculo max tarda de segundos a un minuto, así que POST /v1/optimize/2d con engine:"max" no devuelve un plan — devuelve 202 Accepted con un jobId, y la llamada se contabiliza en el momento del envío. Sondea GET /v1/jobs/{id} hasta que status sea "succeeded" (result contiene la misma respuesta 2D que devuelve un cálculo síncrono) o "failed" (error contiene el mensaje). El sondeo no consume cuota; solo ves tus propios trabajos. Donde el nivel no está habilitado en un despliegue, engine:"max" falla en cerrado con 503.

fieldtypemeaning
jobIdstringThe 202 body's id. Poll GET /v1/jobs/{id}.
statusstringqueuedrunningsucceeded | failed.
mode · enginestringAlways "2d" and "max".
pollAfterMsinteger202 only — suggested delay before the first poll.
resultobjectPresent once succeeded — the same shape as a synchronous 2D response.
errorstringPresent once failed — the reason.
quotaobject202 only — reserved, used and limit: the one call metered at submission, and where the ACCOUNT stands this month.
createdAtstringPoll only — when the job was submitted (ISO-8601).
finishedAtstring | nullPoll only — when the worker finished; null while queued or running.

Determinismo y versionado

Cada respuesta lleva engineVersion. El algoritmo es determinista, así que mejorarlo cambia la salida para la misma entrada — lo que es un cambio incompatible si cacheas. Fija el comportamiento enviando engine explícitamente y vigilando engineVersion; la versión de la ruta /v1/ solo cambia si cambia la estructura de la respuesta.

Cada motor se versiona de forma independiente, así que un cambio en uno nunca mueve la versión del otro.

Errores

400invalid_requestError de esquema. details.path apunta al campo problemático.
401unauthorizedClave de API ausente o desconocida.
402quota_exceededCuota mensual alcanzada. Retry-After da los segundos que faltan hasta que cambia el mes.
403key_revokedLa clave existe pero no puede usarse: ha sido revocada, o la suscripción a la Engine API de la cuenta ya no está activa. El campo message indica cuál de las dos.
404not_foundNo existe esa ruta — también es lo que obtienes por la ruta correcta con el método equivocado.
413too_largeEntrada por encima de un límite (ver Límites).
429busyMomentáneamente al máximo de capacidad. Retry-After en segundos — esto nunca cuenta contra tu cuota.
500internalError inesperado, o el backend de autenticación es inaccesible (las solicitudes fallan en cerrado).
503service_unavailableUn motor solicitado no puede servirse ahora mismo — el motor de nesting o el motor max asíncrono. Para max hay dos causas, y el campo message indica cuál: el nivel no está incluido en este despliegue, o sí lo está pero el worker que resuelve los trabajos no responde. Fail-closed antes de reservar una llamada, así que nunca cuesta nada.
504solve_timeoutEl cálculo superó su límite estricto de tiempo. En las rutas rectangulares lo impone el proxy; en /v1/optimize/nest el motor impone su propio presupuesto, más corto, y responde con solve_timeout en el envoltorio habitual.

Cuerpo del error

Cada error que produce el propio motor usa el mismo envoltorio. Ramifica sobre error, que es un código estable; nunca sobre message, cuya redacción puede cambiar entre versiones. details está presente en invalid_request, donde path nombra el campo problemático, y en too_large, donde max y got dan el límite y lo que enviaste.

HTTP/1.1 400 Bad Request
Content-Type: application/json; charset=utf-8

{
  "error": "invalid_request",
  "message": "parts[0].h: must be greater than 0",
  "details": { "path": "parts[0].h" }
}
HTTP/1.1 402 Payment Required
Retry-After: 41400
Content-Type: application/json; charset=utf-8

{ "error": "quota_exceeded", "message": "monthly quota exhausted" }
  • 402 y 429 llevan ambos una cabecera Retry-After en segundos. En 402 cuenta hacia atrás hasta el reinicio de la cuota a las 00:00 UTC del día 1 del mes siguiente; en 429 es una breve espera, y un 429 nunca consume cuota — la llamada reservada se devuelve.
  • Un fallo de enrutamiento responde con not_found, un código deliberadamente fuera de la lista anterior porque lo produce el enrutador y no el contrato de la API. Obtienes 404 y no 405 cuando la ruta es correcta pero el método es equivocado: los cuatro endpoints de optimización aceptan solo POST.
  • En las rutas rectangulares el 504 procede del proxy inverso, no del motor, así que su cuerpo es el del proxy y no este envoltorio JSON; dentro de los límites de entrada de abajo debería ser inalcanzable. /v1/optimize/nest es la excepción: ese cálculo es un subproceso con su propio presupuesto, mantenido deliberadamente por debajo del límite del proxy, así que allí un tiempo agotado sí usa este envoltorio, con el código solve_timeout.

Cabeceras de límite de tasa

Una llamada de optimización exitosa incluye X-RateLimit-Limit (el límite mensual de la CUENTA: todas las claves de la cuenta comparten uno) y X-RateLimit-Remaining (las llamadas que le quedan a la cuenta este mes, contando esta). Los envían únicamente los endpoints de optimización: el contador se reserva como parte de la autorización de un cálculo, así que /v1/usage y /v1/health no tienen nada que informar.

X-RateLimit-LimitintegerThe ACCOUNT’s monthly quota — the same number GET /v1/usage returns as limit, not a per-key cap (049).
X-RateLimit-RemainingintegerCalls left this month on the ACCOUNT, after this one.
Retry-AfterintegerSeconds to wait. Sent with 402 and 429 only.

GET /v1/usage

{
  "plan": "studio",
  "used": 137,
  "limit": 10000,
  "remaining": 9863,
  "periodEnd": "2026-08-01",
  "keyPrefix": "co_live_ab12",
  "contractVersion": "1"
}

Solo lectura: no consume una llamada y no envía cabeceras de límite de tasa. ⚠️ used y limit describen la CUENTA, no la clave con la que llamaste: cada clave activa de la cuenta consume de una única asignación compartida, así que crear más claves no crea más cuota. used cuenta el mes natural UTC en curso a través de todas ellas, remaining es limit menos used y nunca es negativo, periodEnd es el día de reinicio como una simple fecha YYYY-MM-DD, y keyPrefix es el prefijo de visualización no secreto de la clave con la que llamaste. La clave en sí nunca la devuelve ningún endpoint — solo se almacena su hash, así que una clave perdida se sustituye, no se recupera.

planstringTier slug frozen onto the key when it was created.
usedintegerCalls counted in the current UTC calendar month across EVERY key on the ACCOUNT — revoked keys included, so revoking a key cannot un-spend what it spent (053).
limitintegerThe ACCOUNT’s cap: the largest monthly quota frozen onto any of its ACTIVE keys (049). Not a per-key allowance — more keys do not add quota.
remainingintegerlimit − used, never negative.
periodEndstringReset day as YYYY-MM-DD — a date, not a timestamp.
keyPrefixstringNon-secret display prefix of the calling key.
contractVersionstringShape version. Currently "1".

GET /v1/health

{
  "status": "healthy",
  "service": "cutoptim-engine",
  "contractVersion": "1",
  "engineVersion": "1.0.0+10e0c941",
  "engines": ["heuristic", "balanced"],
  "modes": ["2d", "1d", "wood", "nest"],
  "nestEngines": ["lbf"],
  "maxEngines": ["max"],
  "uptimeSec": 16
}

Sin clave, sin cuota, sin base de datos. No toca deliberadamente nada con estado, de modo que una caída del almacén de claves no puede hacer que el servicio parezca muerto ante un orquestador. engines lista los ids que este despliegue acepta en engine, y engineVersion es la versión del motor predeterminado.

statusstringAlways "healthy" when the process answers.
servicestringAlways "cutoptim-engine".
contractVersionstringShape version. Currently "1".
engineVersionstringThe DEFAULT engine’s version, not a per-engine list.
enginesstring[]SYNCHRONOUS engine ids this deployment accepts in engine — ["heuristic","balanced"]. The async max tier is reported separately in maxEngines, never here.
modesstring[]Optimize paths this deployment serves: "2d", "1d", "wood", plus "nest" only where the nesting engine is built in. Endpoint discovery without reading this page.
nestEnginesstring[]Nesting engine ids this deployment can serve — ["lbf"] on the production API, [] where the Rust nesting stage is not built in.
maxEnginesstring[]The async tree-search tier — ["max"] where it is enabled, [] otherwise. Health never advertises a capability it cannot serve.
uptimeSecintegerWhole seconds since process start.

Límites

  • 2,000 piezas por solicitud (cantidad total, tras la expansión de qty)
  • 50 filas de material · cuerpo de la solicitud hasta 1 MB
  • 10 claves activas por cuenta — comparten UNA cuota mensual, así que las claves separan entornos e integraciones, no añaden asignación
  • 10 MB de cuerpo de la solicitud en las dos rutas nest que pueden llevar un dibujo (/v1/optimize/nest y /v1/import/nest); un archivo source como máximo 4.000.000 de caracteres, 8.000.000 por solicitud
  • la concurrencia está acotada en el servidor — una ráfaga recibe 429, nunca una cola lenta. Los endpoints validate sin clave tienen además un tope por dirección (429 con Retry-After); con clave nunca se limita así. Una cuenta puede tener 5 trabajos max en queued/running a la vez.

Especificación OpenAPI

Un documento OpenAPI 3.1 legible por máquina describe los doce endpoints, cada cuerpo de solicitud, cada estructura de respuesta y cada error. Apunta tu generador de clientes a él en lugar de transcribir esta página. El documento en sí está solo en inglés: se compone de tokens del contrato, y OpenAPI no tiene mecanismo de localización.

curl -s https://cutoptim.com/engine/openapi.json > cutoptim-engine.json

Abrir el documento OpenAPI 3.1 →

Recurso descargable
Engine API one-pager

A two-page summary of the Engine API — the four modes (2D, 1D, wood and true-shape nest), a request and response, determinism and pricing. Print-ready, with a QR back to the docs.

PDF2 pagesFree
Descargar el PDF