Salta al contenuto principale
← CutOptim Engine API

Riferimento API

Base URL https://api.cutoptim.com · versione del contratto v1

Crea una chiave nella tua dashboard, poi inviala come Authorization: Bearer <key>.

Specifica OpenAPI: /engine/openapi.json

Endpoint

POST/v1/optimize/2dottimizzazione 2D per pannelli
POST/v1/optimize/1dottimizzazione 1D / lineare (barre, profili, tubi)
POST/v1/optimize/woodottimizzazione del legno — 1D con abbinamento della sezione
POST/v1/optimize/nestnesting true-shape — poligoni irregolari su pannelli fissi, con zone di esclusione (laser / plasma / getto d’acqua)
POST/v1/validate/2dvalida una richiesta senza risolverla — gratis, nessuna chiave, nessuna quota (2d / 1d / wood / nest)
POST/v1/validate/1dvalida una richiesta senza risolverla — gratis, nessuna chiave, nessuna quota (2d / 1d / wood / nest)
POST/v1/validate/woodvalida una richiesta senza risolverla — gratis, nessuna chiave, nessuna quota (2d / 1d / wood / nest)
POST/v1/validate/nestvalida una richiesta senza risolverla — gratis, nessuna chiave, nessuna quota (2d / 1d / wood / nest)
GET/v1/jobs/{id}interroga un lavoro asincrono del motore max — restituisce il suo status e, una volta terminato, il piano (nessuna quota; solo i tuoi lavori)
POST/v1/import/nestlegge i contorni dei pezzi da un file SVG o DXF — richiede una chiave, non consuma quota
GET/v1/usageconsumo e quota dell’ACCOUNT nel mese corrente
GET/v1/healthliveness — nessuna chiave, nessun rate limit, nessun database

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.

Unità di misura

L’API è indipendente dall’unità di misura. Scegli un’unità — millimetri, pollici, qualsiasi cosa — usala per ogni numero che invii, e ogni numero che ricevi in risposta sarà nella stessa unità. Nulla viene convertito lato server e nessun nome di campo presuppone un’unità.

Vale per le dimensioni dei pezzi e del materiale, per kerf, tolerance, trim e minOffcut in ingresso, e per ogni coordinata, posizione, lunghezza residua, sfrido e lunghezza di taglio in uscita. Mescolare unità di misura all’interno di una stessa richiesta produce un piano che passa la validazione ed è fisicamente sbagliato, e il server non è in grado di rilevarlo.

Sistema di coordinate

L’origine è l’angolo in alto a sinistra del pannello: x cresce verso destra lungo la larghezza del pannello, y cresce verso il basso lungo l’altezza. x e y di un pezzo indicano il suo angolo in alto a sinistra, mentre w e h sono le dimensioni così come è posizionato — già scambiate quando rotated è true — quindi il rettangolo x, y, w, h è l’ingombro sul pannello senza ulteriori calcoli. I rettangoli degli sfridi usano lo stesso sistema.

Il rifilo sposta i posizionamenti: trim.left sposta ogni pezzo verso destra e trim.top lo sposta verso il basso, perché i pezzi vengono disposti all’interno dell’area utile e poi riportati con un offset sul pannello intero. trim.right e trim.bottom riducono l’area utile senza spostare l’origine. w e h di un pannello sono sempre le dimensioni complete del materiale, rifilo incluso — ed è anche per questo che il rifilo conta come scarto in yieldPct.

2D — richiesta

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"]
}

Campi del pezzo

w, hobbligatori — dimensioni del pezzo
qtydefault 1 — espanso lato server; conta nel limite di 2,000 pezzi
nameetichetta opzionale, riportata su ogni posizionamento
rotatabledefault true — indica se il pezzo può essere ruotato di 90°
grainGroupi membri di un gruppo restano sullo stesso pannello (corrispondenza venatura)
priorityda tagliare assolutamente: si aggiudica lo spazio sul pannello quando il materiale è limitato (con respectStock)
edgeBandingbordatura per lato: indica un riferimento di tipo su uno tra top / right / bottom / left (una stringa libera, un tuo codice) — la risposta somma i metri per riferimento. Solo metadato, non sposta mai un pezzo. Solo 2D
materialtag di materiale (una stringa libera, un tuo codice): i pezzi e il materiale con lo stesso valore vengono disposti solo tra loro. A differenza di edgeBanding, cambia lo schema. Assente = un unico insieme non specificato. Funziona in ogni modalità

