Your first analysis in 5 minutes
Upload an STL, read its geometry, and download a thickness heatmap. Every example is copy-and-paste, in cURL and Python.
1. Get your API keys
Create a free account, then press Generate API keys on the API keys page. Copy the secret key right away: Propermesh only keeps a hash of it, so it's shown once. Every request sends the two keys as headers.
export PROPERMESH_ACCESS_KEY="your access key"
export PROPERMESH_SECRET_KEY="your secret key"The Python examples share these helpers. Put them at the top of your script:
# pip install requests
import os
import time
import requests
BASE = "https://propermesh.com/api/v0.1"
HEADERS = {
"x-access-key": os.environ["PROPERMESH_ACCESS_KEY"],
"x-secret-access-key": os.environ["PROPERMESH_SECRET_KEY"],
}
def post(path, **kwargs):
"""Uploads a part or starts a job, and returns the response body"""
response = requests.post(f"{BASE}/{path}", headers=HEADERS, **kwargs)
response.raise_for_status()
return response.json()
def wait_for(path):
"""Polls a job until it finishes. Returns it, or raises if it failed or was cancelled"""
while True:
response = requests.get(f"{BASE}/{path}", headers=HEADERS)
response.raise_for_status()
job = response.json()
if job["status"] == "complete":
return job
if job["status"] != "in_progress":
raise RuntimeError(f"{path} {job['status']}: {job['failure_reason']}")
time.sleep(2)
def download(path, filename):
"""File routes redirect to a short-lived storage URL. It needs no keys, so don't send them"""
redirect = requests.get(f"{BASE}/{path}", headers=HEADERS, allow_redirects=False)
redirect.raise_for_status()
with open(filename, "wb") as f:
f.write(requests.get(redirect.headers["location"]).content)2. Upload a part
Send an STL, OBJ, 3MF, PLY or glTF file and set units to mm or inches, so the results come back in the right size. The shell examples use jq to pull ids out of the responses.
PART_ID=$(curl -s https://propermesh.com/api/v0.1/parts \
-H "x-access-key: $PROPERMESH_ACCESS_KEY" \
-H "x-secret-access-key: $PROPERMESH_SECRET_KEY" \
-F "file=@bracket.stl" -F "units=mm" | jq -r .id)
echo $PART_IDwith open("bracket.stl", "rb") as f:
part = post("parts", files={"file": f}, data={"units": "mm"})
part_id = part["id"]Sending the file in the request works for files up to about 4 MB. For bigger files (the maximum is in section 6), ask POST /parts/upload-url for an upload URL, PUT the file there, then finish with POST /parts and the upload_token it returned.
3. Read the geometry
Every upload starts a geometry analysis on its own, so there's nothing to start: poll GET /parts/{part_id}/geometry until status is complete. It gives you the volume, surface area, bounding box and whether the mesh is watertight.
# Run it again until "status" is "complete"
curl -s https://propermesh.com/api/v0.1/parts/$PART_ID/geometry \
-H "x-access-key: $PROPERMESH_ACCESS_KEY" \
-H "x-secret-access-key: $PROPERMESH_SECRET_KEY" | jqgeometry = wait_for(f"parts/{part_id}/geometry")
print(geometry["volume"], geometry["surface_area"], geometry["bounding_box_x"]){
"id": "6f1c2a9e-…",
"status": "complete",
"volume": 12653.1,
"surface_area": 26862.5,
"bounding_box_x": 80.0,
"bounding_box_y": 40.0,
"bounding_box_z": 22.5,
"is_watertight": true,
…
}4. Find thin walls and download the heatmap
The other analyses all work the same way: start a job, then poll it until status is complete. Thickness analysis flags the parts of the surface thinner than your threshold, which is always in millimeters, whatever units the part has. The result is a PLY mesh colored by thickness that opens in MeshLab or Blender. Parts you upload through the API also show up in the web analyzer.
# Flag anything thinner than 2 mm
THICKNESS_JOB_ID=$(curl -s https://propermesh.com/api/v0.1/thickness/$PART_ID/start \
-H "x-access-key: $PROPERMESH_ACCESS_KEY" \
-H "x-secret-access-key: $PROPERMESH_SECRET_KEY" \
-H "Content-Type: application/json" -d '{"threshold": 2}' | jq -r .id)
# Run it again until "status" is "complete"
curl -s https://propermesh.com/api/v0.1/thickness/$THICKNESS_JOB_ID \
-H "x-access-key: $PROPERMESH_ACCESS_KEY" \
-H "x-secret-access-key: $PROPERMESH_SECRET_KEY" | jq
# The heatmap redirects to a short-lived storage URL. Fetch that URL without your keys
HEATMAP_URL=$(curl -s -o /dev/null -w '%{redirect_url}' \
https://propermesh.com/api/v0.1/thickness/$THICKNESS_JOB_ID/visual/ply \
-H "x-access-key: $PROPERMESH_ACCESS_KEY" \
-H "x-secret-access-key: $PROPERMESH_SECRET_KEY")
curl -s -o heatmap.ply "$HEATMAP_URL"job = post(f"thickness/{part_id}/start", json={"threshold": 2}) # thinner than 2 mm
job = wait_for(f"thickness/{job['id']}")
print(job["is_thin"], job["thin_surface_area"], job["total_surface_area"])
download(f"thickness/{job['id']}/visual/ply", "heatmap.ply"){
"id": "0b7d…",
"status": "complete",
"threshold": 2.0,
"units": "mm",
"is_thin": true,
"thin_surface_area": 5860.7,
"total_surface_area": 26862.5,
…
}5. Overhang, orientation and repair
Same pattern: start the job, poll it, download the file.
# Overhang: faces at or below the critical angle (in degrees) count as overhangs
curl -s https://propermesh.com/api/v0.1/overhang/$PART_ID/start \
-H "x-access-key: $PROPERMESH_ACCESS_KEY" \
-H "x-secret-access-key: $PROPERMESH_SECRET_KEY" \
-H "Content-Type: application/json" -d '{"critical_angle": 45}' | jq
# Optimal orientation: the best way to put the part on the build plate
curl -s -X POST https://propermesh.com/api/v0.1/orient/$PART_ID/start \
-H "x-access-key: $PROPERMESH_ACCESS_KEY" \
-H "x-secret-access-key: $PROPERMESH_SECRET_KEY" | jq
# Repair: fixes holes and flipped triangles
curl -s -X POST https://propermesh.com/api/v0.1/repair/$PART_ID/start \
-H "x-access-key: $PROPERMESH_ACCESS_KEY" \
-H "x-secret-access-key: $PROPERMESH_SECRET_KEY" | jq
# Each response has an "id". Poll GET /overhang/<id>, /orient/<id> or /repair/<id> the same way,
# then download /overhang/<id>/visual/ply, /orient/<id>/stl or /repair/<id>/stloverhang = post(f"overhang/{part_id}/start", json={"critical_angle": 45})
overhang = wait_for(f"overhang/{overhang['id']}")
print(overhang["overhang_surface_area"], overhang["num_overhang_faces"])
download(f"overhang/{overhang['id']}/visual/ply", "overhang.ply")
orient = post(f"orient/{part_id}/start")
orient = wait_for(f"orient/{orient['id']}")
download(f"orient/{orient['id']}/stl", "oriented.stl")
repair = post(f"repair/{part_id}/start")
repair = wait_for(f"repair/{repair['id']}")
download(f"repair/{repair['id']}/stl", "repaired.stl")6. Limits and errors
- Rate limits. The free beta has daily limits on uploads and analyses, a cap on running jobs, and a storage cap (see pricing). Going over one returns
429with aRetry-Afterheader (in seconds) and adetailmessage. - Timing. Most jobs finish within seconds; large meshes can take a few minutes. How long to poll, and the exact limits, are under Limits & timing in the API reference.
- Failed jobs. A job that can't finish has
statusfailedand afailure_reasonthat says why. The Pythonwait_forhelper raises it as an error. - File types. Ask which file types and what maximum file size are accepted with
GET /parts/supported-file-types.
response = requests.post(f"{BASE}/thickness/{part_id}/start", headers=HEADERS,
json={"threshold": 2})
if response.status_code == 429:
wait_seconds = int(response.headers["Retry-After"])
raise SystemExit(f"{response.json()['detail']} Try again in {wait_seconds}s")
response.raise_for_status()
try:
job = wait_for(f"thickness/{response.json()['id']}")
except RuntimeError as error:
print(error) # e.g. "thickness/0b7d… failed: <the failure_reason>"curl -s https://propermesh.com/api/v0.1/parts/supported-file-types \
-H "x-access-key: $PROPERMESH_ACCESS_KEY" \
-H "x-secret-access-key: $PROPERMESH_SECRET_KEY" | jqNext
The API reference lists every endpoint, parameter and response.