Skip to content

3D model analysis

Upload a 3D model. Get back a parts list.

POST /v3/model/analysis reads a CAD file, works out which solids in it are panels, measures them, and returns them in exactly the shape /v3/calculate wants. The parts array it gives you is that endpoint’s parts array. No field mapping, no conversion.

POST https://api.smartcut.dev/v3/model/analysis

The body is the model file itself, as raw bytes. Not a multipart form, not base64 inside JSON. There is exactly one file and nothing else to send, so everything else is a query parameter.

Terminal window
curl -X POST \
"https://api.smartcut.dev/v3/model/analysis?filename=kitchen.step&units=mm" \
-H "Authorization: YOUR_API_KEY" \
-H "Content-Type: application/octet-stream" \
--data-binary @kitchen.step
Parameter Default What it does
filename none The file’s name. Used to work out the format when the bytes carry no magic.
format none Force the format instead of inferring it. Magic bytes still win where the file has them.
units mm The unit every number in the response is in. One of mm, cm, m, in, ft.
sourceUnits none The unit the model was drawn in. Overrides whatever the file declares.
dedupe true Collapse identical parts into one row with a quantity, which is what a cut list wants.
mirrorDedupe false Treat a part and its mirror image as the same part.
bladeWidth none Your saw’s kerf, in units. Enables the units sanity check below.

The format is decided by the file’s magic bytes, with the extension as a tiebreak. People rename files, and a .stl that is really a zip should fail with something better than a parser crash. OBJ and ASCII STL carry no magic at all, so for those you must send filename or format. Without one the request is refused rather than guessed at.

Format Extensions Supported Notes
STEP .step, .stp Yes The best input. Exact B-rep solids, tessellated server-side, and the file declares its own unit, so sourceUnits is not needed and cannot be got wrong.
STL .stl Yes Binary and ASCII. Carries no part names and no materials, so parts are separated by geometry alone.
OBJ .obj Yes Keeps object names and usemtl material names.
PLY .ply Yes Geometry only.
glTF / GLB .gltf, .glb Yes Embedded-buffer GLB works. External .bin or texture files are not fetched, so geometry only.
Collada .dae No, 415 Needs a browser DOM to parse. Convert to STEP or STL.
3MF .3mf No, 415 Same reason. Convert to STEP or STL.

A 415 is not a dead end. It names what would have worked:

{
"error": "The DAE format cannot be read here. Supported formats: obj, stl, ply, gltf, step.",
"details": {
"format": "dae",
"supported": [ "obj", "stl", "ply", "gltf", "step" ],
"help": "Re-export the model as STEP (best — exact solids and real units) or STL. Collada and 3MF can only be read in the browser."
},
"version": "3"
}

Only STEP declares a unit the server can act on. Every mesh format (STL, OBJ, PLY, glTF) carries nothing, so if you do not say what the model was drawn in, the numbers are taken to be already in units.

Getting this wrong is silent. Every part stays consistent with every other, so a cabinet exported from a tool set to metres and read as millimetres imports as a set of perfectly self-consistent, thousand-times-too-small parts. It is worth being explicit:

?filename=cabinet.obj&sourceUnits=m&units=mm

The response always tells you what it did:

"model": {
"format": "obj",
"units": "mm",
"sourceUnits": "m",
"declaredUnits": null
}

Send bladeWidth and the server can also catch the mistake for you. A kerf is a fraction of a percent of anything worth cutting, so a blade that is an appreciable slice of the whole model means the model is not small. The unit is wrong. When that happens, model.suggestedUnits names the sourceUnits that would make sense of it. Without a bladeWidth there is nothing to measure against and no suggestion is made.

{
"version": "3",
"model": {
"format": "step",
"units": "mm",
"sourceUnits": "mm",
"declaredUnits": "mm",
"stats": {
"parts": 3, "rectangular": 2, "irregular": 1, "unrecognised": 0,
"fixtures": 0, "merged": 2, "triangles": 4820,
"components": 5, "debris": 0,
"modelDiagonal": 1523.4,
"modelSize": { "x": 1200, "y": 600, "z": 720 }
}
},
"parts": [
{ "name": "SIDE", "l": 720, "w": 560, "t": 18, "q": 2, "material": "MDF", "role": "rect" },
{ "name": "SHELF", "l": 564, "w": 540, "t": 18, "q": 1, "material": "MDF", "role": "rect" },
{ "name": "CURVED TOP", "l": 600, "w": 300, "t": 18, "q": 1, "material": "OAK", "role": "nesting",
"outline": [ { "x": 0, "y": 0 }, { "x": 600, "y": 0 } ] }
],
"advice": [
{ "code": "squared-off-waste", "severity": "info", "partIndex": 2, "wastePct": 0.21,
"message": "Squaring \"CURVED TOP\" off to a rectangle wastes 21.0% of its area. Send its outline to a nesting model to keep it." }
],
"warnings": []
}