Campi del materiale

w e h sono obbligatori. qty vale 1 per default ed è un limite rigido solo con respectStock. price è per pannello e determina totalPrice e la modalità costo. priority (booleano) fa consumare prima questo materiale; material (una stringa libera) lo limita ai pezzi dello stesso materiale.

Opzioni

kerfspessore della lama (default 0)
toleranceaccetta tagli che sforano al massimo di questo valore
trimrifilo del bordo per lato: left, right, top, bottom
firstCut'auto' | 'horizontal' | 'vertical' (default 'auto')
minimizeCostordina i candidati per prezzo totale del materiale più basso; con più misure di materiale a prezzo le combina (2D) o sceglie la lunghezza più economica (1D) per ridurre la spesa, anche se così si usa più materiale
respectStocktratta la qty di ogni riga di materiale come limite rigido
minOffcutriporta solo gli sfridi il cui lato corto raggiunge almeno questo valore
maxCutStageslimite di fasi della sezionatrice — un conteggio di fasi, non la profondità dell’albero
minimizeRotationspreferisce gli schemi che ruotano meno pezzi
effort'fast' | 'balanced' (default 'balanced'). Profondità di ricerca: 'balanced' esegue il best-of multi-strategia completo; 'fast' salta l'unica costosa ricerca di combinazioni di aree per pannello — molto più rapido sui lavori grandi al prezzo di qualche punto di utilizzo, sempre tagliabile a guillotina e mai più denso di 'balanced'. Sui lavori piccoli di solito è identico. Solo motore heuristic

include fa due cose. RESTRIZIONE — "cutPlan" e "offcuts" sono attivi per default; un array presente mantiene solo i token di restrizione che elenca (un array vuoto li esclude entrambi). EXPORT AGGIUNTIVO — "svg", "csv" e "dxf" aggiungono ciascuno quell’export alla risposta come STRINGA: svg un disegno dello schema 2D autonomo (solo 2D — una richiesta 1d/wood restituisce invece un avviso), dxf un disegno R12/AC1009 sui layer STOCK/PARTS/LABELS, csv una distinta di taglio. I token di export non influiscono sulla restrizione, quindi include:["svg"] aggiunge svg e — non nominando alcun token di restrizione — esclude cutPlan/offcuts; usa ["cutPlan","offcuts","svg"] per mantenere tutto e aggiungere 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.

L'opzione effort bilancia il tempo di calcolo con l'utilizzo. Ecco questo compromesso, misurato su un lavoro impegnativo — ogni cifra proviene dal packer reale.

Interruttore effort: utilizzo del materiale vs tempo di calcoloInterruttore effort: utilizzo del materiale vs tempo di calcolo. fast: 350 pannelli · 76.2% · ≈1.9 s. balanced: 330 pannelli · 80.8% · ≈4.8 s. max: riservato — più denso = ricerca più lenta. Sulla maggior parte dei lavori (più piccoli) i due sono identici; il divario si apre solo su lavori grandi come questo. balanced è il valore predefinito e non è mai più denso di quanto fast possa raggiungere.Interruttore effort: utilizzo del materiale vs tempo di calcoloUn lavoro impegnativo — circa 1.550 pezzi su un pannello da 2,07 × 5,6 m. Ogni cifra misurata sul packer reale.74%76%78%80%82%84%02 s4 s6 stempo di calcolo · più veloce →utilizzo del materiale · più denso ↑⇄ l'interruttore effort+4,6 p.p. utilizzo · −20 pannelli−5,7% materiale · ≈2,5× più lentofast350 pannelli · 76.2% · ≈1.9 s★ balanced · predefinito330 pannelli · 80.8% · ≈4.8 smaxriservatopiù denso =ricerca più lenta
Sulla maggior parte dei lavori (più piccoli) i due sono identici; il divario si apre solo su lavori grandi come questo. balanced è il valore predefinito e non è mai più denso di quanto fast possa raggiungere.

