Printy3D

Recipes

Exact Check

Exact Check measures a STEP part or assembly as it was designed and changes nothing: the thinnest walls, the narrow spaces inside each part, and how the parts of an assembly sit against each other. You then ask, free of charge, whether it will print with a given process and material, at a given size.

As uploaded
Measured
What a check measures. The top cover (right half) of Framework's 3D-printable mainboard case, checked as uploaded. Left, the file. Right, what the check found, with the part drawn see-through: its thinnest wall, 1.0 mm (blue), and the narrowest gap inside it, 0.29 mm (orange). The check reads the file and changes nothing in it. Model: Top cover, right half, of the 3D-printable mainboard case, Framework Laptop 13 by Framework Computer (github.com/FrameworkComputer/Framework-Laptop-13), CC BY 4.0, checked with Printy3D

How it works#

  1. Upload a .step or .stp file, or a ZIP of an assembly's STEP files. Uploads
  2. Run one check: a job with recipe: "cad_check". It measures the model once.
  3. Ask for an evaluation as often as you like: the stored measurements judged against one process and material, or all of them, at the scale and assembly intent you choose. Each answer is free.

What it accepts#

  • A .step or .stp file, or a ZIP holding an assembly's STEP files. Any other format is refused with 422 validation_failed and a sentence that says why.
  • A ZIP may unpack to at most 2,000 entries and 850 MiB, at a ratio of at most 200:1. Operating-system clutter such as __MACOSX/ and .DS_Store is skipped.
  • In a ZIP, the files no other file references are opened, each with the files it references. Files that only reference each other are opened from the first of them by name. Every file is measured once. A ZIP with so many top-level files that one check cannot be given them all fails with invalid_input and a sentence saying so.
  • Your account's upload limits apply as for any file. Rate limits & limits

Running a check#

POST/v1/jobsjobs:write
input_file_idstringrequiredpattern ^file_[0-9A-HJKMNP-TV-Z]{26}$

A ready STEP file or ZIP of yours.

recipestring

cad_check. There is no tool.

params.max_creditsintegerat least 10

Optional. The most this check may cost. It caps the credits held when the job is admitted and the credits charged when it finishes. One credit is one second, so it is also the checker's time limit when it is under 290: at 15 the checker gets 15 s. A low value can stop a large file before it is measured (time_limit, not charged). No other key is accepted in params.

notify.webhook_endpointstringpattern ^whe_[0-9A-HJKMNP-TV-Z]{26}$

Optional. One of your webhook endpoints, to be told when the job ends instead of polling. Only cad_check jobs take notify.

curl -sS "$API/v1/jobs" -H "Authorization: Bearer $PRINTY3D_API_KEY" \
  -H "Content-Type: application/json" -H "Idempotency-Key: $(uuidgen)" \
  -d "{\"input_file_id\": \"$FILE_ID\", \"recipe\": \"cad_check\", \"params\": {\"max_credits\": 120}}"

Then poll the job until it is succeeded, failed or cancelled, exactly as for any other job (Jobs & polling), or let a webhook tell you.

Where it runs, and how long it may take#

  • Checks run on their own CPU workers, not on the GPU fleet, so GPU capacity never refuses them, and queue.capacity is null.
  • When no Exact Check worker is running (none has asked for work or held a check in the last 2 minutes), a live check is refused with 503 capacity_exhausted, reason: "cad_unavailable" and Retry-After: 300. Nothing is queued or held. A test key is never refused this way.
  • A queued live check fails free with error_code: capacity_exhausted once it has waited 10 minutes with no worker running, or after 24 hours in the queue.
  • The checker gets 290 s, or max_credits seconds when that is less, and stops itself there; one still running 10 s later is stopped. The check then fails with error_code: time_limit and is not charged.
  • A check that dies because the file needs more memory than a check is allowed fails with error_code: memory_limit and is not charged either.
  • A file the checker cannot read fails with invalid_input and costs the 10-credit minimum (When a job fails).
  • A check the checker stops on a fault of its own, not the file's, fails with invalid_result and is not charged.
  • Only a file the checker refuses is charged. Any other failure (the checker crashing or stopped, engine_failed) is ours and is not charged either.

