Every API here is available over the APIs.io API and to AI agents over MCP.
{
"openapi": "3.0.3",
"info": {
"termsOfService": "https://smartcut.dev/terms",
"license": {
"name": "MIT (specification document only — API use governed by the Terms)",
"url": "https://github.com/jgmedialtd/smartcut-api/blob/main/LICENSE"
},
"x-logo": {
"url": "https://smartcut.dev/images/logo.svg",
"altText": "SmartCut"
},
"title": "SmartCut API V3",
"version": "3.1.0",
"description": "Cutting optimisation for sheet, linear and roll stock.\n\nSend a list of parts and the stock you have; get back an optimised cutting layout. Supports guillotine cutting for panel saws and true-shape nesting for CNC, honouring grain direction, edge banding, blade kerf and per-edge trim. Results export to PDF, CSV, DXF, SVG and saw formats including PTX, Biesse XML and Mayer.\n\nCalculations are asynchronous: submit to `/v3/calculate`, then poll `/v3/result/ready` and collect from `/v3/result`. Authentication is your API key in the `Authorization` header, with **no `Bearer` prefix**.\n\n**Beta surfaces:**\n- `parts[].labels` + top-level `labels.customer` — typed saw label-printer fields. Emitted by PTX (named PARTS_INF columns) and Mayer .MAY (part-level fields → MA010 text slots); Biesse XML to follow. `labels.customer` is PTX-only. Schema may change; see `/v3/export/ptx` and `/v3/export/mayer`.",
"contact": {
"name": "SmartCut",
"url": "https://api.smartcut.dev"
}
},
"externalDocs": {
"description": "Specification, runnable examples and MCP configuration",
"url": "https://github.com/jgmedialtd/smartcut-api"
},
"paths": {
"/v3/calculate": {
"post": {
"tags": [
"Calculations"
],
"summary": "Submit calculation",
"description": "Submit a calculation request using V3 format.\n\nThe response contains a job ID. Results are delivered to your webhook when the job completes — the recommended path, see [Result webhooks](https://smartcut.dev/docs/webhooks). Polling `/v3/result/ready` is the fallback.",
"requestBody": {
"required": true,
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/CalculateRequest"
},
"examples": {
"guillotine": {
"summary": "Guillotine (sheet cutting)",
"description": "Two-dimensional sheet cutting with nested rectangular cuts",
"value": {
"saw": {
"cutType": "guillotine",
"cutPreference": "l",
"bladeWidth": 3,
"stockType": "sheet"
},
"stock": [
{
"l": 2400,
"w": 1200,
"t": 18,
"material": "Plywood",
"trim": {
"l1": 0,
"l2": 0,
"w1": 0,
"w2": 0
},
"grain": "l",
"autoAdd": false
}
],
"parts": [
{
"l": 400,
"w": 300,
"t": 18,
"material": "Plywood",
"q": 5,
"extras": {
"banding": {
"sides": {
"l1": "Oak",
"l2": true
}
},
"finish": {
"faces": {
"a": "Gloss"
}
}
}
}
]
}
},
"efficiency": {
"summary": "Efficiency (sheet optimization)",
"description": "Optimized sheet cutting with automatic stock selection",
"value": {
"saw": {
"cutType": "efficiency",
"cutPreference": "l",
"bladeWidth": 3,
"stockType": "sheet"
},
"stock": [
{
"l": 2440,
"w": 1220,
"t": 18,
"material": "MDF",
"q": 10,
"autoAdd": true,
"cost": 45.5
}
],
"parts": [
{
"l": 800,
"w": 600,
"t": 18,
"material": "MDF",
"q": 12,
"grain": "l",
"orientationLock": ""
},
{
"l": 400,
"w": 300,
"t": 18,
"material": "MDF",
"q": 8,
"grain": "",
"extras": {
"banding": {
"sides": {
"l1": "White",
"l2": "White",
"w1": "White",
"w2": "White"
}
}
}
}
]
}
},
"linear": {
"summary": "Linear (bar cutting)",
"description": "One-dimensional cutting for bars, tube, or trim. Linear stock is cut to length, so `cutType` and `cutPreference` are omitted — the optimiser selects the linear calculation from `stockType` alone.",
"value": {
"saw": {
"bladeWidth": 3,
"stockType": "linear"
},
"stock": [
{
"l": 3000,
"w": 100,
"t": 50,
"material": "Pine",
"q": 5,
"autoAdd": true,
"cost": 12.5
}
],
"parts": [
{
"l": 1200,
"w": 100,
"t": 50,
"material": "Pine",
"q": 3
},
{
"l": 800,
"w": 100,
"t": 50,
"material": "Pine",
"q": 5
},
{
"l": 450,
"w": 100,
"t": 50,
"material": "Pine",
"q": 10
}
]
}
},
"withLabels": {
"summary": "Beam saw with label-printer fields (BETA)",
"description": "Same as guillotine, but populates the beta `labels` surface so the PTX export drives the saw's on-board label printer / barcode machine. Unsupported fields for the chosen saw return as `labelWarnings`; the job still runs.",
"value": {
"saw": {
"cutType": "beam",
"cutPreference": "l",
"bladeWidth": 4.8,
"stockType": "sheet",
"vendor": "Homag",
"model": "homag-ptx"
},
"stock": [
{
"l": 2440,
"w": 1220,
"t": 18,
"material": "MDF",
"q": 10,
"autoAdd": true
}
],
"parts": [
{
"name": "Side Panel",
"l": 600,
"w": 400,
"t": 18,
"material": "MDF",
"q": 5,
"code": "ART-1234",
"labels": {
"barcode1": "CNC-SIDE-A",
"barcode2": "CNC-SIDE-B",
"room": "Kitchen",
"order": "PO-2024-042",
"pallet": "PAL-7",
"colour": "Walnut"
}
}
],
"labels": {
"customer": "Acme Joinery"
}
}
}
}
}
}
},
"responses": {
"202": {
"description": "Calculation accepted",
"content": {
"application/json": {
"schema": {
"type": "object",
"properties": {
"jobId": {
"type": "number",
"description": "Job identifier. Identifies the job in the webhook delivery (the recommended way to receive results), or when polling `/v3/result/ready`."
},
"warnings": {
"type": "array",
"items": {
"type": "string"
},
"description": "Saw settings the saw-rules registry resolved differently from what was sent — the job runs; the caller is told. Each entry names the rule that fired, e.g. `[strategy.time-ignored-for-beam] …`; on a mixed job it is prefixed `Saw '<id>': `. Present only when at least one rule rewrote something. Every rule is listed at https://smartcut.dev/api-docs/saw-settings",
"example": [
"[strategy.time-ignored-for-beam] guillotineOptions.strategy 'time' is the fence-consistency knob for a manual table saw; a beam saw positions the fence per cut, so it is ignored — set to 'efficiency'."
]
},
"labelWarnings": {
"type": "array",
"description": "**BETA** — Non-fatal warnings: label fields the request set that the chosen saw's post-processor format(s) will not surface. The job still runs; the unsupported fields are dropped at emit time. Shape may change before the `labels` surface stabilises.",
"items": {
"type": "object",
"properties": {
"category": {
"type": "string",
"enum": [
"label"
]
},
"field": {
"type": "string",
"description": "Name of the label field that was unsupported."
},
"partIndex": {
"type": "integer",
"nullable": true,
"description": "Index in parts[] for part-level fields; null for job-level (top-level labels.customer)."
},
"partId": {
"type": "string",
"description": "Part id when available."
},
"supportedFormats": {
"type": "array",
"items": {
"type": "string"
},
"description": "Formats that DO surface this field. Migrate the saw choice if a value is required."
},
"sawFormats": {
"type": "array",
"items": {
"type": "string"
},
"description": "Formats the chosen saw actually supports."
},
"message": {
"type": "string"
}
}
}
}
},
"required": [
"jobId"
]
}
}
}
},
"400": {
"description": "Invalid V3 format - see error details for migration help",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ErrorResponse"
}
}
}
},
"401": {
"description": "Authentication required"
},
"403": {
"description": "Not authorized"
},
"429": {
"description": "Rate limit exceeded, or too many concurrent jobs — wait before retrying"
},
"500": {
"description": "Server error"
},
"503": {
"description": "Service unavailable"
}
}
}
},
"/v3/validate": {
"post": {
"tags": [
"Calculations"
],
"summary": "Validate inputs without calculating",
"description": "Run the same input validation and stock-matching a calculation would, but return the\nissues immediately — no job is created, nothing is persisted, no usage is counted.\nUse it for instant pre-submit feedback (e.g. \"will not fit on any relevant stock\",\n\"no matching stock material found\", or being over a plan limit).\n\nThe request body is identical to `/v3/calculate`.",
"requestBody": {
"required": true,
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/CalculateRequest"
}
}
}
},
"responses": {
"200": {
"description": "Validation result. `valid` is true only when there are no issues.",
"content": {
"application/json": {
"schema": {
"type": "object",
"properties": {
"valid": {
"type": "boolean",
"description": "True when the inputs produced no issues."
},
"stage": {
"type": "string",
"enum": [
"input",
"match"
],
"description": "`input` = failed the basic validation pipeline (missing dimensions, plan limits); `match` = structurally valid, the issues are stock-fit reasons from the matcher."
},
"issues": {
"type": "array",
"description": "Validation issues. At stage `match`, each issue points at the offending part via `index`/`field`.",
"items": {
"type": "object",
"properties": {
"message": {
"type": "string",
"description": "The reason — an i18n key (e.g. `errors.validation.will_not_fit`) or a message."
},
"type": {
"type": "string",
"enum": [
"error",
"warning"
]
},
"category": {
"type": "array",
"items": {
"type": "string"
}
},
"field": {
"type": "array",
"items": {
"type": "array",
"items": {
"type": "string"
}
},
"description": "Field paths the issue points at, e.g. [[\"material\"],[\"t\"]]."
},
"index": {
"type": "array",
"items": {
"type": "integer"
},
"description": "0-based indices of the affected parts."
},
"identifier": {
"type": "string",
"description": "Human-readable source (part name or 1-based row)."
}
}
}
},
"warnings": {
"type": "array",
"description": "Saw settings the saw-rules registry resolved differently from what was sent — the job runs; the caller is told. Each `message` names the rule that fired, e.g. `[min-spacing.efficiency-only] …`; on a mixed job it is prefixed `Saw '<id>': `. Present only when at least one rule rewrote something, and never affects `valid`. Every rule is listed at https://smartcut.dev/api-docs/saw-settings",
"items": {
"type": "object",
"properties": {
"message": {
"type": "string",
"description": "What was resolved and why, prefixed with the rule id in square brackets."
},
"type": {
"type": "string",
"enum": [
"warning"
]
}
},
"required": [
"message",
"type"
]
}
}
},
"required": [
"valid",
"issues"
]
}
}
}
},
"400": {
"description": "Invalid V3 format - see error details for migration help",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ErrorResponse"
}
}
}
},
"429": {
"description": "Rate limit exceeded - wait before retry"
},
"500": {
"description": "Server error"
}
}
}
},
"/v3/model/analysis": {
"post": {
"tags": [
"Calculations"
],
"summary": "Analyse a 3D model into parts",
"description": "Upload a 3D model and get back a parts list. The `parts` array is `/v3/calculate`'s\n`parts` array — same field names, same units — so you can submit it verbatim.\n\n**The body is the file itself**, not a multipart form: POST the raw bytes with\n`Content-Type: application/octet-stream`. Everything else is a query parameter.\n\n**Formats.** `step` (best — exact solids and a real declared unit), `stl`, `obj`,\n`ply` and `gltf`/`glb` are read server-side. Collada (`dae`) and `3mf` are **not**:\nboth need a browser DOM to parse, so they return `415` with advice to convert. Format\nis detected from the file's magic bytes; OBJ and ASCII STL carry none, so send\n`filename` or `format` for those or the request is refused rather than guessed at.\n\n**Units.** Only STEP, 3MF and Collada declare a unit. For everything else, say what\nthe model was drawn in with `sourceUnits`, or the numbers are taken to be already in\n`units`. Getting this wrong is silent — every part stays consistent with every other\n— so it is worth being explicit.\n\n**What is in `advice`.** Everything the analysis knows that a calculation does not:\nwhether a part is a rectangle or a shape, what squaring it off would waste, why\nsomething was judged a fitting rather than sheet goods, which two parts share space\nin the assembly. Entries point back at `parts` by index.\n\n**Cost.** Billed on the same compute-time meter as a calculation, and counted against\nyour monthly call allowance. A refused format or an over-size model costs nothing.\nMaximum upload 32MB; models above 2,000,000 triangles are refused — decimate first.",
"parameters": [
{
"name": "filename",
"in": "query",
"required": false,
"schema": {
"type": "string"
},
"description": "The file's name, e.g. `kitchen.step`. Used as a tiebreak when the bytes carry no magic — required for OBJ and ASCII STL unless `format` is sent."
},
{
"name": "format",
"in": "query",
"required": false,
"schema": {
"type": "string",
"enum": [
"step",
"stl",
"obj",
"ply",
"gltf",
"glb"
]
},
"description": "Force the format instead of inferring it from `filename`. Magic bytes still win where the file has them."
},
{
"name": "units",
"in": "query",
"required": false,
"schema": {
"type": "string",
"enum": [
"mm",
"cm",
"m",
"in",
"ft"
],
"default": "mm"
},
"description": "The unit every number in the response is in."
},
{
"name": "sourceUnits",
"in": "query",
"required": false,
"schema": {
"type": "string",
"enum": [
"mm",
"cm",
"m",
"in",
"ft"
]
},
"description": "The unit the model was drawn in. Overrides whatever the file declares. Defaults to the file's own declaration, then to `units`."
},
{
"name": "dedupe",
"in": "query",
"required": false,
"schema": {
"type": "boolean",
"default": true
},
"description": "Collapse identical parts into one row with a quantity — what a cut list wants."
},
{
"name": "mirrorDedupe",
"in": "query",
"required": false,
"schema": {
"type": "boolean",
"default": false
},
"description": "Treat a part and its mirror image as the same part. Off by default: a mirrored panel is genuinely different once the sheet has a face, a grain direction or machining on one side."
},
{
"name": "bladeWidth",
"in": "query",
"required": false,
"schema": {
"type": "number"
},
"description": "Your saw's kerf, in `units`. Supply it and the response can tell you when the model is too small to be measured in the unit you chose (`model.suggestedUnits`). Without it no unit suggestion is made."
},
{
"name": "holes",
"in": "query",
"required": false,
"schema": {
"type": "string",
"enum": [
"drill",
"cut"
],
"default": "drill"
},
"description": "Which form a ROUND hole comes back in. `drill` (default) returns it as a `machining.holes` operation — a drilled shelf stays a plain rectangle. `cut` returns it as a polygon ring in `holes`, which is the only form a nesting model reads. Holes that are not round are shape either way; there is no drilled equivalent of a slot."
}
],
"requestBody": {
"required": true,
"description": "The model file, as raw bytes.",
"content": {
"application/octet-stream": {
"schema": {
"type": "string",
"format": "binary"
}
}
}
},
"responses": {
"200": {
"description": "The model was analysed. `parts` is submittable to `/v3/calculate` as-is.",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ModelAnalysisResponse"
}
}
}
},
"400": {
"description": "The body is not a file, the format could not be determined, or the units are not ones we convert.",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ErrorResponse"
}
}
}
},
"401": {
"description": "Authentication required"
},
"403": {
"description": "Over the monthly call limit, or the account is on hold"
},
"413": {
"description": "Body larger than 32MB"
},
"415": {
"description": "A format this server cannot read (Collada and 3MF need a browser). `details.supported` lists the formats that do work.",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ErrorResponse"
}
}
}
},
"422": {
"description": "Too many triangles, or the analysis did not finish inside its budget. Decimate the mesh and retry.",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ErrorResponse"
}
}
}
},
"429": {
"description": "Rate limit exceeded - wait before retry"
},
"500": {
"description": "Server error"
}
}
}
},
"/v3/result/ready": {
"get": {
"tags": [
"Results"
],
"summary": "Check if calculation is complete",
"description": "**Webhooks are the recommended way to receive results** — SmartCut posts the finished result to your URL as soon as the job completes, so there is nothing to poll and no delay. See [Result webhooks](https://smartcut.dev/docs/webhooks) to set one up.\n\nUse this endpoint as a fallback when your client cannot receive inbound HTTP. Poll every 2-3 seconds, stop on `200` (then fetch `/v3/result`) or on `404`/`410`, and back off on `429`.",
"parameters": [
{
"name": "id",
"in": "query",
"required": true,
"schema": {
"type": "string"
},
"description": "Job identifier from /v3/calculate"
}
],
"responses": {
"200": {
"description": "Result is ready - fetch via /v3/result"
},
"404": {
"description": "Job not found or not ready yet"
},
"410": {
"description": "Job expired or failed"
},
"429": {
"description": "Rate limit exceeded - wait before retry"
}
}
}
},
"/v3/result": {
"get": {
"tags": [
"Results"
],
"summary": "Retrieve calculation result",
"description": "Fetch the complete optimization result in V3 format.\n\nIf you use a webhook (recommended), this exact payload is delivered to your URL and you do not need to call this endpoint at all.\n\n**V3 Response Format:**\n- All fields use l1/l2/w1/w2 coordinates\n- Extras in structured format (extras.banding.sides, etc.)\n- Complete metadata with all analysis fields\n- Stock, parts, cuts, segments, offcuts all included",
"parameters": [
{
"name": "id",
"in": "query",
"required": true,
"schema": {
"type": "string"
},
"description": "Job identifier"
}
],
"responses": {
"200": {
"description": "Result retrieved successfully",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ResultResponse"
}
}
}
},
"404": {
"description": "Result not found"
},
"410": {
"description": "Result expired"
},
"500": {
"description": "Server error"
}
}
}
},
"/v3/export/pdf": {
"get": {
"tags": [
"Exports"
],
"summary": "Export result as PDF",
"description": "Generate a PDF file with optimization layouts, cut lists, and part details. V3 format always uses CSV version 2.",
"parameters": [
{
"name": "id",
"in": "query",
"required": true,
"schema": {
"type": "string"
},
"description": "Job ID to export"
},
{
"name": "option",
"in": "query",
"required": false,
"schema": {
"type": "string",
"enum": [
"layout",
"parts",
"machining"
]
},
"description": "Export variant: layout (default, full optimization), parts (parts list only), machining (machining details)"
},
{
"name": "numberFormat",
"in": "query",
"required": false,
"schema": {
"type": "string",
"enum": [
"decimal",
"fraction"
]
},
"description": "Number format (also accepts unitType or units as aliases). Default: decimal"
},
{
"name": "credit",
"in": "query",
"required": false,
"schema": {
"type": "string"
},
"description": "Custom credit text (requires whiteLabel plan option)"
},
{
"name": "cropLength",
"in": "query",
"required": false,
"schema": {
"type": "string",
"enum": [
"y",
"n"
]
},
"description": "Crop stock length to fit parts. Default: n"
},
{
"name": "tables",
"in": "query",
"required": false,
"schema": {
"type": "string",
"enum": [
"y",
"n"
]
},
"description": "Show tables in output. Default: y"
},
{
"name": "showDiagram",
"in": "query",
"required": false,
"schema": {
"type": "string",
"enum": [
# --- truncated at 32 KB (167 KB total) ---
# Full source: https://artifacts.apis.io/smartcut-dev/openapi/_original/smartcut-dev-openapi-openapi.json