Ed è comunque veloce: anche i lavori di produzione più grandi — 2.000 pezzi e oltre — si risolvono in pochi secondi con il motore predefinito, ben entro il budget di tempo dell'API.

2D — risposta

{
  "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 unisce i tagli collineari (una sola regolazione della guida); sawPasses conta ogni singolo passaggio. Due misure oneste dello stesso piano, non la pretesa di coincidere con il conteggio di un concorrente.
  • guillotineValid / cutPlan — Quando uno schema non è tagliabile da bordo a bordo, guillotineValid è false e cutPlan è null. È un’informazione reale — quel piano non è realizzabile su una sezionatrice — non un errore.
  • unplaced + warnings — Un lavoro non soddisfacibile restituisce 200 con i pezzi elencati in unplaced e una nota in warnings. Un piano su cui puoi agire vale più di un codice di stato.
  • edgeBanding — Quando un pezzo porta edgeBanding, la risposta aggiunge un blocco edgeBanding: i metri lineari che ogni riferimento di tipo consuma, per pezzo e come totale dell’ordine. È geometria esatta, senza margine di scarto — l’officina aggiunge il suo — e presuppone input in millimetri (÷1000 per i metri). La chiave manca del tutto in un lavoro senza bordatura.
  • materials / unmatchedMaterials — Quando un pezzo o del materiale porta material, la risposta aggiunge materials (un riepilogo per materiale — material, sheetCount/rodCount, yieldPct, placed, total, totalPrice) e unmatchedMaterials (la domanda il cui materiale non ha stock corrispondente). Nel legno il materiale viaggia invece per ogni sezione. Entrambe le chiavi mancano in un lavoro senza materiale, che resta byte-identico.

Campi della risposta

I nomi e i tipi dei campi sono il contratto, quindi le tabelle qui sotto restano in inglese in tutte le lingue — un nome di campo tradotto documenterebbe un’API che non esiste.

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

Quando un pezzo porta edgeBanding, la risposta aggiunge un blocco edgeBanding: i metri lineari che ogni riferimento di tipo consuma, per pezzo e come totale dell’ordine. È geometria esatta, senza margine di scarto — l’officina aggiunge il suo — e presuppone input in millimetri (÷1000 per i metri). La chiave manca del tutto in un lavoro senza bordatura.

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

Quando un pezzo o del materiale porta material, la risposta aggiunge materials (un riepilogo per materiale — material, sheetCount/rodCount, yieldPct, placed, total, totalPrice) e unmatchedMaterials (la domanda il cui materiale non ha stock corrispondente). Nel legno il materiale viaggia invece per ogni sezione. Entrambe le chiavi mancano in un lavoro senza materiale, che resta byte-identico.

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 — che cosa significa fisicamente uno step

Uno step è un singolo movimento della lama e l’elenco segue l’ordine in cui puoi realmente segare: un taglio padre prima dei tagli interni al pezzo che ha prodotto, perché non puoi troncare una striscia prima di averla staccata. axis "h" significa che la lama si muove lungo x e separa l’alto dal basso; axis "v" significa che si muove lungo y e separa sinistra da destra. pos è il bordo della lama con la COORDINATA PIÙ BASSA — il valore y per "h", il valore x per "v" — non la sua linea centrale: il kerf occupa da pos a pos + kerf, quindi la lama asporta materiale nella direzione in cui cresce la coordinata, cioè verso il basso per "h" e verso destra per "v". Il materiale sul lato della coordinata più bassa — sopra la linea per "h", alla sua sinistra per "v" — è il pezzo che quel taglio libera. length indica quanto si muove la lama in quel singolo taglio: l’estensione della regione che attraversa, non la larghezza dell’intero pannello.

stage è un passaggio macchina. Parte da 1 e aumenta solo quando axis si inverte rispetto al taglio padre: sezionare un pannello in sei strisce è un unico passaggio, troncarle è il passaggio successivo. È il senso che la sezionatrice dà al “taglio a tre fasi”, non la profondità dell’albero di taglio, ed è ciò che maxCutStages vincola. sheet è un indice in sheets che parte da 0, e step riparte da 1 su ogni sheet invece di proseguire per tutto il lavoro.

cutPlan è null — non assente, non vuoto — ogni volta che guillotineValid è false: uno schema che non si può tagliare da bordo a bordo non ha alcuna sequenza di taglio da restituire. Manca invece del tutto dal payload se lo hai escluso da include.

1D — lineare

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 } }
}