What it costs#

  • A live check costs 1 credit per second of run time, with a minimum of 10 credits, like every live job (Credits & billing).
  • When the job is admitted it holds its expected run time: 120 credits by default, more for a larger file, or the measured run time of similar checks once enough have run; never more than max_credits when you set it. The charge never goes above max_credits either.
  • time_limit and memory_limit failures cost nothing; a file the checker cannot read costs the 10-credit minimum.
  • Evaluations are free: they start no job and touch no balance.
  • A check made with a test key is free and simulated (see Test keys).

The result#

When the job has succeeded, job.result holds a count summary. The findings come from the evaluation, not from the job.

partsintegerrequired

Distinct parts: one part can be placed several times in an assembly. A part placed as a mirror image, or scaled, is measured as a part of its own, named "… (mirror image)" or "… (scaled)".

placed_copiesintegerrequired

Parts as placed in the assembly, counting every copy.

pairsintegerrequired

Pairs of placed parts that touch, overlap or lie close to each other.

open_partsintegerrequired

Parts that are not closed solids.

sheet_partsintegerrequired

Parts that are surfaces with no thickness.

accuracy_mmnumberrequired

The tolerance of the measurements, which are taken on a tessellation of the model. A reading is judged under a limit only when it is under it by more than this. An evaluation with a scale scales this tolerance with the model.

sample_limit_per_shapeintegerrequired

The per-shape limit of wall samples in force for this check: 3,000,000 normally, lower for a very large file. A small shape may need fewer samples than the limit.

truncatedbooleanrequired

True when the checker capped the number of pairs or regions it reported.

unsupported_placementsinteger | null

Placements in the file the checker could not read, so the parts they place were not measured. Normally 0; anything else adds a PLACEMENTS_UNREAD warning to every evaluation. null for a file checked before 10 November 2026, whose checker did not count them.

The whole arm
One part, six copies
Parts and placed copies. An open-source robot arm: 85 distinct parts, placed 259 times. A part placed several times is measured once and counted with its quantity. Picked out in amber: one part, SG-ZIJI_15, placed 6 times. Model: SO-101 arm assembly, SO-ARM100 by TheRobotStudio (github.com/TheRobotStudio/SO-ARM100), Apache-2.0, checked with Printy3D
job.result
{
  "schema_version": 1,
  "parts": 36,
  "placed_copies": 36,
  "pairs": 132,
  "open_parts": 1,
  "sheet_parts": 0,
  "accuracy_mm": 0.02,
  "sample_limit_per_shape": 3000000,
  "truncated": false,
  "unsupported_placements": 0
}
The demo part
The open top
A part that is not closed. A demo part Printy3D made to show it: a box whose top face was left out of the file. Its surface has a hole along the four top edges, so it does not enclose a volume and its walls have no thickness. The check reports it as open, and the evaluation gives NOT_CLOSED, which blocks printing on every process. Model: Open demo part, made by Printy3D

How walls are read#

A wall is measured from each sample point straight into the material, to the surface facing it. Where two faces meet at a sharp edge, the reading near the edge measures the edge, not a wall, and is not reported. A blade that is genuinely thin is still reported.

Thinnest wall
Section at the wall
How a wall is read. The same upper arm, drawn see-through. A wall is measured from each sample point on the surface straight into the material, to the surface facing it. The thinnest, 0.95 mm, is near the end of the fork, one of four almost equal spots, on a face that cannot be seen from outside. Right, a section across it: the blue rod spans the wall. Model: Upper arm, SO-ARM100 by TheRobotStudio (github.com/TheRobotStudio/SO-ARM100), Apache-2.0, checked with Printy3D

A wall or gap of 0.01 mm or less is not reported: at that size the reading measures how the curved surfaces were traced, not the model. Neither is a patch of surface too small to hold one sample point.

A gap inside a part is read the same way, from each sample point straight out of the material, across the space, to the surface facing it.

Demo part
Section at the slot
How a narrow gap is read. A demo part Printy3D made to show it: a slot designed at exactly 0.30 mm, beside a prong designed at 1.00 mm. The check read the gap as 0.3 mm and the wall as 1.0 mm, as designed. Judged for SLS, whose minimum gap inside a part is 0.5 mm, the slot gives a GAP_BELOW_MIN warning. Model: Gap demo part, made by Printy3D

