API documentation

The optimiser as an HTTP service: send cuts and stock, receive a verified cutting plan or a print-ready PDF. Stable, versioned, and backward compatible: fields are added, never removed.

Introduction

Base URL https://api.linearcutting.com. Everything is JSON, apart from the PDF endpoint. Requests are stateless: we do not store your cut lists.

You don't need an API key to try this. The line below runs, as written, right now. Paste it into a terminal.

Copy, paste, run
curl -X POST https://api.linearcutting.com/v1/optimise \
  -H "Content-Type: application/json" \
  -d '{"parts": "2400x5, 1800x3", "stock": 6000}'

That is a whole cut list: five parts at 2400, three at 1800, cut from 6000mm bars. You get back the layouts, how many bars it needs, how much is wasted, and whether the plan is provably optimal.

Unauthenticated calls run on a free public tier, so you can evaluate the thing before you pay for it: 30 requests an hour and 250 a calendar month, per IP address. Past that it returns a 429 that tells you when it resets. Failed calls never count against it. When you're building something real, get a key from $5 a month: it lifts both limits, raises the size caps, and gives you your own concurrency so a busy afternoon on the public tier can't slow you down.

Official clients

Zero dependencies, MIT licensed, and they need no key either. If you're in Python or JavaScript, don't hand-roll a fetch:

pip install linearcutting
import linearcutting as lc

plan = lc.optimise(parts="2400x5, 1800x3", stock=6000)
plan.bars_used   # 4
plan.optimal     # True, proved, not guessed
npm install linearcutting
import { optimise } from "linearcutting";

const plan = await optimise({ parts: "2400x5, 1800x3", stock: 6000 });
plan.barsUsed;   // 4
plan.optimal;    // true

Python on PyPI · JavaScript on npm · Python source · JavaScript source

Two guarantees you can build on. verified is always true on success: every plan is re-audited against your inputs before it's returned, so a plan that doesn't actually cut your list can't reach you. optimal is a proof, not a guess, true only when the search has established that no better plan exists, either by exhausting the possibilities or by a certificate. When it can't be proved in the time budget, we say false rather than hoping.

optimal and stockBarsLowerBound are different things, and they disagree on purpose. The lower bound is a relaxation, roughly "total length ÷ bar length", and it is often not achievable. Five 2400s and three 1800s is 17,400mm, which is 2.9 bars of 6000, so the bound says 3. You cannot actually cut those parts from 3 bars; 4 is the true minimum, and the solver proves it. So stockBarsUsed: 4 with stockBarsLowerBound: 3 and optimal: true is not a contradiction, it means the bound was loose, not that the plan was. Use optimal to decide whether to keep searching; use the bound only as a sanity check.

Shorthand

The long form is explicit and a bit of a mouthful. For the common case, say it the short way, a number is a length, and x means how many:

The short form
{"parts": "2400x5, 1800x3, 600", "stock": 6000}

Every one of these means the same thing, so use whichever suits the code you're writing:

All equivalent
{"parts": "2400x5"}
{"parts": ["2400x5"]}
{"parts": [[2400, 5]]}
{"parts": [{"length": 2400, "quantity": 5}]}

A third element is a label on a part, and a price on a stock: [2400, 5, "rail"], [6000, 20, 45.50]. And stock is the friendly name for the old stockLength / stocks split, a bare number means "this length, buy as many as you need".

Shorthand is expanded before anything else happens, so it is a way of typing rather than a different mode: a short request and the long request it expands to return byte-identical plans. Mix and match freely, every existing request still works exactly as it did.

A cut list in a URL

Small jobs fit in a query string, so GET works too. This is handy for a link you want to share, quote, or drop in a bug report:

GET /v1/optimise
curl "https://api.linearcutting.com/v1/optimise?parts=2400x5,1800x3&stock=6000"

Same auth, same limits, same plan as the POST. Open it in a browser if you like.

Authentication

The public tier needs no key. Paid tiers send their key in a header; keys also carry your allowed browser origins, so calls from your own site work without a proxy.

http
X-API-Key: lk_live_9f2c...

A bad or expired key returns 401. Keys are issued per workspace: see plans.

POST /v1/optimise

Solve a cut list. Returns the plan, statistics, and provable bounds.

Request body