I pezzi usano length (più qty, name, priority); il materiale usa length, qty e price. Sia i pezzi sia il materiale accettano anche un tag material facoltativo (il materiale anche priority) — material limita un pezzo allo stock dello stesso materiale, e la risposta aggiunge allora materials e unmatchedMaterials come in 2D. Le opzioni sono kerf, tolerance, trim.start / trim.end, minimizeCost, respectStock e minOffcut. La risposta restituisce rods invece di sheets, ognuna con i propri parts, la lunghezza residua e gli offcuts.

1D — risposta

{
  "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 sostituisce sheets e non esiste guillotineValid, perché un taglio lineare è sempre realizzabile. pos di ogni pezzo è la distanza della sua estremità più vicina dal capo della barra che trim.start rifila, quindi il primo pezzo inizia esattamente a trim.start e ogni pos successivo aggiunge un kerf. remaining è lo sfrido UTILIZZABILE: il kerf del taglio che lo stacca dall’ultimo pezzo è già dedotto, quindi è la lunghezza recuperabile, non il vuoto grezzo. Viene riportato su ogni rod anche quando è inferiore a minOffcut — minOffcut filtra soltanto l’array offcuts, che contiene al massimo una voce. Nel piano di taglio, sheet è l’indice della barra, axis è sempre "v", stage è sempre 1 e length è sempre 0: il troncaggio di una barra non ha alcuna distanza percorsa da riportare, ed è anche per questo che le metrics 1D contengono cuts ma non cutLength.

Legno — sezione

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 }
}

Il legno ha un’identità che una semplice barra non ha: un pezzo 50×150 non può uscire da materiale 50×100, per quanta lunghezza avanzi. Pezzi e materiale portano quindi sw e sh, i due lati della sezione, in qualsiasi ordine — 50×100 e 100×50 sono la stessa trave girata e vengono trattate come un’unica sezione. Il lavoro viene diviso per sezione, ogni sezione è abbinata al proprio materiale e risolta per conto suo, e una sola chiamata restituisce tutto. Anche i pezzi e il materiale accettano un tag material facoltativo (il materiale anche priority): con esso, un rovere 50×100 e un pino 50×100 diventano due sezioni separate, e ogni sezione porta il suo materiale. Le opzioni sono le stesse di 1D.

Legno — risposta

{
  "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 sostituisce rods al livello superiore: ogni voce è una sezione con i propri rods (identici nella forma a 1D) e le proprie metrics, così i valori per materiale ci sono già senza ricalcolarli. unmatched non ha equivalente in 1D — è la domanda per la cui sezione non hai fornito alcun materiale, un problema diverso da unplaced (pezzi che avevano materiale e non sono entrati) e con una soluzione diversa, per questo i due non si mescolano mai. metrics.total conta ogni pezzo richiesto, compresi quelli in unmatched. Nel piano di taglio ogni passo indica anche la sua section, e sheet è l’indice della barra ALL’INTERNO di quella sezione, non un contatore globale del lavoro.

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.

Nesting true-shape

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 }]
    }
  ]
}

