Skip to main content
On this page▾

Engine API

If you just cut panels, you do not need this page. The optimizer inside CutOptim already does everything described here. The Engine API is the same engine with no screen attached, for when other software needs cutting plans without a person opening the app.

Everything CutOptim does in your browser starts with one calculation: given these parts and this stock, what is the best way to cut them? The Engine API exposes exactly that calculation over the internet, so a different program can ask the question and get the answer back — no browser, no clicking, nobody logged in.

That is the whole idea. It is not a new optimizer, not a better optimizer, and not a bigger plan. It is the same engine, reachable by software instead of by a person.


Is this for me?

For the overwhelming majority of CutOptim users, the honest answer is no. If your working day involves opening CutOptim, typing in parts and stock, and printing or exporting the plan, the app is the product and this page is irrelevant to you.

The Engine API is for one situation: cutting plans need to appear inside software you already use, without anyone visiting CutOptim. In practice that means one of two people:

  • You already run software that the cutting plan belongs inside. An ERP that holds your orders, a quoting tool that prices jobs, or a machine’s own software. Instead of an operator re-typing the same part list into CutOptim, that program asks the engine directly and shows the plan where the work already happens.
  • You are having software built for you. By an in-house developer, a local software company, or the supplier of your machine. The Engine API is what they connect to.

Using the Engine API means somebody writes software against it. There is no interface, no spreadsheet to fill in and nothing to install — it is a service for programs, and the work is done by whoever writes that program. If nobody on your side is writing code, the app is what you want.

If you are not sure which side of that line you are on, a good test: could the plan appear without anyone asking for it? If yes, the API is relevant. If a person always decides to make a cutting plan, the app is already the right tool.


What it does

Your software sends the same two things you would type into the app — a list of parts and a list of stock — as JSON. The engine sends back a complete answer:

  • The full layout. Every part, positioned on a specific sheet or bar, including whether it was rotated.
  • A cut plan. Not just a picture of rectangles: the actual guillotine cut sequence, in order, so the plan can be executed on a panel saw.
  • The numbers. How many sheets or bars the job takes, the yield percentage, how many cuts are needed, and the total price of the material used. For panels you also get the two honest cut counts — cut lines, which merges cuts that share one fence setting, and saw passes, which counts every pass — plus the total length sawn.
  • Optionally, the drawing. Ask for include: ["svg","csv","dxf"] and the response also carries the layout as a ready-made file — a self-contained 2D SVG drawing, an R12/AC1009 DXF, or a CSV cut list — inline in the JSON, with no storage and no second call. (SVG is 2D only.) 2D requests can also ask for labels (per-piece label data), planHash (a fingerprint of the plan), pdf (the cutting-plan PDF) and labelsPdf (the label PDF) — the PDFs base64-encoded, drawn by the same code as the app.
  • Your own identifiers, returned. Attach a meta object — an article number, an order line, a customer reference — to any part or stock row, and it comes back unchanged on every placed piece and every sheet or bar, so the plan lines up with your own system. This is the field to use when you want to send all your boards, tag each with your own code, and read back which board the optimizer chose — put the code in stock[].meta and it is echoed on sheets[].meta. It never affects placement. Do not use the material field for identification: material is a hard partition (a part is only cut from stock of the same material), so tagging every stock row with a material while leaving the parts untagged makes every part come back unmatched with an empty result — see the material-aware section below.

There are four optimization modes, all matching the app — one for 2D panels, one for 1D linear material such as bars, profiles and tube, one for timber, where the material has a cross-section, and true-shape nesting (POST /v1/optimize/nest), which packs arbitrary polygons for laser, plasma and waterjet cutting (the app’s Nesting mode). Each optimize endpoint also has a free validation endpoint that checks a request without solving it (see below).

Endpoints

The base URL is https://api.cutoptim.com. The optimize and usage endpoints carry a key in an Authorization: Bearer <key> header; the validate and health endpoints need no key.

Endpoint What it does
POST /v1/optimize/2d 2D panel optimization
POST /v1/optimize/1d 1D / linear optimization — bars, profiles, tube
POST /v1/optimize/wood Timber optimization — 1D with cross-section matching
POST /v1/optimize/nest True-shape nesting — irregular polygons for laser, plasma and waterjet
POST /v1/validate/2d · /1d · /wood · /nest Validate a request without solving — free, no key, no quota
GET /v1/jobs/{id} Poll an asynchronous max-engine job — your own jobs only, no quota
POST /v1/import/nest Read part outlines out of an SVG or DXF file — needs a key, spends no quota
GET /v1/usage The ACCOUNT’s usage and quota for the current month (all keys share one)
GET /v1/health Liveness check — needs no key

Most calls are answered directly with the finished result. The one exception is the asynchronous max engine (see Three engines below): a max submission returns a job id, and you poll GET /v1/jobs/{id} until the plan is ready.

The validate endpoints take the same body as the matching optimize endpoint and check it without running the solve: a malformed request comes back as a 400 naming the exact bad field, and a well-formed one returns valid: true plus feasibility warnings (for example, a part that fits no stock). They cost nothing and need no key, so you can validate your payloads while you build the integration — before you even have a key — and confirm a request will not be rejected without spending one of your monthly calls.

Grain direction on panels

