Aller au contenu principal
← CutOptim Engine API

Documentation de l'API

URL de base https://api.cutoptim.com · version du contrat v1

Créez une clé sur votre tableau de bord, puis envoyez-la sous la forme Authorization: Bearer <key>.

Spécification OpenAPI: /engine/openapi.json

Endpoints

POST/v1/optimize/2doptimisation de panneaux 2D
POST/v1/optimize/1doptimisation 1D / linéaire (barres, profilés, tubes)
POST/v1/optimize/woodoptimisation du bois — 1D avec correspondance de section
POST/v1/optimize/nestimbrication à forme réelle — polygones irréguliers sur feuilles fixes, avec zones d'exclusion (laser / plasma / jet d'eau)
POST/v1/validate/2dvalider une requête sans la résoudre — gratuit, sans clé, sans quota (2d / 1d / wood / nest)
POST/v1/validate/1dvalider une requête sans la résoudre — gratuit, sans clé, sans quota (2d / 1d / wood / nest)
POST/v1/validate/woodvalider une requête sans la résoudre — gratuit, sans clé, sans quota (2d / 1d / wood / nest)
POST/v1/validate/nestvalider une requête sans la résoudre — gratuit, sans clé, sans quota (2d / 1d / wood / nest)
GET/v1/jobs/{id}interroger un travail asynchrone du moteur max — renvoie son statut et, une fois terminé, le plan (sans quota ; vos propres travaux uniquement)
POST/v1/import/nestlit les contours de pièces dans un fichier SVG ou DXF — nécessite une clé, ne consomme aucun quota
GET/v1/usageconsommation et quota du COMPTE sur le mois en cours
GET/v1/healthétat de disponibilité — sans clé, sans limite de débit, sans base de données

POST /v1/validate/{2d,1d,wood,nest} — response

validbooleanAlways true on a 200 — a malformed body is a 400 with the exact bad field path instead. The body is the SAME one the matching optimize endpoint takes.
modestring"2d" | "1d" | "wood" | "nest" — the endpoint you called.
contractVersionstringShape version. Currently "1".
engineEnabledbooleanNEST ONLY — whether the nesting engine is built into this deployment. Absent for the rectangular modes.
partsobject{ rows, total } — part rows sent, and the total quantity after qty expansion. Check it against the part cap before you spend a call.
stockobject{ rows, total } — the same for stock.
warningsstring[]Approximate, SOLVE-FREE feasibility notes — e.g. a part that fits no stock (2D and nest: bounding box; wood: no matching section long enough). Empty ⇒ every part fits something. Ignores trim, kerf and material: a shape check, not a solve.

Unités

L'API est agnostique en unités. Choisissez une unité — millimètres, pouces, peu importe —, utilisez-la pour tous les nombres que vous envoyez, et tous les nombres renvoyés seront exprimés dans cette même unité. Rien n'est converti côté serveur, et aucun nom de champ n'impose une unité.

Cela vaut pour les dimensions des pièces et du stock, kerf, tolerance, trim et minOffcut en entrée, et pour chaque coordonnée, position, longueur restante, chute et longueur de coupe en sortie. Mélanger les unités dans une même requête produit un plan qui passe la validation et qui est physiquement faux, sans que le serveur puisse le détecter.

Système de coordonnées

L'origine est le coin supérieur gauche du panneau : x croît vers la droite le long de la largeur du panneau, y croît vers le bas le long de sa hauteur. Les x et y d'une pièce désignent son coin supérieur gauche, et ses w et h sont les dimensions telles que posées — déjà permutées lorsque rotated vaut true — de sorte que le rectangle x, y, w, h est l'empreinte sur le panneau, sans calcul supplémentaire. Les rectangles de chutes utilisent le même système.

Le délignage (trim) déplace les placements : trim.left décale toutes les pièces vers la droite et trim.top les décale vers le bas, car les pièces sont imbriquées dans la zone utile, puis ramenées par décalage sur le panneau entier. trim.right et trim.bottom réduisent la zone utile sans déplacer l'origine. Les w et h d'un panneau sont toujours les dimensions complètes du stock, délignage inclus — ce qui explique aussi pourquoi le délignage compte comme de la perte dans yieldPct.

2D — requête

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 },
    "minimizeCost": true,
    "effort": "balanced"
  },
  "engine": "heuristic",
  "include": ["cutPlan", "offcuts"]
}

Champs d'une pièce

w, hobligatoires — dimensions de la pièce
qty1 par défaut — développé côté serveur ; compte dans le plafond de 2,000
namelibellé facultatif, repris sur chaque placement
rotatabletrue par défaut — la pièce peut-elle être tournée de 90°
grainGrouples membres d'un groupe restent sur un même panneau (correspondance du grain)
priorityà découper impérativement : obtient la place sur le panneau quand le stock est plafonné (avec respectStock)
edgeBandingbande de chant par côté : nommez une référence de type sur l'un de top / right / bottom / left (une chaîne libre, votre propre code) — la réponse totalise les mètres par référence. Métadonnée seulement, ne déplace jamais une pièce. 2D uniquement
materialétiquette de matériau (une chaîne libre, votre propre code) : les pièces et le stock du même matériau ne sont regroupés qu'entre eux. Contrairement à edgeBanding, elle change la disposition. Absent = un seul pool non spécifié. Fonctionne dans tous les modes

Champs du stock

w et h sont obligatoires. qty vaut 1 par défaut et n'est une limite stricte qu'avec respectStock. price s'entend par panneau et alimente totalPrice ainsi que le mode coût. priority (booléen) épuise ce stock en premier ; material (une chaîne libre) le restreint aux pièces du même matériau.

Options

kerflargeur du trait de scie (0 par défaut)
toleranceaccepter les coupes qui dépassent jusqu'à cette valeur
trimdélignage par côté : left, right, top, bottom
firstCut'auto' | 'horizontal' | 'vertical' ('auto' par défaut)
minimizeCostclasser les candidats par prix total du stock le plus bas ; avec plusieurs tailles de stock tarifées, il les combine (2D) ou choisit la longueur la moins chère (1D) pour réduire la facture, même si cela consomme plus de matière
respectStocktraiter le qty de chaque ligne de stock comme une limite stricte
minOffcutne remonter que les chutes dont le petit côté atteint au moins cette valeur
maxCutStageslimite de phases pour scie à panneaux — un nombre de phases, pas la profondeur brute de l'arbre
minimizeRotationspréférer les dispositions qui tournent moins de pièces
effort'fast' | 'balanced' ('balanced' par défaut). Profondeur de recherche : 'balanced' exécute le best-of multi-stratégie complet ; 'fast' saute l'unique recherche coûteuse de combinaisons d'aires par panneau — nettement plus rapide sur les gros travaux, au prix de quelques points d'utilisation, toujours guillotine-valide et jamais plus dense que 'balanced'. Sur les petits travaux, le résultat est généralement identique. Uniquement moteur heuristic

include fait deux choses. RESTRICTION — "cutPlan" et "offcuts" sont actifs par défaut ; un tableau présent ne conserve que les jetons de restriction qu'il énumère (un tableau vide retire les deux). EXPORT ADDITIF — "svg", "csv" et "dxf" ajoutent chacun cet export à la réponse sous forme de CHAÎNE : svg un dessin de disposition 2D autonome (2D uniquement — une requête 1d/wood renvoie plutôt un avertissement), dxf un dessin R12/AC1009 sur les calques STOCK/PARTS/LABELS, csv une liste de coupe. Les jetons d'export n'affectent pas la restriction : include:["svg"] ajoute svg et — ne nommant aucun jeton de restriction — retire cutPlan/offcuts ; utilisez ["cutPlan","offcuts","svg"] pour tout conserver et ajouter svg.