Le tre modalità qui sopra dispongono rettangoli. POST /v1/optimize/nest dispone POLIGONI ARBITRARI: un pezzo è un contorno (polygon, con holes interni facoltativi), non una larghezza×altezza, quindi i pezzi si incastrano nelle rientranze concave gli uni degli altri e l’aria delle tacche che un bounding box spreca viene recuperata — su un lavoro rappresentativo, 6 pannelli dove gli stessi pezzi per bounding box ne richiedono 9. È una classe di algoritmo diversa (un motore di collisione geometrico, non il packer guillotine), per il taglio a laser, plasma e getto d’acqua. Con esso arrivano due cose che l’API rettangolare non può esprimere: le zone di esclusione per pannello (stock[].exclusions — un difetto, l’ingombro di un morsetto, un’area pre-stampata; una zona con quality 0 è una regione vietata a qualsiasi pezzo) e gli holes true-shape. La partizione per material e il passaggio di meta funzionano come ovunque. L’esempio qui sotto è una chiamata reale acquisita — otto pezzi su un unico pannello con un angolo danneggiato escluso.

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 — risposta

{
  "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 }
}

Ogni voce in sheets è un pannello utilizzato; un pezzo posizionato porta la trasformazione rigida (rotation in gradi, poi traslazione x/y), NON un poligono ri-emesso — ruota il tuo contorno di input di rotation attorno alla sua origine e aggiungi (x, y) per ricostruire esattamente il posizionamento. rotation può essere negativo; la ricostruzione è esatta indipendentemente dal segno. ⚠️ density è l’area del POLIGONO posizionato sull’area del pannello utilizzato — il riempimento onesto, con le rientranze concave contate come vuote — e NON è confrontabile con lo yieldPct di un packer rettangolare (che conta ogni bounding box come pieno, quindi risulta più alto per un risultato peggiore); la metrica confrontabile tra i due è sheetCount sugli stessi pezzi. Lo schema è deterministico: imposta options.seed per riprodurlo. exclusions viene riportato su ogni pannello per il rendering.

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.

Pezzi da un file (SVG · DXF)

Un pezzo non deve per forza arrivare come coordinate. Metti un documento SVG o DXF in parts[].source e il server ne estrae il contorno — e i suoi fori — con lo stesso parser che l'app CutOptim usa quando trascini un disegno sulla sua modalità Nesting. Il file sostituisce SOLO la geometria: qty, material, allowedRotations, minQuality, priority e meta si comportano esattamente come su un pezzo polygon, quindi una libreria di pezzi che esiste già come file CAD non richiede un tuo appiattitore di curve e archi. Una source descrive UN pezzo; un disegno che contiene più componenti separati produce un 400 che rimanda all'endpoint di import qui sotto. La risposta porta allora un blocco imported: quante righe provengono da un file, quanti vertici hanno prodotto e quali unità quei file dichiaravano — riportate, mai applicate, perché questa API non converte nulla.

Nulla viene memorizzato. I byte esistono solo come corpo della richiesta, vengono analizzati in memoria e spariscono quando la risposta è scritta: nessun disco, nessun database, nessun file temporaneo, nessuna riga di log. Non c'è nulla da cancellare dopo e nulla resta — la stessa assenza di stato che ogni altro endpoint mantiene.

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 }]
}

I nomi e i tipi dei campi sono il contratto, quindi le tabelle qui sotto restano in inglese in tutte le lingue — un nome di campo tradotto documenterebbe un’API che non esiste.

