Skip to main content
← API reference

2D sheet optimization

Rectangular parts nested into sheets: the request, the response and every field of it.

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 },
    "minimizeCost": true,
    "effort": "balanced"
  },
  "engine": "heuristic",
  "include": ["cutPlan", "offcuts"]
}
Uses a public demo key — rate-limited, small jobs only, never billed.

Part fields

FieldDescription
w, hrequired — part dimensions
qtydefault 1 — expanded server-side; counts toward the 2,000 cap
nameoptional label, echoed on every placement
rotatabledefault true — may the part be turned 90°
graingrain direction: 'w' or 'h', the side of the part the grain runs along. On a sheet whose stock row has a grain, the part is turned so its grain runs along the sheet's (rotatable is then ignored). No grained stock → no effect, with a warning
grainGroupdeprecated — still accepted and working exactly as before: members of a group are kept on one sheet, but not next to each other, so the grain does not run on across them. For that, use matchedSets (grain matching, below)
prioritymust-cut: wins board space when stock is capped (with respectStock)
edgeBandingper-edge banding: name a type reference on any of top / right / bottom / left (a free string, your own code) — the response totals the metres by reference. Metadata only, it never moves a part. 2D only
materialmaterial tag (a free string, your own code): parts and stock of the same material are packed only together. Unlike edgeBanding it changes the layout. Absent = one unspecified pool. Works on every mode

Stock fields

w and h are required. qty defaults to 1 and is a hard cap only with respectStock. price is per sheet and drives totalPrice and cost mode. priority (boolean) uses this stock up first; material (a free string) restricts it to parts of the same material. grain ('w' or 'h') says which side of the sheet the grain runs along; absent means no grain (MDF), and it is echoed on every sheet cut from the row.

Rows whose grain runs different ways may be mixed: trim and a forced firstCut still apply to each sheet in its own orientation.

Options

FieldDescription
kerfblade width (default 0)
toleranceaccept cuts that overshoot by up to this much
trimper-side edge trim: left, right, top, bottom
firstCut'auto' | 'horizontal' | 'vertical' (default 'auto')
minimizeCostrank candidates by lowest total stock price; with several priced stock sizes it mixes them (2D) or picks the cheapest length and moves the last bars onto another length when that is cheaper (1D) to minimise the bill, even if that uses more material
respectStocktreat each stock row's qty as a hard cap
minOffcutonly report offcuts whose short side is at least this
maxCutStagespanel-saw stage limit — a phase count, not raw tree depth
minimizeRotationsprefer layouts that turn fewer parts
effort'fast' | 'balanced' (default 'balanced'). Search depth: 'balanced' runs the full multi-strategy best-of; 'fast' skips the expensive per-board refinements (the area-combination search and moving the last boards onto a smaller stock size) — markedly quicker on large jobs for a few points of yield, still guillotine-valid and never denser than 'balanced'. Small jobs are usually identical. Heuristic engine only

include does two things. NARROWING — "cutPlan" and "offcuts" are on by default; a present array keeps only the narrowing tokens it lists (an empty array drops both). ADDITIVE EXPORT — "svg", "csv" and "dxf" each add that export to the response as a STRING: svg a self-contained 2D layout drawing (2D only — a 1d/wood request returns a warning instead), dxf an R12/AC1009 drawing on layers STOCK/PARTS/LABELS, csv a cut list.

Export tokens do not affect narrowing, so include:["svg"] adds svg and — naming no narrowing token — drops cutPlan/offcuts; use ["cutPlan","offcuts","svg"] to keep everything and add svg.

Four more tokens add 2D data to the response: "labels" per-piece label data (see the Label2D table), "planHash" a 64-hex fingerprint of the plan (the same plan always gives the same hash), "pdf" the cutting-plan PDF and "labelsPdf" the label PDF, both base64-encoded and drawn by the same code as the CutOptim app. They are 2D only — a 1d/wood request returns a warning instead — and "pdf" renders at most 500 parts (total quantity).

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

FieldTypeDescription
parts[].meta, stock[].metaobjectOpaque JSON object (any keys) echoed back VERBATIM on the matching output (a part's meta on every placed piece of it; a stock row's meta on every sheet/rod cut from it). Attach ERP article numbers, order-line ids, customer refs to reconcile the cut plan with your system. Never parsed, never affects placement. Part meta follows its request row: the meta of each row lands on exactly its qty of placed pieces, even when rows share a name; stock meta by stock-row order.