parts[].meta · stock[].meta — passthrough (2d · 1d · wood · nest)

parts[].metaobjectOpaque JSON of your own — an ERP article number, an order-line id, a customer ref. Echoed VERBATIM on every placed piece of that part. The optimizer never reads it, so it can never change a layout.
stock[].metaobjectThe same on a stock row: echoed on every sheet / rod / nested sheet cut from it, so the plan reconciles with your system without a lookup table.

L'option effort arbitre entre le temps de calcul et l'utilisation. Voici ce compromis, mesuré sur un travail exigeant — chaque chiffre provient du vrai packer.

Sélecteur effort : utilisation matière vs temps de calculSélecteur effort : utilisation matière vs temps de calcul. fast: 350 panneaux · 76.2% · ≈1.9 s. balanced: 330 panneaux · 80.8% · ≈4.8 s. max: réservé — plus dense = recherche plus lente. Sur la plupart des travaux (plus petits), les deux sont identiques ; l'écart ne se creuse que sur les gros travaux comme celui-ci. balanced est la valeur par défaut et n'est jamais plus dense que ce que fast peut atteindre.Sélecteur effort : utilisation matière vs temps de calculUn travail exigeant — environ 1 550 pièces sur un panneau de 2,07 × 5,6 m. Chaque chiffre mesuré sur le vrai packer.74%76%78%80%82%84%02 s4 s6 stemps de calcul · plus rapide →utilisation matière · plus dense ↑⇄ le sélecteur effort+4,6 pts utilisation · −20 panneaux−5,7 % matière · ≈2,5× plus lentfast350 panneaux · 76.2% · ≈1.9 s★ balanced · par défaut330 panneaux · 80.8% · ≈4.8 smaxréservéplus dense =recherche pluslente
Sur la plupart des travaux (plus petits), les deux sont identiques ; l'écart ne se creuse que sur les gros travaux comme celui-ci. balanced est la valeur par défaut et n'est jamais plus dense que ce que fast peut atteindre.

Et c'est rapide dans tous les cas : même les plus gros travaux de production — 2 000 pièces et plus — se résolvent en quelques secondes avec le moteur par défaut, largement dans le budget de temps de l'API.

2D — réponse

{
  "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": 600, "h": 400, "rotated": false },
        { "name": "Door", "x": 613, "y": 10, "w": 600, "h": 400, "rotated": false },
        { "name": "Door", "x": 1216, "y": 10, "w": 600, "h": 400, "rotated": false },
        { "name": "Door", "x": 1819, "y": 10, "w": 600, "h": 400, "rotated": false }
      ],
      "offcuts": [
        { "x": 2422, "y": 10, "w": 18, "h": 400 },
        { "x": 10, "y": 413, "w": 2430, "h": 807 }
      ]
    }
  ],
  "metrics": {
    "sheetCount": 1,
    "yieldPct": 32.25,
    "placed": 4,
    "total": 4,
    "cutLines": 5,
    "sawPasses": 5,
    "cutLength": 4030,
    "totalPrice": 42
  },
  "unplaced": [],
  "warnings": [],
  "guillotineValid": true,
  "timing": { "solveMs": 3.13 },
  "cutPlan": [
    { "sheet": 0, "step": 1, "axis": "h", "pos": 410, "length": 2430, "stage": 1 },
    { "sheet": 0, "step": 2, "axis": "v", "pos": 610, "length": 400, "stage": 2 },
    { "sheet": 0, "step": 3, "axis": "v", "pos": 1213, "length": 400, "stage": 2 },
    { "sheet": 0, "step": 4, "axis": "v", "pos": 1816, "length": 400, "stage": 2 },
    { "sheet": 0, "step": 5, "axis": "v", "pos": 2419, "length": 400, "stage": 2 }
  ]
}
  • cutLines vs sawPasses — cutLines fusionne les coupes colinéaires (un seul réglage de butée) ; sawPasses compte chaque passe. Deux mesures honnêtes du même plan, et non la prétention de retrouver le décompte d'un concurrent.
  • guillotineValid / cutPlan — Quand une disposition ne peut pas être coupée de rive à rive, guillotineValid vaut false et cutPlan vaut null. C'est une information réelle — le plan n'est pas réalisable sur une scie à panneaux — et non une erreur.
  • unplaced + warnings — Un travail impossible à satisfaire renvoie 200, avec les pièces listées dans unplaced et une note dans warnings. Un plan exploitable vaut mieux qu'un code de statut.
  • edgeBanding — Dès qu'une pièce porte edgeBanding, la réponse ajoute un bloc edgeBanding : les mètres linéaires que consomme chaque référence de type, par pièce et en total de commande. C'est de la géométrie exacte, sans marge de perte — l'atelier ajoute la sienne — et elle suppose une saisie en millimètres (÷1000 pour les mètres). La clé est entièrement absente pour un travail sans chant.
  • materials / unmatchedMaterials — Dès qu'une pièce ou un stock porte material, la réponse ajoute materials (un récapitulatif par matériau — material, sheetCount/rodCount, yieldPct, placed, total, totalPrice) et unmatchedMaterials (la demande dont le matériau n'a aucun stock correspondant). En bois, le matériau accompagne plutôt chaque section de section transversale. Les deux clés sont absentes pour un travail sans matériau, qui reste identique au bit près.

Champs de la réponse

Les noms et les types des champs constituent le contrat : les tableaux ci-dessous restent donc en anglais dans toutes les langues — un nom de champ traduit documenterait une API qui n'existe pas.

POST /v1/optimize/2d — top level

enginestringWhich engine ran: "heuristic", "balanced" or "max".
engineVersionstringAlgorithm identity — package version + a content hash of the algorithm source. Moves automatically on any packer change, and independently per engine.
contractVersionstringShape version, matching the /v1/ in the path. Currently "1".
deterministicbooleanAlways true. Present so a client can assert the guarantee it relies on.
sheetsarrayOne entry per sheet used, in cutting order.
metricsobjectAggregate numbers for the whole job.
cutPlanarray | nullThe sawing plan. null when guillotineValid is false; absent when excluded via include.
unplacedarrayParts that did not fit, aggregated by name + size. Empty when everything fitted.
warningsstring[]Free-text notes about the plan. Do not parse — branch on unplaced, guillotineValid and metrics.
guillotineValidbooleanTrue when the layout is producible with edge-to-edge cuts, i.e. on a panel saw.
edgeBandingobjectLinear metres of edge banding, grouped by type reference. ABSENT unless a part requested banding via parts[].edgeBanding. 2D only.
materialsarrayPer-material rollup. ABSENT unless a part or stock row carried material — a material-free job stays byte-identical. (OPEN-256)
unmatchedMaterialsarrayDemand whose material has no matching stock at all. ABSENT when it does not happen. A missing-material report, not a did-not-fit one.
svgstringInline SVG of the 2D layout (self-contained, no external refs). Present ONLY when include contains "svg". 2D only. (OPEN-223)
csvstringInline CSV cut list. Present ONLY when include contains "csv".
dxfstringInline DXF (R12/AC1009) on layers STOCK/PARTS/LABELS. Present ONLY when include contains "dxf".
timing.solveMsnumberMilliseconds inside the packer, 2 decimals. Excludes parsing, auth and queueing.

sheets[]

w, hnumberFULL stock dimensions, trim included — not the usable area.
pricenumber | nullPrice of the stock row this sheet came from, or null if none was given.
partsarrayPlacements on this sheet.
offcutsarrayUsable leftover rectangles, filtered by options.minOffcut. Absent (not empty) when excluded via include.
metaobjectPresent only when the stock row carried meta — echoed verbatim from stock[].meta. (OPEN-224)

