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/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, 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)

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

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 invece che per numero di pannelli
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

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

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

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

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

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

Apri il documento OpenAPI 3.1 →