Plywood and veneered boards have a grain, and a visible part must be cut with it running the right way. On POST /v1/optimize/2d, send grain on a stock row — "w" or "h", the side of the sheet the grain runs along — and grain on a part, the side of the part it must run along. Every such part is placed with its grain along its sheet’s: turned 90° when that is what it takes (rotated: true on the placement, as for any turn), and rotatable is ignored for it. A part without grain still rotates freely, also on a stock row without grain (MDF, melamine); a part WITH a grain keeps the orientation the grained rows give it on such a row too — send material to keep grained and plain boards apart. A part that would fit only turned against the grain is reported in unplaced in the size you sent, with a warnings line naming it, and POST /v1/validate/2d reports it before a call is spent.

Stock rows whose grain runs different ways can sit in one request — a 2440 × 1220 with "w" and a 1220 × 2440 with "h" — and each sheet comes back in its row’s own orientation with grain echoed on sheets[]. With material, a part is only held to a grain when its own material has a grained board. A part’s grain with no grained stock in the request has no effect, and a line in warnings says so. options.trim and a forced options.firstCut apply to each sheet in its own row’s orientation, also on a row whose grain runs the other way. The CutOptim app’s Grain direction mode runs the same code. (Since 2026-10-02.)

Grain matching on panels (matched sets)

Grain direction keeps each part’s grain the right way. Some fronts need more than that: a drawer front over two doors whose figure runs on from one to the next, as if the three were still one board. On POST /v1/optimize/2d, give those part rows an id and name them in matchedSets:

{
  "parts": [
    { "id": "DF", "name": "Drawer front", "w": 48.125, "h": 6 },
    { "id": "DOOR", "name": "Door", "w": 24, "h": 30, "qty": 2 },
    { "name": "Shelf", "w": 22.5, "h": 11.25, "qty": 4 }
  ],
  "stock": [{ "w": 96, "h": 48, "qty": 2, "grain": "w" }],
  "options": { "kerf": 0.125 },
  "matchedSets": [
    { "id": "base-1", "name": "Base cabinet fronts", "grain": "w",
      "layout": { "dir": "rows", "lines": [["DF"], ["DOOR", "DOOR"]] } }
  ]
}
  • What a set is. layout.lines lists the members by parts[].id. With dir: "rows" each line is a row of members side by side and the rows stack top to bottom, so [["DF"], ["DOOR", "DOOR"]] is a drawer front over two doors; with "cols" each line is a column and the columns stand side by side. A part may appear more than once in one set (Door, Door), but it can belong to one set only. qty (default 1) is how many instances you need — three identical cabinets are qty: 3 — and the parts’ own qty must cover every instance. A set has at least 2 and at most 24 members, and one request takes at most 50 sets.
  • How it is cut. The set goes through the optimizer as one rectangle — the block — and comes back as its members, exactly one kerf apart, out of one piece of board. The set’s grain ("w" or "h", along the block’s width or height) decides the direction of every member; a member’s own grain is not used inside the set, and a warning names any member it overrides. Without a set grain, the block may turn when every member is rotatable. installGap is the gap between the members on the cabinet (default: the kerf); on the sheet they still stand one kerf apart.
  • What comes back. Every placed member carries matchedSet: the set’s id, which instance, and its position in reading order on the cabinet front — top to bottom, left to right, from 1 — plus a hint such as "left" or "right" where that is unambiguous. The response adds a matchedSets rollup, one entry per set: its letter (A, B … in request order), how many instances were cut as one block (asBlock), split or left unplaced, the block size and the set’s warnings. When a block fits no stock sheet, onMiss: "split" (the default) places the members one by one with a warning, and onMiss: "unplaced" lists them in unplaced.
  • Warnings, not errors. A size that does not line up is a warning on the set, not a refusal. Each line starts with its code — align: (the lines are not flush), gap: (the kerf is wider than installGap), grain:, split:, unplaced:, unmatched: (no stock of the set’s material), priority: and grainGroup: — and align and gap are judged at 1/64 of your unit or a quarter kerf, whichever is smaller. A set that cannot be laid out at all — a member id no part carries, a referenced id on two parts, fewer than two members, too few parts for qty — is a 400 naming the exact field, before a call is charged.
  • parts[].id is safe to send. Only an id a set refers to is checked (a non-empty printable string of at most 64 characters, or an integer, on exactly one part). Every other id is ignored, whatever it holds, so your ERP article numbers can stay on the rows. And nothing changes for a request without sets: only a non-empty matchedSets array switches the feature on — absent, null, [] or any value that is not an array is answered exactly as before.
  • Exports. With sets, csv gains Set and Position columns at the end, svg prints the position code (A1, A2 …) in the part labels and draws each set’s frame in a separate matched-sets group — options.setOutlines: false leaves that group out — and dxf carries the position code on the LABELS layer. pdf and labelsPdf do not mark sets: they draw the members as ordinary parts, and warnings says so; the set marks are in sheets[].parts[].matchedSet, csv and svg.
  • Engines and endpoints. The heuristic and balanced engines lay out sets (balanced does not rank priority, so under respectStock a set can lose its place there, with a priority: warning). The max engine refuses a non-empty matchedSets with a 400 whose details.unsupported names it, before a call is charged; an empty one is an ordinary max job. matchedSets and parts[].id are 2D only: the 1D, wood and nest endpoints ignore them.
  • grainGroup is deprecated. It is still accepted and still works exactly as before: parts that share a group are kept on one sheet. It never placed them next to each other, so it gives no continuous grain across them — for that, use matchedSets.