FieldTypeDefaultDescription
parts required array- The parts you need. Each: length (> 0), quantity (int ≥ 1), optional label, material, leftAngle, rightAngle.
stockLength number- A single unlimited stock length. Use this or stocks, not both.
stocks array- Purchasable lengths. Each: length, quantity (null = unlimited), price (null = optimise material instead of money), optional material, priority.
offcuts array[] Lengths you already own: length, quantity, optional material, priority. Free material, never counted as purchases.
kerfnumber ≥ 00Blade width consumed per cut.
trimbooleanfalseTake one kerf off both ends of every source before cutting.
methodstringbalanced balanced, least_waste, offcuts_first, fewest_setups (alias: fewest_setups). Prices on stocks switch balanced to lowest-cost.
minRemnantnumber ≥ 00Leftovers this long or longer come back as usableRemnants instead of waste.
angleCutsobjectoff{enabled, width, axialSymmetry}. See angle cuts.
unitstring"mm"Echoed back and printed on PDFs. Does not convert anything.
unitFormatstringdecimalfraction renders PDF dimensions as nearest-1/64 fractions.
timeLimitsecondsautoSolve budget, capped by tier. The search is anytime: a longer budget only improves the answer.

Response

200 application/json
{
  "plan": {
    "combinations": [
      {
        "usedComb": {
          "combination": [2400, 2400, 1200],
          "length": 6500,
          "source": "stock",
          "waste": { "total": 492, "remnant": 484, "saw": 8 }
        },
        "count": 2
      }
    ],
    "purchases":      [{ "length": 6500, "count": 2 }],
    "usableRemnants": [],
    "unusedOffcuts":  [],
    "stockBarsUsed":  2,
    "stockBarsLowerBound": 2,
    "totalWaste":     984,
    "optimal":        true,
    "verified":       true,
    "solveTimeMs":    12.5
  },
  "stats": {
    "totalParts": 8, "totalPartsLength": 15600, "totalUsedLength": 16600,
    "yieldPercent": 93.98, "cutCount": 8, "layoutCount": 1,
    "totalCost": null, "trueWaste": 984
  },
  "costLowerBound": null,
  "meta": { "method": "balanced", "kerf": 4, "tier": "public",
            "poweredBy": "linearcutting.com" }
}
FieldDescription
plan.combinations[]One entry per distinct layout. usedComb.combination lists the parts cut from that bar, length is the source, source is stock or offcut, and count is how many bars to cut that way.
usedComb.wastetotal = source − parts; split into saw (kerf + trim) and remnant (the leftover end).
plan.purchases[]The shopping list: length, count, plus price/cost when you supplied prices.
plan.usableRemnantsLeftovers ≥ minRemnant, largest first: tomorrow's offcut stock.
plan.unusedOffcutsOffcuts the plan didn't need.
plan.stockBarsUsedTotal parts to purchase.
plan.optimalProof. True only when the plan met a provable floor (or the search completed exhaustively). Safe to show users as "best possible".
plan.verifiedAlways true on success: demand matched, every layout physically fits, offcut supply respected.
plan.stockBarsLowerBound, costLowerBoundProvable minimums. When optimal is false, the gap tells you the most this plan could be off by.
statsJob totals: parts, lengths, yieldPercent, cutCount, layoutCount, totalCost, trueWaste, and byMaterial when material groups are used.

POST /v1/optimise/pdf

Same request body, plus an optional jobName. Returns application/pdf: page 1 is a project-manager summary (purchase order, costs, the optimality certificate); the pages after it are saw-operator sheets with running cut positions, piece labels and a tick box per bar. Keyed callers get their attribution or branding rendered.

bash
curl -X POST https://api.linearcutting.com/v1/optimise/pdf \
  -H "Content-Type: application/json" \
  -H "X-API-Key: lk_live_9f2c..." \
  -d '{ "parts": [{"length": 2400, "quantity": 4}], "stockLength": 6500,
       "kerf": 4, "jobName": "Smith deck order" }' \
  --output cutting-plan.pdf

GET /v1/health

json
{ "status": "ok", "version": "1.3.0", "solver": "Linear Cutlist Optimizer v2" }

Material groups

Tag parts, stocks and offcuts with material and each material is solved as its own job inside one request: cuts only ever land on stock of the same material. Layouts and purchases come back tagged, and stats.byMaterial breaks the numbers down per group.

