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/2d | ottimizzazione 2D per pannelli |
| POST | /v1/optimize/1d | ottimizzazione 1D / lineare (barre, profili, tubi) |
| GET | /v1/usage | consumo e quota del mese corrente della chiave chiamante |
| GET | /v1/health | liveness — nessuna chiave, nessun rate limit, nessun database |
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 } },
"engine": "heuristic",
"include": ["cutPlan", "offcuts"]
} Campi del pezzo
| w, h | obbligatori — dimensioni del pezzo |
| qty | default 1 — espanso lato server; conta nel limite di 2,000 pezzi |
| name | etichetta opzionale, riportata su ogni posizionamento |
| rotatable | default true — indica se il pezzo può essere ruotato di 90° |
| grainGroup | i membri di un gruppo restano sullo stesso pannello (corrispondenza venatura) |
| priority | da tagliare assolutamente: si aggiudica lo spazio sul pannello quando il materiale è limitato (con respectStock) |
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.
Opzioni
| kerf | spessore della lama (default 0) |
| tolerance | accetta tagli che sforano al massimo di questo valore |
| trim | rifilo del bordo per lato: left, right, top, bottom |
| firstCut | 'auto' | 'horizontal' | 'vertical' (default 'auto') |
| minimizeCost | ordina i candidati per prezzo totale del materiale invece che per numero di pannelli |
| respectStock | tratta la qty di ogni riga di materiale come limite rigido |
| minOffcut | riporta solo gli sfridi il cui lato corto raggiunge almeno questo valore |
| maxCutStages | limite di fasi della sezionatrice — un conteggio di fasi, non la profondità dell’albero |
| minimizeRotations | preferisce gli schemi che ruotano meno pezzi |
include riduce la risposta: passa ["cutPlan","offcuts"] (il default è entrambi), oppure ometti uno dei due per escluderlo dal payload.
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": 400, "h": 600, "rotated": true },
{ "name": "Door", "x": 413, "y": 10, "w": 400, "h": 600, "rotated": true },
{ "name": "Door", "x": 816, "y": 10, "w": 400, "h": 600, "rotated": true },
{ "name": "Door", "x": 1219, "y": 10, "w": 400, "h": 600, "rotated": true }
],
"offcuts": [
{ "x": 1622, "y": 10, "w": 818, "h": 600 },
{ "x": 10, "y": 613, "w": 2430, "h": 607 }
]
}
],
"metrics": {
"sheetCount": 1,
"yieldPct": 32.25,
"placed": 4,
"total": 4,
"cutLines": 5,
"sawPasses": 5,
"cutLength": 4830,
"totalPrice": 42
},
"unplaced": [],
"warnings": [],
"guillotineValid": true,
"timing": { "solveMs": 3.13 },
"cutPlan": [
{ "sheet": 0, "step": 1, "axis": "h", "pos": 610, "length": 2430, "stage": 1 },
{ "sheet": 0, "step": 2, "axis": "v", "pos": 410, "length": 600, "stage": 2 },
{ "sheet": 0, "step": 3, "axis": "v", "pos": 813, "length": 600, "stage": 2 },
{ "sheet": 0, "step": 4, "axis": "v", "pos": 1216, "length": 600, "stage": 2 },
{ "sheet": 0, "step": 5, "axis": "v", "pos": 1619, "length": 600, "stage": 2 }
]
} - 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.
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
| engine | string | Which engine ran: "heuristic" or "balanced". |
| engineVersion | string | Algorithm identity — package version + a content hash of the algorithm source. Moves automatically on any packer change, and independently per engine. |
| contractVersion | string | Shape version, matching the /v1/ in the path. Currently "1". |
| deterministic | boolean | Always true. Present so a client can assert the guarantee it relies on. |
| sheets | array | One entry per sheet used, in cutting order. |
| metrics | object | Aggregate numbers for the whole job. |
| cutPlan | array | null | The sawing plan. null when guillotineValid is false; absent when excluded via include. |
| unplaced | array | Parts that did not fit, aggregated by name + size. Empty when everything fitted. |
| warnings | string[] | Free-text notes about the plan. Do not parse — branch on unplaced, guillotineValid and metrics. |
| guillotineValid | boolean | True when the layout is producible with edge-to-edge cuts, i.e. on a panel saw. |
| timing.solveMs | number | Milliseconds inside the packer, 2 decimals. Excludes parsing, auth and queueing. |
sheets[]
| w, h | number | FULL stock dimensions, trim included — not the usable area. |
| price | number | null | Price of the stock row this sheet came from, or null if none was given. |
| parts | array | Placements on this sheet. |
| offcuts | array | Usable leftover rectangles, filtered by options.minOffcut. Absent (not empty) when excluded via include. |
sheets[].parts[]
| name | string | The requested name, or the generated default "Part <row>". |
| x, y | number | Top-left corner of the part, from the top-left corner of the sheet. |
| w, h | number | Dimensions AS PLACED — already swapped when rotated is true. No client-side swap needed. |
| rotated | boolean | True if the part was turned 90° from the requested w×h. Informational only. |
sheets[].offcuts[]
| x, y, w, h | number | A leftover rectangle, in the same coordinate system as the placements. |
metrics (2D)
| sheetCount | integer | Sheets used. Equals sheets.length. |
| yieldPct | number | Placed part area ÷ total FULL sheet area × 100, 2 decimals. Trim and kerf count as waste. |
| placed | integer | Pieces placed, after qty expansion. |
| total | integer | Pieces requested, after qty expansion. |
| cutLines | integer | Collinear cuts merged: same axis, same coordinate, same stage counted once — one fence setting. |
| sawPasses | integer | Every cut, one per strip crossed — how many separate passes the saw makes. |
| cutLength | number | Total distance sawn, 3 decimals. Same under either counting convention. In your unit. |
| totalPrice | number | Sum of the used sheets’ prices, 2 decimals. 0 when no stock row carried a price. |
unplaced[] (2D)
| name | string | The part name. |
| w, h | number | As requested. |
| qty | integer | How many of this part could not be placed. |
cutPlan[]
| sheet | integer | 0-based index into sheets (2D) or rods (1D). Named sheet in both. |
| step | integer | 1-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. |
| pos | number | The 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. |
| length | number | Distance the blade travels on this cut: the extent of the region crossed. Always 0 in 1D. |
| stage | integer | 1-based machine pass. Increments only when the axis flips relative to the parent cut. |
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. 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": 587,
"parts": [
{ "name": "Rail", "pos": 10, "length": 1200 },
{ "name": "Rail", "pos": 1213, "length": 1200 }
],
"offcuts": [587]
},
{
"length": 3000,
"price": 12.5,
"remaining": 587,
"parts": [
{ "name": "Rail", "pos": 10, "length": 1200 },
{ "name": "Rail", "pos": 1213, "length": 1200 }
],
"offcuts": [587]
},
{
"length": 3000,
"price": 12.5,
"remaining": 587,
"parts": [
{ "name": "Rail", "pos": 10, "length": 1200 },
{ "name": "Rail", "pos": 1213, "length": 1200 }
],
"offcuts": [587]
}
],
"metrics": {
"rodCount": 3,
"yieldPct": 80,
"placed": 6,
"total": 6,
"cuts": 6,
"totalPrice": 37.5,
"toleranceAcceptedCount": 0
},
"unplaced": [],
"warnings": [],
"timing": { "solveMs": 2.19 },
"cutPlan": [
{ "sheet": 0, "step": 1, "axis": "v", "pos": 1210, "length": 0, "stage": 1 },
{ "sheet": 0, "step": 2, "axis": "v", "pos": 2413, "length": 0, "stage": 1 },
{ "sheet": 1, "step": 1, "axis": "v", "pos": 1210, "length": 0, "stage": 1 },
{ "sheet": 1, "step": 2, "axis": "v", "pos": 2413, "length": 0, "stage": 1 },
{ "sheet": 2, "step": 1, "axis": "v", "pos": 1210, "length": 0, "stage": 1 },
{ "sheet": 2, "step": 2, "axis": "v", "pos": 2413, "length": 0, "stage": 1 }
]
} 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 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.
rods[]
| length | number | FULL bar length as supplied in stock. |
| price | number | null | Price of the stock row, or null. |
| remaining | number | Unused length left on this bar, 3 decimals. Reported even when below minOffcut. |
| parts | array | Placements, in cutting order along the bar. |
| offcuts | number[] | At most one entry: [remaining] when it is > 0 and ≥ minOffcut, else []. Absent when excluded via include. |
rods[].parts[]
| name | string | The requested name, or the generated default. |
| pos | number | Offset of the part’s NEAR end from the bar end that trim.start trims. |
| length | number | The part length, as requested. |
metrics (1D)
| rodCount | integer | Bars used. Equals rods.length. |
| yieldPct | number | Placed length ÷ total FULL bar length × 100, 2 decimals. Trim and kerf count as waste. |
| placed | integer | Pieces placed, after qty expansion. |
| total | integer | Pieces requested, after qty expansion. |
| cuts | integer | Total crosscuts across all used bars — one per placed piece. |
| totalPrice | number | Sum of used bar prices, 2 decimals. |
| toleranceAcceptedCount | integer | Pieces that fitted only because options.tolerance allowed an overshoot. |
unplaced[] (1D)
| name | string | The part name. |
| length | number | As requested. |
| qty | integer | How many could not be placed. |
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.
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
| 400 | invalid_request | Errore di schema. details.path indica il campo che ha causato il problema. |
| 401 | unauthorized | Chiave API mancante o sconosciuta. |
| 402 | quota_exceeded | Quota mensile raggiunta. Retry-After indica i secondi che mancano al rinnovo del mese. |
| 403 | key_revoked | La chiave esiste ma è stata revocata. |
| 404 | not_found | Route inesistente — è anche ciò che ottieni per un path corretto con il metodo sbagliato. |
| 413 | too_large | Input oltre un limite (vedi Limiti). |
| 429 | busy | Capacità momentaneamente esaurita. Retry-After in secondi — non viene mai conteggiato nella tua quota. |
| 500 | internal | Errore inatteso, oppure il backend di autenticazione non è raggiungibile (in tal caso le richieste vengono rifiutate — fail closed). |
| 504 | solve_timeout | La risoluzione ha superato il limite rigido di tempo (wall-clock) applicato dal proxy. |
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: entrambi gli endpoint di ottimizzazione accettano solo POST.
- 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.
Header del rate limit
Una chiamata di ottimizzazione riuscita riporta X-RateLimit-Limit (il limite mensile della chiave) e X-RateLimit-Remaining (le chiamate rimaste 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-Limit | integer | The key’s monthly quota. |
| X-RateLimit-Remaining | integer | Calls left this month, after this one. |
| Retry-After | integer | Seconds 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"
} Di sola lettura: non consuma una chiamata e non invia header di rate limit. plan e limit sono il piano e il limite congelati sulla chiave nel momento in cui è stata creata, quindi un successivo cambio di prezzo del prodotto non riscrive mai una chiave già attiva. used conta il mese di calendario corrente in UTC, remaining è limit meno used e non diventa mai negativo, periodEnd è il giorno di azzeramento come semplice data YYYY-MM-DD, e keyPrefix è il prefisso di visualizzazione — non segreto — della chiave con cui hai chiamato. La chiave stessa non viene mai restituita da alcun endpoint: ne viene memorizzato solo l’hash, quindi una chiave perduta si sostituisce, non si recupera.
| plan | string | Tier slug frozen onto the key when it was created. |
| used | integer | Calls counted in the current UTC calendar month. |
| limit | integer | The monthly cap frozen onto the key. |
| remaining | integer | limit − used, never negative. |
| periodEnd | string | Reset day as YYYY-MM-DD — a date, not a timestamp. |
| keyPrefix | string | Non-secret display prefix of the calling key. |
| contractVersion | string | Shape version. Currently "1". |
GET /v1/health
{
"status": "healthy",
"service": "cutoptim-engine",
"contractVersion": "1",
"engineVersion": "1.0.0+10e0c941",
"engines": ["heuristic", "balanced"],
"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.
| status | string | Always "healthy" when the process answers. |
| service | string | Always "cutoptim-engine". |
| contractVersion | string | Shape version. Currently "1". |
| engineVersion | string | The DEFAULT engine’s version, not a per-engine list. |
| engines | string[] | Engine ids this deployment accepts in engine. |
| uptimeSec | integer | Whole 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
- la concorrenza è limitata lato server — una raffica di richieste riceve 429, mai una coda lenta
Specifica OpenAPI
Un documento OpenAPI 3.1 leggibile dalle macchine descrive tutti e quattro gli endpoint, entrambi i corpi 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