Zum Hauptinhalt springen
← CutOptim Engine API

API-Referenz

Basis-URL https://api.cutoptim.com · Contract-Version v1

Erstellen Sie einen Schlüssel in Ihrem Dashboard und senden Sie ihn dann als Authorization: Bearer <key>.

OpenAPI-Spezifikation: /engine/openapi.json

Endpoints

POST /v1/optimize/2d 2D-Plattenoptimierung
POST /v1/optimize/1d 1D-/Linearoptimierung (Stäbe, Profile, Rohre)
GET /v1/usage Verbrauch und Kontingent des aufrufenden Schlüssels im laufenden Monat
GET /v1/health Liveness — kein Schlüssel, kein Rate-Limit, keine Datenbank

Einheiten

Die API ist einheitenneutral. Wählen Sie eine Einheit — Millimeter, Zoll, was auch immer —, verwenden Sie sie für jede Zahl, die Sie senden, und jede Zahl, die Sie zurückbekommen, steht in derselben Einheit. Serverseitig wird nichts umgerechnet, und kein Feldname legt eine Einheit fest.

Das gilt eingehend für die Maße von Teilen und Ausgangsmaterial, kerf, tolerance, trim und minOffcut und ausgehend für jede Koordinate, jede Position, jede Restlänge, jedes Reststück und jede Schnittlänge. Einheiten innerhalb einer Anfrage zu mischen ergibt einen Plan, der die Validierung besteht und physikalisch falsch ist — und der Server kann das nicht erkennen.

Koordinatensystem

Der Ursprung ist die linke obere Ecke der Platte: x wächst nach rechts entlang der Plattenbreite, y wächst nach unten entlang der Plattenhöhe. x und y eines Teils bezeichnen seine linke obere Ecke, und w und h sind die Maße wie platziert — bei rotated true bereits getauscht —, sodass das Rechteck x, y, w, h ohne weitere Rechnung die Grundfläche auf der Platte ist. Für die Rechtecke der Reststücke gilt dasselbe System.

Die Besäumung verschiebt Platzierungen: trim.left schiebt jedes Teil nach rechts und trim.top jedes Teil nach unten, weil die Teile innerhalb der nutzbaren Fläche verschachtelt und anschließend auf die volle Platte zurückversetzt werden. trim.right und trim.bottom verkleinern die nutzbare Fläche, ohne den Ursprung zu verschieben. w und h einer Platte sind immer die vollen Maße des Ausgangsmaterials, Besäumung eingeschlossen — deshalb zählt die Besäumung in yieldPct auch als Abfall.

2D — Request

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

Felder eines Teils

w, herforderlich — Maße des Teils
qtyStandard 1 — wird serverseitig aufgelöst; zählt auf das Limit von 2,000 Teilen
nameoptionale Bezeichnung, wird bei jeder Platzierung mit zurückgegeben
rotatableStandard true — darf das Teil um 90° gedreht werden
grainGroupMitglieder einer Gruppe bleiben auf einer Platte (Fasergruppen)
priorityMuss-Teil: erhält bei begrenztem Ausgangsmaterial den Vorrang auf der Platte (mit respectStock)

Felder des Ausgangsmaterials

w und h sind erforderlich. qty ist standardmäßig 1 und wirkt nur mit respectStock als hartes Limit. price gilt pro Platte und bestimmt totalPrice und den Kostenmodus.

Optionen

kerfSägeblattdicke (Standard 0)
toleranceSchnitte akzeptieren, die um maximal diesen Wert überschreiten
trimBesäumung je Seite: left, right, top, bottom
firstCut'auto' | 'horizontal' | 'vertical' (Standard 'auto')
minimizeCostKandidaten nach dem Gesamtmaterialpreis statt nach der Plattenanzahl bewerten
respectStockqty jeder Materialzeile als hartes Limit behandeln
minOffcutnur Reststücke ausgeben, deren kurze Seite mindestens diesen Wert hat
maxCutStagesStufenbegrenzung der Plattensäge — eine Anzahl von Schnittstufen, nicht die reine Baumtiefe
minimizeRotationsLayouts bevorzugen, die weniger Teile drehen