The limits table#

The limits that apply to your account, one row per process and material. Read it to offer your users the processes to choose from, and to show them the data notice.

versionintegerrequired

Changes whenever the table changes. Each evaluation answer carries the table_version it used.

gap_floor_mmnumberrequired

No gap limit is applied below it, because readings are not reliable under it. It is 0.2 mm; the Printy3D team may set a higher floor for your account, never a lower one.

noticeDataNoticerequired

processing_region and retention_days: where files are processed and how long the measurements are kept. Show it to your users before they upload.

rows[]TechnologyRow[]

process, material and a label for people; wall_min_mm; gap_inside_mm (inside one part), gap_assembled_mm (between parts printed together) and gap_moving_mm (between parts that must move); interior (closed_ok, needs_holes or not_offered), hole_count and hole_min_diameter_mm for drain holes; the source of the values; and optional wall_by_size bands. A value the process does not define is null.

GET /v1/technologies (one row shown)
{
  "version": 3,
  "gap_floor_mm": 0.2,
  "notice": { "processing_region": "...", "retention_days": 30 },
  "rows": [
    {
      "process": "fdm",
      "material": "pla_abs_petg",
      "label": "FDM, PLA / ABS / PETG",
      "wall_min_mm": 1.2,
      "gap_inside_mm": 0.5,
      "gap_assembled_mm": 0.5,
      "gap_moving_mm": 0.5,
      "interior": "closed_ok",
      "hole_count": null,
      "hole_min_diameter_mm": null,
      "source": "...",
      "wall_by_size": null
    }
  ]
}

Evaluation#

Judges a succeeded check's stored measurements against the limits table. It is free, so change the question and ask again: another material, a larger print, parts printed together or apart.

FDM
SLA / DLP
SLS
MJF
One check, judged for each process. The upper arm of an open-source robot arm, whose thinnest wall is 0.95 mm, judged against the limits table. FDM's minimum wall is 1.2 mm, so it gives a WALL_BELOW_MIN warning; SLA / DLP (0.6 mm), SLS (0.8 mm) and MJF (0.7 mm) accept it. The check ran once; each evaluation is free. Model: Upper arm, SO-ARM100 by TheRobotStudio (github.com/TheRobotStudio/SO-ARM100), Apache-2.0, checked with Printy3D
processstring

A process from the table. Without it, every process and material is judged.

materialstring

A material of that process. Without it, every material of the process is judged. A material without a process, or a name the table does not have, answers 422.

scalenumberdefault 1greater than 0

The model is judged as if multiplied by it: at 2, every wall and gap is twice as wide, and so is the accuracy_mm tolerance.

assembly_intentstringdefault "separate"

separate: the parts are printed one by one, so parts touching each other is information only. together: they are printed as one assembly, so it is a warning.

movingbooleandefault false

Judge gaps between parts against gap_moving_mm instead of gap_assembled_mm.

Printed separately
Printed together
Parts that touch or overlap. A square-to-round duct and its insulation: two parts that overlap, the whole duct lying inside the insulation. Judged for FDM printed separately (assembly_intent separate), each overlap is information and the summary is ok. Printed together, the same overlaps are PARTS_OVERLAP warnings: the parts would fuse. Left, the two are drawn apart; the file is unchanged. Model: Square-to-round duct, FreeCAD library (github.com/FreeCAD/FreeCAD-library), CC BY 3.0, checked with Printy3D
Scale 1
Scale 1.3
Judged at another scale, free. The upper arm again, judged for FDM. At scale 1 its 0.95 mm wall is under FDM's 1.2 mm minimum. Judged at scale 1.3, the same check counts that wall as 1.24 mm (0.95 x 1.3), and FDM accepts it. No new check is run. Model: Upper arm, SO-ARM100 by TheRobotStudio (github.com/TheRobotStudio/SO-ARM100), Apache-2.0, checked with Printy3D
curl -sS "$API/v1/jobs/$JOB_ID/evaluation?process=fdm&material=pla_abs_petg&assembly_intent=together" \
  -H "Authorization: Bearer $PRINTY3D_API_KEY"