Every field on a part except role is a /v3/calculate part field. role is not, and /v3/calculate ignores fields it does not recognise rather than rejecting them, so you can post the array back exactly as it arrived. You do not have to strip anything:

Terminal window
# 1. analyse
curl -sS -X POST \
"https://api.smartcut.dev/v3/model/analysis?filename=kitchen.step" \
-H "Authorization: YOUR_API_KEY" \
-H "Content-Type: application/octet-stream" \
--data-binary @kitchen.step > analysis.json
# 2. cut it. The parts array goes straight across
jq '{
saw: { cutType: "guillotine", cutPreference: "l", bladeWidth: 3.2, stockType: "sheet" },
stock: [ { l: 2440, w: 1220, t: 18, material: "MDF", q: 20 } ],
parts: .parts
}' analysis.json > calculate.json
curl -X POST https://api.smartcut.dev/v3/calculate \
-H "Authorization: YOUR_API_KEY" \
-H "Content-Type: application/json" \
--data @calculate.json

role says what each part is for, and it is the one thing worth acting on before you cut:

role Meaning What to do
rect A plain rectangle. Nothing. Cut it.
nesting A shaped part. outline (and holes) carry its real geometry. Submit it to a nesting model to keep the shape, or let a guillotine model square it off to l x w and accept the waste. advice tells you how much that costs.
fixture A fitting such as a handle, a hinge or a knob, never cut from sheet at all. Filter these out before cutting. A fixture-detected entry in advice names each one.
Terminal window
# drop the fittings
jq '[ .parts[] | select( .role != "fixture" ) ]' analysis.json

advice is everything the analysis knows that a calculation does not, as data rather than prose. Each entry has a stable code, a severity of info, warning or error, a human-readable message, and, where it is about a specific part, a partIndex into parts.

Field When
partIndex The advice is about one part. Absent when it is about the file as a whole.
otherPartIndex The advice is about a pair, currently only parts-overlap.
penetrationFraction On parts-overlap: how deeply, as a fraction of the smaller part’s smallest dimension.
wastePct On squared-off-waste: the fraction of the bounding rectangle that is waste, 0..1.
confidence On fixture-detected / role-uncertain: how sure the verdict was, 0..1.

Codes worth handling:

Code Severity Means
fixture-detected info This is a fitting, not sheet goods. Drop it.
role-uncertain info The fitting question was close. Worth a human look.
squared-off-waste info Squaring this shaped part off costs wastePct of its area.
dimensions-approximate warning The dimensions came from a fallback, not from measured flat faces. Check them.
parts-overlap warning Two parts share space. Often deliberate (a housing, a rebate, a biscuit), so this is never a blocker.
not-flat-panel warning Turned, curved, tapered or multi-body. The bounding box is the best available answer.
open-mesh / non-manifold-edge warning The mesh is not a closed solid. Dimensions may be unreliable.
model-too-heavy warning Very dense. It worked, but a coarser export would be faster.
scanned-mesh-file error This is a scan or a conversion, not a CAD model of parts.
Maximum upload 32MB. Larger bodies get 413 without being buffered.
Maximum complexity 2,000,000 triangles. Above that, 422. Export with a coarser mesh setting, or decimate.
Time budget 30 seconds. A model that does not finish returns 422 with the same advice.
Rate limit The same per-minute budget as /v3/calculate.
Billing The same compute-time meter as a calculation, and it counts against your monthly call allowance.

A refused request is not billed and does not spend a call: an unsupported format, an over-size model, a timeout.

Every error is { "error": "…", "details": { … }, "version": "3" }.

Status Means
400 The body is not a file, the format could not be determined, or units/sourceUnits are not ones we convert.
401 Missing or unknown API key.
403 Over the monthly call limit, or the account is on hold.
413 Body larger than 32MB.
415 A format this server cannot read. details.supported lists the ones that work.
422 Too many triangles, or the analysis did not finish in its budget.
429 Rate limited. Wait and retry.
  • API reference: the generated schema for this endpoint and every other.
  • Guide: getting started with the optimisation API.
  • Result webhooks: how to receive the calculation you submit next.