sheets[].parts[]

namestringThe requested name, or the generated default "Part <row>".
x, ynumberTop-left corner of the part, from the top-left corner of the sheet.
w, hnumberDimensions AS PLACED — already swapped when rotated is true. No client-side swap needed.
rotatedbooleanTrue if the part was turned 90° from the requested w×h. Informational only.
metaobjectPresent only when the part carried meta — echoed verbatim from parts[].meta on every placed piece. (OPEN-224)

sheets[].offcuts[]

x, y, w, hnumberA leftover rectangle, in the same coordinate system as the placements.

metrics (2D)

sheetCountintegerSheets used. Equals sheets.length.
yieldPctnumberPlaced part area ÷ total FULL sheet area × 100, 2 decimals. Trim and kerf count as waste.
placedintegerPieces placed, after qty expansion.
totalintegerPieces requested, after qty expansion.
cutLinesintegerCollinear cuts merged: same axis, same coordinate, same stage counted once — one fence setting.
sawPassesintegerEvery cut, one per strip crossed — how many separate passes the saw makes.
cutLengthnumberTotal distance sawn, 3 decimals. Same under either counting convention. In your unit.
totalPricenumberSum of the used sheets’ prices, 2 decimals. 0 when no stock row carried a price.

unplaced[] (2D)

namestringThe part name.
w, hnumberAs requested.
qtyintegerHow many of this part could not be placed.

edgeBanding (2D — present only when a part is banded)

totalMetersnumberOrder-wide total across every banded edge, 3 decimals. ⚠️ Assumes mm input: top/bottom edges run the part width, left/right the height, ×qty, ÷1000. In another unit it is your raw edge length ÷ 1000.
byType[]{ reference, meters }Order total split by the caller-supplied type reference, sorted by reference.
byPart[]{ name, meters, byType }One entry per requested part row that has any banded edge; its byType splits that part’s metres by reference.

cutPlan[]

sheetinteger0-based index into sheets (2D) or rods (1D). Named sheet in both.
stepinteger1-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.
posnumberThe 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.
lengthnumberDistance the blade travels on this cut: the extent of the region crossed. Always 0 in 1D.
stageinteger1-based machine pass. Increments only when the axis flips relative to the parent cut.

2D · edgeBanding

Dès qu'une pièce porte edgeBanding, la réponse ajoute un bloc edgeBanding : les mètres linéaires que consomme chaque référence de type, par pièce et en total de commande. C'est de la géométrie exacte, sans marge de perte — l'atelier ajoute la sienne — et elle suppose une saisie en millimètres (÷1000 pour les mètres). La clé est entièrement absente pour un travail sans chant.

POST /v1/optimize/2d

{
  "parts": [
    {
      "name": "Door",
      "w": 600,
      "h": 400,
      "qty": 2,
      "edgeBanding": {
        "top": "ABS oak 22",
        "bottom": "ABS oak 22",
        "left": "ABS white 22",
        "right": "ABS white 22"
      }
    },
    { "name": "Shelf", "w": 800, "h": 300, "edgeBanding": { "top": "ABS oak 22" } }
  ],
  "stock": [{ "w": 2440, "h": 1220, "qty": 10, "price": 42 }],
  "options": { "kerf": 3 }
}
{
  "edgeBanding": {
    "totalMeters": 4.8,
    "byType": [
      { "reference": "ABS oak 22", "meters": 3.2 },
      { "reference": "ABS white 22", "meters": 1.6 }
    ],
    "byPart": [
      {
        "name": "Door",
        "meters": 4,
        "byType": [
          { "reference": "ABS oak 22", "meters": 2.4 },
          { "reference": "ABS white 22", "meters": 1.6 }
        ]
      },
      {
        "name": "Shelf",
        "meters": 0.8,
        "byType": [{ "reference": "ABS oak 22", "meters": 0.8 }]
      }
    ]
  }
}

material

Dès qu'une pièce ou un stock porte material, la réponse ajoute materials (un récapitulatif par matériau — material, sheetCount/rodCount, yieldPct, placed, total, totalPrice) et unmatchedMaterials (la demande dont le matériau n'a aucun stock correspondant). En bois, le matériau accompagne plutôt chaque section de section transversale. Les deux clés sont absentes pour un travail sans matériau, qui reste identique au bit près.

POST /v1/optimize/2d

{
  "parts": [
    { "name": "Door", "w": 600, "h": 400, "qty": 4, "material": "MDF 18" },
    { "name": "Shelf", "w": 800, "h": 300, "qty": 6, "material": "Oak 18" },
    { "name": "Back panel", "w": 1000, "h": 500, "qty": 2, "material": "Ply 6" }
  ],
  "stock": [
    { "w": 2440, "h": 1220, "qty": 10, "price": 42, "material": "MDF 18" },
    { "w": 2440, "h": 1220, "qty": 10, "price": 68, "material": "Oak 18", "priority": true }
  ],
  "options": { "kerf": 3 },
  "engine": "heuristic"
}
{
  "materials": [
    {
      "material": "MDF 18",
      "sheetCount": 1,
      "yieldPct": 32.25,
      "placed": 4,
      "total": 4,
      "totalPrice": 42
    },
    {
      "material": "Oak 18",
      "sheetCount": 1,
      "yieldPct": 48.37,
      "placed": 6,
      "total": 6,
      "totalPrice": 68
    }
  ],
  "unmatchedMaterials": [
    {
      "material": "Ply 6",
      "parts": [{ "name": "Back panel", "w": 1000, "h": 500, "qty": 2 }]
    }
  ]
}

materials[] — per-material rollup (2d · 1d · nest)

materialstringThe tag exactly as you sent it. Free text, matched exactly.
sheetCount | rodCountintegerStock consumed for this material — sheetCount on 2D and nest, rodCount on 1D.
yieldPct | densitynumberThis material’s own fill — yieldPct on the rectangular modes, density on nest (a polygon fill, not comparable to yieldPct).
placed, totalintegerPieces placed and requested for this material, after qty expansion.
totalPricenumberSum of the prices of the stock used for this material.

unmatchedMaterials[] — demand with no matching stock

materialstringThe tag that has no stock of its own anywhere in the request.
partsarrayThe demand rows in that material, in the mode’s unplaced shape: name, w, h, qty on 2D; name, length, qty on 1D; name, qty on nest.

cutPlan — ce que représente physiquement une étape

Une étape (step) correspond à un seul mouvement de lame, et la liste suit l'ordre dans lequel vous pouvez réellement scier : une coupe parente avant les coupes internes à la pièce qu'elle a produite, car vous ne pouvez pas tronçonner une bande avant de l'avoir délignée. axis "h" signifie que la lame se déplace le long de x et sépare le haut du bas ; axis "v" signifie qu'elle se déplace le long de y et sépare la gauche de la droite. pos est le bord de la lame situé du côté des COORDONNÉES FAIBLES — la valeur de y pour "h", celle de x pour "v" — et non son axe médian : le kerf occupe la plage pos à pos + kerf, si bien que la lame mord dans le sens où croît la coordonnée, c'est-à-dire vers le bas pour "h" et vers la droite pour "v". La matière située du côté faible de la ligne — au-dessus d'elle pour "h", à sa gauche pour "v" — est la pièce que cette coupe libère. length est la distance parcourue par la lame sur cette seule coupe : l'étendue de la région traversée, et non la largeur du panneau entier.