json
{
  "parts": [
    { "length": 2400, "quantity": 4, "material": "40x40 SHS" },
    { "length": 1300, "quantity": 6, "material": "Pine 90x45" }
  ],
  "stocks": [
    { "length": 6500, "quantity": null, "material": "40x40 SHS" },
    { "length": 5400, "quantity": null, "material": "Pine 90x45", "priority": true }
  ],
  "kerf": 3
}

priority: true on a stock or offcut is a soft preference: among plans that are equally good, prioritised items get used first. It never trades away plan quality.

Angle / miter cuts

Give parts a leftAngle and rightAngle and enable angleCuts. Angles are the bottom angles of the piece as it lies; enter a top angle as a negative number (120 and -60 are the same cut). Valid range after that transform: 10 to 170 degrees.

angleCuts fieldDescription
enabledMaster switch. Angles on parts are only accepted when true.
widthMaterial width, same unit as lengths. An angled cut reaches sideways by width / tan(angle).
axialSymmetrynone, X, Y, Z or XYZ: which flips the material allows, which decides how mitres may nest. Square tube is typically XYZ; one-sided material is none.

Where two neighbouring mitres are parallel, one saw cut makes both edges. Angle-mode layouts add an ordered parts array (with the orientation used) and cutPositions: each cut measured from the bar's zero end, as a [top, bottom] pair when it's angled, or a single number when it's square.

angle-mode layout (excerpt)
"cutPositions": [[1200, 1300], [2442.265, 2500], 3300]

Errors

Error bodies are { "error": "...", "detail": "..." }. The detail is written to be shown to a human: it names the offending part or material.

StatuserrorWhen
400validationBad lengths or quantities, unknown method, a part that fits no source, demand exceeding finite stock, angle rules broken, stocks and stockLength both sent.
401badKeyMissing, unknown or disabled API key.
413partsLimitMore parts than your tier allows.
429rateLimitedRate or monthly cap reached. Respect Retry-After.
503busySolver at capacity. Retry shortly.

Rate limits

Limits throttle; they never generate an overage bill. Requests that fail validation don't count toward your monthly cap. Full numbers are on the plans page.

GuardFreePaid tiers
Rate10 requests / 5 min per IP20-240 / min by tier
Total parts300500 - 5,000
Distinct part lines60up to 400
Solve budget20 s60 - 300 s

Distinct part lines are capped as well as total parts because solve cost comes from length diversity: 300 identical cuts prove optimal in milliseconds, 300 different ones do not.

Recipes

Cheapest mix from several stock lengths (JavaScript)

javascript
const res = await fetch("https://api.linearcutting.com/v1/optimise", {
  method: "POST",
  headers: {
    "Content-Type": "application/json",
    "X-API-Key": "lk_live_9f2c...",
  },
  body: JSON.stringify({
    parts: [
      { length: 3000, quantity: 8 },
      { length: 1200, quantity: 10 },
    ],
    stocks: [
      { length: 6500, quantity: null, price: 45 },   // unlimited
      { length: 12000, quantity: 4, price: 70 },     // only four on hand
    ],
    offcuts: [{ length: 2600, quantity: 1 }],
    kerf: 4,
    minRemnant: 400,
    method: "balanced",   // prices present -> optimises dollars
  }),
});

const { plan, stats } = await res.json();
console.log(plan.purchases);     // what to buy
console.log(stats.totalCost);    // what it costs
console.log(plan.optimal);       // true = provably the cheapest

Mitered frame parts (Python)

python
import requests

r = requests.post(
    "https://api.linearcutting.com/v1/optimise",
    json={
        "parts": [
            {"length": 1300, "quantity": 2, "leftAngle": 90, "rightAngle": 45},
            {"length": 1200, "quantity": 2, "leftAngle": 135, "rightAngle": 60},
        ],
        "stockLength": 3310,
        "kerf": 0,
        "angleCuts": {"enabled": True, "width": 100, "axialSymmetry": "none"},
    },
    timeout=30,
)
r.raise_for_status()

for combo in r.json()["plan"]["combinations"]:
    print(combo["count"], "x", combo["usedComb"]["cutPositions"])

Handling a slow, big job

Large lists can take seconds to minutes. The search is anytime, so set timeLimit to whatever your UI can wait for and show optimal to tell users whether the answer is proven or simply the best found so far.

json
{ "parts": [ /* 900 parts */ ], "stockLength": 6500,
  "kerf": 4, "timeLimit": 60 }