The CutOptim app’s Grain matching lays out sets with the same core.

Why timber has its own endpoint

Linear material knows one dimension, its length, so any bar can serve any part. Timber does not work that way: a 50×150 part cannot come out of a 50×100 bar, however much length is left in it. The wood endpoint therefore takes both cross-section sides on every part and every stock row, splits the job by cross-section, matches each section to its own stock, and returns the sections separately — each with its own bars and its own totals, alongside the figures for the whole job.

Two details worth knowing before you integrate it:

  • The two cross-section sides can be sent in either order. 50×100 and 100×50 are the same bar turned over, so they are matched as one section. That means a difference in how your data happens to be entered cannot make material disappear.
  • “No stock of this cross-section” and “did not fit” are reported separately. The first is a missing-material report, the second is a capacity problem, and they have different fixes — so mixing them into one list would send your user looking in the wrong place.

You could approximate this with several 1d calls of your own, grouping the parts yourself. It would cost one request from your quota per cross-section instead of one for the job, put the stock matching in your code, and produce a job total you would have to assemble yourself — one that would not necessarily agree with what the CutOptim app shows for the same job.


Parts from a CAD file

The nesting endpoint does not only take coordinates. A part may carry a source instead — an SVG or DXF document — and the server reads its outline, and any holes in it, out of the file. It is the same reader the app uses when you drop a drawing onto its Nesting mode, so a part library that already exists as CAD files does not have to be converted into coordinate lists first.

The file replaces only the geometry. Quantity, material, allowed rotations and your own metadata stay ordinary fields on the part row, exactly as they are when you send coordinates. One file describes one part; when a single drawing holds several separate components, POST /v1/import/nest splits it into ready-made part rows first — and that call needs a key but does not spend a request from your monthly quota.

Nothing is stored. The file exists only as the request itself: it is read in memory and gone by the time the answer is written. No copy is kept on disk or in a database, nothing is written to a log, and there is nothing to delete afterwards. Units are treated the same way — a DXF may declare millimetres or inches and we report what it said, but coordinates are never converted, because no value in this API carries a unit.

The same answer every time

The engine is deterministic: the same input always produces the same output. There is no randomness and no clock inside the algorithm.

This sounds academic but it is the practical reason to build on it. It means results can be cached — if you already asked about this exact job, you can safely reuse the answer instead of asking again. It also means the integration can be tested: a plan can be compared against a known-good result, and a difference is a real difference, not noise. Software that quotes a customer a price on Monday will quote the same price on Friday.

There is one opt-in exception, and it says so: the nesting max engine (below) spends a fixed time budget searching, so how far it gets depends on how much work fits into that time. Its responses carry deterministic: false. Every other engine — including nesting’s default, lbf — is deterministic.

Retrying safely. If a connection drops, you cannot tell whether your request arrived. Send an Idempotency-Key header — a value you choose per job — and retry with the same key: within 24 hours the same request returns the stored answer, marked Idempotency-Replayed: true, and is not charged a second time. A max job is not queued twice either. The public demo key ignores this header. (Since 2026-10-15.)