{
  "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 file, tutti i contorni

Quando un singolo disegno contiene più pezzi diversi, importalo prima: questo endpoint restituisce ogni contorno chiuso che contiene, dal più grande, esattamente nella forma che una riga parts[] si aspetta. Incolla quelli che ti servono, aggiungi il tuo qty e material e invia il risultato a /v1/optimize/nest. È anche il modo per vedere cosa c'è in un file prima di spendere una risoluzione. Richiede una chiave — appiattire geometria arbitraria è lavoro di CPU reale, e la CPU anonima è un cattivo affare — ma non riserva nulla: la tua quota resta intatta e non tornano header di rate limit, esattamente come quando interroghi 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".

Motori

  • heuristic (predefinito) — l’algoritmo di posizionamento guillotine multi-strategia. Utilizzo massimo, ogni schema tagliabile alla sega, sempre un cutPlan completo.
  • balanced — un algoritmo di posizionamento MaxRects a nesting libero. Molto più rapido sui lavori grandi (misurato ~25× su 2.000 pezzi) a fronte di una piccola perdita di utilizzo, e i suoi schemi spesso non sono guillotine (guillotineValid: false, cutPlan: null). Non modella tolerance, minimizeCost, grainGroup, maxCutStages o minimizeRotations — se ne imposti uno, un avviso ti segnala che è stato ignorato.
  • max — il livello di ricerca ad albero asincrono (solo 2D): raggiunge l’ottimo dimostrato su molti più lavori, al costo di secondi-fino-a-un-minuto per risoluzione. Sempre deterministico e tagliabile a guillotina. Non restituisce un piano direttamente — vedi Lavori asincroni qui sotto. Modella UN SOLO formato di stock a piena dimensione con disponibilità illimitata e uno schema a guillotina fisso a 3 fasi: una seconda riga di stock, trim, respectStock, material o grainGroup vengono rifiutati con 400 prima che una chiamata sia riservata; tolerance, minimizeCost, maxCutStages, minimizeRotations, firstCut ed effort passano ma vengono ignorati con un avviso, e un risultato max non riporta ritagli. Manda quei lavori al motore heuristic.

Lavori asincroni (engine = max)

Una risoluzione max richiede da secondi a un minuto, quindi POST /v1/optimize/2d con engine:"max" non restituisce un piano — restituisce 202 Accepted con un jobId, e la chiamata viene conteggiata all’invio. Interroga GET /v1/jobs/{id} finché status è "succeeded" (result contiene la stessa risposta 2D che restituisce una risoluzione sincrona) oppure "failed" (error contiene il messaggio). L’interrogazione non consuma quota; vedi solo i tuoi lavori. Dove il livello non è abilitato in un deployment, engine:"max" fallisce in modo chiuso (fail closed) 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 e versionamento

Ogni risposta contiene engineVersion. L’algoritmo è deterministico, quindi migliorarlo cambia l’output a parità di input — il che è una breaking change se usi la cache. Fissa il comportamento inviando engine in modo esplicito e tenendo d’occhio engineVersion; la versione nel path /v1/ cambia solo se cambia la struttura della risposta.

Ogni motore ha un versionamento indipendente, quindi una modifica a uno non sposta mai la versione dell’altro.

Errori

400invalid_requestErrore di schema. details.path indica il campo che ha causato il problema.
401unauthorizedChiave API mancante o sconosciuta.
402quota_exceededQuota mensile raggiunta. Retry-After indica i secondi che mancano al rinnovo del mese.
403key_revokedLa chiave esiste ma non può essere usata: è stata revocata, oppure l’abbonamento Engine API dell’account non è più attivo. Il campo message dice quale dei due.
404not_foundRoute inesistente — è anche ciò che ottieni per un path corretto con il metodo sbagliato.
413too_largeInput oltre un limite (vedi Limiti).
429busyCapacità momentaneamente esaurita. Retry-After in secondi — non viene mai conteggiato nella tua quota.
500internalErrore inatteso, oppure il backend di autenticazione non è raggiungibile (in tal caso le richieste vengono rifiutate — fail closed).
503service_unavailableUn motore richiesto non può essere servito ora — il motore di nesting o il motore max asincrono. Per max le cause sono due, e il campo message dice quale: il livello non è incluso in questo deployment, oppure è incluso ma il worker che risolve i lavori non risponde. Fail-closed prima che una chiamata sia riservata, quindi non costa mai nulla.
504solve_timeoutLa risoluzione ha superato il proprio limite rigido di tempo. Sui percorsi rettangolari lo impone il proxy; su /v1/optimize/nest è il motore a imporre il proprio budget, più breve, e risponde con solve_timeout nella busta normale.

Corpo dell’errore

Ogni errore prodotto dal motore stesso usa la stessa struttura. Basa la logica condizionale su error, che è un codice stabile; mai su message, la cui formulazione può cambiare da una release all’altra. details è presente su invalid_request, dove path indica il campo che ha causato il problema, e su too_large, dove max e got riportano il limite e ciò che hai inviato.

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" }
  • Sia 402 sia 429 riportano un header Retry-After in secondi. Su 402 conta alla rovescia fino all’azzeramento della quota alle 00:00 UTC del 1° del mese successivo; su 429 è una breve attesa prima di riprovare, e un 429 non consuma mai quota — la chiamata riservata viene restituita.
  • Un errore di routing risponde con not_found, un codice volutamente fuori dall’elenco qui sopra perché lo produce il router e non il contratto dell’API. Ottieni 404 e non 405 quando il path è corretto ma il metodo è sbagliato: tutti e quattro gli endpoint di ottimizzazione accettano solo POST.
  • Sui percorsi rettangolari il 504 arriva dal reverse proxy, non dal motore, quindi il suo corpo è quello del proxy e non questa struttura JSON; entro i limiti di input indicati sotto non dovrebbe mai verificarsi. /v1/optimize/nest è l’eccezione: quella risoluzione è un sottoprocesso con un proprio budget, tenuto deliberatamente sotto il limite del proxy, quindi lì un timeout usa questa struttura, con il codice solve_timeout.

Header del rate limit

Una chiamata di ottimizzazione riuscita riporta X-RateLimit-Limit (il limite mensile dell’ACCOUNT: tutte le chiavi dell’account ne condividono uno solo) e X-RateLimit-Remaining (le chiamate rimaste all’account per il mese in corso, già al netto di questa). Vengono inviati solo dagli endpoint di ottimizzazione: il contatore viene riservato come parte dell’autorizzazione di una risoluzione, quindi /v1/usage e /v1/health non hanno nulla da riportare.

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"
}