stage est une passe machine. Il commence à 1 et n'augmente que lorsque axis change par rapport à la coupe parente : déligner un panneau en six bandes est donc une seule phase, et les tronçonner est la suivante. C'est le sens que la scie à panneaux donne à la « coupe en trois phases », et non la profondeur de l'arbre de coupe — et c'est ce que maxCutStages contraint. sheet est un index à base 0 dans sheets, et step repart à 1 sur chaque panneau au lieu de se poursuivre sur tout le travail.

cutPlan vaut null — ni absent, ni vide — dès que guillotineValid vaut false : une disposition qui ne peut pas être coupée de rive à rive n'a aucune séquence de coupes à renvoyer. Le champ ne figure pas du tout dans la charge utile si vous l'avez exclu de include.

1D — linéaire

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

Les pièces prennent length (plus qty, name, priority) ; le stock prend length, qty et price. Les pièces comme le stock acceptent aussi une étiquette material facultative (le stock aussi priority) — material restreint une pièce au stock du même matériau, et la réponse ajoute alors materials et unmatchedMaterials comme en 2D. Les options sont kerf, tolerance, trim.start / trim.end, minimizeCost, respectStock et minOffcut. La réponse renvoie rods au lieu de sheets, chaque barre avec ses pièces, sa longueur restante et ses chutes.

1D — réponse

{
  "engine": "heuristic",
  "engineVersion": "1.0.0+10e0c941",
  "contractVersion": "1",
  "deterministic": true,
  "rods": [
    {
      "length": 3000,
      "price": 12.5,
      "remaining": 584,
      "parts": [
        { "name": "Rail", "pos": 10, "length": 1200 },
        { "name": "Rail", "pos": 1213, "length": 1200 }
      ],
      "offcuts": [584]
    },
    {
      "length": 3000,
      "price": 12.5,
      "remaining": 584,
      "parts": [
        { "name": "Rail", "pos": 10, "length": 1200 },
        { "name": "Rail", "pos": 1213, "length": 1200 }
      ],
      "offcuts": [584]
    },
    {
      "length": 3000,
      "price": 12.5,
      "remaining": 584,
      "parts": [
        { "name": "Rail", "pos": 10, "length": 1200 },
        { "name": "Rail", "pos": 1213, "length": 1200 }
      ],
      "offcuts": [584]
    }
  ],
  "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 remplace sheets et il n'y a pas de guillotineValid, car une coupe linéaire est toujours réalisable. Le pos de chaque pièce est le décalage de son extrémité proche par rapport à l'extrémité de barre rognée par trim.start : la première pièce commence donc exactement à trim.start, et chaque pos suivant ajoute la valeur de kerf. remaining est la chute UTILISABLE : le trait de scie de la coupe qui la libère de la dernière pièce est déjà déduit, c'est donc la longueur récupérable, et non l'espace brut. Il est indiqué sur chaque barre, même lorsqu'il est inférieur à minOffcut — minOffcut ne filtre que le tableau offcuts, qui contient au plus une entrée. Dans le plan de découpe, sheet est l'index de la barre, axis vaut toujours "v", stage vaut toujours 1 et length vaut toujours 0 : un tronçonnage de barre n'a aucune distance de déplacement à indiquer, ce qui explique aussi pourquoi les metrics 1D portent cuts mais pas cutLength.

Bois — section

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

Le bois a une identité qu'une simple barre n'a pas : une pièce 50×150 ne peut pas sortir d'un stock 50×100, quelle que soit la longueur restante. Les pièces et le stock portent donc sw et sh, les deux côtés de la section, dans n'importe quel ordre — 50×100 et 100×50 sont la même poutre retournée et forment une seule section. Le travail est découpé par section, chaque section est associée à son propre stock et résolue séparément, et un seul appel renvoie l'ensemble. Les pièces et le stock acceptent aussi une étiquette material facultative (le stock aussi priority) : avec elle, un chêne 50×100 et un pin 50×100 deviennent deux sections distinctes, et chaque section porte son matériau. Les options sont les mêmes qu'en 1D.

Bois — réponse

{
  "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": 391,
          "parts": [
            { "name": "Rafter", "pos": 0, "length": 2400 },
            { "name": "Noggin", "pos": 2403, "length": 600 },
            { "name": "Noggin", "pos": 3006, "length": 600 }
          ],
          "offcuts": [391]
        },
        {
          "length": 4000,
          "price": 12.5,
          "remaining": 391,
          "parts": [
            { "name": "Rafter", "pos": 0, "length": 2400 },
            { "name": "Noggin", "pos": 2403, "length": 600 },
            { "name": "Noggin", "pos": 3006, "length": 600 }
          ],
          "offcuts": [391]
        }
      ],
      "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": 997,
          "parts": [{ "name": "Beam", "pos": 0, "length": 3000 }],
          "offcuts": [997]
        },
        {
          "length": 4000,
          "price": 18,
          "remaining": 997,
          "parts": [{ "name": "Beam", "pos": 0, "length": 3000 }],
          "offcuts": [997]
        }
      ],
      "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 remplace rods au premier niveau : chaque entrée est une section avec ses propres rods (de forme identique à 1D) et ses propres metrics, si bien que les chiffres par matériau sont déjà là sans recalcul. unmatched n'a pas d'équivalent en 1D — ce sont les besoins dont la section n'a reçu aucun stock, un problème différent de unplaced (des pièces qui avaient du stock et n'ont pas tenu) et qui appelle une autre correction ; les deux ne sont donc jamais mélangés. metrics.total compte chaque pièce demandée, y compris celles de unmatched. Dans le plan de coupe, chaque étape indique aussi sa section, et sheet est l'index de la barre AU SEIN de cette section, pas un compteur global.

POST /v1/optimize/wood — top level (what differs from 1D)

sectionsarrayReplaces rods at the top level: one entry per cross-section, each matched to its own stock and solved on its own.
unmatchedarrayDemand whose cross-section has NO stock at all. Deliberately separate from unplaced (had stock, did not fit) — the fix differs, so the two are never mixed.
unplacedarrayDid-not-fit demand, aggregated across the sections that DID have stock. Same shape as 1D.
metricsobjectJob-wide totals across every section (below).
cutPlanarray | nullAs 1D, except every step also carries section, and sheet is the rod index WITHIN that section, not a job-wide counter.
csv, dxfstringInline export, present only when include names the token. svg is 2D-only — a wood request asking for it gets a warning instead.

sections[]

sectionstringNormalised cross-section key, e.g. "50x100" — short side first, so 50×100 and 100×50 are one section.
sw, shnumberThe two cross-section sides: sw the SHORT one, sh the long one, whatever order they arrived in.
stockNamestring | nullThe name of the stock row this section was matched to, or null when that row carried none.
materialstringPresent only when the section carries a material — an oak 50×100 and a pine 50×100 are two sections. (OPEN-256)
rodsarrayIdentical in shape to the 1D rods[] above, meta and offcuts included.
metricsobjectThis section’s own totals: rodCount, yieldPct, placed, total, cuts, totalPrice — so the per-section figures need no recomputation.
unplacedarrayThis section’s parts that had stock and still did not fit.

metrics (wood)

sectionCountintegerCross-sections solved. Equals sections.length.
rodCountintegerBars used across every section.
yieldPctnumberPlaced length ÷ total FULL bar length × 100 over the whole job, 2 decimals.
placedintegerPieces placed, after qty expansion.
totalintegerPieces REQUESTED, after qty expansion — unmatched ones included.
cutsintegerCrosscuts across every used bar.
totalPricenumberSum of the used bars’ prices, 2 decimals.
toleranceAcceptedCountintegerPieces that fitted only because options.tolerance allowed an overshoot.

unmatched[]