The effort option trades solve time against yield. Here is that trade, measured on one demanding job — every figure comes from the real packer.

Effort switch: material yield vs solve timeEffort switch: material yield vs solve time. fast: 350 boards · 76.2% · ≈1.9 s. balanced: 330 boards · 80.8% · ≈4.8 s. max: reserved — denser needs a slower search. On most (smaller) jobs the two are identical; the gap only opens on large jobs like this one. balanced is the default and is never denser than fast can reach.Effort switch: material yield vs solve timeOne demanding job — about 1,550 parts on a 2.07 × 5.6 m board. Every figure measured on the real packer.74%76%78%80%82%84%02 s4 s6 ssolve time · faster →material yield · denser ↑⇄ the effort switch+4.6 pts yield · −20 boards−5.7% material · ≈2.5× slowerfast350 boards · 76.2% · ≈1.9 s★ balanced · default330 boards · 80.8% · ≈4.8 smaxreserveddenser needs aslower search
On most (smaller) jobs the two are identical; the gap only opens on large jobs like this one. balanced is the default and is never denser than fast can reach.

And it is fast either way: even the largest production jobs — 2,000 parts and more — solve in single-digit seconds on the default engine, comfortably inside the API time budget.

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": 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 merges collinear cuts (one fence setting); sawPasses counts every pass. Two honest measures of the same plan, not a claim to match any competitor’s count.
  • guillotineValid / cutPlan — When a layout cannot be cut edge-to-edge, guillotineValid is false and cutPlan is null. That is real information — it cannot be made on a panel saw — not an error.
  • unplaced + warnings — An unsatisfiable job returns 200 with the pieces listed in unplaced and a note in warnings. A plan you can act on beats a status code.
  • edgeBanding — When any part carries edgeBanding, the response adds an edgeBanding block: the linear metres each type reference consumes, per part and as an order total. It is exact geometry with no waste margin — the workshop adds its own — and it assumes millimetre input (÷1000 for metres). The key is absent entirely for an unbanded job.
  • materials / unmatchedMaterials — When any part or stock carries material, the response adds materials (a per-material rollup — material, sheetCount/rodCount, yieldPct, placed, total, totalPrice) and unmatchedMaterials (demand whose material has no matching stock). On wood the material rides per cross-section section instead. Both keys are absent for a material-free job, which stays byte-identical.

Response fields

Field names and types are the contract, so the tables below stay in English in every language — a translated field name would document an API that does not exist.

POST /v1/optimize/2d — top level