Sola lettura: non consuma una chiamata e non invia header di rate limit. ⚠️ used e limit descrivono l’ACCOUNT, non la chiave con cui hai chiamato: ogni chiave attiva dell’account attinge a un unico monte condiviso, quindi creare più chiavi non crea più quota. used conta il mese solare UTC in corso su tutte, remaining è limit meno used e non va mai sotto zero, periodEnd è il giorno di azzeramento come semplice data YYYY-MM-DD e keyPrefix è il prefisso non segreto della chiave usata. La chiave stessa non viene mai restituita da alcun endpoint — se ne memorizza solo l’hash, quindi una chiave persa si sostituisce, non si 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
}

Nessuna chiave, nessuna quota, nessun database. Non tocca deliberatamente nulla che abbia stato, così un guasto nell’archivio delle chiavi non può far sembrare il servizio morto a un orchestratore. engines elenca gli id che questo deployment accetta in engine, mentre engineVersion è la versione del motore predefinito.

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.

Limiti

  • 2,000 pezzi per richiesta (quantità totale, dopo l’espansione di qty)
  • 50 righe di materiale · corpo della richiesta fino a 1 MB
  • 10 chiavi attive per account — condividono UN’UNICA quota mensile: le chiavi separano ambienti e integrazioni, non aumentano il monte
  • 10 MB di corpo della richiesta sui due percorsi nest che possono trasportare un disegno (/v1/optimize/nest e /v1/import/nest); un file source al massimo 4.000.000 di caratteri, 8.000.000 per richiesta
  • la concorrenza è limitata lato server — un burst riceve 429, mai una coda lenta. Gli endpoint validate senza chiave hanno inoltre un tetto per indirizzo (429 con Retry-After); con una chiave non si viene mai limitati così. Un account può tenere 5 lavori max in queued/running contemporaneamente.

Specifica OpenAPI

Un documento OpenAPI 3.1 leggibile dalle macchine descrive tutti e dodici gli endpoint, ogni corpo di richiesta, ogni struttura di risposta e ogni errore. Fai puntare lì il generatore del tuo client, invece di trascrivere questa pagina. Il documento in sé è solo in inglese: è composto da token del contratto, e OpenAPI non prevede alcun meccanismo di localizzazione.

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

Apri il documento OpenAPI 3.1 →

Risorsa scaricabile
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
Scarica il PDF