sectionstringThe cross-section key nothing in stock matched.
sw, shnumberThat cross-section’s two sides.
materialstringPresent when the cross-section DOES exist in stock but only in a different material. (OPEN-256)
partsarrayThe demand rows in that section, in the 1D unplaced shape: name, length, qty.
qtyintegerTotal pieces in this section that had no stock at all.

Imbrication à forme réelle

POST /v1/optimize/nest

{
  "parts": [
    {
      "name": "bracket",
      "polygon": [[0, 0], [300, 0], [300, 100], [100, 100], [100, 300], [0, 300]],
      "qty": 4,
      "allowedRotations": [0, 90, 180, 270]
    },
    {
      "name": "gusset",
      "polygon": [[0, 0], [280, 0], [0, 280]],
      "qty": 4,
      "allowedRotations": [0, 90, 180, 270]
    }
  ],
  "stock": [
    {
      "w": 2440,
      "h": 1220,
      "price": 12.5,
      "exclusions": [{ "polygon": [[0, 0], [300, 0], [0, 300]], "quality": 0 }]
    }
  ]
}

Les trois modes ci-dessus placent des rectangles. POST /v1/optimize/nest place des POLYGONES ARBITRAIRES : une pièce est un contour (polygon, avec des holes intérieurs facultatifs), et non un width×height, de sorte que les pièces s'imbriquent dans les poches concaves les unes des autres et que l'air perdu dans une encoche par un rectangle englobant est récupéré — sur un travail représentatif, 6 feuilles là où les mêmes pièces en rectangle englobant en exigent 9. C'est une classe d'algorithme différente (un moteur de collision géométrique, pas le placeur guillotine), pour la découpe laser, plasma et jet d'eau. Il apporte deux choses que l'API rectangulaire ne peut pas exprimer : des zones d'exclusion par feuille (stock[].exclusions — un défaut, l'empreinte d'une bride, une zone pré-imprimée ; une zone de quality 0 est une région interdite à toute pièce) et des trous à forme réelle. La partition par material et le transfert de meta fonctionnent comme partout ailleurs. L'exemple ci-dessous est un véritable appel capturé — huit pièces sur une seule feuille, avec un coin endommagé exclu.

POST /v1/optimize/nest — request (top level)

partsarrayOne or more NestPart (see below). Required.
stockarrayOne or more NestStock sheet types (see below). Required.
optionsobjectSolve options (see below). Optional.
enginestring"lbf" (default, single-pass, instant) or "sparrow" (advertised for a future higher-density build; currently served by lbf with a warning).
includestring[]Additive inline export of the ACHIEVED nest: "svg" a self-contained styled drawing (one titled band per sheet, parts as filled polygons with holes, exclusion zones hatched), "dxf" an R12/AC1009 document on layers STOCK/PARTS/HOLES/ZONES/LABELS. Unit-agnostic, exactly like the request coordinates. The geometry in sheets[] is returned either way.

parts[] (NestPart)

polygonnumber[][]The part outline: an ordered ring of ≥3 [x, y] vertices. Given closed (first == last) or open; winding order is not required — it is oriented internally. Required UNLESS the row carries source.
sourceobjectOPEN-262 — read the outline out of an SVG or DXF FILE instead of listing coordinates (fields below). EXACTLY ONE of polygon / source: both, or neither, is a 400.
holesnumber[][][]Optional interior holes — a part with a cut-out. Each hole is a ring like polygon. NOT accepted alongside source: the file already carries its own holes, and two sources of truth for one geometry is not a thing we resolve silently.
qtyintegerCopies to place. Default 1.
allowedRotationsnumber[] | "continuous"Allowed rotations in DEGREES (e.g. [0,90,180,270]). Omit or "continuous" for free rotation.
minQualityintegerThe part may only be placed where sheet quality ≥ this. Default 1 → it avoids every quality-0 exclusion zone. Raise it to keep the part off inferior-but-not-forbidden zones too.
prioritybooleanMust-cut: wins sheet space when stock is capped (options.respectStock).
materialstringOPEN-256 — a part of material X nests only on material-X sheets; the job is partitioned by material.
namestringOptional label, echoed on every placement. Defaults to "Part <1-based row index>".
metaobjectOPEN-224 — opaque JSON (your ERP ids), echoed verbatim on every placed copy. Never affects the layout.

stock[] (NestStock)

w, hnumberRectangular sheet size. Give w & h OR polygon, not both.
polygonnumber[][]Arbitrary sheet outline (an off-cut remnant, a non-rectangular board); overrides w/h.
qtyintegerDefault 1. A HARD cap only when options.respectStock is true.
pricenumberPer-sheet price, for cost mode + totalPrice.
exclusionsobject[]OPEN-233 — per-sheet excluded / inferior zones. Each: { polygon: number[][], quality?: integer }. quality 0 (the default) = a hard no-go region for any part; a higher quality only excludes parts whose minQuality demands at least that. This is what the rectangular API cannot express.
materialstringOPEN-256 — this sheet serves only material-matching parts.
metaobjectOPEN-224 — echoed on every sheet cut from this stock row.

options (NestOptions)

minSeparationnumberMinimum clearance between parts and between a part and any hazard (sheet edge / exclusion zone). Use for kerf / beam / torch width. Default 0.
seedintegerDeterminism: a fixed seed → a reproducible layout. Omit and the server pins a fixed default so the response stays reproducible.
minimizeCostbooleanRank plans by total sheet price rather than sheet count.
respectStockbooleanTreat each stock qty as a hard cap.
simplifyTolerancenumberPolygon simplification tolerance (max area deviation as a fraction). Speeds up dense DXF outlines with hundreds of vertices. 0 disables.
timeBudgetMsintegerWall-clock budget for the metaheuristic (engine "sparrow"). Ignored by "lbf" (single-pass).

Nest — réponse

{
  "engine": "lbf",
  "engineVersion": "1.0.0+nest-d4046d5-07546022",
  "contractVersion": "1",
  "deterministic": true,
  "sheets": [
    {
      "w": 2440,
      "h": 1220,
      "price": 12.5,
      "density": 0.1217,
      "parts": [
        { "name": "bracket", "sheet": 0, "x": 300.003, "y": 400.226, "rotation": -180 },
        { "name": "bracket", "sheet": 0, "x": 300.133, "y": 700.617, "rotation": -180 },
        { "name": "bracket", "sheet": 0, "x": 300.061, "y": 1000.969, "rotation": -180 },
        { "name": "bracket", "sheet": 0, "x": 400.095, "y": 1102.332, "rotation": -180 },
        { "name": "gusset", "sheet": 0, "x": 530.102, "y": 50.205, "rotation": 90 },
        { "name": "gusset", "sheet": 0, "x": 300.058, "y": 380.171, "rotation": -90 },
        { "name": "gusset", "sheet": 0, "x": 580.15, "y": 380.193, "rotation": 90 },
        { "name": "gusset", "sheet": 0, "x": 300.204, "y": 660.982, "rotation": -90 }
      ],
      "exclusions": [{ "polygon": [[0, 0], [300, 0], [0, 300]], "quality": 0 }]
    }
  ],
  "metrics": { "sheetCount": 1, "density": 0.1217, "placed": 8, "total": 8, "totalPrice": 12.5 },
  "unplaced": [],
  "warnings": [],
  "timing": { "solveMs": 44.19 }
}