FieldTypeDescription
engine"heuristic" | "balanced" | "max"Which engine actually ran. max appears only inside the result of a finished async job (GET /v1/jobs/{id}), never in a synchronous 200.
engineVersionstringAlgorithm identity: the package version plus a content hash of the algorithm source. Changes automatically whenever the packer changes, and independently per engine. Pin or compare this if you cache.
contractVersionstringShape version, matching the /v1/ in the path. Currently "1".
deterministictrueAlways true. Present so a client can assert the guarantee it relies on.
sheetsSheet[]One entry per sheet used, in cutting order.
metricsMetrics2DAggregate numbers for the whole job.
cutPlanCutStep[] | nullThe sawing plan, ordered. null when guillotineValid is false — a non-guillotine layout has no edge-to-edge cut sequence, so there is nothing honest to return. ABSENT entirely when include excludes cutPlan. In 2D step restarts per sheet.
unplacedUnplaced2D[]Parts that did not fit, aggregated by name + size. Empty when everything fitted.
warningsstring[]Human-readable notes about the plan: unplaced parts, an unsatisfiable maxCutStages, a grain group split across sheets, a non-guillotine layout, options the chosen engine does not model, or — in a request with matchedSets — a set not cut as one block (the detail per set is in matchedSets[].warnings) and the pdf / labelsPdf note below. Free text — do not parse it; branch on unplaced, guillotineValid, metrics and matchedSets instead.
guillotineValidbooleanTrue when the layout can be produced with edge-to-edge cuts, i.e. on a panel saw. False is information, not an error: the nesting is denser but a panel saw cannot make it. heuristic is always true; balanced often is not.
edgeBandingEdgeBandingLinear metres of edge banding the order consumes, grouped by the caller-supplied type reference and reported per part AND as an order total. Present ONLY when at least one part requested banding, so a banding-free response omits the key. Exact geometry, no waste margin — the workshop adds its own. ⚠️ The metres ASSUME MILLIMETRE input: top and bottom edges run the part width, left and right run the height, multiplied by qty, divided by 1000. This is the one response figure named for a unit (edge banding is bought by the metre, and the division only means metres in millimetres); in another unit the raw edge length is w/h × qty and these numbers are that divided by 1000. 2D only.
materialsMaterialSummary2D[]Per-material rollup (sheet count, yield, placed, requested total, price). The flat sheets stay the primary output — each carries material. ABSENT for a material-free job, which stays byte-identical.
unmatchedMaterialsUnmatchedMaterial2D[]Demand whose material has NO matching-material stock (distinct from unplaced = did not fit; the fix is to declare/buy the material). ABSENT when there is no mismatch.
matchedSetsMatchedSetSummary[]One entry per requested matched set, in request order. ABSENT for a request without sets.
svgstringInline SVG of the 2D layout (self-contained, no external references). Present ONLY when include contains svg. 2D only. With matchedSets the part labels carry the position code (A1, A2 …) and a matched-sets group draws each set's frame (options.setOutlines: false leaves the group out).
csvstringInline CSV cut list. Present ONLY when include contains csv. With matchedSets two columns, Set and Position, are appended at the end of every row.
dxfstringInline DXF (R12/AC1009) on layers STOCK/PARTS/LABELS. Present ONLY when include contains dxf. With matchedSets the LABELS text of a member carries its position code.
labelsLabel2D[]Per-piece label data, one entry per placed piece in plan order. Present ONLY when include contains labels. 2D only.
planHashstringsha256 hex fingerprint (64 hex characters) of the canonical plan: the same plan always gives the same hash. Present ONLY when include contains planHash. 2D only.
pdfstring (byte)Worksheet (cutting-plan) PDF of this plan, base64, drawn by the same code as the CutOptim app. Present ONLY when include contains pdf; at most 500 parts (total quantity), else 413 too_large. 2D only. With matchedSets the members are drawn as ordinary parts, with no set frame or position code, and warnings says so — the set marks are in sheets[].parts[].matchedSet, csv and svg.
labelsPdfstring (byte)Label PDF of this plan, base64. Present ONLY when include contains labelsPdf. 2D only. With matchedSets the members get ordinary labels, with no position code, and warnings says so.
timing.solveMsnumberWall-clock milliseconds spent inside the packer, rounded to 2 decimals. Excludes parsing, authorization and queueing.

sheets[]

FieldTypeDescription
wnumberFULL sheet width as supplied in stock, trim included — not the usable width.
hnumberFULL sheet height, trim included.
pricenumber | nullThe price of the stock row this sheet came from, or null if none was given.
partsPlacedPart2D[]Placements on this sheet.
offcutsRect[]Usable leftovers, filtered by options.minOffcut. ABSENT (not empty) when include excludes offcuts.
materialstringThe material this sheet (and every part/offcut on it) was cut from. Absent when the job carries no material.
grain"w" | "h"The stock row's grain, echoed: which side of this sheet the grain runs along; every grained part on it lies with its grain along that side. Absent when the row has none.
metaobjectOpaque JSON object (any keys) echoed back VERBATIM on the matching output (a part's meta on every placed piece of it; a stock row's meta on every sheet/rod cut from it). Attach ERP article numbers, order-line ids, customer refs to reconcile the cut plan with your system. Never parsed, never affects placement. Part meta follows its request row: the meta of each row lands on exactly its qty of placed pieces, even when rows share a name; stock meta by stock-row order.

sheets[].parts[]

A part on a board. x/y is its top-left corner measured from the top-left corner of the sheet, x growing right and y growing downward.