include beschneidet die Antwort: übergeben Sie ["cutPlan","offcuts"] (Standard sind beide) oder lassen Sie einen der beiden weg, um ihn nicht mitzuliefern.

2D — Response

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

Felder der Antwort

Feldnamen und Typen sind der Vertrag, deshalb bleiben die folgenden Tabellen in jeder Sprache englisch — ein übersetzter Feldname würde eine API dokumentieren, die es nicht gibt.

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 — was ein step physisch bedeutet

Ein step ist eine Bewegung des Sägeblatts, und die Liste steht in der Reihenfolge, in der Sie tatsächlich sägen können: ein übergeordneter Schnitt vor den Schnitten innerhalb des Stücks, das er erzeugt hat — denn Sie können einen Streifen nicht ablängen, bevor Sie ihn längs herausgetrennt haben. axis "h" bedeutet, das Sägeblatt bewegt sich entlang x und trennt oben von unten; axis "v" bedeutet, es bewegt sich entlang y und trennt links von rechts. pos ist die Kante des Sägeblatts mit der NIEDRIGEREN Koordinate — der Wert y bei "h", der Wert x bei "v" —, nicht seine Mittellinie: kerf belegt pos bis pos + kerf, das Sägeblatt frisst also in die Richtung, in die die Koordinate wächst — bei "h" nach unten, bei "v" nach rechts. Das Material auf der niedrigen Seite der Linie — bei "h" darüber, bei "v" links davon — ist das Stück, das dieser Schnitt freigibt. length ist die Strecke, die das Sägeblatt bei diesem einen Schnitt zurücklegt: die Ausdehnung des Bereichs, den es durchquert, nicht die Breite der ganzen Platte.

stage ist ein Maschinendurchgang. Der Wert beginnt bei 1 und erhöht sich nur, wenn axis gegenüber dem übergeordneten Schnitt umschlägt — eine Platte längs in sechs Streifen aufzutrennen ist also eine Stufe, und diese Streifen abzulängen die nächste. Das ist der Plattensägen-Sinn von „dreistufigem Zuschnitt“, nicht die Tiefe des Schnittbaums, und genau das begrenzt maxCutStages. sheet ist ein 0-basierter Index in sheets, und step beginnt auf jeder Platte wieder bei 1, statt über den gesamten Auftrag durchzulaufen.

cutPlan ist null — nicht fehlend, nicht leer —, sobald guillotineValid false ist: ein Layout, das sich nicht von Kante zu Kante schneiden lässt, hat keine Schnittsequenz, die zurückgegeben werden könnte. Ganz aus der Antwort verschwindet es nur, wenn Sie es in include weggelassen haben.

1D — Linear

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

Teile erhalten length (dazu qty, name, priority); das Ausgangsmaterial erhält length, qty und price. Die Optionen sind kerf, tolerance, trim.start / trim.end, minimizeCost, respectStock und minOffcut. Die Antwort liefert rods statt sheets — jeweils mit den zugehörigen parts, der Restlänge und den offcuts.

1D — Response