Chaque entrée de sheets est une feuille utilisée ; une pièce placée porte la transformation rigide (rotation en degrés, puis translation x/y), et NON un polygone réémis — faites tourner votre contour d'entrée de rotation autour de son origine et ajoutez (x, y) pour reconstruire le placement exactement. rotation peut être négatif ; la reconstruction est exacte quel que soit le signe. ⚠️ density est l'aire du POLYGONE placé rapportée à l'aire de la feuille utilisée — le remplissage honnête, les poches concaves comptées comme vides — et n'est PAS comparable au yieldPct d'un placeur rectangulaire (qui compte chaque rectangle englobant comme plein, si bien qu'il affiche une valeur plus élevée pour un résultat moins bon) ; la métrique comparable entre les deux est sheetCount sur les mêmes pièces. La disposition est déterministe : définissez options.seed pour la reproduire. exclusions est repris sur chaque feuille pour le rendu.

POST /v1/optimize/nest — top level

enginestringWhich nesting engine ran: "lbf" or "sparrow".
engineVersionstringThe nesting engine's algorithm identity (jagua-rs revision + build hash). Versions independently of the rectangular engines.
contractVersionstringNest contract version, currently "1". Versions independently of the /v1/ rectangular contract — it is a different path and engine family.
deterministicbooleanAlways true — guaranteed by the pinned seed.
sheetsarrayOne entry per used sheet.
metricsobjectJob totals (see below).
unplacedarrayDemand that could not be placed — a plan-plus-warning, not an error.
warningsstring[]e.g. an engine substitution ("sparrow" served by "lbf"), unplaced parts, or a material with no matching stock.
materialsarrayOPEN-256 per-material rollup — present only when parts/stock carry material.
unmatchedMaterialsarrayParts whose material has no matching stock — present only when it happens.
importedobjectOPEN-262 — what the request’s source files contributed (fields above). ABSENT for a coordinates-only job, which is what keeps such a request byte-identical to before the feature existed.
svg, dxfstringThe achieved nest as an inline drawing — present ONLY when include contains that token. svg is self-contained (no external refs); dxf is R12/AC1009.
timingobject{ solveMs: number } — the solve time; environment-dependent.

sheets[] (nest)

w, hnumberPresent for rectangular sheets.
polygonnumber[][]Present for arbitrary-outline sheets instead of w/h.
pricenumber | nullThe stock row's price, or null.
densitynumberThis sheet's fill = placed polygon area / sheet area.
partsarrayPlacements on this sheet (see below).
exclusionsobject[]The zones that applied to this sheet, echoed for rendering.
materialstringPresent when the sheet carried a material (OPEN-256).
metaobjectEchoed from the stock row's meta (OPEN-224).

sheets[].parts[] (nest — placed)

namestringThe requested name, or the generated default.
sheetinteger0-based index into sheets.
x, ynumberTranslation, applied AFTER rotation about the part's origin.
rotationnumber⚠️ Degrees, applied about the part's own origin FIRST. A placed part carries this rigid transform, NOT a re-emitted polygon: rotate your input outline by rotation, then add (x, y) to reconstruct the placement exactly. May be negative; the reconstruction is exact regardless of sign.
materialstringThe material this copy nested from (OPEN-256).
metaobjectThe part's opaque passthrough (OPEN-224).

metrics (nest)

sheetCountintegerSheets used. Equals sheets.length. ⚠️ The honest, cross-comparable metric between nesting and the rectangular modes.
densitynumber⚠️ Placed POLYGON area / used sheet area — the honest fill (concave pockets count as empty). NOT comparable to a rectangular packer's yieldPct, which counts each bounding box as solid and so reads higher for a worse result.
placedintegerPart copies placed, after qty expansion.
totalintegerPart copies requested, after qty expansion.
totalPricenumberSum of used sheet prices.

unplaced[] (nest)

namestringThe part name.
qtyintegerHow many copies could not be placed.

rods[]

lengthnumberFULL bar length as supplied in stock.
pricenumber | nullPrice of the stock row, or null.
remainingnumberUSABLE off-cut left on this bar, 3 decimals — the kerf of the cut that frees it from the last piece is already deducted (OPEN-257), so it is the reclaimable length, not the raw gap. Reported even when below minOffcut.
partsarrayPlacements, in cutting order along the bar.
offcutsnumber[]At most one entry: [remaining] (the kerf-corrected usable off-cut, OPEN-257) when it is > 0 and ≥ minOffcut, else []. Absent when excluded via include.
metaobjectPresent only when the stock row carried meta — echoed from stock[].meta (OPEN-224). Applies to 1D and wood rods.

rods[].parts[]

namestringThe requested name, or the generated default.
posnumberOffset of the part’s NEAR end from the bar end that trim.start trims.
lengthnumberThe part length, as requested.
metaobjectPresent only when the part carried meta — echoed from parts[].meta (OPEN-224).

metrics (1D)

rodCountintegerBars used. Equals rods.length.
yieldPctnumberPlaced length ÷ total FULL bar length × 100, 2 decimals. Trim and kerf count as waste.
placedintegerPieces placed, after qty expansion.
totalintegerPieces requested, after qty expansion.
cutsintegerTotal crosscuts across all used bars — one per placed piece.
totalPricenumberSum of used bar prices, 2 decimals.
toleranceAcceptedCountintegerPieces that fitted only because options.tolerance allowed an overshoot.

unplaced[] (1D)

namestringThe part name.
lengthnumberAs requested.
qtyintegerHow many could not be placed.

Des pièces depuis un fichier (SVG · DXF)

Une pièce n'a pas à arriver sous forme de coordonnées. Placez un document SVG ou DXF dans parts[].source et le serveur en extrait le contour — et ses trous — avec le même lecteur que l'application CutOptim utilise lorsque vous déposez un dessin sur son mode Nesting. Le fichier ne remplace QUE la géométrie : qty, material, allowedRotations, minQuality, priority et meta se comportent exactement comme sur une pièce polygon, si bien qu'une bibliothèque de pièces qui existe déjà en fichiers CAO n'exige aucun aplatisseur de courbes et d'arcs de votre côté. Une source décrit UNE pièce ; un dessin contenant plusieurs composants distincts renvoie un 400 qui vous oriente vers l'endpoint d'import ci-dessous. La réponse porte alors un bloc imported : combien de lignes viennent d'un fichier, combien de sommets ils ont produits et quelles unités ces fichiers déclaraient — signalées, jamais appliquées, car cette API ne convertit rien.

Rien n'est conservé. Les octets n'existent que comme corps de la requête, sont analysés en mémoire et disparaissent une fois la réponse écrite : pas de disque, pas de base de données, pas de fichier temporaire, pas de ligne de journal. Il n'y a rien à supprimer ensuite et rien ne subsiste — la même absence d'état que tient chaque autre endpoint.

POST /v1/optimize/nest

{
  "parts": [
    {
      "source": {
        "content": "<svg xmlns=\"http://www.w3.org/2000/svg\" viewBox=\"0 0 400 200\">\n  <path d=\"M0 0 H120 V80 H0 Z\"/>\n  <path d=\"M40 30 H80 V50 H40 Z\"/>\n</svg>",
        "filename": "washer-plate.svg"
      },
      "qty": 4,
      "meta": { "erpId": "ART-8891" }
    }
  ],
  "stock": [{ "w": 2440, "h": 1220 }]
}

Les noms et les types des champs constituent le contrat : les tableaux ci-dessous restent donc en anglais dans toutes les langues — un nom de champ traduit documenterait une API qui n'existe pas.

{
  "imported": { "parts": 1, "vertices": 8, "units": [] },
  "parts_after_import": [
    {
      "name": "washer-plate",
      "polygon": [[0, 0], [120, 0], [120, 80], [0, 80]],
      "holes": [[[40, 30], [80, 30], [80, 50], [40, 50]]],
      "qty": 4,
      "meta": { "erpId": "ART-8891" }
    }
  ]
}

parts[].source — geometry from an SVG / DXF file

