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/2d optimisation de panneaux 2D
POST /v1/optimize/1d optimisation 1D / linéaire (barres, profilés, tubes)
GET /v1/usage consommation du mois en cours et quota de la clé appelante
GET /v1/health état de disponibilité — sans clé, sans limite de débit, sans base de données

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

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.

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 plutôt que par nombre de panneaux
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

include réduit la réponse : passez ["cutPlan","offcuts"] (les deux par défaut), ou omettez l'un des deux pour qu'il ne figure pas dans la charge utile.

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

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

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 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 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": 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 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 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.

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.

Moteurs

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

400 invalid_request Erreur de schéma. details.path indique le champ fautif.
401 unauthorized Clé API absente ou inconnue.
402 quota_exceeded Quota mensuel atteint. Retry-After donne le nombre de secondes avant le passage au mois suivant.
403 key_revoked La clé existe mais a été révoquée.
404 not_found Route inexistante — c'est aussi ce que vous obtenez pour le bon chemin avec la mauvaise méthode.
413 too_large Entrée au-delà d'une limite (voir Limites).
429 busy Capacité momentanément saturée. Retry-After en secondes — cela ne compte jamais dans votre quota.
500 internal Erreur inattendue, ou backend d'authentification injoignable (les requêtes échouent en mode fermé).
504 solve_timeout Le calcul a dépassé la limite stricte de temps réel (appliquée au niveau du proxy).

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

En-têtes de limite de débit

Un appel d'optimisation réussi porte X-RateLimit-Limit (le plafond mensuel de la clé) et X-RateLimit-Remaining (les appels restants 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-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"
}

En lecture seule : cet appel ne consomme pas de requête et n'envoie aucun en-tête de limite de débit. plan et limit sont l'offre et le plafond figés sur la clé au moment de sa création — re-tarifer le produit plus tard ne réécrit donc jamais une clé active. used compte le mois calendaire UTC en cours, remaining vaut limit moins used et ne devient jamais négatif, periodEnd est le jour de réinitialisation sous forme de simple date YYYY-MM-DD, et keyPrefix est le préfixe d'affichage non secret de la clé avec laquelle vous avez appelé. La clé elle-même n'est jamais renvoyée par aucun endpoint — seul son hachage est stocké : une clé perdue se remplace, elle ne se récupère pas.

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
}

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.

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.

Limites

Spécification OpenAPI

Un document OpenAPI 3.1 exploitable par une machine décrit les quatre endpoints, les deux 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 →