Webhooks for async jobs. Instead of polling GET /v1/jobs/{id}, register an endpoint (an https:// URL) on your dashboard and get a signed POST — job.succeeded or job.failed — when an async max job finishes. Check the CutOptim-Signature header with the secret you received, answer within 10 seconds, and read the plan with GET /v1/jobs/{id}. A delivery that fails is retried, 8 attempts over 24 hours. (Since 2026-10-19.)

Official SDKs. Open-source clients for TypeScript, Python and PHP — npm install @cutoptim/engine, pip install cutoptim, composer require cutoptim/engine — give you typed requests, retries that are never charged twice (every call carries an Idempotency-Key), a wait helper for async jobs and webhook verification. Source on GitHub; details in the SDK guide (English).

MCP server for AI assistants. Claude Code, Cursor, VS Code and Claude Desktop can call the engine as tools through the hosted Model Context Protocol server at https://api.cutoptim.com/mcp, with the same API key (Authorization: Bearer …) and the same quota: one tool call is one API call, and the validate_* tools are free. It also runs locally as the package npx -y @cutoptim/mcp (key in CUTOPTIM_API_KEY), and the official MCP Registry lists it as io.github.cutoptim/engine. Client configurations are in the MCP guide (English).


Three engines

The API offers a choice of engine. The first two run synchronously; the third is asynchronous. The difference is not quality — it is a trade-off between speed, whether the result can be cut on a panel saw, and how close it gets to the theoretical minimum.

  • heuristic is the default and the same one the app uses. It produces guillotine layouts: highest yield, every layout cuttable on a panel saw, and always a full cut plan. Synchronous.
  • balanced is opt-in. It uses free nesting instead, which is dramatically faster on very large jobs — measured roughly 25× faster on a 2,000-part job — at the cost of slightly lower yield. The important catch: its layouts often cannot be cut edge-to-edge, so for those it returns no cut plan at all. Synchronous.
  • max is opt-in, for 2D panels (and, separately, for true-shape nesting — see below). On 2D it is a server-side tree search that reaches the proven optimum on far more jobs than the default, and its layouts are still guillotine-cuttable. The cost is time: a max solve takes seconds to a minute, so it does not answer in the response. Instead, POST /v1/optimize/2d with engine: "max" returns a job id, and you poll GET /v1/jobs/{id} until it is ready. It is still deterministic. Reach for it when a large, valuable job is worth the wait for the last few boards. It models one stock format at full sheet size with unlimited supply, so a request that also carries a second stock format, per-side trim, limited stock (respectStock), materials, grain groups or a non-empty matchedSets is refused up front with a 400 naming exactly what it cannot do — before a call is charged. Send those jobs to heuristic, which models all of them.

Nesting: lbf and max

True-shape nesting has two engines of its own. lbf is the default: instant, synchronous and deterministic. max is opt-in and asynchronous, exactly like the 2D max — POST /v1/optimize/nest with engine: "max" returns a job id, and you poll GET /v1/jobs/{id}.

The nesting max runs the lbf pass first, so it always has a valid answer in hand. Then it spends a time budget (options.timeBudgetMs, 60 seconds by default, between 10 and 180) searching for a layout with fewer sheets, filling one sheet at a time. It never returns more sheets than lbf: when the search finds no improvement, you get the lbf layout and a warning that says so. Because the search is time-limited, its responses carry deterministic: false. It accepts several sheet sizes per material and respectStock, and ranks its result with the same rule as lbf (below): it replaces the lbf plan only when it is better — never just because it has fewer sheets. A polygon sheet or exclusion zones, which its strip search cannot model, are served by the lbf pass inside the job, with a warning — never refused. The older name sparrow is accepted as an alias of max.

Rectangular parts get an exact grid (since 2026-09-28). lbf finds positions by sampling, which almost never lands on a zero-slack grid: three 500 mm parts across a 1500 mm sheet at zero spacing is an exact fit, and a sampling search places two. So both engines also try two alternatives and keep whichever ranks best by the same rule: the parts restricted to the right angles they allow, and the parts’ bounding boxes packed by an exact rectangle packer (boxes that do not overlap cannot hold overlapping parts, so this is valid for any outline). On a tie — same sheets, same sheet area — the plan that fills its sheets front to back wins, then the one with fewer parts turned off the right angles. Nothing to switch on, and a result can only get better by the ranking; a plan that the rectangles bring to the fewest sheets the part area allows also makes max answer at once instead of searching.

A job of a few dozen parts usually needs about 15 seconds of budget (timeBudgetMs: 15000); the job finishes roughly one to two seconds after the budget, plus any time spent in the queue. Poll GET /v1/jobs/{id} every two to three seconds — polling costs no quota. No budget can promise a particular number of sheets.

Nesting: edge clearance, sheet sizes and what each sheet cost

  • Edge clearance. options.minSeparation is the spacing between parts, and on its own it also applies at the sheet edge. options.edgeClearance sets the edge separately — for example 5 between parts and 0 or 10 at the edge. Leave it out and the edge keeps minSeparation, exactly as before. It works on both engines, needs rectangular sheets (w and h), and exclusion zones keep minSeparation like parts. A polygon sheet, or a value that leaves no room on a sheet, is refused with a 400 before a call is charged.
  • Choosing a sheet size. When a material has two or more stock rows, lbf tries them in your order, each size on its own and a cheapest-first order, and keeps the best — so the result no longer depends on the order you list the rows in. The ranking is: most must-cut parts placed (see below), then most parts placed, then the least total sheet area (material), then the fewest sheets. With options.minimizeCost it ranks by total price instead (price, or cost for a row without a price) — but only when every row of that material carries one; otherwise it falls back to area and says so in warnings.
  • Mixing sizes. After that, each sheet — the least-filled first — is re-nested on its own onto the cheaper or smaller rows, and replaced when the whole plan ranks better. So sizes can be mixed within one material: typically two large sheets and a smaller last one, where the half-empty last sheet used to be a large one. With options.respectStock, no row is used more often than its qty. The max engine applies the same ranking.
  • Must-cut parts. parts[].priority: true marks a part that must be cut. It matters with options.respectStock — a capped supply, where not every part may fit: must-cut parts are then placed first, so a plain part never takes the sheet space a must-cut part needs, and every ranking above puts the plan that keeps more of them ahead, even if it places fewer parts in total. Both engines honour it (max keeps the lbf layout whenever anything is left unplaced). Without respectStock every part that fits is placed anyway, so the flag changes nothing. A must-cut part that fits no sheet at all is still listed in unplaced. (Since 2026-09-22 — before that the nesting engine accepted the field but ignored it.)
  • A capped supply that leaves parts over. With options.respectStock, when the plan still leaves parts unplaced, the engine also fills the sheets one at a time with the parts’ bounding boxes — each sheet as full as it can, and a part that fits only one sheet size goes there first (a tall side on the one big sheet, not on an offcut strip). That plan is taken only when it places more parts (must-cut first) or uses less material; a job that places everything comes back exactly as before. It respects every part’s allowed rotations, minSeparation and edgeClearance. (Since 2026-10-14.)
  • What each sheet cost. Every sheet carries stock — the index of the request’s stock row it was cut from — and the response adds stockUsage: one entry per stock row used, with its sheet count, total price and density. The unit is the stock row, not the size: two rows with the same dimensions but a different price or meta are two entries.
  • Cut length and pierces. Every sheet carries cutLength — the summed perimeter of its placed parts’ outlines and holes, in your unit — and pierces, one per closed contour; metrics totals both. It is the geometric contour length: no common-line cutting, lead-ins or micro-joints — your CAM adds those.
  • Used area, for partial-sheet charging. Every sheet carries usedWidth and usedHeight — the farthest point any placed part reaches from the sheet’s origin corner, in your unit, on the real sheet (with edgeClearance the edge gap is included) — and usedArea, their product; stockUsage[].usedArea totals it per stock row. That is what an ERP needs to charge a horizontal cut (usedHeight × sheet width), a vertical cut (usedWidth × sheet height) or the bounding box (usedArea) instead of the full sheet — the charging rule stays yours. To make those numbers small, both engines compact the least-filled sheet of each material by default: its parts are re-nested on the same sheet with rotation subsets of what each part allows — 0/180, 0/90/180/270 and one orientation for every copy, intersected with a discrete allowedRotations, never a new angle — and the layout with the smallest used area is kept. With engine: "max" that sheet is then also re-packed by the search itself — along the sheet’s height and, on a one-sheet plan, also along its width — and the tighter layout is kept under the same rule. Sheet count, sizes, price and placed parts never change; options.compact: false returns the uncompacted layout. If you charge a strip rather than the bounding box, set options.compactFor to match: "horizontal" keeps the layout with the lowest usedHeight (a strip across the full sheet width), "vertical" the one with the smallest usedWidth (a strip over the full sheet height); the default "box" keeps the smallest usedArea. The strip modes also try every copy turned a quarter (90° or 270°) where allowedRotations permits. How small the used area gets depends on the parts. (Since 2026-09-22.)

balanced is not “better results”. It is faster and yields slightly less, and when its layout is not guillotine-cuttable there is no cut plan to hand to a saw operator. Choose it only when speed on a very large job matters more than a saw-ready plan. If you are unsure, stay on the default.


Limits

Each request is bounded, so a runaway job fails clearly instead of hanging:

Limit Value
Parts per request 2,000
Stock rows per request 50
Request body size 1 MB
Request body size — nest paths that may carry a drawing 10 MB
Active keys per account 10
Keyless validate calls per address 120 / minute
Unknown API keys per address 30 / minute — then 429 for the rest of the minute
Wait for a free solve slot (synchronous paths) about 8 s — then 429, nothing charged
max jobs queued or running per account 5
Nesting max search budget per job 10–180 s (default 60 s)
Idempotency-Key kept for 24 hours
Webhook endpoints per account 5
Webhook delivery attempts 8 over 24 hours

Getting access

Start on the Engine API page. The Engine API is billed separately from the app plans, and no plan upgrade turns it on.

  1. Try it, then subscribe — or ask. You can call the API before paying: the Run this example buttons on the API reference use a public demo key, and any signed-in account can create free test keys on the dashboard (their own small monthly allowance, never billed, no subscription needed). When you are ready, subscribing on the Engine API page is the fastest route — the entry plan, Studio, starts with a free trial, and access lands on your account by itself. If you would rather describe your integration first, or you need a volume beyond the standard plan, get in touch instead and say what you want to connect and roughly how many cutting plans a month it will need.
  2. Access appears on your account. Nothing else about your account changes.
  3. Create a key. The API keys card is on your dashboard for every signed-in account: without a subscription it creates test keys, with API access live keys too. You create, name and delete keys there yourself.
  4. Copy the key immediately. The full key is shown exactly once, at the moment you create it.

A key is displayed only once. CutOptim stores only a sha256 hash of it, never the key itself — so it cannot be read back out later, by you or by us. Copy it straight into your software’s configuration when you create it. If you lose a key, delete it and create a new one; if a key is ever exposed, delete it and the calls stop working immediately.

Treat a key like a password: it belongs in your software’s configuration, not in an email, a spreadsheet or a screenshot.

What the API keys card on your dashboard does

Four controls, and it is worth being precise about what each one changes — especially the last one, which people expect to touch billing and does not.

  • Plan, Switch and Billing. Shows which plan the account is on (Studio or Scale). Switch to Scale (or Switch to Studio) changes the plan after a confirmation, and the account’s monthly quota changes straight away. Billing opens your billing settings, where you update the card, see invoices or cancel.
  • Create key. Generates a new key and shows it once, right there. You give it a name (ERP integration, staging) purely so you can tell your keys apart later. Up to 10 active keys per account. Test keys are created the same way and counted separately: they have their own small monthly allowance and are never billed.
  • The usage bar. Two numbers: your account’s total for the calendar month against the monthly quota, and per key, how much that key has spent. The per-key figure is there to answer which integration is eating the allowance — it is not a separate budget.
  • Revoke. Stops that one key working, from the next call onwards. The row stays visible so its history is not lost.

Revoking a key has nothing to do with your subscription. It does not cancel anything, does not refund anything and does not free up quota — the plan keeps running and the allowance keeps standing, you simply no longer hold that particular key. Revoke when a key has been exposed or an integration is retired. To stop being billed, cancel the subscription instead; access then continues to the end of the period you already paid for, and after that even existing keys stop working.

One quota for the account, not one per key

Every active key on your account draws on the same monthly allowance. Creating a second key does not create a second quota — keys exist so you can separate staging from production, give each integration its own credential, and revoke one without disturbing the others.

GET /v1/usage reports the account’s position (used, limit, remaining), so it answers the question you actually have — how much is left before calls start failing — regardless of which key you asked with. When the allowance runs out, every key returns 402, not just the one that spent it.


Plans, price and quota

The Engine API is billed separately from the app plans, monthly, in EUR, with two plans:

Plan What differs
Studio The entry plan, with a free trial
Scale A larger monthly request quota, no trial — you switch to it from Studio

Everything else is the same on both: the same engines, all four modes, every option, the same answers. Every request counts the same — a nesting job costs one request, exactly like a 2D or 1D one.

  • Hard cap, no overage. When the monthly quota is used up, calls return 402 until it resets on the first day of the next month. There is no surprise invoice.
  • Two emails, so the cap does not surprise you. We email the account at 90% of the quota and again when it is used up.
  • Switching plans is one button on the API keys card of your dashboard (Switch to Scale / Switch to Studio); the new quota applies straight away. An upgrade is charged at once for the rest of the period; a downgrade is not refunded, and the lower price applies from the next renewal. Switching is available once the trial is over.
  • Larger volumes than the Scale plan are priced individually — get in touch.

The current prices and quotas are listed on the Engine API page — that page reads them from our pricing configuration, so it is always the accurate figure.

For the technical detail — the exact request and response shape, every option, all error codes and how versioning works — see the API reference.


Open-source components

CutOptim’s engines build on open-source libraries, used unmodified on our servers:

  • jagua-rs — the geometric collision engine behind true-shape nesting (MPL-2.0).
  • sparrow — the search behind the nesting max engine (MIT, KU Leuven).
  • PackingSolver — the tree search behind the 2D max engine (MIT).

Everything around them — the API, the mapping between the contract and these engines, the rectangular packers and the app itself — is CutOptim’s own code.


What this does not change

Worth stating plainly, because the Engine API is easy to misread as a change to the product:

  • The optimizer in the app is unchanged. It still runs in your browser, exactly as before.
  • Free, Pro and Workshop are unaffected. They still include the in-app optimizer, with the same limits as before. Nothing was moved behind the API.
  • Nothing you do in the app consumes API requests. The monthly API quota is only touched by calls your own software makes.

The Engine API is an addition for people integrating CutOptim into other software. If that is not you, nothing changed.

FAQ

What is the difference between the Studio and Scale plans?
Only the number of requests per month and the trial. Both plans run the same engines, all four modes and every option, and every request counts the same — a nesting job costs one request, exactly like a 2D one. Studio is the entry plan and comes with a free trial; Scale has a larger monthly quota and no trial, because you reach it by switching from Studio. The current prices and quotas are on the Engine API page.
Can I switch between Studio and Scale?
Yes, once the trial is over and the first payment has gone through, with the Switch to Scale (or Switch to Studio) button on the API keys card of your dashboard. The new monthly quota applies to your account straight away. An upgrade is charged at once for the rest of the billing period; a downgrade is not refunded, and the lower price applies from the next renewal.
Can I try the API before paying?
Yes, two ways. The API reference has Run this example buttons that use a public demo key, with no account at all — small jobs only. And any signed-in CutOptim account can create test keys on the dashboard: they have their own small monthly allowance, are never billed and need no subscription, so you can build the whole integration before you buy. Studio then starts with a free trial.
Will I be warned before my monthly quota runs out?
Yes. We email the account at 90% of the monthly quota and again when it is used up. After that, calls return 402 until the quota resets on the first day of the next month — or straight away if you switch to the larger plan.
Do I need the Engine API?
Almost certainly not. If you cut panels or bars by opening CutOptim in your browser, the app already does everything the API does — the API is the same engine with no screen attached. It exists for one situation only: other software needs cutting plans without a person opening the app. If nobody is writing code against CutOptim, you can ignore it.
Who is the Engine API actually for?
Two kinds of customer: someone who already runs software the cutting plan should live inside — an ERP, a quoting or order-entry tool, a machine's own software — and someone having such software built for them, by an in-house developer or an agency. In both cases the work is done by whoever writes that software, not by you in the CutOptim interface.
How do I get access to the Engine API?
You can subscribe on the Engine API page — it runs a trial period, and access lands on your account by itself once you do. That is the fastest route. If you would rather describe your integration first, or need a volume beyond the standard plan, get in touch instead. Either way you then create live keys yourself on the API keys card of your dashboard. The Engine API is billed separately from the app plans and no plan upgrade turns it on.
Can the Engine API cut timber, or only panels and bars?
All four modes the app has: panels, bars, timber and true-shape nesting. The wood endpoint is cross-section aware: parts and stock both carry the two cross-section sides, so a 50×150 part is only ever cut from 50×150 stock. The job is split by cross-section and each section comes back with its own bars and totals, and demand whose cross-section has no stock at all is reported separately from parts that had stock and did not fit.
Does the Engine API change my Free, Pro or Workshop plan?
No. The Engine API is a separate add-on and changes nothing about the regular plans. The optimizer inside the app is unchanged, still runs in your browser, and is still included in Free, Pro and Workshop exactly as before. Nothing you do in the app starts consuming API requests.
Is there an SDK or client library for the Engine API?
Yes. Official, open-source SDKs exist for TypeScript (npm @cutoptim/engine), Python (PyPI cutoptim) and PHP (Composer cutoptim/engine) — source on github.com/cutoptim, guide at /engine/reference/guides/sdks. They are optional: the API stays plain HTTPS with JSON, which any language can call without a CutOptim package. Webhooks exist for the async jobs: register an endpoint on your dashboard and get a signed POST when a max job finishes, instead of polling. Most calls are answered directly with the finished result; the one exception is the asynchronous max engine — for 2D panels and for true-shape nesting — where POST /v1/optimize/2d or /v1/optimize/nest with engine "max" returns a job id you poll with GET /v1/jobs/{id} until the result is ready.
Is the balanced engine better than the default?
No — it is a trade-off, not an upgrade. The default heuristic engine gives the highest yield and every layout it produces can be cut on a panel saw, so it always returns a cut plan. The balanced engine is much faster on very large jobs but yields slightly less, and its layouts often cannot be cut edge-to-edge, so it returns no cut plan at all. Use it only when speed on a big job matters more than a saw-ready plan.
What happens if I revoke an API key — do I get money back?
No, and it does not cancel anything either. Revoking stops that one key from working, from the next call onwards; your subscription keeps running and the monthly quota keeps standing. Revoke a key when it has been exposed or an integration is retired, then create a replacement. To stop being billed you cancel the subscription instead — access then continues to the end of the period you have already paid for, and after that even existing keys stop working.
Does creating more API keys give me more requests?
No. The monthly quota belongs to the account, and every active key draws on the same allowance — a second key does not add a second quota. Keys exist so you can separate staging from production, give each integration its own credential, and revoke one without disturbing the others. When the allowance runs out, every key returns 402, not just the one that spent it. GET /v1/usage reports the account's position, whichever key you ask with.
Why is my API key only shown once?
Because CutOptim never stores the key itself — only a sha256 hash of it. That means nobody, including us, can read your key back out of the database, so it can only be displayed at the moment it is created. Copy it into your software's configuration straight away; if you lose it, delete the key and create a new one.
Can the Engine API return a drawing, not just coordinates?
Yes. Add include: ["svg","csv","dxf"] to an optimize request and the response carries the layout as a ready-made file, inline in the JSON: a self-contained 2D SVG drawing, an R12/AC1009 DXF on layers STOCK/PARTS/LABELS, or a CSV cut list. There is no storage and no second call. SVG is 2D only; a 1D or wood request returns a warning instead. You can also attach a meta object (your ERP article number, order-line id or customer reference) to any part or stock row, and it comes back verbatim on the output so the plan reconciles with your system. meta is also the right field for reading back which board the optimizer chose: put your code in stock[].meta and it is echoed on sheets[].meta, and it never affects placement. Do not use the material field for identification — material is a hard partition, so tagging every stock row with a material while leaving the parts untagged makes every part come back as unmatched with an empty result.
Can I check a request without spending one of my monthly calls?
Yes. POST /v1/validate/2d, /v1/validate/1d, /v1/validate/wood or /v1/validate/nest takes the same body as the matching optimize endpoint and checks it without solving — for free, with no API key and no quota spent. A malformed request comes back as a 400 naming the exact bad field, and a well-formed one returns valid: true plus feasibility warnings (such as a part that fits no stock). Use it to validate your payloads while building the integration, before you even hold a key.
What happens to an SVG or DXF file I send to the API?
Nothing is kept. The file travels as the request body, is read in memory to get the part outline, and is gone as soon as the answer is written — there is no copy on disk, no copy in a database, no log entry containing it, and therefore nothing to delete afterwards and no retention period to ask about. This is the same statelessness the rest of the API keeps: we hold your cutting plans no longer than the request that produced them.
Does true-shape nesting have a max engine too?
Yes. POST /v1/optimize/nest with engine "max" is asynchronous like the 2D max: the call answers 202 with a job id and you poll GET /v1/jobs/{id}. It runs the instant lbf pass first, then spends a time budget — 60 seconds by default, 10 to 180 through options.timeBudgetMs — looking for a layout with fewer sheets, filling one sheet at a time. It never returns more sheets than lbf would; when it finds none, you get the lbf layout and a warning that says so. Because the search is time-limited, its responses carry deterministic: false. It accepts several sheet sizes per material and respectStock, and keeps its own result only when it ranks better than the lbf plan (by price with options.minimizeCost, otherwise by sheet area). A polygon sheet or exclusion zones, which its search cannot model, are served by the lbf pass inside the job with a warning — never refused.
Why did rectangular nesting parts come back tilted or on an extra sheet?
Before 2026-09-28 that could happen: the lbf nesting engine finds positions by sampling, so with free rotation it could turn a near-rectangular part by a fraction of a degree, and it rarely found a zero-slack grid such as three 500 mm parts across a 1500 mm sheet at zero spacing. Both nesting engines now also try the parts at the right angles they allow and pack the parts' bounding boxes with an exact rectangle packer, and keep whichever plan ranks best; on a tie, the plan that fills its sheets front to back wins, then the one with fewer tilted parts. You do not need to change your request. If your parts must never turn off the right angles, send allowedRotations: [0, 90, 180, 270] as before.
Can I set the clearance at the sheet edge separately from the spacing between parts?
Yes. options.minSeparation is the spacing between parts, and on its own it also applies at the sheet edge. Add options.edgeClearance to set the edge separately — for example 5 between parts and 0 or 10 at the edge. It works on both nesting engines, and if you leave it out the edge keeps minSeparation exactly as before. It needs rectangular sheets (w and h): with a polygon sheet, or with a value that leaves no room on a sheet, the request is refused with a 400 before a call is charged. Exclusion zones keep minSeparation, like parts.
How does nesting choose between several sheet sizes?
When a material has two or more stock rows, the lbf engine tries them in your order, each size on its own and a cheapest-first order, and keeps the best — so the answer no longer depends on which row you list first. It ranks by the most must-cut parts placed (parts marked priority, which matters with options.respectStock), then the most parts placed, then the least total sheet area; with options.minimizeCost it ranks by total price instead (price, or cost when a row has no price), provided every row of that material has one. Then every sheet, the least-filled first, is re-nested on its own onto the cheaper or smaller rows and replaced when the whole plan ranks better — so sizes can be mixed within a material: typically two large sheets and a smaller last one. With options.respectStock no row is used more often than its qty. The response says which row every sheet came from (sheets[].stock) and totals the sheets, price and density per row (stockUsage). The max engine applies the same ranking.
Can I mark nesting parts as must-cut?
Yes: parts[].priority: true. It matters when the sheet supply is capped (options.respectStock), because then not every part may fit: must-cut parts are placed first, so a plain part never takes the sheet space a must-cut part needs, and every plan comparison ranks the plan that keeps more of them ahead. Both nesting engines honour it. Without respectStock every part that fits is placed anyway, so the flag changes nothing. A must-cut part that fits no sheet at all still comes back in unplaced. (Before 2026-09-22 the nesting engine accepted this field but did not act on it.)
Does the nesting response include the cut length?
Yes. Every sheet carries cutLength — the summed perimeter of its placed parts' outer outlines and holes, in your unit — and pierces, one per closed contour (each part's outline plus one per hole); metrics totals both for the whole job. It is the geometric contour length: common-line cutting, lead-ins and lead-outs and micro-joints are not included, because your machine's CAM adds those — so it is a sound base for machine time and consumables, not the final tool path.
Can I charge only for the part of a sheet a nest uses?
The API gives you the numbers; the charging rule stays yours. Every sheet carries usedWidth and usedHeight — the farthest point any placed part reaches, measured from the sheet's origin corner, in your unit — and usedArea, their product; stockUsage[].usedArea totals it per stock row. A horizontal cut is usedHeight × the sheet width, a vertical cut usedWidth × the sheet height, a bounding box usedArea. Both nesting engines also compact the least-filled sheet of each material by default: its parts are re-nested on the same sheet with rotation subsets of what each part allows (0/180, 0/90/180/270, one orientation for every copy — never a new angle), and the layout with the smallest used area is kept. Sheet count, sizes, price and placed parts never change. options.compact: false returns the uncompacted layout. If you charge a strip rather than the bounding box, set options.compactFor to "horizontal" (the lowest used height, for a strip across the full sheet width) or "vertical" (the smallest used width, for a strip over the full sheet height); the default "box" minimises the used area. How small the used area gets depends on the parts, so no fraction of a sheet can be promised in advance.
How much time budget should I give the nesting max engine?
The job finishes about one to two seconds after its budget, plus any time it waits in the queue. For a job of a few dozen parts, 15 seconds (options.timeBudgetMs: 15000) is usually enough; the default is 60 seconds and the range is 10 to 180. A larger budget gives the search more chances to save a sheet, but no budget can promise a particular sheet count — that depends on the parts and the spacing. Poll GET /v1/jobs/{id} every two to three seconds; polling costs no quota.
Can the API respect the grain direction of a part?
Yes. On POST /v1/optimize/2d, give a stock row grain: "w" or "h" — the side of the sheet the grain runs along — and a part grain: "w" or "h" — the side of the part it must run along. Each such part is placed with its grain along its sheet's, turned 90° when it has to be (rotated: true), and rotatable is then ignored for it. Stock rows whose grain runs different ways can be mixed. With material, only boards of the part's own material count. Every sheet echoes its row's grain. If no stock row has a grain, a part's grain has no effect and warnings says so. All three 2D engines support it (max with its usual single stock row).
Can the API cut matched fronts so the grain runs on from one part to the next?
Yes, on POST /v1/optimize/2d. Give the part rows an id and name them in matchedSets — for a drawer front over two doors, a layout with dir rows and the lines [DF] and [DOOR, DOOR]. The set is cut side by side from one piece of board, exactly one kerf apart, and the set's grain decides the direction of every member. Every placed member comes back with matchedSet (the set id, the instance and its position), and the response adds a matchedSets rollup whose warnings say, for example, when the sizes do not line up. The heuristic and balanced engines lay out sets; the max engine refuses a non-empty matchedSets with a 400 before a call is charged. The pdf and labelsPdf exports draw the members as ordinary parts, and the response warns about it; csv, svg and dxf mark the sets. Without a non-empty matchedSets array nothing changes, and parts[].id is only checked on the rows a set names.
Is grainGroup still supported?
Yes. grainGroup is deprecated but still accepted, and it still works exactly as before: parts that share a group are kept on one sheet. It never placed them next to each other, so it gives no continuous grain across them — for fronts whose figure must run on, use matchedSets instead. The max engine refuses grainGroup, as it always has.

Last updated: October 13, 2026