Documentație API
URL de bază https://api.cutoptim.com · versiunea contractului v1
Creează o cheie în panoul de control, apoi trimite-o ca Authorization: Bearer <key>.
Specificația OpenAPI: /engine/openapi.json
Endpointuri
| POST | /v1/optimize/2d | optimizare 2D pentru panouri |
| POST | /v1/optimize/1d | optimizare 1D / liniară (bare, profile, țeavă) |
| POST | /v1/optimize/wood | optimizarea lemnului — 1D cu potrivirea secțiunii |
| GET | /v1/usage | consumul și cota CONTULUI în luna curentă |
| GET | /v1/health | verificare de disponibilitate — fără cheie, fără limitare de rată, fără bază de date |
Unități de măsură
API-ul este agnostic față de unitatea de măsură. Alege o singură unitate — milimetri, inch, orice — folosește-o pentru fiecare număr pe care îl trimiți, iar fiecare număr pe care îl primești înapoi este exprimat în aceeași unitate. Nimic nu este convertit pe server și niciun nume de câmp nu impune o unitate.
Acest lucru acoperă dimensiunile pieselor și ale materialului de bază, kerf, tolerance, trim și minOffcut la intrare, precum și fiecare coordonată, poziție, lungime rămasă, rest și lungime de tăiere la ieșire. Amestecarea unităților în cadrul aceleiași cereri produce un plan care trece validarea, dar este greșit din punct de vedere fizic, iar serverul nu poate detecta asta.
Sistemul de coordonate
Originea este colțul din stânga sus al plăcii: x crește spre dreapta, pe lățimea plăcii, iar y crește în jos, pe înălțimea plăcii. Valorile x și y ale unei piese indică colțul ei din stânga sus, iar w și h sunt dimensiunile așa cum a fost plasată — deja interschimbate atunci când rotated este true — astfel încât dreptunghiul x, y, w, h este amprenta pe placă, fără niciun calcul suplimentar. Dreptunghiurile resturilor folosesc același sistem.
Tăierea de margine deplasează plasările: trim.left împinge fiecare piesă spre dreapta, iar trim.top împinge fiecare piesă în jos, deoarece piesele sunt aranjate în interiorul zonei utilizabile și apoi decalate înapoi pe placa întreagă. trim.right și trim.bottom micșorează zona utilizabilă fără să mute originea. Valorile w și h ale unei plăci sunt întotdeauna dimensiunile complete ale materialului, inclusiv tăierea de margine — și tocmai de aceea tăierea de margine se numără ca deșeu în yieldPct.
2D — cerere
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"]
} Câmpurile pieselor
| w, h | obligatorii — dimensiunile piesei |
| qty | implicit 1 — expandat pe server; se numără în plafonul de 2,000 piese |
| name | etichetă opțională, returnată la fiecare plasare |
| rotatable | implicit true — poate fi rotită piesa la 90° |
| grainGroup | membrii unui grup sunt păstrați pe aceeași placă (potrivirea fibrei) |
| priority | de tăiat obligatoriu: câștigă spațiu pe placă atunci când stocul este plafonat (împreună cu respectStock) |
Câmpurile materialului de bază
w și h sunt obligatorii. qty este implicit 1 și devine plafon strict doar împreună cu respectStock. price este per placă și determină totalPrice și modul cost.
Opțiuni
| kerf | lățimea lamei (implicit 0) |
| tolerance | acceptă tăieri care depășesc cu până la această valoare |
| trim | tăiere de margine pe fiecare latură: left, right, top, bottom |
| firstCut | 'auto' | 'horizontal' | 'vertical' (implicit 'auto') |
| minimizeCost | ordonează variantele după prețul total al materialului, nu după numărul de plăci |
| respectStock | tratează qty de pe fiecare rând de material ca plafon strict |
| minOffcut | raportează doar resturile a căror latură scurtă atinge cel puțin această valoare |
| maxCutStages | limită de etape pentru ferăstrăul de panouri — un număr de faze, nu adâncimea brută a arborelui |
| minimizeRotations | preferă aranjamentele care rotesc mai puține piese |
include restrânge răspunsul: trimite ["cutPlan","offcuts"] (implicit sunt ambele) sau omite oricare dintre ele pentru a-l exclude din payload.
2D — răspuns
{
"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 unifică tăierile coliniare (o singură reglare a opritorului); sawPasses numără fiecare trecere. Două măsuri oneste ale aceluiași plan, nu o pretenție de a coincide cu numărul raportat de vreun concurent.
- guillotineValid / cutPlan — Când un aranjament nu poate fi tăiat de la o margine la alta, guillotineValid este false și cutPlan este null. Aceasta este o informație reală — nu se poate realiza pe un ferăstrău pentru panouri — nu o eroare.
- unplaced + warnings — O lucrare imposibil de satisfăcut returnează 200, cu piesele listate în unplaced și o notă în warnings. Un plan cu care poți lucra este mai util decât un cod de stare.
Câmpurile răspunsului
Numele și tipurile câmpurilor fac parte din contract, așa că tabelele de mai jos rămân în engleză în toate limbile — un nume de câmp tradus ar documenta un API care nu există.
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 — ce înseamnă fizic un pas
Un step este o singură mișcare a lamei, iar lista este exact în ordinea în care poți tăia efectiv: o tăiere părinte înaintea tăierilor din interiorul bucății pe care a produs-o, pentru că nu poți reteza transversal o fâșie înainte de a o fi desprins. axis "h" înseamnă că lama se deplasează pe direcția x și separă partea de sus de cea de jos; axis "v" înseamnă că se deplasează pe direcția y și separă stânga de dreapta. pos este muchia lamei cu coordonata MAI MICĂ — valoarea y pentru "h", valoarea x pentru "v" — nu axa ei mediană: banda de kerf ocupă intervalul de la pos la pos + kerf, deci lama mușcă în direcția în care crește coordonata, adică în jos pentru "h" și spre dreapta pentru "v". Materialul aflat pe partea cu coordonata mai mică a liniei — deasupra ei pentru "h", la stânga ei pentru "v" — este bucata pe care o eliberează acea tăiere. length arată cât parcurge lama la acea singură tăiere: întinderea zonei pe care o traversează, nu lățimea întregii plăci.
stage este o trecere a mașinii. Începe de la 1 și crește doar atunci când axa se schimbă față de tăierea părinte, așa că despicarea unei plăci în șase fâșii este o singură etapă, iar retezarea lor transversală este următoarea. Acesta este sensul în care se vorbește despre „tăiere în trei etape” la ferăstrăul pentru panouri, nu adâncimea arborelui de tăiere, și este exact ceea ce limitează maxCutStages. sheet este un index în sheets care pornește de la 0, iar step reîncepe de la 1 pe fiecare placă, în loc să continue pe toată lucrarea.
cutPlan este null — nu lipsește, nu este gol — de fiecare dată când guillotineValid este false: un aranjament care nu poate fi tăiat de la o margine la alta nu are nicio secvență de tăiere de returnat. Lipsește complet din payload dacă l-ai exclus din include.
1D — liniar
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 } }
} Piesele primesc length (plus qty, name, priority); materialul de bază primește length, qty și price. Opțiunile sunt kerf, tolerance, trim.start / trim.end, minimizeCost, respectStock și minOffcut. Răspunsul returnează rods în loc de sheets, fiecare cu piesele sale (parts), lungimea rămasă și resturile (offcuts).
1D — răspuns
{
"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 înlocuiește sheets și nu există guillotineValid, pentru că o tăiere liniară poate fi realizată întotdeauna. Valoarea pos a fiecărei piese este distanța de la capătul ei apropiat până la capătul barei pe care îl retează trim.start, așa că prima piesă începe exact la trim.start, iar fiecare pos următor adaugă câte un kerf. remaining este raportat pentru fiecare bară, chiar și atunci când se află sub minOffcut — minOffcut filtrează doar lista offcuts, care conține cel mult o intrare. În planul de tăiere, sheet este indexul barei, axis este întotdeauna "v", stage este întotdeauna 1 și length este întotdeauna 0: o retezare de bară nu are o distanță de parcurs de raportat, motiv pentru care metrics din 1D conține cuts, dar nu cutLength.
Lemn — secțiune
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 }
} Lemnul are o identitate pe care o bară simplă nu o are: o piesă 50×150 nu poate ieși din material 50×100, oricâtă lungime ar rămâne. De aceea piesele și materialul poartă sw și sh, cele două laturi ale secțiunii, în orice ordine — 50×100 și 100×50 sunt aceeași grindă întoarsă și formează o singură secțiune. Lucrarea se împarte pe secțiuni, fiecare secțiune este potrivită cu materialul ei și rezolvată separat, iar un singur apel returnează totul. Opțiunile sunt aceleași ca la 1D.
Lemn — răspuns
{
"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 înlocuiește rods la nivelul de sus: fiecare intrare este o secțiune cu propriile rods (identice ca formă cu cele din 1D) și propriile metrics, așa că cifrele pe material există fără să le recalculezi. unmatched nu are echivalent în 1D — este cererea pentru a cărei secțiune nu ai furnizat deloc material, o problemă diferită de unplaced (piese care aveau material și nu au încăput) și cu altă rezolvare, de aceea cele două nu se amestecă niciodată. metrics.total numără fiecare piesă cerută, inclusiv cele din unmatched. În planul de tăiere fiecare pas își indică și section, iar sheet este indexul barei ÎN INTERIORUL acelei secțiuni, nu un contor pe toată lucrarea.
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. |
Motoare
- heuristic (implicit) — algoritmul de împachetare ghilotină, cu mai multe strategii. Cea mai bună utilizare, fiecare aranjament tăiabil pe ferăstrău, întotdeauna un cutPlan complet.
- balanced — un algoritm de împachetare MaxRects cu nesting liber. Mult mai rapid la lucrările mari (măsurat ~25× la 2.000 de piese), cu o utilizare puțin mai mică, iar aranjamentele sale adesea nu sunt ghilotină (guillotineValid: false, cutPlan: null). Nu modelează tolerance, minimizeCost, grainGroup, maxCutStages sau minimizeRotations — dacă setezi una, un avertisment îți spune că a fost ignorată.
Determinism și versionare
Fiecare răspuns conține engineVersion. Algoritmul este determinist, deci îmbunătățirea lui schimbă rezultatul pentru aceeași intrare — ceea ce este o modificare incompatibilă dacă folosești cache. Fixează comportamentul trimițând engine explicit și urmărind engineVersion; versiunea din cale, /v1/, se schimbă doar dacă se schimbă structura răspunsului.
Fiecare motor este versionat independent, așa că o modificare la unul nu mișcă niciodată versiunea celuilalt.
Erori
| 400 | invalid_request | Eroare de schemă. details.path indică câmpul problematic. |
| 401 | unauthorized | Cheie API lipsă sau necunoscută. |
| 402 | quota_exceeded | Cota lunară a fost atinsă. Retry-After indică secundele rămase până la începutul lunii următoare. |
| 403 | key_revoked | Cheia există, dar a fost revocată. |
| 404 | not_found | Nu există o astfel de rută — este și răspunsul pe care îl primești pentru calea corectă cu metoda greșită. |
| 413 | too_large | Intrare peste o limită (vezi Limite). |
| 429 | busy | Capacitate atinsă momentan. Retry-After în secunde — acest răspuns nu se scade niciodată din cota ta. |
| 500 | internal | Eroare neașteptată sau backendul de autentificare este inaccesibil (cererile sunt respinse — fail closed). |
| 504 | solve_timeout | Rezolvarea a depășit limita strictă de timp real (impusă la nivelul proxy-ului). |
Corpul erorii
Fiecare eroare produsă de motorul propriu-zis folosește același înveliș. Ramifică logica după error, care este un cod stabil; niciodată după message, a cărui formulare se poate schimba de la o versiune la alta. details este prezent la invalid_request, unde path numește câmpul problematic, și la too_large, unde max și got indică plafonul și valoarea pe care ai trimis-o.
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" } - Atât 402, cât și 429 conțin un header Retry-After exprimat în secunde. La 402 numără invers până la resetarea cotei, la 00:00 UTC în prima zi a lunii următoare; la 429 este o scurtă pauză înainte de reîncercare, iar un 429 nu consumă niciodată cotă — apelul rezervat este restituit.
- O eroare de rutare răspunde cu not_found, un cod aflat intenționat în afara listei de mai sus, pentru că îl produce routerul, nu contractul API. Primești 404, și nu 405, atunci când calea este corectă, dar metoda este greșită: toate cele trei endpointuri de optimizare acceptă doar POST.
- 504 vine de la reverse proxy, nu de la motor, așa că corpul răspunsului este al proxy-ului și nu acest înveliș JSON. În limitele de intrare de mai jos, ar trebui să fie imposibil de atins.
Headere de limitare a ratei
Un apel de optimizare reușit conține X-RateLimit-Limit (plafonul lunar al cheii) și X-RateLimit-Remaining (apelurile rămase în luna curentă, după acesta). Sunt trimise doar de endpointurile de optimizare: contorul este rezervat ca parte a autorizării unei rezolvări, deci /v1/usage și /v1/health nu au nimic de raportat.
| 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"
} Doar citire: nu consumă un apel și nu trimite anteturi de rate limit. ⚠️ used și limit descriu CONTUL, nu cheia cu care ai apelat: fiecare cheie activă a contului consumă dintr-o singură alocare comună, deci mai multe chei nu înseamnă mai multă cotă. used numără luna calendaristică UTC curentă pentru toate, remaining este limit minus used și nu coboară niciodată sub zero, periodEnd este ziua de resetare ca dată simplă YYYY-MM-DD, iar keyPrefix este prefixul public al cheii folosite. Cheia în sine nu este returnată de niciun endpoint — se stochează doar hash-ul ei, așa că o cheie pierdută se înlocuiește, nu se recuperează.
| 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
} Fără cheie, fără cotă, fără bază de date. Nu atinge intenționat nimic care are stare, astfel încât o defecțiune a depozitului de chei să nu poată face serviciul să pară mort în ochii unui orchestrator. engines listează id-urile pe care această instalare le acceptă în engine, iar engineVersion este versiunea motorului implicit.
| 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. |
Limite
- 2,000 piese per cerere (cantitate totală, după expandarea qty)
- 50 rânduri de material · corpul cererii până la 1 MB
- 10 chei active per cont — împart O SINGURĂ cotă lunară: cheile separă mediile și integrările, nu adaugă alocare
- concurența este limitată pe server — un vârf de trafic primește 429, niciodată o coadă lentă
Specificația OpenAPI
Un document OpenAPI 3.1, procesabil automat, descrie toate cele cinci endpointuri, fiecare corp de cerere, fiecare structură de răspuns și fiecare eroare. Îndreaptă generatorul tău de client către el, în loc să transcrii această pagină. Documentul în sine este doar în engleză: este alcătuit din tokenuri de contract, iar OpenAPI nu are niciun mecanism de localizare.
curl -s https://cutoptim.com/engine/openapi.json > cutoptim-engine.json