contentstringThe file TEXT — not base64, not a URL, not a multipart upload. Both formats are text documents, so they travel inside the JSON body like any other field. At most 4,000,000 characters per file and 8,000,000 across one request; over either is a 413.
format"svg" | "dxf"Omit and the server sniffs the content (an <svg tag ⇒ svg, otherwise dxf). An explicit value ALWAYS wins — including over a filename whose extension disagrees, which is the case worth setting it for.
filenamestringUsed for format detection AND as the part name when the row has no name of its own (extension stripped). It never touches a filesystem — there is no file on our side to name.
flattenTolerancenumberCurve and arc flattening tolerance, in the FILE’s own coordinate unit. Default 0.2. Larger = fewer vertices; this is the lever when a dense outline trips the 2,000-vertex per-ring cap.

imported — what the files contributed (response, absent without a source)

partsintegerPart ROWS whose geometry came from a file.
verticesintegerTotal vertices those files produced after flattening, outlines and holes together — the number to watch against the per-ring cap.
unitsstring[]⚠️ Units the files DECLARED (a DXF $INSUNITS), not units we applied. Empty when none declared one. More than one entry also raises a warning: mixing a millimetre drawing with an inch one produces a plan that validates and is physically wrong, and the server must not "fix" that by converting — no field in this API asserts a unit.

POST /v1/import/nest — un fichier, tous les contours

Lorsqu'un seul dessin contient plusieurs pièces différentes, importez-le d'abord : cet endpoint renvoie chaque contour fermé qu'il contient, du plus grand au plus petit, exactement dans la forme qu'attend une ligne parts[]. Collez ceux dont vous avez besoin, ajoutez vos propres qty et material, puis envoyez cela à /v1/optimize/nest. C'est aussi la façon de voir ce que contient un fichier avant d'y dépenser un calcul. Il exige une clé — aplatir une géométrie quelconque est un vrai travail de CPU, et du CPU anonyme est un mauvais marché — mais il ne réserve rien : votre quota reste intact et aucun en-tête de rate limit ne revient, exactement comme lors de l'interrogation d'un job.

POST /v1/import/nest
Authorization: Bearer co_live_…

{
  "content": "<svg xmlns=\"http://www.w3.org/2000/svg\" viewBox=\"0 0 400 200\">\n  <path d=\"M0 0 H120 V80 H0 Z\"/>\n  <path d=\"M40 30 H80 V50 H40 Z\"/>\n</svg>",
  "filename": "washer-plate.svg"
}
{
  "format": "svg",
  "parts": [
    {
      "name": "washer-plate",
      "polygon": [[0, 0], [120, 0], [120, 80], [0, 80]],
      "holes": [[[40, 30], [80, 30], [80, 50], [40, 50]]]
    }
  ],
  "vertices": 8,
  "warnings": [],
  "engineEnabled": true,
  "contractVersion": "1"
}

POST /v1/import/nest — request

contentstringThe file text. Required. Same 4,000,000-character cap as parts[].source.
format"svg" | "dxf"Omit to sniff; an explicit value wins over the filename extension.
filenamestringUsed for detection and to NAME the results: a file with one outline keeps the bare name, several are numbered "<name> 1", "<name> 2", …
flattenTolerancenumberAs on parts[].source. Default 0.2.

POST /v1/import/nest — response

format"svg" | "dxf"The parser that actually ran — the useful bit when you let the server sniff.
parts[]{ name, polygon, holes? }One entry per closed outline, LARGEST FIRST. Each is already in the shape a parts[] row wants: paste it in and add your own qty / material / allowedRotations. Deterministic — the same bytes always give the same vertices in the same order.
verticesintegerTotal vertices after flattening. Compare it against the 2,000-per-ring cap before you build a large request.
sourceUnit"mm" | "in"A DXF $INSUNITS declaration, when the file carries one. REPORTED, NEVER APPLIED. Absent for SVG (the format has no unit) and for a DXF that declares none.
warningsstring[]What the parser could not honour — an SVG transform= attribute (CAD part exports are flat, so we do not apply them), or geometry that never closed into a contour. Empty means the file was read whole.
engineEnabledbooleanWhether THIS deployment can also SOLVE a nest. Importing is pure parsing and works everywhere; where the nesting engine is not built in, /v1/optimize/nest answers 503 and this flag says so up front. Same field /v1/validate/nest carries.
contractVersionstringNest contract version. Currently "1".

Moteurs

  • heuristic (par défaut) — le placeur guillotine multi-stratégies. Utilisation maximale, toute disposition sciable, toujours un cutPlan complet.
  • balanced — un placeur MaxRects à imbrication libre. Beaucoup plus rapide sur les gros travaux (mesuré à ~25× pour 2 000 pièces) au prix d'une utilisation légèrement plus faible, et ses dispositions ne sont souvent pas guillotine (guillotineValid: false, cutPlan: null). Il ne modélise ni tolerance, ni minimizeCost, ni grainGroup, ni maxCutStages, ni minimizeRotations — si vous en définissez une, un avertissement vous indique qu'elle a été ignorée.
  • max — le palier de recherche arborescente asynchrone (2D uniquement) : il atteint l'optimum prouvé sur bien plus de travaux, au prix de quelques secondes à une minute par calcul. Toujours déterministe et guillotine-valide. Il ne renvoie pas de plan directement — voir Travaux asynchrones ci-dessous. Il modélise UN SEUL format de stock en pleine dimension, en quantité illimitée, avec un schéma guillotine fixe à 3 étapes : une deuxième ligne de stock, trim, respectStock, material ou grainGroup sont refusés avec 400 avant qu'un appel ne soit réservé ; tolerance, minimizeCost, maxCutStages, minimizeRotations, firstCut et effort passent mais sont ignorés avec un avertissement, et un résultat max ne rapporte aucune chute. Envoyez ces travaux au moteur heuristic.

Travaux asynchrones (engine = max)

