The Engine API is live. Create a key from your account, send parts and stock as JSON, and get the cutting plan back in one call.
No session, no state to manage, no polling. A single synchronous call returns the complete plan.
The Barcut Pro Engine API exposes the same 1D linear cutting optimiser that powers the web app. You post the parts you need and the stock you hold; you get back which bars to cut, how to cut each one, and what — if anything — could not be made.
There is nothing to install and no state to manage. Each call is independent and synchronous, so a quote screen can call it inline and render the result.
To start: create an API key on your account page, then POST to the endpoint below. A free account is all that is required.
Availability: 1D linear optimisation is the live engine in the product today. 2D sheet nesting is in development and is not part of this API. Nothing here returns 2D results.
Requests authenticate with a secret API key sent as a bearer token. Keys belong to your account and can be rotated or revoked without changing your integration code.
Authorization: Bearer bcp_live_...Create and revoke keys on your account page. A key is shown exactly once, at creation — we store only a hash of it, so it cannot be recovered. If you lose one, revoke it and create another. Revocation takes effect immediately.
A secret key in browser code is a public key — anyone can read it from the network tab. Call this API from your server and pass the result to your front end.
/api/v1/optimize/1dLIVESolves a one-dimensional cutting-stock problem: given required lengths with quantities and the stock bars available, return the cutting plan that uses the least stock, honouring saw kerf, end trim, minimum useful offcut and per-length availability.
curl https://app.barcutpro.com/api/v1/optimize/1d \
-H "Authorization: Bearer $BARCUTPRO_API_KEY" \
-H "Content-Type: application/json" \
-d '{"parts":[{"length":2400,"quantity":4,"label":"Head"}],"stock":[{"length":6200,"quantity":10,"priority":1}]}'All lengths are millimetres. parts and stock are required; settings is optional and falls back to the product’s own defaults.
| Field | Type | Description |
|---|---|---|
| parts[].length | number | Finished length in mm. Must be greater than 0. |
| parts[].quantity | integer | How many of this length are required. Minimum 1. |
| parts[].label | string? | Free text, echoed back on every placed cut. Optional. |
| stock[].length | number | Stock bar length in mm. Must be greater than 0. |
| stock[].quantity | integer | null? | Bars available at this length. Omit or null for unlimited. |
| stock[].priority | integer? | Relative preference, 1 = use first. Omit for no preference. |
| settings.kerf | number? | Saw blade width lost at every cut, in mm. |
| settings.endTrim | number? | Material lost off both ends of every bar, in mm. |
| settings.minOffcut | number? | Leftover at or above this counts as a reusable offcut, not scrap. |
| settings.trailingCut | boolean? | Default true: the last piece on a bar needs its own cut. |
{
"parts": [
{
"length": 2400,
"quantity": 4,
"label": "Head"
},
{
"length": 1485,
"quantity": 6,
"label": "Jamb"
},
{
"length": 865,
"quantity": 3,
"label": "Transom"
}
],
"stock": [
{
"length": 6200,
"quantity": 10,
"priority": 1
},
{
"length": 4000,
"quantity": null,
"priority": 2
}
],
"settings": {
"kerf": 5,
"endTrim": 15,
"minOffcut": 300,
"trailingCut": true
}
}On priority: preference is ranked by the distinct values you send, not by their magnitude. If every entry carries the same number — or none carries one — the plan is identical to sending no priority at all. Priority only changes the result when two entries genuinely differ.
bars lists every physical bar to cut, in order, each with its own cut sequence. cuts[].sequence is the order to cut in — a position, not a distance along the bar.
| Field | Type | Description |
|---|---|---|
| version | string | The contract version that produced this body. |
| valid | boolean | false when the engine could not satisfy every constraint. |
| summary.partsRequested | integer | Always equals partsPlaced + partsUnplaced. |
| summary.partsPlaced | integer | Cuts the plan actually makes. |
| summary.partsUnplaced | integer | Parts the plan does not make. |
| summary.barsUsed | integer | Stock bars consumed. |
| summary.yieldPercent | number | Placed material as a percentage of stock consumed. |
| summary.largestOffcut | number | Longest single remnant, in mm. |
| summary.solveTimeMs | integer | Engine time. The only field that varies between identical calls. |
| bars[].barNumber | integer | 1-based, continuous across the job. |
| bars[].cuts[] | array | sequence, length and label for each cut on that bar. |
| bars[].used / remnant | number | Sum of cuts, and the physical leftover. |
| unplaced[] | array | ALWAYS present. Empty means everything was placed. |
| issues[] | string[] | Engine diagnostics explaining a partial or invalid result. |
{
"version": "v1",
"valid": true,
"summary": {
"partsRequested": 13,
"partsPlaced": 13,
"partsUnplaced": 0,
"barsUsed": 4,
"yieldPercent": 94.2,
"totalStockLength": 24800,
"totalPartLength": 23365,
"largestOffcut": 950,
"solveTimeMs": 61
},
"bars": [
{
"barNumber": 1,
"stockLength": 6200,
"cuts": [
{
"sequence": 1,
"length": 2400,
"label": "Head"
},
{
"sequence": 2,
"length": 2400,
"label": "Head"
},
{
"sequence": 3,
"length": 1355,
"label": ""
}
],
"used": 6155,
"remnant": 45
},
{
"barNumber": 2,
"stockLength": 6200,
"cuts": [
{
"sequence": 1,
"length": 1485,
"label": "Jamb"
},
{
"sequence": 2,
"length": 1485,
"label": "Jamb"
},
{
"sequence": 3,
"length": 1485,
"label": "Jamb"
},
{
"sequence": 4,
"length": 1485,
"label": "Jamb"
}
],
"used": 5940,
"remnant": 260
}
],
"unplaced": [],
"issues": []
}Deterministic. The same request returns the same plan, every time — so responses are safe to cache, and a regression in your integration is distinguishable from a change in ours. Only summary.solveTimeMs varies.
A cutting plan can be partial. If you render bars without checking unplaced, your users will cut a job they believe is complete and discover the shortfall on the shop floor. Check summary.partsUnplaced on every response.
unplaced is always present. An empty array is a positive assertion that everything was placed — it is never omitted when empty, so absence can never be mistaken for success.
Each entry carries a reason, because the two causes need opposite fixes:
| Field | Type | Description |
|---|---|---|
| too_long_for_any_stock | reason | Longer than any stock length allows once kerf and end trim are taken off. No quantity of stock will ever place it — a longer bar is required. |
| insufficient_stock | reason | The part fits, but the available bars ran out. More bars of an existing length will place it. |
{
"version": "v1",
"valid": false,
"summary": {
"partsRequested": 279,
"partsPlaced": 266,
"partsUnplaced": 13,
"barsUsed": 104,
"yieldPercent": 91.8,
"totalStockLength": 644800,
"totalPartLength": 591926,
"largestOffcut": 1180,
"solveTimeMs": 412
},
"bars": [],
"unplaced": [
{
"length": 7400,
"quantity": 2,
"reason": "too_long_for_any_stock"
},
{
"length": 2400,
"quantity": 11,
"reason": "insufficient_stock"
}
],
"issues": [
"Stock cannot cover all demand: a proven maximum of 266 piece(s) can be produced with the available stock.",
"1 part length(s) are longer than the longest available stock (6165mm) and cannot be cut: 7400mm x 2."
]
}Errors return a non-2xx status and a single error object. A 4xx means the request needs changing; a 5xx is safe to retry — a failed solve is never partially applied.
Note the distinction: a job the engine could not fully cut is a 200 with a populated unplaced array, not an error. Only a malformed or unauthorised request is an error.
{
"error": {
"code": "invalid_part",
"message": "parts[2].length must be greater than 0.",
"field": "parts[2].length"
}
}| Status | Code | Meaning |
|---|---|---|
| 400 | invalid_request | The body is not valid JSON, or a top-level field is missing or the wrong type. |
| 400 | invalid_part | A parts[] entry has a non-positive length, a quantity below 1, or a bad label. |
| 400 | invalid_stock | A stock[] entry has a non-positive length, a negative quantity, or a bad priority. |
| 400 | demand_too_large | Total requested piece count exceeds the per-request limit. Split the job by material. |
| 401 | unauthenticated | Missing, malformed, unknown or revoked API key. |
| 405 | method_not_allowed | The endpoint accepts POST only. |
| 429 | rate_limited | Rate limit exceeded. Retry after the interval in the Retry-After header. |
| 500 | solver_error | The engine failed. Safe to retry; the request was not partially applied. |
| 504 | solver_timeout | The solve exceeded the time limit. Split the job into smaller batches. |
Three shapes cover almost every integration we have been asked about:
| Pattern | Where it runs | What it does |
|---|---|---|
| Quote-time costing | Server, inline | Optimise at quote time to price the real bar count rather than an estimate. Sub-second on typical orders. |
| Works-order release | Server, on release | Optimise when the order is released, store the plan against the job, and drive picking and cutting from it. |
| Nightly batch | Scheduled job | Consolidate a day of orders into one optimisation so identical lengths share bars across jobs. |
For the saw itself, the plan is normally converted to whatever your machine reads. The product already exports a documented, versioned CSV for exactly that — see exporting a cutting plan.
60 requests per minute, per key. Exceeding it returns 429 rate_limited with a Retry-After header. Limits are per key, not per IP, so one noisy integration cannot exhaust another’s budget.
Per-request size limits, all returning a 400:
| Limit | Value | Error code |
|---|---|---|
| Part rows | 5000 | invalid_request |
| Stock rows | 500 | invalid_request |
| Total pieces | 100,000 | demand_too_large |
| Solve time | 30 s | solver_timeout (504) |
Split genuinely enormous jobs by material. A solve is deterministic, so batching is safe: the same subset always returns the same plan.
The version is in the path (/v1/) and echoed in every response body. Within a version the response is append-only: new fields may be added, existing fields are never renamed, removed or given a new meaning. Parse defensively — ignore fields you do not recognise — and a new field will never break you.
Anything breaking ships as a new path segment. Optimiser improvements that change a plan for the better are not breaking changes and will land within a version; if you depend on byte-identical plans over time, store the response you acted on.
| Capability | Status | Notes |
|---|---|---|
| 1D linear optimisation | Live | Bars, profiles, pipe, timber, rebar. The endpoint documented above. |
| 2D sheet nesting | In development | Not available in the product or the API. Nothing here returns 2D results. |
| Webhooks | Not planned yet | Solves are fast and synchronous, so there is nothing to call back about. |
| SDKs | Not planned yet | One JSON endpoint needs no client library. HTTP is the SDK. |
| Machine file generation | Under research | Linear cutting has no universal CNC format; a documented CSV plus a per-machine conversion step is the real integration route. |
A free Barcut Pro account is all you need. Keys are created, listed and revoked from your account page.
Authorization: Bearer <key> from your server.