The MCP server also runs locally over stdio as the open-source npm package @cutoptim/mcp (MIT, Node 18+, source cutoptim/engine-mcp): npx -y @cutoptim/mcp, with your API key in CUTOPTIM_API_KEY (CUTOPTIM_BASE_URL overrides the default https://api.cutoptim.com). Same tools, same quota and billing as the hosted server; the client sends an Idempotency-Key with every optimize call. Setup: MCP guide, Local (stdio).
The hosted server and the package are listed in the official MCP Registry as io.github.cutoptim/engine.
The REST contract is unchanged.
The OpenAPI document marks the four validate endpoints as keyless
Fixed
POST /v1/validate/2d, /v1/validate/1d, /v1/validate/wood and /v1/validate/nest now carry security: [] in the OpenAPI document, like GET /v1/health. The endpoints have always answered without an API key; the document said otherwise, so a generated client could insist on a key it does not need. Behaviour is unchanged.
GET /v1/usage summary: “The account’s current-month usage and quota” (it said “the calling key’s”). The figures have always been the account’s — every key on the account draws on one allowance. Behaviour is unchanged.
MCP server for AI assistants
Added
A hosted Model Context Protocol server at https://api.cutoptim.com/mcp (Streamable HTTP, stateless). Claude Code, Cursor, VS Code and Claude Desktop can call the engine as tools: optimize_2d, optimize_1d, optimize_wood, optimize_nest, validate_2d, validate_1d, validate_wood, validate_nest, get_job and get_usage. Guide: MCP server for AI assistants.
Authentication is the API key you already have, sent as Authorization: Bearer <key>; the public demo key works for small 2D, 1D and timber jobs. Listing the tools and the validate_* tools need no key.
The tool input schemas are generated from this OpenAPI document (the request bodies), and each result carries the endpoint’s JSON answer as structuredContent.
One tool call is one API call: the same quota, rate limits and billing as the REST request. The REST contract itself is unchanged.
Several stock sizes without respectStock — the default 2D engine tries mixes
Changed
POST /v1/optimize/2d with the default engine (heuristic), two or more stock sizes and no options.respectStock: the solver also tries mixes of the sizes — one sheet fewer of a size the plan uses and one to three of another size in its place — and returns a mix only when every part the plan placed still fits on less sheet area. A response can therefore list more sheets than before for the same job, but never more sheet area and never fewer placed parts.
The same with engine: "balanced" (MaxRects free layout), in two rounds — the second only when the first found a mix: measured −0.85 % material on a two-size corpus at about 1 ms extra.
With options.minimizeCost a mix is returned only when it is also cheaper (the cost goal is never dearer than the waste goal). Not applied with options.maxCutStages, matched sets (grainGroup), options.effort: "fast" or options.respectStock, nor on very large jobs (the work is capped; above about 1,100 parts it is not tried). One stock size: responses are unchanged.
engineVersion of the heuristic and the balanced engine moves with this change, and with it the engineVersion and planHash of the 2D labels response example (a plan hash covers the engine version). The request and response schemas are unchanged.
Official SDKs for TypeScript, Python and PHP
Added
Official SDKs, version 1.0.0: @cutoptim/engine on npm (TypeScript / JavaScript, Node 18+), cutoptim on PyPI (Python 3.9+) and cutoptim/engine on Packagist (PHP 8.1+). MIT-licensed, no dependencies, source at github.com/cutoptim. Guide: Official SDKs.
The SDK types are generated from this OpenAPI document; a later contract change ships as a minor SDK release.
Every optimize call through an SDK sends an Idempotency-Key, the same on each retry; 429 busy, 503, 409 idempotency_in_progress and network errors are retried. Each SDK verifies CutOptim-Signature on webhook deliveries.
The API contract itself is unchanged: plain HTTPS with JSON keeps working without an SDK.
Webhooks for async max jobs
Added
Webhooks: register up to 5 endpoints on your dashboard and receive a signed POST when an async max job (2D or nest) finishes — job.succeeded or job.failed — instead of polling GET /v1/jobs/{id}. Each endpoint chooses which key type it hears (live, test or both). The OpenAPI document describes the events under the top-level webhooks (schema WebhookEvent).
The event body is thin (id, type, created, env, data.jobId, data.mode, data.engine, data.status); the plan itself is still read with GET /v1/jobs/{id}, without quota.
Every delivery carries CutOptim-Signature: t=<unix>,v1=<hex HMAC-SHA256(secret, "<t>.<raw body>")>, CutOptim-Event-Id, CutOptim-Event-Type and CutOptim-Delivery-Attempt. Rotating a secret keeps the old one signing for 24 hours (two v1 values).
Retries: a delivery that is not answered with a 2xx within 10 seconds is retried at 1 min, 5 min, 30 min, 2 h, 5 h, 10 h and 24 h after the first attempt, then given up; an endpoint that fails for 3 days is disabled and the account is e-mailed. Delivery is at least once — deduplicate on CutOptim-Event-Id. The delivery log is kept 30 days.
A worked example for the 2D data tokens
Added
POST /v1/optimize/2d has a fifth named request example, labels: a job with include: ["labels", "planHash"] and one banded part. The 200 response has the matching labels example: it shows labels[] and planHash, and shows that a request with include returns no cutPlan or offcuts unless they are listed too. The existing examples are unchanged.
Fixed
POST /v1/optimize/2d: two rows with the same name and size but different meta used to give every placed piece the meta of the last of those rows in sheets[].parts[].meta. Since 2026-10-05 each piece carries the meta of its own row. Requests whose rows differ in name or size were never affected.
Changed
The GET /v1/usage description of keyClass says the value marks a key CutOptim issues to its own first-party service. Text only; the value and when it appears are unchanged.
Cleaner field descriptions
Fixed
Field and operation descriptions no longer carry our internal ticket numbers (for example OPEN-256 — … or (OPEN-224)). The text of each description is otherwise unchanged; no field, type, default or limit moved.
The Limits page now says what the Idempotency-Key reference already did: the public demo key ignores the header.
Reference field tables generated from the OpenAPI document
Added
Descriptions for the response and request fields that had none: the nest response (sheets[].parts[].name, material, meta, metrics.sheetCount, stockUsage[].sheetCount, unplaced[], warnings, timing), the nest request (parts, stock, parts[].qty, stock[].w / h / polygon), the per-material rollups and unmatched-material blocks of every mode, the 1D and wood unplaced / metrics fields, sheets[].parts, the off-cut rectangle, the matched-set layout.dir / layout.lines and rollup id, the import response parts[].name and contractVersion, the validate mode / parts / stock counts, and contractVersion / status / service on usage and health.
Changed
The field tables on the reference pages are now rendered from this document: each table is a schema, the meaning column is the field’s description. Wording that only the pages carried moved into the descriptions, for example: rods[].remaining is the usable off-cut with the separating kerf already deducted; sheets[] come in cutting order; a placed 2D part’s rotated is also true when its grain forced the turn; wood cutPlan steps carry section and a rod index within that section; nest engine: "max" runs lbf first, then a time-budgeted search for fewer sheets; a parts[].source file travels as text inside the JSON body.
Fixed
GET /v1/usageused counts the calls of EVERY key on the account, revoked keys included (revoking a key cannot un-spend what it spent); the description said “every active key”. limit now says it is the largest monthly quota frozen onto any active key. The numbers the server sends are unchanged.
Postman collection, and the nesting 504 body documented
Added
A Postman collection generated from the OpenAPI document: https://cutoptim.com/engine/postman.json. One folder per tag, one request per operation with its documented example; set the apiKey variable.
Fixed
POST /v1/optimize/nest documents its 504 body: on nesting the timeout is the JSON error envelope with error: "solve_timeout" (on the rectangular paths the proxy still answers with its own body).
OpenAPI document - tags, operation id and nullable fields
Added
The validate and nest tags are declared at the top of the document (operations already used them), so reference tools group the operations correctly.
GET /v1/jobs/{id} has an operationId (getJob) and the optimize tag.
Each POST /v1/validate/* operation has a 200 example: the server’s answer to the request example of the same path.
Fixed
GET /v1/jobs/{id}finishedAt and the nest sheets[].price were marked nullable: true, an OpenAPI 3.0 keyword that 3.1 tools ignore. They are now type: [..., "null"]. The values the server sends are unchanged.
Documented - 2D data tokens, the PDF cap, the public demo key, test keys
Added
The include description explains the four 2D data tokens: labels (per-piece label data), planHash (a 64-hex plan fingerprint), pdf (the cutting-plan PDF) and labelsPdf (the label PDF), both PDFs base64-encoded. They are 2D only; on 1D and wood each returns a warnings entry instead. They have been accepted since 2026-10-05.
The public demo key is documented: it runs small synchronous POST /v1/optimize/2d, /1d and /wood jobs without an account, every answer carries X-CutOptim-Demo: true, and on a demo 200 the X-RateLimit-* headers count the per-address daily demo allowance.
Error.details declares the demo refusal keys: testKeyUrl, limit, maxParts, maxStockRows, and the reason values demo_scope and demo_rate_limit.
Fixed
The 413 too_large description names the include: ["pdf"] cap: a 2D request with pdf may carry at most 500 parts (total quantity).
GET /v1/usageenv and keyClass have real descriptions: env: "test" marks a test key (never billed, its own monthly allowance); keyClass marks a key CutOptim issues to its own configurator service.
The authentication section says that POST /v1/validate/* needs no key, like GET /v1/health.
Every Label2D field has a description (w/h are the entered size, x/y the placed position, edges only the banded sides).
Idempotency-Key on the optimize endpoints
Added
Optional Idempotency-Key request header on POST /v1/optimize/2d, /1d, /wood and /nest (8–128 characters of A-Z a-z 0-9 _ -). Within 24 hours, a retry with the same key and the same request (method, path, body bytes) returns the stored answer — same status, same body — with the response header Idempotency-Replayed: true, and is not charged again. For engine: "max" the retry returns the same jobId instead of queuing a second job.
Two error codes, both 409 and both sent only to a request that carried the header: idempotency_conflict (the key was already used for a different request) and idempotency_in_progress (the first request with this key is still running; Retry-After says when to retry).
Only a delivered 2xx is stored: after an error or a refusal, a retry with the same key runs again. Keys are scoped to the account and to the key type, so test and live keys never collide. Requests without the header are unchanged.
Reference accuracy - error details, HEAD /v1/health, part meta
Fixed
Error.details now documents every key the server sends: maxBytes (body too large), available (unknown engine), unsupported (an option the max engine cannot model), and reason / itemId (a nest part outline that intersects itself).
HEAD /v1/health is documented: it answers like GET without a body.
meta on 1D and wood parts follows the request row it was sent on, as it already did on 2D; the description no longer says meta is keyed by part name.
Matched sets (grain matching) on 2D
Added
matchedSets on POST /v1/optimize/2d: parts named in a set (by parts[].id) are cut side by side from one piece of board, one kerf apart. Each placed member carries matchedSet (set and position), and the response adds a matchedSets[] summary.
parts[].id: an optional identifier a matched set refers to. Without a non-empty matchedSets array, a request is answered byte for byte as before.
Deprecated
parts[].grainGroup: still accepted and still honoured, now marked deprecated. Use matchedSets instead.
Labels, plan fingerprint and inline PDFs on 2D plans
Added
include: ["labels"] on POST /v1/optimize/2d: per-piece label data (labels[]).
include: ["planHash"]: one canonical fingerprint of the plan (64 hex characters): the same plan always gives the same hash.
include: ["pdf"] and include: ["labelsPdf"]: the cutting-plan PDF and the label PDF, base64-encoded, drawn by the same code as the CutOptim app.
Grain direction on 2D parts and stock
Added
parts[].grain and stock[].grain (w or h) on POST /v1/optimize/2d: a grained part is placed with its grain along the sheet’s grain. A part or stock row without grain behaves as before.
Nesting - mixed sheet sizes, material cost and used area
Added
Nesting accepts several stock sheet sizes in one request; sheets[].stock is the index of the stock[] row each sheet was cut from, and stockUsage[] totals the sheets, price and density per stock row.
options.edgeClearance: a margin kept free along every sheet edge.
sheets[].cutLength and sheets[].pierces (and job-wide metrics.cutLength / metrics.pierces): the contour length the cutting head follows and the number of pierce points.
sheets[].usedWidth, sheets[].usedHeight, sheets[].usedArea: the part of the sheet actually used, for pricing by partial sheet. The last sheet is compacted.
options.compactFor (box · horizontal · vertical, default box): which used measure the compaction minimises.
Fixed
parts[].priority (must-cut) is honoured: with options.respectStock, these parts are placed first.
Asynchronous max engine for true-shape nesting
Added
POST /v1/optimize/nest accepts engine: "max": a longer search that is never worse than lbf. It answers 202 with a jobId; poll GET /v1/jobs/{id} until status is succeeded (the result is an OptimizeNestResponse) or failed.
Where the tier is not enabled, engine: "max" is refused with 503 service_unavailable before any quota is spent.