FieldTypeDescription
namestringThe requested name, or the generated default Part <1-based row index>.
xnumberDistance from the sheet's left edge. Unit-agnostic: pick one unit (mm, inch, …) for every length in the request and every length in the response is in that same unit. Nothing is converted server-side.
ynumberDistance from the sheet's top edge. Unit-agnostic: pick one unit (mm, inch, …) for every length in the request and every length in the response is in that same unit. Nothing is converted server-side.
wnumberPLACED width — already swapped with the requested height when rotated is true. No client-side swap needed.
hnumberPLACED height. See w.
rotatedbooleanTrue if the part was turned 90° from the requested w×h — also when its grain made the turn necessary. Informational: w/h here already reflect it.
metaobjectOpaque JSON object (any keys) echoed back VERBATIM on the matching output (a part's meta on every placed piece of it; a stock row's meta on every sheet/rod cut from it). Attach ERP article numbers, order-line ids, customer refs to reconcile the cut plan with your system. Never parsed, never affects placement. Part meta follows its request row: the meta of each row lands on exactly its qty of placed pieces, even when rows share a name; stock meta by stock-row order.
matchedSetPlacedMatchedSetPresent ONLY on a placed member of a matched set (never in a request without matchedSets): which set and instance it belongs to and where in it. position counts the members in reading order on the cabinet front — top to bottom, left to right — from 1; with the set's letter it is the code the csv Position column and the svg labels print (A1, A2 …).

sheets[].offcuts[]

A leftover rectangle, in the same coordinate system as the placements.

FieldTypeDescription
xnumberDistance of the leftover's top-left corner from the sheet's left edge.
ynumberDistance of the leftover's top-left corner from the sheet's top edge.
wnumberWidth of the leftover.
hnumberHeight of the leftover.

metrics (2D)

FieldTypeDescription
sheetCountintegerSheets used. Equals sheets.length.
yieldPctnumberPlaced part area ÷ total FULL sheet area × 100, rounded to 2 decimals. Trim and kerf count as waste, which is why a trimmed job scores lower than a bare area sum.
placedintegerPieces placed, after qty expansion.
totalintegerPieces requested, after qty expansion.
cutLinesintegerCollinear cuts merged: same axis, same coordinate, same stage count once. Physically "how many fence settings the operator makes". Deliberately NOT claimed to equal any competitor's count — only measured to land in the same range.
sawPassesintegerEvery cut, one per strip crossed — how many separate passes the saw makes.
cutLengthnumberTotal distance sawn, summed over every cut, rounded to 3 decimals. Convention-independent: merging collinear cuts does not shorten the material. Unit-agnostic: pick one unit (mm, inch, …) for every length in the request and every length in the response is in that same unit. Nothing is converted server-side.
totalPricenumberSum of the used sheets’ prices, rounded to 2 decimals. 0 when no stock row carried a price.

labels[] (include "labels", 2D)

Label data for one placed piece. w/h are the ENTERED size, rotated says the piece was turned 90 degrees on the sheet, x/y equal the placed position in sheets[sheet].parts. edges are in the entered orientation and list only banded sides.

FieldTypeDescription
sheetintegerIndex into sheets.
namestringThe part name, as on the placed piece in sheets[sheet].parts.
partNointeger1-based row of the request parts array this piece came from.
wnumberThe ENTERED width — not swapped when the piece was rotated on the sheet.
hnumberThe ENTERED height — not swapped when the piece was rotated on the sheet.
rotatedbooleanTrue when the piece was turned 90 degrees on the sheet.
x, ynumberPlaced position on the sheet, the same as the piece in sheets[sheet].parts.
materialstringMaterial of the sheet it was cut from; absent when unspecified.
grain"w" | "h"The part's own grain axis as entered; absent when none.
edges{ top?, right?, bottom?, left? }Banded sides only, in the entered orientation (a LabelEdge each); absent when the part has no banding.
metaobjectOpaque JSON object (any keys) echoed back VERBATIM on the matching output (a part's meta on every placed piece of it; a stock row's meta on every sheet/rod cut from it). Attach ERP article numbers, order-line ids, customer refs to reconcile the cut plan with your system. Never parsed, never affects placement. Part meta follows its request row: the meta of each row lands on exactly its qty of placed pieces, even when rows share a name; stock meta by stock-row order.

labels[].edges.* (LabelEdge)

FieldTypeDescription
referencestringThe banding type reference exactly as sent in edgeBanding.
lengthnumberLength of this edge on one piece: the entered w for top/bottom, the entered h for left/right.

