Overview
A compression experiment is dispatched by POSTing a zip (a manifest plus mesh files) to the /experiments endpoint. The server creates a Metafold project, runs the compression workflow(s), and makes the results available in the Digital Test Bench and via the projects/assets API.
Units
Two unit systems coexist in a manifest, and conflating them is the most common source of errors:
What | Units |
Mesh geometry (the .ply/.stl) | millimetres (the convention; the server scales mesh-space to metres) |
max_time, delt_min, delt_max, output_interval | seconds |
Material moduli/stresses ( shear_modulus, bulk_modulus, E*, G*, yield stress…) | pascals (Pa) — genuinely SI, not mesh-scaled |
density | kg/m³ |
pistion velocity | m/s |
Post-processing rescales some outputs for readability: von Mises stress is emitted in MPa (Pa × 1e-6) and particle displacement in mm (m × 1e+3). The raw compress output keeps SI units (stresses in pascals, positions in metres) if you need them.
Base URLs
Environment | Base URL |
Production | https://api.metafold3d.com/ |
Development | https://dev-api.metafold3d.com/ |
⚠️ The base URL must match the audience of your access token.
You can work at two levels:
- Raw HTTP — post the experiment zip yourself (what Grasshopper does).
- Python SDK (
pip install metafold) —run_experiment/run_experiment_from_zipbuild and submit the zip for you, plusMetafoldClientfor project/asset access.
Authentication
All endpoints require a bearer token:
Authorization: Bearer <access_token>Endpoints are scoped (e.g. jobs:write, workflows:read, projects:write). A user JWT from a logged-in session carries the scopes for the operations that user can perform.
With the SDK:
from metafold import MetafoldClient
metafold = MetafoldClient(access_token="...", project_id="123")run_experiment accepts auth in one of three ways (provide exactly one):
access_token=+ matchingbase_url=— forward a user JWT.credentials=dict (client_id,client_secret,auth_domain,base_url) — server-side service-account auth, no environment reads.- neither — credentials are read from the environment (via
.env), for local use.
The experiment endpoint
POST /experiments
Dispatch a compression experiment.
- Auth scope:
jobs:write. - Body:
multipart/form-datawith a single file field namedfile— a zip (application/ziporapplication/octet-stream). - Zip contents:
experiment.json— the manifest, at the zip root (required)..ply/.stl— every mesh referenced byparts[].fileandvarying[].files.- Success:
202 Acceptedwith{"project_id": "<sqid>"}. The simulation runs asynchronously; use the project/workflow endpoints (or the DTB) to follow it. - Errors:
400with{"msg": "..."}— the manifest failed validation (missingproject_name, unknown material type, missing material parameter, bad part, experimental material without the flag, etc.). The message says what’s wrong.500— an unexpected server error. If a validation-type problem returns 500 instead of 400, the deployed server may predate a fix; check versions.
💡 Validation runs before the project is created, so a rejected manifest leaves no empty project behind.
The manifest (experiment.json)
The manifest describes the whole experiment. Minimal shape:
Top-level keys
Key | Type | Meaning |
project_name | string | Required. Names the Metafold project (reused if it already exists). |
parts | list | The parts (samples + piston). |
varying | list | Optional. Sweep meshes/materials/velocity/fields across variants. |
simulation | object | Simulation settings. |
workflow_steps | list | Which steps to run. Default: all except stress_strain. |
use_experimental_solver | bool | Run compress on the experimental (GPU) solver. Default false. Required for orthotropic/hyperfoam materials. |
simulation_names | list | Optional display name per variant, index-aligned. null/blank → <name>_sim<i>. Must be unique, no path separators. |
generator | string | Optional free-text tag identifying the client. |
Parts
Each entry has a type:
mesh— a deformable sample. Requiresname,file,material.piston_mesh— a rigid piston from a mesh.file, optionalvelocity,material,name,force_limit.support_mesh— a stationary rigid support the sample rests on (a base, platen, or fixture). Requiresfile; optionalname(default"support"),material(defaultdefault_support_material), andvelocity(default stationary). Unlike pistons it takes noforce_limit, and it defaults to specified-friction contact withmu = 0.1rather than frictionless. This is what the Grasshopper MF Rigid component emits when its Type is Support Mesh.piston_cylinder— a built-in cylinder piston. Optionalshape_parameters({top, bottom, radius}),velocity,material.piston_box— a built-in box piston. Optionalshape_parameters({min, max}),velocity,material.
Any other type value falls through to the deformable-mesh branch: it is treated as a mesh part, and must therefore supply name, file, and material or the manifest is rejected.
⚠️ Unit mismatch on primitive pistons. Primitive pistons (piston_cylinder,piston_box) take theirshape_parametersin metres, unlike mesh parts whose coordinates follow the millimetre mesh convention. Check the numbers you pass against the defaults.
Common part fields:
Field | Meaning |
name | Part name (referenced by varying, metrics, results). |
file | Mesh filename in the zip (for mesh-based parts). |
material | A preset key (string) or an inline material dict. |
velocity | Piston velocity keyframes: a list of [t, vx, vy, vz] (seconds, mm/s). A negative component drives the piston along that axis. |
force_limit | Optional load limit: {"face": <face>, "value": <force>}. When the force on that face reaches value, the piston jumps to zero velocity — it stops pressing rather than continuing. Use it to model a load-limited rig. |
Piston parts omit material/velocity to use the defaults (DEFAULT_PISTON_MATERIAL and a built-in press-and-release ramp). They also carry a contact_type (default "rigid") and a friction coefficient mu (default 0.0, i.e. frictionless).
Material dicts
A material is either a preset key or an inline dict:
{
"density": 200.0,
"thermal_conductivity": 45,
"specific_heat": 4.8e-4,
"constitutive_model": { "type": "<type>", "params": { ... } }
}constitutive_model.type and its params for each supported model:
type | params keys |
rigid | shear_modulus, bulk_modulus |
comp_neo_hook | bulk_modulus, shear_modulus |
comp_mooney_rivlin | he_constant_1, he_constant_2, he_PR |
UCNH | shear_modulus, bulk_modulus, useModifiedEOS, usePlasticity, yield_stress, hardening_modulus, alpha |
hypo_elastic | G, K |
elastic_plastic | shear_modulus, bulk_modulus, and three nested objects: yield_condition, stability_check, and flow_model (a Johnson–Cook model: {A, B, C, n, m}) |
Maxwell_Weichert | bulk_modulus, terminal_shear_modulus, viscoelastic_series (list of {mode, relaxation_time, partial_shear_modulus}) |
visco_trans_iso_hyper | bulk_modulus, c1–c5, fiber_stretch, direction_of_symm, failure_option, max_fiber_strain, max_matrix_strain, y1–y6, t1–t6 |
orthotropic (experimental) | E1E2E3, nu12nu13nu23, G12G13G23, direction_a, direction_b (3-vectors) |
hyperfoam (experimental) | terms (list of {mu, alpha, poisson_ratio}, up to 3); optional terminal_shear_modulus + viscoelastic_series |
Notes:
orthotropicandhyperfoamrequireuse_experimental_solver: true.hyperfoamhas nobulk_modulus. A legacybulk_modulus/Ksent by an older client is ignored.
Varying (sweeps)
Produce multiple variants from one manifest. Each varying entry targets a part or a field; every entry must supply the same number of values (one per variant):
{"part", "files"}— vary a part’s mesh.{"part", "material"}— vary a part’s material (presets or inline dicts).{"part", "velocity"}— vary a piston’s velocity; each value is a full profile.{"field", "values"}— vary a simulation field.
Simulation settings
"simulation": {
"max_time": 0.04,
"max_resolution": 512,
"force_source": "boundary_force",
"force_source_part": "piston",
"boundary_conditions": {"z-": "symmetric", "y+": "velocity_neumann"}
}Field | Meaning |
max_time | Total simulated time (s). |
max_resolution | Sampling points along the longest axis of the combined bounding box; parts share spatial density. |
delt_min / delt_max | Timestep bounds. Lower delt_max if a stiff material is unstable. |
output_interval | How often a full-field frame is written. |
force_source | boundary_force (default) or rigid_reaction_force. |
force_source_part | Part the reaction force is measured on (with rigid_reaction_force; defaults to the piston). |
boundary_conditions | Per-face overrides. Faces: x±, y±, z±. Only listed faces change; the rest keep defaults (symmetric on the x/y faces, velocity_dirichlet on the z faces). Values: symmetric (a symmetry plane / frictionless wall), velocity_dirichlet (velocity fixed to zero at the face), velocity_neumann (zero velocity gradient / free face). |
Workflow steps
workflow_steps selects which steps run. Available: compute_bvh, metrics, compress, von_mises_stress, effective_strain, force_displacement, particle_displacement, energy_metrics. Opt-in (not run by default): stress_strain. A step entry may be a string, or {"step": "metrics", "parts": ["sample"]} to scope it to parts.
If omitted, all steps except stress_strain run.
Material presets
Use any of these as a material string:
Defaults: default_midsole_nominal, default_outsole, default_upper_foam, default_piston_material, default_support_material.
Named: material_abs, material_aluminum, material_basf_epd, material_basf_pp1400_xy, material_basf_pp1400_z, material_basf_rg3280, material_basf_tpu01, material_eos_pa11, material_eos_tpe300, material_epu_41, material_epu_45, material_nylon_6, material_nylon_12, material_pla, material_stainless_steel, material_ti64, material_tpu.
Python SDK
run_experiment(config, ...)
Build and dispatch an experiment from a config dict (the manifest plus a few local keys like ply_folder/output_path).
By default it returns once simulations are dispatched (results stay server-side). wait_for_results=True blocks until every simulation finishes and downloads results into output_path.
run_experiment_from_zip(zip, ...)
Same, but from a prepared zip (experiment.json + meshes at the root) — matching the raw POST /experiments payload.
Validation helpers
validate_experiment_config(config)— raisesValueErrorif the manifest can’t produce a runnable experiment (the same checks the API runs; use it to fail fast).num_simulations(config)— the number of variants the manifest will produce.
Retrieving results
Results live under the project. Key endpoints (all bearer-authenticated):
Method & path | Scope | Purpose |
GET /projects | projects:read | List projects. |
POST /projects | projects:write | Create a project. |
GET /projects/{id} | projects:read | Project details. |
GET /projects/{id}/assets | projects:read | List assets (result files). |
GET /projects/{id}/assets/{asset_id} | projects:read | Download an asset. |
POST /projects/{id}/assets | projects:write | Upload an asset. |
GET /projects/{id}/jobs | jobs:read | List jobs. |
GET /projects/{id}/jobs/{job_id} | jobs:read | Job details. |
GET /projects/{id}/workflows | workflows:read | List workflows. |
GET /workflows | workflows:read | List workflows across your projects. |
POST /projects/{id}/workflows | workflows:write | Submit a custom workflow. |
GET /projects/{id}/workflows/{wf_id} | workflows:read | Workflow details. |
GET /projects/{id}/workflows/{wf_id}/status | workflows:read | Workflow status (poll this). |
POST /projects/{id}/workflows/{wf_id}/cancel | workflows:write | Cancel a workflow. |
GET /user/usage | — | Account usage. |
With the SDK, these are wrapped by metafold.projects, metafold.assets, metafold.jobs, and metafold.workflows. Long-running requests can be waited on with MetafoldClient.poll(status_url), which polls while the endpoint returns 202.
Then poll the project’s workflows for completion and download result assets.