Un calcul max prend de quelques secondes à une minute, si bien que POST /v1/optimize/2d avec engine:"max" ne renvoie pas de plan — il renvoie 202 Accepted avec un jobId, et l'appel est décompté à la soumission. Interrogez GET /v1/jobs/{id} jusqu'à ce que status vaille "succeeded" (result contient alors la même réponse 2D qu'un calcul synchrone) ou "failed" (error contient le message). L'interrogation ne consomme aucun quota ; vous ne voyez que vos propres travaux. Là où le palier n'est pas activé sur un déploiement, engine:"max" échoue en mode fermé avec 503.

fieldtypemeaning
jobIdstringThe 202 body's id. Poll GET /v1/jobs/{id}.
statusstringqueuedrunningsucceeded | failed.
mode · enginestringAlways "2d" and "max".
pollAfterMsinteger202 only — suggested delay before the first poll.
resultobjectPresent once succeeded — the same shape as a synchronous 2D response.
errorstringPresent once failed — the reason.
quotaobject202 only — reserved, used and limit: the one call metered at submission, and where the ACCOUNT stands this month.
createdAtstringPoll only — when the job was submitted (ISO-8601).
finishedAtstring | nullPoll only — when the worker finished; null while queued or running.

Déterminisme et versionnage

Chaque réponse porte engineVersion. L'algorithme est déterministe : l'améliorer change donc le résultat pour une même entrée — ce qui constitue une rupture de compatibilité si vous mettez en cache. Figez le comportement en envoyant engine explicitement et en surveillant engineVersion ; la version du chemin /v1/ ne change que si la structure de la réponse change.

Chaque moteur est versionné indépendamment : une modification de l'un ne fait jamais bouger la version de l'autre.

Erreurs

400invalid_requestErreur de schéma. details.path indique le champ fautif.
401unauthorizedClé API absente ou inconnue.
402quota_exceededQuota mensuel atteint. Retry-After donne le nombre de secondes avant le passage au mois suivant.
403key_revokedLa clé existe mais ne peut pas être utilisée : elle a été révoquée, ou l'abonnement Engine API du compte n'est plus actif. Le champ message précise lequel des deux.
404not_foundRoute inexistante — c'est aussi ce que vous obtenez pour le bon chemin avec la mauvaise méthode.
413too_largeEntrée au-delà d'une limite (voir Limites).
429busyCapacité momentanément saturée. Retry-After en secondes — cela ne compte jamais dans votre quota.
500internalErreur inattendue, ou backend d'authentification injoignable (les requêtes échouent en mode fermé).
503service_unavailableUn moteur demandé ne peut pas être servi actuellement — le moteur de nesting, ou le moteur max asynchrone. Pour max il y a deux causes, et le champ message précise laquelle : le palier n'est pas intégré à ce déploiement, ou il l'est mais le worker qui résout les travaux ne répond pas. Fail-closed avant qu'un appel ne soit réservé : cela ne coûte donc jamais rien.
504solve_timeoutLe calcul a dépassé sa limite stricte de temps. Sur les chemins rectangulaires, c'est le proxy qui l'applique ; sur /v1/optimize/nest, le moteur applique son propre budget, plus court, et répond solve_timeout dans l'enveloppe habituelle.

Corps d'erreur

Toutes les erreurs produites par le moteur lui-même utilisent la même enveloppe. Branchez votre code sur error, qui est un code stable ; jamais sur message, dont la formulation peut changer d'une version à l'autre. details est présent sur invalid_request, où path nomme le champ fautif, et sur too_large, où max et got donnent le plafond et ce que vous avez envoyé.

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 et 429 portent tous deux un en-tête Retry-After en secondes. Sur 402, il décompte jusqu'à la réinitialisation du quota, à 00:00 UTC le 1er du mois suivant ; sur 429, il s'agit d'un court délai d'attente, et un 429 ne consomme jamais de quota — l'appel réservé est restitué.
  • Un échec de routage répond not_found, un code délibérément absent de la liste ci-dessus parce qu'il est produit par le routeur et non par le contrat de l'API. Vous obtenez 404 et non 405 lorsque le chemin est bon mais la méthode mauvaise : les quatre endpoints d'optimisation sont uniquement en POST.
  • Sur les chemins rectangulaires, le 504 provient du reverse proxy et non du moteur : son corps est donc celui du proxy et non cette enveloppe JSON ; dans les limites d'entrée ci-dessous, il devrait être inatteignable. /v1/optimize/nest fait exception : ce calcul est un sous-processus doté de son propre budget, délibérément maintenu sous la limite du proxy — un dépassement y utilise donc cette enveloppe, avec le code solve_timeout.

En-têtes de limite de débit

Un appel d'optimisation réussi porte X-RateLimit-Limit (le plafond mensuel du COMPTE — toutes les clés du compte en partagent un seul) et X-RateLimit-Remaining (les appels restants au compte ce mois-ci, celui-ci déduit). Ils ne sont envoyés que par les endpoints d'optimisation : le compteur est réservé au moment d'autoriser un calcul, /v1/usage et /v1/health n'ont donc rien à signaler.

X-RateLimit-LimitintegerThe ACCOUNT’s monthly quota — the same number GET /v1/usage returns as limit, not a per-key cap (049).
X-RateLimit-RemainingintegerCalls left this month on the ACCOUNT, after this one.
Retry-AfterintegerSeconds 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"
}

En lecture seule : ne consomme pas d'appel et n'envoie pas d'en-têtes de limitation. ⚠️ used et limit décrivent le COMPTE, pas la clé avec laquelle vous avez appelé : chaque clé active du compte puise dans une seule enveloppe commune, créer davantage de clés ne crée donc pas davantage de quota. used compte le mois calendaire UTC en cours pour toutes, remaining vaut limit moins used et ne devient jamais négatif, periodEnd est le jour de remise à zéro sous forme de date YYYY-MM-DD, et keyPrefix est le préfixe d'affichage non secret de la clé utilisée. La clé elle-même n'est jamais renvoyée par un endpoint — seul son hachage est stocké, une clé perdue se remplace donc, elle ne se récupère pas.

planstringTier slug frozen onto the key when it was created.
usedintegerCalls counted in the current UTC calendar month across EVERY key on the ACCOUNT — revoked keys included, so revoking a key cannot un-spend what it spent (053).
limitintegerThe ACCOUNT’s cap: the largest monthly quota frozen onto any of its ACTIVE keys (049). Not a per-key allowance — more keys do not add quota.
remainingintegerlimit − used, never negative.
periodEndstringReset day as YYYY-MM-DD — a date, not a timestamp.
keyPrefixstringNon-secret display prefix of the calling key.
contractVersionstringShape 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", "nest"],
  "nestEngines": ["lbf"],
  "maxEngines": ["max"],
  "uptimeSec": 16
}

Sans clé, sans quota, sans base de données. Cet endpoint ne touche volontairement à rien de persistant : une panne du magasin de clés ne peut donc pas faire passer le service pour mort aux yeux d'un orchestrateur. engines liste les identifiants que ce déploiement accepte dans engine, et engineVersion est la version du moteur par défaut.

statusstringAlways "healthy" when the process answers.
servicestringAlways "cutoptim-engine".
contractVersionstringShape version. Currently "1".
engineVersionstringThe DEFAULT engine’s version, not a per-engine list.
enginesstring[]SYNCHRONOUS engine ids this deployment accepts in engine — ["heuristic","balanced"]. The async max tier is reported separately in maxEngines, never here.
modesstring[]Optimize paths this deployment serves: "2d", "1d", "wood", plus "nest" only where the nesting engine is built in. Endpoint discovery without reading this page.
nestEnginesstring[]Nesting engine ids this deployment can serve — ["lbf"] on the production API, [] where the Rust nesting stage is not built in.
maxEnginesstring[]The async tree-search tier — ["max"] where it is enabled, [] otherwise. Health never advertises a capability it cannot serve.
uptimeSecintegerWhole seconds since process start.

Limites

  • 2,000 pièces par requête (quantité totale, après développement des qty)
  • 50 lignes de stock · corps de requête jusqu'à 1 Mo
  • 10 clés actives par compte — elles partagent UN SEUL quota mensuel : les clés séparent les environnements et les intégrations, elles n'augmentent pas l'enveloppe
  • 10 Mo de corps de requête sur les deux routes nest susceptibles de porter un dessin (/v1/optimize/nest et /v1/import/nest) ; un fichier source au maximum 4 000 000 de caractères, 8 000 000 par requête
  • la concurrence est bornée côté serveur — un burst reçoit 429, jamais une file d'attente lente. Les endpoints validate sans clé ont en plus un plafond par adresse (429 avec Retry-After) ; avec une clé, jamais de limitation de ce type. Un compte peut avoir 5 travaux max en queued/running simultanément.

Spécification OpenAPI

Un document OpenAPI 3.1 exploitable par une machine décrit les douze endpoints, chaque corps de requête, chaque structure de réponse et chaque erreur. Pointez votre générateur de client dessus plutôt que de recopier cette page. Le document lui-même est uniquement en anglais : il est constitué de jetons du contrat, et OpenAPI ne dispose d'aucun mécanisme de localisation.

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

Ouvrir le document OpenAPI 3.1 →

Ressource à télécharger
Engine API one-pager

A two-page summary of the Engine API — the four modes (2D, 1D, wood and true-shape nest), a request and response, determinism and pricing. Print-ready, with a QR back to the docs.

PDF2 pagesFree
Télécharger le PDF