unplaced[] (2D)

FieldTypeDescription
namestringThe part name.
w, hnumberAs requested.
qtyintegerHow many of this part are unplaced.

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

FieldTypeDescription
totalMetersnumberOrder-wide total across every banded edge, rounded to 3 decimals.
byTypeEdgeBandingByType[]Order total split by reference, sorted by reference.
byType[].referencestringThe caller-supplied type reference this metreage is grouped under.
byType[].metersnumberLinear metres of this reference, rounded to 3 decimals. See EdgeBanding for the millimetre assumption.
byPartEdgeBandingByPart[]One entry per requested part row that has any banded edge.
byPart[].namestringThe part label, or Part <1-based row> when the request omitted a name.
byPart[].metersnumberTotal banded metres for this part row, already multiplied by qty, rounded to 3 decimals.
byPart[].byTypeEdgeBandingByType[]This part's metreage split by reference.

cutPlan[]

One blade movement, in physically executable order (parent before children: you cannot crosscut a strip before you have ripped it off).

FieldTypeDescription
sheetinteger0-based index into sheets (2D) or rods (1D). Named sheet in both.
stepinteger1-based order within THIS sheet/rod. It RESTARTS at 1 on every sheet — it is not a running number across the whole job.
axis"h" | "v"h = a horizontal cut line: the blade travels along x and separates top from bottom. v = a vertical cut line: the blade travels along y and separates left from right. In 1D axis is always v.
posnumberThe blade's LOW-COORDINATE edge, not its centre line: the y value for h, the x value for v. The kerf occupies pos to pos + kerf, i.e. it advances in the direction the coordinate GROWS — downward for h, rightward for v (see the coordinate system in info.description). The material on the low side of the line — above it for h, to its left for v — is the piece that cut frees. In 1D pos is the offset along the bar at which that part is freed, measured from the bar end that trim.start trims. Unit-agnostic: pick one unit (mm, inch, …) for every length in the request and every length in the response is in that same unit. Nothing is converted server-side.
lengthnumberHow far the blade travels for this cut — the extent of the REGION it crosses, not the whole board (the industry convention). Always 0 in 1D: a bar crosscut has no travel distance to report. Unit-agnostic: pick one unit (mm, inch, …) for every length in the request and every length in the response is in that same unit. Nothing is converted server-side.
stageinteger1-based machine pass. Starts at 1 and increments ONLY when the axis flips relative to the parent cut, so ripping a board into six strips is one stage and crosscutting them is the next. This is the panel-saw sense of "3-stage cutting", not tree depth, and it is what options.maxCutStages constrains.

include: labels, planHash (2D)

POST /v1/optimize/2d

{
  "parts": [
    { "name": "Side", "w": 720, "h": 560, "qty": 2, "edgeBanding": { "top": "ABS oak 22" } },
    { "name": "Shelf", "w": 564, "h": 300, "qty": 2 }
  ],
  "stock": [{ "w": 2800, "h": 2070 }],
  "options": { "kerf": 3 },
  "include": ["labels", "planHash"]
}
// the response also carries sheets, metrics, unplaced, warnings and edgeBanding as above — no cutPlan or offcuts,
// because include narrows the response to what it names
{
  "labels": [
    {
      "sheet": 0,
      "name": "Side",
      "partNo": 1,
      "w": 720,
      "h": 560,
      "rotated": false,
      "x": 0,
      "y": 0,
      "edges": { "top": { "reference": "ABS oak 22", "length": 720 } }
    },
    {
      "sheet": 0,
      "name": "Side",
      "partNo": 1,
      "w": 720,
      "h": 560,
      "rotated": false,
      "x": 723,
      "y": 0,
      "edges": { "top": { "reference": "ABS oak 22", "length": 720 } }
    },
    {
      "sheet": 0,
      "name": "Shelf",
      "partNo": 2,
      "w": 564,
      "h": 300,
      "rotated": false,
      "x": 1446,
      "y": 0
    },
    {
      "sheet": 0,
      "name": "Shelf",
      "partNo": 2,
      "w": 564,
      "h": 300,
      "rotated": false,
      "x": 2013,
      "y": 0
    }
  ],
  "planHash": "e243bf29aaebc9e3283157c844647c1b210f4806060b2a36845f67ce63976243"
}