{
  "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 ersetzt sheets, und guillotineValid gibt es nicht, weil ein Linearschnitt immer herstellbar ist. pos eines Teils ist der Abstand seines vorderen Endes von jenem Stabende, das trim.start abschneidet — das erste Teil beginnt also genau bei trim.start, und jedes folgende pos kommt um ein kerf weiter. remaining wird für jeden Stab ausgegeben, auch wenn der Wert unter minOffcut liegt: minOffcut filtert nur das Array offcuts, das höchstens einen Eintrag enthält. Im Schnittplan ist sheet der Index des Stabs, axis immer "v", stage immer 1 und length immer 0 — ein Ablängschnitt am Stab hat keine Verfahrstrecke zu melden, und genau deshalb enthalten die metrics in 1D cuts, aber kein 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.

Engines

Determinismus & Versionierung

Jede Antwort enthält engineVersion. Der Algorithmus ist deterministisch, deshalb ändert eine Verbesserung die Ausgabe für dieselbe Eingabe — was ein Breaking Change ist, wenn Sie cachen. Legen Sie das Verhalten fest, indem Sie engine explizit senden und engineVersion beobachten; die Pfadversion /v1/ ändert sich nur, wenn sich die Struktur der Antwort ändert.

Jede Engine wird unabhängig versioniert — eine Änderung an der einen verschiebt die Version der anderen nie.

Fehler

400 invalid_request Schema-Fehler. details.path verweist auf das betreffende Feld.
401 unauthorized API-Schlüssel fehlt oder ist unbekannt.
402 quota_exceeded Monatskontingent erreicht. Retry-After gibt die Sekunden bis zum Monatswechsel an.
403 key_revoked Der Schlüssel existiert, wurde aber widerrufen.
404 not_found Diese Route gibt es nicht — dasselbe erhalten Sie für den richtigen Pfad mit der falschen Methode.
413 too_large Eingabe über einem Limit (siehe Limits).
429 busy Kapazität momentan ausgeschöpft. Retry-After in Sekunden — das zählt nie auf Ihr Kontingent.
500 internal Unerwarteter Fehler, oder das Auth-Backend ist nicht erreichbar (Anfragen werden dann abgewiesen — fail closed).
504 solve_timeout Die Berechnung hat die harte Zeitgrenze überschritten (durchgesetzt im Proxy).

Fehler-Body

Jeder Fehler, den die Engine selbst erzeugt, verwendet denselben Umschlag. Verzweigen Sie über error — ein stabiler Code —, niemals über message, dessen Wortlaut sich zwischen Releases ändern kann. details ist bei invalid_request vorhanden, wo path das betreffende Feld benennt, und bei too_large, wo max und got das Limit und Ihren gesendeten Wert angeben.

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

Rate-Limit-Header

Ein erfolgreicher Optimierungsaufruf liefert X-RateLimit-Limit (das Monatslimit des Schlüssels) und X-RateLimit-Remaining (die in diesem Monat verbleibenden Aufrufe, diesen eingerechnet). Gesendet werden sie ausschließlich von den Optimierungs-Endpoints: der Zähler wird als Teil der Freigabe einer Berechnung reserviert, deshalb haben /v1/usage und /v1/health nichts zu melden.

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

Nur lesend: der Aufruf verbraucht kein Kontingent und sendet keine Rate-Limit-Header. plan und limit sind Paket und Limit, die bei der Erstellung auf dem Schlüssel festgeschrieben wurden — eine spätere Neubepreisung des Produkts schreibt einen aktiven Schlüssel also nie um. used zählt den laufenden UTC-Kalendermonat, remaining ist limit minus used und wird nie negativ, periodEnd ist der Tag des Zurücksetzens als einfaches Datum in der Form YYYY-MM-DD, und keyPrefix ist das nicht geheime Anzeigepräfix des Schlüssels, mit dem Sie aufgerufen haben. Der Schlüssel selbst wird von keinem Endpoint jemals zurückgegeben — gespeichert ist nur sein Hash, ein verlorener Schlüssel wird deshalb ersetzt, nicht wiederhergestellt.

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
}

Kein Schlüssel, kein Kontingent, keine Datenbank. Der Endpoint berührt bewusst nichts Zustandsbehaftetes, damit ein Ausfall des Schlüsselspeichers den Dienst für einen Orchestrator nicht tot aussehen lässt. engines listet die ids, die diese Installation in engine akzeptiert, und engineVersion ist die Version der Standard-Engine.

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.

Limits

OpenAPI-Spezifikation

Ein maschinenlesbares OpenAPI 3.1-Dokument beschreibt alle vier Endpoints, beide Request-Bodies, jede Antwortstruktur und jeden Fehler. Richten Sie Ihren Client-Generator darauf, statt diese Seite abzuschreiben. Das Dokument selbst ist ausschließlich englisch: es besteht aus Contract-Tokens, und OpenAPI besitzt keinen Lokalisierungsmechanismus.

curl -s https://cutoptim.com/engine/openapi.json > cutoptim-engine.json

OpenAPI 3.1-Dokument öffnen →