Dokumentacja API
Bazowy URL https://api.cutoptim.com · wersja kontraktu v1
Utwórz klucz w swoim panelu, a następnie przesyłaj go jako Authorization: Bearer <key>.
Specyfikacja OpenAPI: /engine/openapi.json
Endpointy
| POST | /v1/optimize/2d | optymalizacja cięcia płyt 2D |
| POST | /v1/optimize/1d | optymalizacja 1D / liniowa (pręty, profile, rury) |
| POST | /v1/optimize/wood | optymalizacja drewna — 1D z dopasowaniem przekroju |
| GET | /v1/usage | zużycie i limit KONTA w bieżącym miesiącu |
| GET | /v1/health | liveness — bez klucza, bez limitu zapytań, bez bazy danych |
Jednostki
API nie narzuca jednostek. Wybierz jedną jednostkę — milimetry, cale, cokolwiek — używaj jej w każdej przesyłanej liczbie, a każda liczba, którą otrzymasz z powrotem, będzie w tej samej jednostce. Nic nie jest przeliczane po stronie serwera i żadna nazwa pola nie zakłada konkretnej jednostki.
Dotyczy to wymiarów elementów i materiału bazowego, kerf, tolerance, trim oraz minOffcut na wejściu, a także każdej współrzędnej, pozycji, pozostałej długości, resztki i długości cięcia na wyjściu. Mieszanie jednostek w jednym zapytaniu daje plan, który przechodzi walidację, a fizycznie jest błędny — i serwer nie jest w stanie tego wykryć.
Układ współrzędnych
Początek układu to lewy górny narożnik płyty: x rośnie w prawo wzdłuż szerokości płyty, y rośnie w dół wzdłuż wysokości płyty. Pola x i y elementu wskazują jego lewy górny narożnik, a w i h to wymiary w takim ułożeniu, w jakim element został rozmieszczony — już zamienione, gdy rotated ma wartość true — więc prostokąt x, y, w, h to gotowy obrys na płycie, bez żadnych dalszych obliczeń. Prostokąty resztek korzystają z tego samego układu.
Okrawanie przesuwa rozmieszczenia: trim.left przesuwa każdy element w prawo, a trim.top przesuwa każdy element w dół, ponieważ elementy są rozmieszczane wewnątrz obszaru użytkowego, a następnie przesuwane z powrotem na całą płytę. trim.right i trim.bottom zmniejszają obszar użytkowy, nie przesuwając początku układu. Pola w i h płyty to zawsze pełne wymiary materiału bazowego, razem z okrawaniem — i właśnie dlatego okrawanie liczy się jako odpad w yieldPct.
2D — zapytanie
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"]
} Pola elementu
| w, h | wymagane — wymiary elementu |
| qty | domyślnie 1 — rozwijane po stronie serwera; liczy się do limitu 2,000 elementów |
| name | opcjonalna etykieta, zwracana przy każdym rozmieszczeniu |
| rotatable | domyślnie true — czy element można obrócić o 90° |
| grainGroup | elementy jednej grupy pozostają na tej samej płycie (dopasowanie słojów) |
| priority | element obowiązkowy: wygrywa miejsce na płycie, gdy zapas jest ograniczony (przy respectStock) |
Pola materiału bazowego
w oraz h są wymagane. qty domyślnie wynosi 1 i jest twardym limitem tylko przy respectStock. price dotyczy jednej płyty i zasila totalPrice oraz tryb kosztowy.
Opcje
| kerf | szerokość rzazu piły (domyślnie 0) |
| tolerance | akceptuj cięcia przekraczające materiał o nie więcej niż tę wartość |
| trim | okrawanie każdej krawędzi osobno: left, right, top, bottom |
| firstCut | 'auto' | 'horizontal' | 'vertical' (domyślnie 'auto') |
| minimizeCost | oceniaj warianty według łącznej ceny materiału, a nie liczby płyt |
| respectStock | traktuj qty każdego wiersza materiału jako twardy limit |
| minOffcut | raportuj tylko resztki, których krótszy bok jest nie mniejszy niż ta wartość |
| maxCutStages | limit etapów piły panelowej — liczba faz, a nie surowa głębokość drzewa |
| minimizeRotations | preferuj układy obracające mniej elementów |
include przycina odpowiedź: przekaż ["cutPlan","offcuts"] (domyślnie oba), albo pomiń jedno z nich, aby nie trafiło do odpowiedzi.
2D — odpowiedź
{
"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 scala cięcia współliniowe (jedno ustawienie prowadnicy); sawPasses liczy każde przejście. To dwie uczciwe miary tego samego planu, a nie deklaracja zgodności z liczbami któregokolwiek konkurenta.
- guillotineValid / cutPlan — Gdy układu nie da się pociąć od krawędzi do krawędzi, guillotineValid ma wartość false, a cutPlan jest null. To realna informacja — takiego układu nie wykonasz na pile panelowej — a nie błąd.
- unplaced + warnings — Niewykonalne zlecenie zwraca 200 z elementami wypisanymi w unplaced i adnotacją w warnings. Plan, na którym można działać, jest wart więcej niż kod statusu.
Pola odpowiedzi
Nazwy i typy pól są częścią kontraktu, dlatego poniższe tabele pozostają po angielsku we wszystkich językach — przetłumaczona nazwa pola dokumentowałaby API, które nie istnieje.
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 fizycznie oznacza jeden krok
Jeden step to jeden ruch piły, a lista jest w takiej kolejności, w jakiej faktycznie da się ciąć: cięcie nadrzędne przed cięciami wewnątrz kawałka, który z niego powstał, bo paska nie przetniesz w poprzek, dopóki go nie odetniesz. axis "h" oznacza, że piła przesuwa się wzdłuż x i oddziela górę od dołu; axis "v" oznacza, że przesuwa się wzdłuż y i oddziela lewą stronę od prawej. pos to krawędź rzazu o NIŻSZEJ współrzędnej — wartość y dla "h", wartość x dla "v" — a nie jego linia środkowa: rzaz zajmuje przedział od pos do pos + kerf, więc piła zabiera materiał w kierunku rosnącej współrzędnej, czyli w dół dla "h" i w prawo dla "v". Materiał po stronie niższej współrzędnej — nad linią dla "h", po jej lewej stronie dla "v" — to kawałek, który uwalnia to cięcie. length to droga, jaką piła przebywa w tym jednym cięciu: rozpiętość obszaru, przez który przechodzi, a nie szerokość całej płyty.
stage to jedno przejście maszyny. Zaczyna się od 1 i zwiększa się tylko wtedy, gdy axis zmienia się względem cięcia nadrzędnego, więc rozcięcie płyty na sześć pasków to jeden etap, a ich poprzeczne przecięcie to następny. To właśnie tak rozumie się „cięcie trzyetapowe” na pile panelowej — nie jako głębokość drzewa cięć — i właśnie to ogranicza opcja maxCutStages. sheet to liczony od 0 indeks w sheets, a step zaczyna się od 1 na każdej płycie, zamiast biec przez całe zlecenie.
cutPlan ma wartość null — nie brakuje go i nie jest pusty — zawsze wtedy, gdy guillotineValid ma wartość false: układ, którego nie da się pociąć od krawędzi do krawędzi, nie ma sekwencji cięcia do zwrócenia. W odpowiedzi nie ma go w ogóle, jeśli pominięto go w include.
1D — liniowe
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 } }
} Elementy przyjmują length (oraz qty, name, priority); materiał bazowy przyjmuje length, qty i price. Dostępne opcje to kerf, tolerance, trim.start / trim.end, minimizeCost, respectStock i minOffcut. Odpowiedź zwraca rods zamiast sheets, każdy z przypisanymi elementami, pozostałą długością i resztkami.
1D — odpowiedź
{
"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 zastępuje sheets i nie ma pola guillotineValid, ponieważ cięcie liniowe zawsze da się wykonać. Pole pos każdego elementu to odległość jego bliższego końca od tego końca pręta, który okrawa trim.start, więc pierwszy element zaczyna się dokładnie na trim.start, a każde kolejne pos dodaje jedną szerokość rzazu. remaining jest raportowane dla każdego pręta, nawet gdy jest mniejsze niż minOffcut — minOffcut filtruje wyłącznie tablicę offcuts, która zawiera najwyżej jeden wpis. W planie cięcia sheet to indeks pręta, axis ma zawsze wartość "v", stage ma zawsze wartość 1, a length zawsze 0: poprzeczne przecięcie pręta nie ma drogi przejazdu, którą można by zaraportować — i właśnie dlatego metrics dla 1D zawiera cuts, ale nie cutLength.
Drewno — przekrój
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 }
} Drewno ma tożsamość, której nie ma zwykły pręt: elementu 50×150 nie da się wyciąć z materiału 50×100, niezależnie od tego, ile długości zostało. Elementy i materiał niosą więc sw i sh, dwa boki przekroju, w dowolnej kolejności — 50×100 i 100×50 to ta sama belka obrócona i trafiają do jednego przekroju. Zlecenie jest dzielone według przekroju, każdy przekrój jest dopasowywany do własnego materiału i rozwiązywany osobno, a jedno wywołanie zwraca całość. Opcje są takie same jak w 1D.
Drewno — odpowiedź
{
"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": 394,
"parts": [
{ "name": "Rafter", "pos": 0, "length": 2400 },
{ "name": "Noggin", "pos": 2403, "length": 600 },
{ "name": "Noggin", "pos": 3006, "length": 600 }
],
"offcuts": [394]
},
{
"length": 4000,
"price": 12.5,
"remaining": 394,
"parts": [
{ "name": "Rafter", "pos": 0, "length": 2400 },
{ "name": "Noggin", "pos": 2403, "length": 600 },
{ "name": "Noggin", "pos": 3006, "length": 600 }
],
"offcuts": [394]
}
],
"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": 1000,
"parts": [{ "name": "Beam", "pos": 0, "length": 3000 }],
"offcuts": [1000]
},
{
"length": 4000,
"price": 18,
"remaining": 1000,
"parts": [{ "name": "Beam", "pos": 0, "length": 3000 }],
"offcuts": [1000]
}
],
"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 zastępuje rods na najwyższym poziomie: każdy wpis to jeden przekrój z własnymi rods (identycznymi w formie jak w 1D) i własnymi metrics, więc wartości dla danego materiału są od razu dostępne, bez przeliczania. unmatched nie ma odpowiednika w 1D — to zapotrzebowanie, dla którego przekroju nie podałeś żadnego materiału. To inny problem niż unplaced (elementy, które miały materiał i się nie zmieściły) i wymaga innej korekty, dlatego oba nigdy się nie mieszają. metrics.total liczy każdy zamówiony element, łącznie z tymi z unmatched. W planie cięcia każdy krok podaje też swoją section, a sheet to indeks pręta W OBRĘBIE tego przekroju, a nie licznik dla całego zlecenia.
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. |
Silniki
- heuristic (domyślny) — wielostrategiowy algorytm pakowania gilotynowego. Najwyższe wykorzystanie, każdy układ da się pociąć na pile, zawsze pełny cutPlan.
- balanced — algorytm pakowania MaxRects ze swobodnym nestingiem. Znacznie szybszy przy dużych zleceniach (zmierzone ~25× przy 2000 elementów) za cenę niewielkiego spadku wykorzystania, a jego układy często nie są gilotynowe (guillotineValid: false, cutPlan: null). Nie modeluje tolerance, minimizeCost, grainGroup, maxCutStages ani minimizeRotations — po ustawieniu któregoś z nich ostrzeżenie poinformuje, że został zignorowany.
Determinizm i wersjonowanie
Każda odpowiedź zawiera engineVersion. Algorytm jest deterministyczny, więc jego ulepszenie zmienia wynik dla tych samych danych wejściowych — a to zmiana łamiąca zgodność, jeśli buforujesz wyniki. Przypnij zachowanie, przesyłając engine jawnie i obserwując engineVersion; wersja w ścieżce /v1/ zmienia się tylko wtedy, gdy zmienia się struktura odpowiedzi.
Każdy silnik jest wersjonowany niezależnie, więc zmiana w jednym nigdy nie przesuwa wersji drugiego.
Błędy
| 400 | invalid_request | Błąd schematu. details.path wskazuje pole, które go wywołało. |
| 401 | unauthorized | Brakujący lub nieznany klucz API. |
| 402 | quota_exceeded | Wyczerpany miesięczny limit. Retry-After podaje liczbę sekund do początku nowego miesiąca. |
| 403 | key_revoked | Klucz istnieje, ale został unieważniony. |
| 404 | not_found | Nie ma takiej trasy — to samo otrzymasz, gdy ścieżka jest poprawna, ale metoda nie. |
| 413 | too_large | Dane wejściowe przekraczają limit (patrz Limity). |
| 429 | busy | Chwilowy brak wolnych zasobów. Retry-After w sekundach — to nigdy nie obciąża Twojego limitu. |
| 500 | internal | Nieoczekiwany błąd albo backend uwierzytelniania jest nieosiągalny (zapytania są wtedy odrzucane — fail closed). |
| 504 | solve_timeout | Obliczenia przekroczyły twardy limit czasu rzeczywistego (wymuszany na proxy). |
Treść błędu
Każdy błąd generowany przez sam silnik korzysta z tej samej koperty. Rozgałęziaj logikę na polu error, które jest stabilnym kodem; nigdy na message, którego brzmienie może się zmienić między wydaniami. details występuje przy invalid_request, gdzie path wskazuje pole, które wywołało błąd, oraz przy too_large, gdzie max i got podają limit i to, co zostało przesłane.
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" } - Zarówno 402, jak i 429 zwracają nagłówek Retry-After w sekundach. Przy 402 odlicza on czas do zerowania limitu o 00:00 UTC pierwszego dnia następnego miesiąca; przy 429 jest to krótkie wstrzymanie, a 429 nigdy nie zużywa limitu — zarezerwowane wywołanie jest oddawane.
- Błąd routingu odpowiada kodem not_found, który celowo znajduje się poza powyższą listą, bo generuje go router, a nie kontrakt API. Otrzymasz 404, a nie 405, gdy ścieżka jest poprawna, ale metoda nie: wszystkie trzy endpointy optymalizacji przyjmują wyłącznie POST.
- 504 pochodzi z reverse proxy, a nie z silnika, więc jego treść należy do proxy i nie jest tą kopertą JSON. W granicach poniższych limitów wejściowych nie powinien być osiągalny.
Nagłówki limitu zapytań
Udane wywołanie optymalizacji zwraca X-RateLimit-Limit (miesięczny limit klucza) oraz X-RateLimit-Remaining (liczba wywołań pozostałych w tym miesiącu, już po tym wywołaniu). Wysyłają je wyłącznie endpointy optymalizacji: licznik jest rezerwowany w ramach autoryzacji obliczeń, więc /v1/usage i /v1/health nie mają o czym raportować.
| 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"
} Tylko do odczytu: nie zużywa wywołania i nie wysyła nagłówków limitu. ⚠️ used i limit opisują KONTO, a nie klucz, którym wywołałeś: każdy aktywny klucz konta czerpie z jednej wspólnej puli, więc tworzenie kolejnych kluczy nie tworzy dodatkowego limitu. used liczy bieżący miesiąc kalendarzowy UTC dla wszystkich, remaining to limit minus used i nigdy nie schodzi poniżej zera, periodEnd to dzień zerowania jako zwykła data YYYY-MM-DD, a keyPrefix to jawny prefiks użytego klucza. Sam klucz nie jest zwracany przez żaden endpoint — przechowywany jest wyłącznie jego hash, więc zgubiony klucz się wymienia, a nie odzyskuje.
| 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"],
"modes": ["2d", "1d", "wood"],
"uptimeSec": 16
} Bez klucza, bez limitu, bez bazy danych. Celowo nie sięga do niczego, co przechowuje stan, więc awaria magazynu kluczy nie może sprawić, że usługa wygląda dla orkiestratora na martwą. engines wypisuje identyfikatory, które to wdrożenie przyjmuje w polu engine, a engineVersion to wersja silnika domyślnego.
| 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 elementów na zapytanie (łączna ilość, po rozwinięciu qty)
- 50 wierszy materiału bazowego · treść zapytania do 1 MB
- 10 aktywnych kluczy na konto — dzielą JEDEN miesięczny limit: klucze oddzielają środowiska i integracje, nie zwiększają puli
- współbieżność jest ograniczona po stronie serwera — nagły skok ruchu otrzymuje 429, a nie wolną kolejkę
Specyfikacja OpenAPI
Czytelny maszynowo dokument OpenAPI 3.1 opisuje wszystkie pięć endpointów, każdą treść zapytania, każdą strukturę odpowiedzi i każdy błąd. Skieruj na niego swój generator klienta, zamiast przepisywać tę stronę. Sam dokument jest wyłącznie w języku angielskim: składa się z elementów kontraktu, a OpenAPI nie ma mechanizmu lokalizacji.
curl -s https://cutoptim.com/engine/openapi.json > cutoptim-engine.json