Evaluation (abridged)
{
  "table_version": 3,
  "items": [
    {
      "process": "fdm",
      "material": "pla_abs_petg",
      "summary": "blocked",
      "findings": [
        {
          "code": "NOT_CLOSED",
          "severity": "blocking",
          "message": "Planet Gear is not a closed solid, so it cannot be printed as it is.",
          "part": "Planet Gear",
          "value_mm": null,
          "limit_mm": null,
          "location": null,
          "frame": null
        },
        {
          "code": "WALL_BELOW_MIN",
          "severity": "warning",
          "message": "Sun Gear wall 0.2773 mm, under the 1.2 mm minimum.",
          "part": "Sun Gear",
          "value_mm": 0.277328,
          "limit_mm": 1.2,
          "location": [-0.4596, 6.1625, 9.5642],
          "frame": "part"
        },
        {
          "code": "PARTS_TOUCH",
          "severity": "warning",
          "message": "Actuator Housing and Bearing Retainer touch. Printed together they will fuse: print them separately, or open a gap in the design.",
          "part": null,
          "value_mm": 0.0,
          "limit_mm": 0.5,
          "location": [40.3617, 37.3564, 62.5],
          "frame": "assembly"
        }
      ]
    }
  ]
}

summary is blocked when any finding is blocking, warnings when any is a warning, and ok otherwise. A finding is {code, severity, message, part, value_mm, limit_mm, location, frame}; message is a sentence you can show as it is.

codeseverityMeaning
NOT_CLOSEDblockingThe part is not a closed solid, so it cannot be printed as it is.
SHEET_NO_THICKNESSblockingThe part is a surface with no thickness.
WALL_BELOW_MINwarningThe part's thinnest wall is under the row's wall_min_mm.
GAP_BELOW_MIN inside one partwarningA space inside one part, or between two bodies of one part, is narrower than gap_inside_mm.
PARTS_TOUCH, PARTS_OVERLAP, GAP_BELOW_MIN between two partswarning with assembly_intent=together, otherwise informationTwo parts touch, overlap, or are closer than gap_assembled_mm (gap_moving_mm with moving=true). Printed together, they fuse.
WALL_BELOW_SIZE_RECOMMENDATIONinformationThe wall is under what the row recommends for a part of this size (only rows with wall_by_size).
SAMPLING_REDUCEDinformationThe file was large enough to be measured with fewer samples. At most one, with no part or location.
PLACEMENTS_UNREADwarning, whatever the processSome placements in the file could not be read, so the parts they place were not checked. At most one, with no part or location; the message says how many.
  • Bodies of one part that touch or overlap are how multi-body parts are modelled and are not reported; only a real space between them is a gap inside the part.
  • A part has at most one gap-inside finding, for its smallest gap, whether that gap lies inside one body or between two bodies, and however many times the part is placed. Findings name parts by their names in the file, so parts that share a name share that one finding, for the smallest gap among them.
  • location is a point in millimetres, or null, and frame says which frame it is in. A wall, or a gap measured inside one body, is in that part's own frame (part), before it is placed in the assembly, and applies to every copy of the part. A finding between two parts, or between two bodies of one part, is in the frame of the whole assembly (assembly). frame is null when location is.
  • 409 when the job is not a cad_check, has not succeeded, or its stored measurements have expired.

Artifacts#

labelWhat it is
cad_reportEverything measured: every part, wall and gap region, and pair of parts. Up to 256 MiB of JSON.
cad_digestThe small summary the evaluation reads, up to 2 MiB.
previewA GLB of the model, for a web viewer.

In both JSON files, a part's volume is null unless every shape in the part is closed. The positions of a region (centroid, bbox_min, bbox_max) and of a part's thinnest wall and narrowest gap (wall_location, gap_location) are in the shape's own frame; the location of a pair is in the assembly's. Downloading works as for any artifact. Artifacts & exports

How long the measurements are kept#

A check's stored measurements are kept as long as its other outputs, the artifact retention of your plan, and no longer. After that the evaluation answers 409; run a new check to judge the file again. The region where files are processed and the retention period are in the limits table's notice.

Test keys#

A check made with a test key is free and runs in the sandbox. The file is not read: the result and the artifacts describe one simulated part named SIMULATED SANDBOX PART, and the preview is a placeholder. The evaluation judges that simulated part, and the webhook is sent as in live mode, so you can build your whole flow before your account is approved.