API-referencia
Alap-URL https://api.cutoptim.com · szerződésverzió v1
Készíts kulcsot az irányítópultodon, majd így küldd el: Authorization: Bearer <key>.
OpenAPI-specifikáció: /engine/openapi.json
Végpontok
| POST | /v1/optimize/2d | 2D lapoptimalizálás |
| POST | /v1/optimize/1d | 1D / lineáris optimalizálás (rúd, profil, cső) |
| GET | /v1/usage | a hívó kulcs aktuális hónapra eső felhasználása és kvótája |
| GET | /v1/health | életjel — nincs kulcs, nincs kéréslimit, nincs adatbázis |
Mértékegységek
Az API mértékegység-független. Válassz egy egységet — milliméter, hüvelyk, bármi —, azt használd minden beküldött számhoz, és minden visszakapott szám ugyanabban az egységben lesz. A szerver semmit nem konvertál, és egyetlen mezőnév sem állít egységet.
Ez érvényes a darab- és alapanyag-méretekre, a kerf, tolerance, trim és minOffcut értékekre a bemeneten, valamint minden koordinátára, pozícióra, fennmaradó hosszra, maradékra és vágáshosszra a kimeneten. Ha egy kérésen belül egységeket keversz, olyan tervet kapsz, amely átmegy az ellenőrzésen, és fizikailag hibás — a szerver ezt nem tudja észlelni.
Koordináta-rendszer
Az origó a tábla bal felső sarka: az x a tábla szélessége mentén jobbra, az y a magassága mentén lefelé növekszik. Egy darab x és y értéke a bal felső sarka, a w és h pedig az elhelyezés szerinti mérete — ha a rotated true, akkor már felcserélve —, így az x, y, w, h téglalap minden további számolás nélkül a táblán elfoglalt helyet adja. A maradék-téglalapok ugyanezt a rendszert használják.
A széllevágás elmozdítja az elhelyezéseket: a trim.left minden darabot jobbra, a trim.top minden darabot lefelé tol, mert a darabok a hasznos területen belülre kerülnek, majd azt toljuk vissza a teljes táblára. A trim.right és a trim.bottom a hasznos területet szűkíti, az origót nem mozdítja el. Egy tábla w és h értéke mindig a teljes alapanyag-méret, a széllevágással együtt — és épp ezért számít a széllevágás hulladéknak a yieldPct-ben.
2D — kérés
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"]
} A darab mezői
| w, h | kötelező — a darab méretei |
| qty | alapérték 1 — a szerver bontja ki; beleszámít a 2,000 darabos plafonba |
| name | opcionális megnevezés, minden elhelyezésnél visszaadjuk |
| rotatable | alapérték true — elforgatható-e a darab 90°-kal |
| grainGroup | egy csoport tagjai egy táblán maradnak (erezet-egyeztetés) |
| priority | kötelezően kivágandó: korlátozott készlet esetén elsőbbséget kap a táblán (respectStock mellett) |
Az alapanyag mezői
A w és a h kötelező. A qty alapértéke 1, és csak respectStock mellett kemény korlát. A price táblánkénti ár, ez határozza meg a totalPrice-t és a költség-módot.
Beállítások
| kerf | fűrészlap-vastagság (alapérték 0) |
| tolerance | legfeljebb ennyivel túllépő vágásokat is elfogad |
| trim | oldalankénti széllevágás: left, right, top, bottom |
| firstCut | 'auto' | 'horizontal' | 'vertical' (alapérték 'auto') |
| minimizeCost | a jelölteket a teljes anyagár szerint rangsorolja, nem a táblák száma szerint |
| respectStock | minden alapanyag-sor qty értékét kemény korlátként kezeli |
| minOffcut | csak azokat a maradékokat jelenti, amelyek rövidebb oldala legalább ennyi |
| maxCutStages | panelfűrész fázis-korlát — fázisszám, nem nyers famélység |
| minimizeRotations | a kevesebb darabot elforgató elrendezéseket részesíti előnyben |
Az include szűkíti a választ: adj meg ["cutPlan","offcuts"] értéket (alapértelmezésben mindkettő), vagy hagyd ki bármelyiket, ha nem kell a válaszban.
2D — válasz
{
"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 — A cutLines összevonja az egy vonalba eső vágásokat (egy ütköző-beállítás); a sawPasses minden áthaladást számol. Ugyanannak a tervnek két őszinte mértéke — nem azt állítjuk, hogy bármely versenytárs számával egyezik.
- guillotineValid / cutPlan — Ha egy elrendezés nem vágható éltől élig, a guillotineValid false, a cutPlan pedig null. Ez valódi információ — panelfűrészen nem gyártható le —, nem hiba.
- unplaced + warnings — A teljesíthetetlen munka 200-at ad, a ki nem fért darabokkal az unplaced listában és egy megjegyzéssel a warnings-ban. Egy terv, amivel dolgozni tudsz, többet ér egy státuszkódnál.
Válaszmezők
A mezőnevek és a típusok a szerződés részei, ezért az alábbi táblázatok minden nyelven angolul maradnak — egy lefordított mezőnév olyan API-t dokumentálna, amely nem létezik.
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 — mit jelent fizikailag egy lépés
Egy step egy fűrészlap-mozgatás, és a lista abban a sorrendben áll, ahogy tényleg fűrészelni lehet: a szülővágás előbb szerepel, mint az általa létrehozott darabon belüli vágások, mert egy csíkot nem lehet keresztbe vágni, amíg le nem szabtad. Az axis "h" azt jelenti, hogy a fűrészlap az x mentén halad, és a felső részt választja el az alsótól; az axis "v" azt, hogy az y mentén halad, és a bal oldalt választja el a jobbtól. A pos a fűrészlap KISEBB KOORDINÁTÁJÚ éle — "h" esetén az y, "v" esetén az x érték —, nem a középvonala: a fűrészrés a pos és a pos + kerf között foglal helyet, tehát a fűrészlap abba az irányba eszik, amerre a koordináta növekszik — "h" esetén lefelé, "v" esetén jobbra. A vonal kisebb koordinátájú oldalán lévő anyag — "h" esetén a vonal fölött, "v" esetén tőle balra — az a darab, amelyet ez a vágás leválaszt. A length az, hogy a fűrészlap azon az egy vágáson milyen messzire jut: az általa átvágott terület kiterjedése, nem a teljes tábla szélessége.
A stage egy gépi áthaladás. 1-nél kezdődik, és csak akkor lép eggyel, ha az axis a szülővágáshoz képest átfordul, tehát egy tábla hat csíkra szabása egy fázis, a csíkok keresztbe vágása pedig a következő. Ez a „háromfázisú vágás” panelfűrészes értelme, nem a vágásfa mélysége, és ezt korlátozza a maxCutStages. A sheet a sheets tömb 0-alapú indexe, a step pedig minden táblán újraindul 1-től, nem fut végig az egész munkán.
A cutPlan értéke null — nem hiányzó, nem üres —, valahányszor a guillotineValid false: egy éltől élig nem vágható elrendezéshez nincs visszaadható vágási sorrend. A válaszból teljesen kimarad, ha nem szerepeltetted az include-ban.
1D — lineáris
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 } }
} A darabok length értéket kapnak (plusz qty, name, priority); az alapanyag length, qty és price értéket. A beállítások: kerf, tolerance, trim.start / trim.end, minimizeCost, respectStock és minOffcut. A válasz sheets helyett rods elemeket ad, mindegyikben a rá eső darabokkal, a fennmaradó hosszal és a maradékokkal.
1D — válasz
{
"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 }
]
} A rods váltja fel a sheets-et, és nincs guillotineValid, mert a lineáris vágás mindig legyártható. Minden darab pos értéke a közelebbi végének eltolása attól a rúdvégtől, amelyet a trim.start levág, tehát az első darab pontosan a trim.start értékénél kezdődik, és minden következő pos eggyel több kerf-fel nő. A remaining minden rúdnál szerepel, akkor is, ha kisebb, mint a minOffcut — a minOffcut csak az offcuts tömböt szűri, amelyben legfeljebb egy elem van. A vágástervben a sheet a rúd indexe, az axis mindig "v", a stage mindig 1, a length pedig mindig 0: egy rúd keresztvágásának nincs jelenthető úthossza, és épp ezért ad az 1D metrics cuts értéket, cutLength-et viszont nem.
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. |
Motorok
- heuristic (alapértelmezett) — a guillotine, több stratégiát végigpróbáló pakoló. Legjobb kihasználás, minden elrendezés fűrésszel vágható, mindig teljes cutPlan.
- balanced — MaxRects alapú, szabad beágyazású pakoló. Nagy munkákon sokkal gyorsabb (2000 darabon mérve ~25×) kissé rosszabb kihasználás árán, és az elrendezései gyakran nem guillotine-vághatók (guillotineValid: false, cutPlan: null). Nem modellezi a tolerance, minimizeCost, grainGroup, maxCutStages és minimizeRotations beállítást — ha megadod valamelyiket, figyelmeztetés jelzi, hogy nem vettük figyelembe.
Determinizmus és verziózás
Minden válasz tartalmazza az engineVersion értéket. Az algoritmus determinisztikus, ezért a továbbfejlesztése megváltoztatja a kimenetet ugyanarra a bemenetre — ami törő változás, ha cache-elsz. A viselkedést úgy szögezheted le, hogy az engine paramétert kifejezetten megadod, és figyeled az engineVersion értékét; az útvonal-verzió (/v1/) csak akkor változik, ha a válasz szerkezete változik.
A motorok verziói egymástól függetlenek, így az egyik változása soha nem mozdítja el a másik verzióját.
Hibák
| 400 | invalid_request | Sémahiba. A details.path a hibás mezőre mutat. |
| 401 | unauthorized | Hiányzó vagy ismeretlen API-kulcs. |
| 402 | quota_exceeded | A havi kvóta elfogyott. A Retry-After megadja, hány másodperc van a hónap átfordulásáig. |
| 403 | key_revoked | A kulcs létezik, de vissza lett vonva. |
| 404 | not_found | Nincs ilyen útvonal — ezt kapod akkor is, ha a helyes útvonalat rossz metódussal hívod. |
| 413 | too_large | A bemenet átlép egy korlátot (lásd a Korlátokat). |
| 429 | busy | Épp betelt a kapacitás. A Retry-After másodpercben — ez soha nem számít bele a kvótádba. |
| 500 | internal | Váratlan hiba, vagy nem érhető el az azonosítási backend (a kérések fail-closed módon elbuknak). |
| 504 | solve_timeout | A számítás túllépte a kemény falióra-korlátot (a proxy kényszeríti ki). |
A hiba törzse
Minden hiba, amelyet maga a motor állít elő, ugyanezt a borítékot használja. Az error értékére ágazz, mert az stabil kód; a message-re soha, mert a szövegezése verziók között változhat. A details az invalid_request esetén van jelen, ahol a path nevezi meg a hibás mezőt, és a too_large esetén, ahol a max és a got adja meg a korlátot, illetve azt, amit beküldtél.
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" } - A 402 és a 429 is Retry-After fejlécet visz, másodpercben. A 402-nél a kvóta visszaállásáig számol vissza, ami a következő hónap 1-jén 00:00 UTC; a 429-nél rövid várakozás, és a 429 soha nem fogyaszt kvótát — a lefoglalt hívást visszaadjuk.
- Az útvonal-hiba not_found kóddal válaszol; ez a kód szándékosan nincs a fenti listában, mert a router állítja elő, nem az API-szerződés. 404-et kapsz és nem 405-öt, ha az útvonal helyes, de a metódus nem: mindkét optimalizáló végpont csak POST-ot fogad.
- Az 504 a reverse proxytól jön, nem a motortól, ezért a törzse a proxyé, nem ez a JSON-boríték. Az alábbi bemeneti korlátokon belül elérhetetlen kell lennie.
Kéréslimit-fejlécek
A sikeres optimalizáló hívás X-RateLimit-Limit (a kulcs havi plafonja) és X-RateLimit-Remaining (a hónapból hátralévő hívások száma, ezt a hívást már leszámítva) fejlécet visz. Ezeket kizárólag az optimalizáló végpontok küldik: a számláló a számítás engedélyezésének részeként foglalódik le, így a /v1/usage és a /v1/health nem tud mit jelenteni.
| 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"
} Csak olvasás: nem fogyaszt hívást, és nem küld kéréslimit-fejlécet. A plan és a limit az a szint és plafon, amely a kulcs létrehozásakor rögzült rá, így a termék későbbi átárazása soha nem írja át egy élő kulcsot. A used az aktuális UTC naptári hónapot számolja, a remaining a limit mínusz a used, és soha nem lesz negatív, a periodEnd a visszaállás napja egyszerű YYYY-MM-DD dátumként, a keyPrefix pedig a hívó kulcs nem titkos, megjelenítésre szánt előtagja. Magát a kulcsot egyetlen végpont sem adja vissza — csak a hash-e van eltárolva, tehát az elveszett kulcsot nem visszaszerezni, hanem lecserélni kell.
| 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
} Nincs kulcs, nincs kvóta, nincs adatbázis. Szándékosan nem nyúl semmilyen állapothoz, így a kulcstároló kiesése nem tudja halottnak mutatni a szolgáltatást egy orchestrator felé. Az engines felsorolja azokat az id-ket, amelyeket ez a telepítés elfogad az engine paraméterben, az engineVersion pedig az alapértelmezett motor verziója.
| 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. |
Korlátok
- 2,000 darab kérésenként (összes mennyiség, a qty kibontása után)
- 50 alapanyag-sor · a kéréstörzs legfeljebb 1 MB
- 10 aktív kulcs fiókonként
- a párhuzamosság szerver-oldalon korlátozott — a burst 429-et kap, nem kerül lassú várólistára
OpenAPI-specifikáció
Egy gépi olvasásra szánt OpenAPI 3.1 dokumentum leírja mind a négy végpontot, mindkét kéréstörzset, minden válaszszerkezetet és minden hibát. Erre állítsd rá a kliens-generátorodat, ahelyett hogy ezt a lapot írnád át kézzel. Maga a dokumentum csak angol nyelvű: szerződés-tokenekből áll, és az OpenAPI-nak nincs lokalizációs mechanizmusa.
curl -s https://cutoptim.com/engine/openapi.json > cutoptim-engine.json