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/analysisThe request is the file
Section titled “The request is the file”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.
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. |
Formats
Section titled “Formats”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=mmThe 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.
The response
Section titled “The response”{ "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:
# 1. analysecurl -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 acrossjq '{ 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.jsonrole 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. |
# drop the fittingsjq '[ .parts[] | select( .role != "fixture" ) ]' analysis.jsonAdvice
Section titled “Advice”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. |
Limits and cost
Section titled “Limits and cost”| 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.
Errors
Section titled “Errors”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. |
Related
Section titled “Related”- 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.