Reference API
Základní URL https://api.cutoptim.com · verze kontraktu v1
Klíč si vytvořte na své nástěnce a pak jej posílejte jako Authorization: Bearer <key>.
Specifikace OpenAPI: /engine/openapi.json
Endpointy
| POST | /v1/optimize/2d | optimalizace 2D desek |
| POST | /v1/optimize/1d | optimalizace 1D / lineárního materiálu (tyče, profily, trubky) |
| GET | /v1/usage | spotřeba a kvóta volajícího klíče v aktuálním měsíci |
| GET | /v1/health | kontrola dostupnosti — bez klíče, bez rate limitu, bez databáze |
Jednotky
API je vůči jednotkám neutrální. Zvolte si jednu jednotku — milimetry, palce, cokoli — používejte ji pro každé číslo, které posíláte, a každé číslo, které dostanete zpátky, bude ve stejné jednotce. Na serveru se nic nepřepočítává a žádný název pole žádnou jednotku nepředepisuje.
Platí to pro rozměry dílů i materiálu, kerf, tolerance, trim a minOffcut na vstupu a pro každou souřadnici, pozici, zbývající délku, zbytek i délku řezu na výstupu. Míchání jednotek v jednom požadavku vytvoří plán, který projde validací a přitom je fyzicky špatný — a server to nemá jak poznat.
Souřadný systém
Počátek je levý horní roh desky: x roste doprava po šířce desky, y roste dolů po výšce desky. x a y dílu jsou jeho levý horní roh a w a h jsou rozměry tak, jak je díl umístěný — už prohozené, když je rotated true — takže obdélník x, y, w, h je přímo obrys na desce, bez dalšího přepočítávání. Obdélníky zbytků používají stejný systém.
Ořez posouvá umístění: trim.left posune každý díl doprava a trim.top každý díl dolů, protože díly se rozmisťují do využitelné plochy a ta se pak přesouvá zpátky na celou desku. trim.right a trim.bottom využitelnou plochu zmenšují, ale počátek neposouvají. w a h desky jsou vždy plné rozměry materiálu, včetně ořezu — a právě proto se ořez v yieldPct počítá jako odpad.
2D — požadavek
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"]
} Pole dílu
| w, h | povinné — rozměry dílu |
| qty | výchozí 1 — rozepisuje se na serveru; počítá se do limitu 2,000 |
| name | nepovinné označení, vrací se u každého umístění |
| rotatable | výchozí true — smí se díl otočit o 90° |
| grainGroup | členové skupiny zůstanou na jedné desce (shoda vzoru dřeva) |
| priority | musí být vyříznut: při omezeném materiálu má přednost na desce (spolu s respectStock) |
Pole materiálu
w a h jsou povinné. qty má výchozí hodnotu 1 a pevným limitem je jen ve spojení s respectStock. price je cena za desku a vstupuje do totalPrice a do režimu nákladů.
Volby
| kerf | šířka pilového kotouče (výchozí 0) |
| tolerance | přijme řezy, které přesahují nejvýše o tuto hodnotu |
| trim | ořez hran po jednotlivých stranách: left, right, top, bottom |
| firstCut | 'auto' | 'horizontal' | 'vertical' (výchozí 'auto') |
| minimizeCost | hodnotí kandidáty podle celkové ceny materiálu místo počtu desek |
| respectStock | bere qty každého řádku materiálu jako pevný limit |
| minOffcut | vrací jen zbytky, jejichž krátká strana je alespoň takto velká |
| maxCutStages | limit fází pro formátovací pilu — počet fází, ne hloubka stromu řezů |
| minimizeRotations | preferuje rozvržení, která otáčejí méně dílů |
include zúží odpověď: pošlete ["cutPlan","offcuts"] (výchozí jsou obě), nebo jednu z nich vynechte a v odpovědi nebude.
2D — odpověď
{
"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 sdružuje řezy ležící na stejné souřadnici (jedno nastavení dorazu); sawPasses počítá každý průjezd. Dvě poctivé míry téhož plánu, ne tvrzení, že se shodují s číslem některého konkurenta.
- guillotineValid / cutPlan — Když rozvržení nelze rozřezat průchozími řezy od hrany k hraně, je guillotineValid false a cutPlan null. To je skutečná informace — na formátovací pile se vyrobit nedá — ne chyba.
- unplaced + warnings — Nesplnitelná zakázka vrací 200, díly jsou vypsané v unplaced a poznámka je ve warnings. Plán, se kterým se dá pracovat, je lepší než stavový kód.
Pole odpovědi
Názvy polí a jejich typy jsou součástí kontraktu, takže tabulky níže zůstávají ve všech jazycích v angličtině — přeložený název pole by dokumentoval API, které neexistuje.
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 — co jeden krok fyzicky znamená
Jeden krok je jeden pohyb kotouče a seznam je v tom pořadí, ve kterém se skutečně dá řezat: nadřazený řez stojí před řezy uvnitř kusu, který jím vznikl, protože pásek nemůžete zkrátit napříč, dokud ho neodříznete. axis "h" znamená, že kotouč jede podél x a odděluje horní část od dolní; axis "v" znamená, že jede podél y a odděluje levou část od pravé. pos je NIŽŠÍ hrana kotouče z hlediska souřadnic — hodnota y pro "h", hodnota x pro "v" — nikoli jeho osa: šířka řezu (kerf) zabírá úsek od pos do pos + kerf, takže kotouč ubírá materiál ve směru, ve kterém souřadnice roste — u "h" tedy dolů a u "v" doprava. Materiál na nižší straně řezné linie — u "h" nad ní, u "v" vlevo od ní — je ten kus, který daný řez uvolní. length je vzdálenost, kterou kotouč na tom jednom řezu ujede: rozsah oblasti, kterou přejíždí, ne šířka celé desky.
stage je jeden průjezd stroje. Začíná na 1 a zvyšuje se jen tehdy, když se axis oproti nadřazenému řezu překlopí — takže rozříznutí desky na šest pásků je jedna fáze a jejich příčné zkrácení je fáze další. Přesně takto rozumí „třífázovému řezání“ formátovací pila; není to hloubka stromu řezů a je to právě to, co omezuje maxCutStages. sheet je index do sheets počítaný od nuly a step začíná na každé desce znovu od 1, místo aby běžel přes celou zakázku.
cutPlan je null — ne chybějící, ne prázdný — vždy, když je guillotineValid false: rozvržení, které nelze rozřezat průchozími řezy od hrany k hraně, nemá žádnou sekvenci řezů, kterou by šlo vrátit. Z odpovědi zmizí úplně jen v případě, že jste jej vynechali z include.
1D — lineární
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 } }
} Díly mají length (plus qty, name, priority); materiál má length, qty a price. Volby jsou kerf, tolerance, trim.start / trim.end, minimizeCost, respectStock a minOffcut. Odpověď vrací rods místo sheets, každou s vlastními díly, zbývající délkou a zbytky.
1D — odpověď
{
"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 nahrazuje sheets a guillotineValid tady není, protože lineární řez je vždy vyrobitelný. pos každého dílu je odstup jeho bližšího konce od toho konce tyče, který ořezává trim.start, takže první díl začíná přesně na trim.start a každé další pos přidává jednu šířku řezu (kerf). remaining se uvádí u každé tyče, i když je menší než minOffcut — minOffcut filtruje pouze pole offcuts, které obsahuje nejvýše jeden záznam. V plánu řezání je sheet index tyče, axis je vždy "v", stage je vždy 1 a length je vždy 0: příčné zkrácení tyče nemá žádnou dráhu, kterou by šlo hlásit, a proto také 1D metrics obsahují cuts, ale žádné 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. |
Jádra
- heuristic (výchozí) — gilotinový rozmisťovací algoritmus s více strategiemi. Nejvyšší využití, každé rozvržení je řezatelné na pile, vždy plný cutPlan.
- balanced — rozmisťovací algoritmus MaxRects s volným nestingem. Na velkých zakázkách výrazně rychlejší (naměřeno ~25× při 2 000 dílech) za cenu malé ztráty využití a jeho rozvržení často nejsou gilotinová (guillotineValid: false, cutPlan: null). Nemodeluje tolerance, minimizeCost, grainGroup, maxCutStages ani minimizeRotations — když některou nastavíte, upozornění vám řekne, že byla ignorována.
Determinismus a verzování
Každá odpověď nese engineVersion. Algoritmus je deterministický, takže jeho zlepšení změní výstup pro stejný vstup — a to je pro toho, kdo cachuje, nekompatibilní změna. Chování si zafixujete tím, že budete engine posílat explicitně a sledovat engineVersion; verze v cestě /v1/ se mění jen tehdy, když se změní podoba odpovědi.
Každé jádro se verzuje samostatně, takže změna jednoho nikdy neposune verzi druhého.
Chyby
| 400 | invalid_request | Chyba schématu. details.path ukazuje na problematické pole. |
| 401 | unauthorized | Chybějící nebo neznámý API klíč. |
| 402 | quota_exceeded | Vyčerpaná měsíční kvóta. Retry-After udává počet sekund do začátku nového měsíce. |
| 403 | key_revoked | Klíč existuje, ale byl zneplatněn. |
| 404 | not_found | Taková cesta neexistuje — a totéž dostanete, když je cesta správná, ale metoda špatná. |
| 413 | too_large | Vstup překračuje limit (viz Limity). |
| 429 | busy | Momentálně plná kapacita. Retry-After v sekundách — do vaší kvóty se to nikdy nepočítá. |
| 500 | internal | Neočekávaná chyba, nebo je nedostupný autentizační backend (požadavky se odmítají — fail closed). |
| 504 | solve_timeout | Výpočet překročil tvrdý časový limit (vynucuje ho proxy). |
Tělo chyby
Každá chyba, kterou vytvoří samotné jádro, používá stejnou obálku. Rozhodujte se podle error, což je stabilní kód; nikdy podle message, jehož formulace se mezi vydáními může změnit. details je přítomné u invalid_request, kde path pojmenovává problematické pole, a u too_large, kde max a got udávají limit a to, co jste poslali.
HTTP/1.1 400 Bad Request
Content-Type: application/json; charset=utf-8
{
"error": "invalid_request",
"message": "parts[0].h: must be greater than 0",
"details": { "path": "parts[0].h" }
} HTTP/1.1 402 Payment Required
Retry-After: 41400
Content-Type: application/json; charset=utf-8
{ "error": "quota_exceeded", "message": "monthly quota exhausted" } - 402 i 429 nesou hlavičku Retry-After v sekundách. U 402 odpočítává čas do resetu kvóty v 00:00 UTC prvního dne následujícího měsíce; u 429 jde o krátké počkání a 429 nikdy nespotřebuje kvótu — rezervované volání se vrací zpět.
- Chyba směrování odpovídá kódem not_found, který záměrně není v seznamu výše, protože jej vytváří router, a ne kontrakt API. Když je cesta správná, ale metoda špatná, dostanete 404, a ne 405: oba optimalizační endpointy přijímají jen POST.
- 504 přichází z reverzní proxy, ne z jádra, takže jeho tělo patří proxy, a ne této JSON obálce. V rámci vstupních limitů níže by nemělo být možné jej vůbec dostat.
Hlavičky rate limitu
Úspěšné volání optimalizace nese X-RateLimit-Limit (měsíční strop klíče) a X-RateLimit-Remaining (kolik volání v tomto měsíci zbývá po tomto). Posílají je pouze optimalizační endpointy: počítadlo se rezervuje jako součást autorizace výpočtu, takže /v1/usage a /v1/health nemají co hlásit.
| 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"
} Jen pro čtení: nespotřebovává volání a neposílá žádné hlavičky rate limitu. plan a limit jsou tarif a strop zafixované na klíč v okamžiku jeho vytvoření, takže pozdější změna ceníku už existující klíč nikdy nepřepíše. used počítá aktuální kalendářní měsíc v UTC, remaining je limit minus used a nikdy nejde do minusu, periodEnd je den resetu jako obyčejné datum ve formátu YYYY-MM-DD a keyPrefix je netajný zobrazovaný prefix klíče, kterým jste volali. Samotný klíč nevrací žádný endpoint — ukládá se jen jeho hash, takže ztracený klíč se nahrazuje, ne obnovuje.
| 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
} Bez klíče, bez kvóty, bez databáze. Záměrně se nedotýká ničeho stavového, takže výpadek úložiště klíčů nemůže službu ukázat orchestrátoru jako mrtvou. engines vypisuje id, která tato instalace přijímá v engine, a engineVersion je verze výchozího jádra.
| 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. |
Limity
- 2,000 dílů na požadavek (celkové množství, po rozepsání qty)
- 50 řádků materiálu · tělo požadavku až 1 MB
- 10 aktivních klíčů na účet
- paralelní zpracování je omezené na serveru — nárazová špička dostane 429, nikdy pomalou frontu
Specifikace OpenAPI
Strojově čitelný dokument OpenAPI 3.1 popisuje všechny čtyři endpointy, obě těla požadavků, každou podobu odpovědi a každou chybu. Nasměrujte na něj svůj generátor klienta místo toho, abyste přepisovali tuto stránku. Samotný dokument je jen v angličtině: skládá se z tokenů kontraktu a OpenAPI žádný lokalizační mechanismus nemá.
curl -s https://cutoptim.com/engine/openapi.json > cutoptim-engine.json