---
title: "Instant quote"
description: "Quote a price for a part in each material on a price list, with the print time, the resin including supports, and the cost of resin, machine time, and setup."
url: https://developer.formlabs.com/examples/instant-quote/
---

# Instant quote

This script quotes a price for a part in each material on a price list. For each material, the
quote shows the print time, the resin the parts use on their own and with supports, and the total
price split into resin, machine time, and a setup fee.

[View on GitHub](https://github.com/Formlabs/formlabs-api-python/tree/v1.0.0-alpha/examples/instant-quote) [Download instant-quote.zip](https://github.com/Formlabs/formlabs-api-python/releases/download/v1.0.0-alpha/instant-quote.zip)

The price comes from the oriented and supported build and its print time, so it still holds
when the job prints. Passing in `--json`, the script returns the quotes in a machine readable JSON format.

The script uses the PreForm API to orient, support, and lay out the parts on the build platform,
calculate the resin the build uses with its supports and raft, and slice the build for its print
time. `pricing.toml` stands in for a price list, with a machine hour rate, a setup fee, and a price
per mL for each material.

![The sample part: an L-bracket with a gusset and two mounting holes in each leg](https://developer.formlabs.com/examples/instant-quote-part.png)

![Ten copies of the bracket oriented, supported, and laid out on a Form 4 build platform, the build that the Grey V5 quote prices](https://developer.formlabs.com/examples/instant-quote-build.png)

This example contains three files: `instant_quote.py` prepares and prices the build through the
PreForm API, `pricing.toml` stands in for the shop's price list, and `bracket.stl` is a sample
part.

## Read the price list

`pricing.toml` starts with the shop's machine type, machine hour rate, and setup fee:

```toml
[shop]
currency = "USD"
machine_type = "FORM-4-0"          # Form 4; "FRML-4-0" for Form 4L
machine_hour_rate = 6.00           # charged per hour of print time
setup_fee = 5.00                   # charged once per job
```

Each material follows as its own table, with its scene settings and a price per mL:

```toml
[materials.tough-2000]
label = "Tough 2000 V2"
material_code = "FLTO2002"
layer_thickness_mm = 0.1
# The catalog's default settings for this material on Form 4. SUPPORTSV2 gives each
# part a larger raft, so fewer copies fit on the build platform.
print_setting = "SUPPORTSV1"
price_per_ml = 0.22
```

The material code, layer thickness, and print setting of each entry must be a combination that
`/v1/print-setting/list/` returns for the machine type. Tough 2000 V2 uses `SUPPORTSV1`, which
that list labels as the default settings; with `SUPPORTSV2`, each bracket gets a larger raft and
10 copies no longer fit on a Form 4 build platform.

`main` reads the price list from `PRICING_FILE`, next to the script, unless `--pricing` names
another:

```python
PRICING_FILE = Path(__file__).parent / "pricing.toml"
PREFORM_URL = os.environ.get("FL_LOCAL_BASE_URL", "http://localhost:44388")
```

It then parses the arguments, splits the price list into the shop and its materials, and checks
the names passed with `--materials`:

```python
def main() -> None:
    parser = argparse.ArgumentParser(
        description=__doc__, formatter_class=argparse.RawDescriptionHelpFormatter
    )
    parser.add_argument(
        "parts", nargs="+", type=Path, help="STL, OBJ, or 3MF files to quote"
    )
    parser.add_argument(
        "--copies", type=int, default=1, help="copies of each part (default: 1)"
    )
    parser.add_argument(
        "--materials", help="comma-separated names from the price list (default: all)"
    )
    parser.add_argument(
        "--pricing", type=Path, default=PRICING_FILE,
        help="price list (default: pricing.toml)",
    )
    parser.add_argument("--json", action="store_true", help="print the quotes as JSON")
    args = parser.parse_args()

    pricing = tomllib.loads(args.pricing.read_text())
    shop, materials = pricing["shop"], pricing["materials"]
    chosen = args.materials.split(",") if args.materials else list(materials)
    unknown = [name for name in chosen if name not in materials]
    if unknown:
        sys.exit(
            f"Unknown materials {unknown}; the price list has {', '.join(materials)}"
        )
```

## Connect to PreForm

Make sure [PreForm is ready to accept connections](https://developer.formlabs.com/getting-started/install.md#connect-to-a-running-preform).

`main` connects to PreForm, exits with a hint if nothing answers, and quotes each chosen material:

```python
with FormlabsClient() as client:
    try:
        preform = client.preform.connect(base_url=PREFORM_URL)
    except PreFormApiError as exc:
        sys.exit(
            f"{exc}\nNo PreForm at {PREFORM_URL}; "
            "open PreForm and start its API server"
        )
    quotes = [
        quote(preform, shop, materials[name], args.parts, args.copies)
        for name in chosen
    ]
```

`PREFORM_URL` defaults to `http://localhost:44388`. Set `FL_LOCAL_BASE_URL` to reach PreForm on
another port or host.

## Create a scene for each material

`quote` creates one scene for each material on the list, with that material's scene settings:

```python
def quote(
    preform: PreFormApi, shop: dict, material: dict, parts: list[Path], copies: int
) -> dict:
    """Prepare the parts as one build in this material and price it."""
    scene = preform.create_scene(
        machine_type=shop["machine_type"],
        material_code=material["material_code"],
        layer_thickness_mm=material["layer_thickness_mm"],
        print_setting=material.get("print_setting", "DEFAULT"),
    )
```

A scene holds one material and layer thickness, so each material is quoted on its own build.
`print_setting` falls back to `DEFAULT` when the price list names none.

## Import and copy the parts

`quote` imports every part, then duplicates the models to the requested number of copies:

```python
for part in parts:
    # The server resolves the path from its own working directory.
    preform.import_models(
        scene_id=scene.scene_id, file=str(part.resolve()), name=part.stem
    )
if copies > 1:
    preform.duplicate_models(scene_id=scene.scene_id, count=copies - 1)
```

`duplicate_models` makes `count` copies of every model in the scene, so passing `--copies 10` to the script gives 10
of each part.

## Prepare the build as it prints

The scripts run orientation, supports, and layout to produce a build.

```python
# Supports depend on the orientation, and the layout on both.
preform.auto_orient_models(scene_id=scene.scene_id)
preform.auto_support_models(scene_id=scene.scene_id)
try:
    preform.auto_layout_models(scene_id=scene.scene_id)
except PreFormApiError as exc:
    # By default a layout that can't place every model fails and leaves the
    # scene unchanged, so the material is reported unpriced.
    return {
        "material": material["label"],
        "error": f"does not fit on one build platform ({exc.code})",
    }
```

When the copies don't fit, the layout fails with `OPERATION_FAILED` and the table reports the
material unpriced.

## Get an estimate of the print time

`calculate_print_time` slices the build and estimates the print time based on the generated slices. `get_scene` returns the scene's material usage and layer count:

```python
print_time_s = preform.calculate_print_time(scene_id=scene.scene_id).total_print_time_s
state = preform.get_scene(scene_id=scene.scene_id)
```

## Delete the scene

The `finally` block deletes the scene, including after a failed layout:

```python
finally:
    preform.delete_scene(scene_id=scene.scene_id)
```

## Price the build

The price is the resin times the price per mL, plus the print hours times the machine hour rate,
plus the setup fee:

```python
# volume_ml includes the supports and the raft; unsupported_volume_ml is the
# parts alone.
resin_ml = state.material_usage.volume_ml
cost = {
    "resin": round(resin_ml * material["price_per_ml"], 2),
    "machine": round(print_time_s / 3600 * shop["machine_hour_rate"], 2),
    "setup": shop["setup_fee"],
}
# The total is the sum of the rounded lines, so the quote adds up as printed.
cost["total"] = round(sum(cost.values()), 2)
```

`volume_ml` is the same figure as `MaterialUsage_mL` in the scene's job metadata. For 10 brackets
in Grey V5, the supports and raft add 37.7 mL to the 89.2 mL of the parts, so pricing the parts
alone would undercharge the resin by about 30%.

## Run the example

In a terminal, run the script from the folder you unzipped. With pip, [install the Formlabs SDK](https://developer.formlabs.com/getting-started/install.md) first; with [uv](https://docs.astral.sh/uv/getting-started/installation/), `uv run` installs it from the example's `pyproject.toml`.

**pip:**

```bash
python instant_quote.py bracket.stl --copies 10
```

**uv:**

```bash
uv run instant_quote.py bracket.stl --copies 10
```

The script reads the parts named on the command line and `pricing.toml` from its own folder, and
writes nothing. To quote only some materials, pass their names from the price list, such as
`--materials grey,tough-2000`; to use another price list, pass `--pricing`. The script prints one line for each material:

```
1 part(s) x 10 on FORM-4-0: bracket.stl

material           layers print time parts mL resin mL   resin machine  setup  total USD
Grey V5               420     1h 15m     89.2    126.9   19.04    7.56   5.00      31.60
Clear V5              420     1h 09m     89.2    129.5   19.42    6.91   5.00      31.33
Tough 2000 V2         420     1h 43m     89.0    141.3   31.09   10.33   5.00      46.42
Rigid 10K V1.1        420     1h 33m     89.3    126.8   44.40    9.32   5.00      58.72
Precision Model V1    840     2h 23m     89.1    142.1   19.89   14.30   5.00      39.19
```

The layout uses a random seed, so the print time and the machine line can differ by a few seconds
and cents between runs.

## What's next

- [Part labels](https://developer.formlabs.com/examples/part-labels.md)
