11 min read

Zoo text-to-CAD API tutorial: from curl to production

A practical tutorial for the Zoo.dev text-to-CAD API, starting with a curl command and ending with a Python script that generates STEP files in a loop.

Quick answer

The Zoo.dev API accepts POST requests with text prompts and returns CAD geometry. Start with curl for testing, then use the kittycad Python SDK for automation. The API supports STEP, glTF, OBJ, and STL output. Authentication requires an API token from zoo.dev.

I wanted to generate a STEP file from a text prompt without opening a browser. Just a terminal, a curl command, and a file on disk at the end. The kind of thing that sounds simple and is simple, once you've figured out the three or four things the documentation assumes you already know. This tutorial is the version I wish I'd had when I started: beginning with a single curl command that proves the API works, and ending with a Python script that generates STEP files from a list of part descriptions in a loop.

Everything here uses the Zoo.dev API, which is currently the only production text-to-CAD service that returns real B-Rep geometry. If you want background on the API landscape and how Zoo fits into it, the text-to-CAD API overview covers that. This post is pure hands-on.

Get an API token#

Before anything else, you need a Zoo account and an API token.

  1. Create an account at zoo.dev if you don't have one.
  2. Go to account settings and generate an API token.
  3. Save it somewhere safe. You'll use it for every request.

Set it as an environment variable so the examples below work as written:

export ZOO_API_TOKEN=your-token-here

Zoo gives you $10 of free API usage per month, roughly enough for 15 to 50 generations depending on complexity. Failed calls aren't charged.

Step 1: prove it works with curl#

The fastest way to test the API is a single curl command. This submits a text prompt and gets back a job object with an ID:

curl -s -X POST \
  "https://api.zoo.dev/ai/text-to-cad/step" \
  -H "Authorization: Bearer $ZOO_API_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{"prompt": "rectangular plate, 80mm by 50mm by 5mm, four 4.2mm holes on a 60mm by 30mm bolt pattern centered on the plate"}' \
  | python3 -m json.tool

You'll get back a JSON response with an id field (a UUID), a status (probably queued), and your prompt echoed back. The id is what you need for the next step.

The generation happens asynchronously. This first request just starts the job. You don't get the geometry back immediately, which confused me the first time I tried it because I was expecting a file download and got a JSON status blob instead.

Step 2: poll for completion#

Copy the id from the response and check the status:

curl -s \
  "https://api.zoo.dev/user/text-to-cad/YOUR-UUID-HERE" \
  -H "Authorization: Bearer $ZOO_API_TOKEN" \
  | python3 -m json.tool | grep status

Keep running this until the status changes from queued or in_progress to completed (or failed). Typical wait is 15 to 90 seconds depending on what you asked for. A simple plate like the one above usually comes back in under 30 seconds.

Once the status is completed, the outputs field in the response contains your generated files. The keys are filenames (like output.step, output.gltf) and the values are base64-encoded file content.

Step 3: save the STEP file#

You can extract and decode the STEP file from the JSON response with a bit of shell piping:

curl -s \
  "https://api.zoo.dev/user/text-to-cad/YOUR-UUID-HERE" \
  -H "Authorization: Bearer $ZOO_API_TOKEN" \
  | python3 -c "
import sys, json, base64
data = json.load(sys.stdin)
for name, content in data['outputs'].items():
    if name.endswith('.step'):
        with open('plate.step', 'w') as f:
            f.write(base64.b64decode(content).decode('utf-8'))
        print(f'Saved plate.step')
"

Open plate.step in Fusion 360, SolidWorks, or any STEP-compatible tool. You should see a rectangular plate with four holes. The geometry is real B-Rep: selectable faces, measurable edges, the kind of solid you can fillet and chamfer and argue with. Whether the dimensions are exactly what you asked for is a separate conversation (they'll be close but not perfect, see the accuracy post).

That's the entire API workflow in three curl commands. Submit, poll, save. Everything else is automation, error handling, and making the prompts better.

Step 4: move to Python#

Curl is fine for testing. For anything repeatable, Python is where you want to be. Install the SDK:

pip install kittycad

Here's a self-contained script that does everything the curl commands did, but in a form you can actually build on:

import time
import base64
from kittycad.client import ClientFromEnv
from kittycad.api.ml import create_text_to_cad, get_text_to_cad_part_for_user
from kittycad.models import TextToCadCreateBody, ApiCallStatus

client = ClientFromEnv()

prompt = (
    "rectangular plate, 80mm by 50mm by 5mm, "
    "four 4.2mm holes on a 60mm by 30mm bolt pattern "
    "centered on the plate"
)

print(f"Submitting: {prompt}")
result = create_text_to_cad.sync(
    client=client,
    output_format="step",
    body=TextToCadCreateBody(prompt=prompt),
)
print(f"Job ID: {result.id}")

while True:
    response = get_text_to_cad_part_for_user.sync(
        client=client,
        id=result.id,
    )
    print(f"  Status: {response.status}")
    if response.status in (ApiCallStatus.COMPLETED, ApiCallStatus.FAILED):
        break
    time.sleep(5)

if response.status == ApiCallStatus.COMPLETED:
    for name, content in response.outputs.items():
        if name.endswith(".step"):
            with open("plate.step", "w") as f:
                f.write(base64.b64decode(content).decode("utf-8"))
            print("Saved plate.step")
else:
    print(f"Failed: {response.error}")

Run it. Watch the status updates tick by. Get a STEP file. Open it. Measure the holes. Feel the mild satisfaction of having automated the first step of a CAD workflow from a terminal. Then feel the mild annoyance of discovering that one of the four holes is 0.6mm off center, because that's just how text-to-CAD works right now.

Step 5: generate multiple parts#

This is where things get interesting and where the API starts earning its keep. Instead of generating one part at a time, let's read from a list and generate a batch:

import time
import base64
import os
from kittycad.client import ClientFromEnv
from kittycad.api.ml import create_text_to_cad, get_text_to_cad_part_for_user
from kittycad.models import TextToCadCreateBody, ApiCallStatus

client = ClientFromEnv()

parts = [
    {
        "name": "mounting_plate",
        "prompt": "rectangular plate, 80mm by 50mm by 5mm, four 4.2mm holes on a 60mm by 30mm bolt pattern centered on the plate",
    },
    {
        "name": "standoff",
        "prompt": "cylindrical standoff, 20mm outer diameter, 10mm inner bore, 15mm tall",
    },
    {
        "name": "l_bracket",
        "prompt": "L-bracket, 3mm thick, 40mm equal legs, two 5mm holes per leg spaced 25mm apart, 10mm from edges, 2mm fillet at bend",
    },
    {
        "name": "cable_clip",
        "prompt": "C-shaped cable clip for 8mm cable, 2mm wall thickness, 15mm wide, with a single M3 mounting hole on the flat base",
    },
]

os.makedirs("output", exist_ok=True)

def generate_and_save(part):
    print(f"\nGenerating {part['name']}...")
    try:
        result = create_text_to_cad.sync(
            client=client,
            output_format="step",
            body=TextToCadCreateBody(prompt=part["prompt"]),
        )
    except Exception as e:
        print(f"  Submit failed: {e}")
        return False

    for _ in range(60):
        response = get_text_to_cad_part_for_user.sync(
            client=client,
            id=result.id,
        )
        if response.status in (ApiCallStatus.COMPLETED, ApiCallStatus.FAILED):
            break
        time.sleep(5)

    if response.status == ApiCallStatus.COMPLETED:
        for name, content in response.outputs.items():
            if name.endswith(".step"):
                path = f"output/{part['name']}.step"
                with open(path, "w") as f:
                    f.write(base64.b64decode(content).decode("utf-8"))
                print(f"  Saved {path}")
                return True

    print(f"  Failed: {response.error}")
    return False

results = []
for part in parts:
    success = generate_and_save(part)
    results.append((part["name"], success))

print("\n--- Summary ---")
for name, success in results:
    status = "OK" if success else "FAILED"
    print(f"  {name}: {status}")

I ran a version of this with twelve parts. Nine succeeded on the first try. Two succeeded after I rewrote the prompts to be more specific (the originals were too vague and the API returned errors). One just wouldn't generate no matter how I phrased it, a spring clip with internal snap features that was probably too complex for the current model. That's roughly the success rate I've seen across a few hundred generations: around 85 percent on the first attempt, closer to 90 percent after prompt adjustments.

Writing better prompts#

Prompt quality is the single biggest factor in whether you get usable geometry. This is the part that no amount of SDK knowledge can substitute for. You need to think like you're writing a work order for someone who knows CAD vocabulary but has no context about your project.

Prompts that work well:

  • Include specific dimensions for every feature you care about
  • Name features using CAD terms: bore, boss, fillet, chamfer, counterbore, pocket, rib, standoff
  • Specify material thickness explicitly
  • Describe hole patterns with center-to-center spacing, not "evenly distributed" (the AI's idea of "even" may differ from yours)
  • Keep it to one part per prompt

Prompts that reliably cause problems:

  • "A small bracket" (how small? what shape? for what?)
  • Descriptions that reference other parts ("a bracket that attaches to the sensor mount") without describing the geometry
  • Parts with more than about 10 features or multiple interacting feature sets
  • Anything involving springs, threads, or moving mechanisms
  • Organic shapes with complex curvature

Here's a prompt I iterated on three times before the output was usable:

First try: "motor mount bracket." Generated something. Wrong in every dimension, because I gave it nothing to work with.

Second try: "L-bracket motor mount, NEMA 17 pattern." Better shape, but the bolt pattern was off and the overall size was too small.

Third try: "L-bracket, 3mm aluminum, 60mm by 40mm base, 60mm by 40mm vertical face, four M3 clearance holes on 31mm square NEMA 17 pattern centered on vertical face, two M4 clearance holes on base 50mm apart." Got a bracket I could actually use as starting geometry. Still needed to fix one hole position and add a fillet, but the shape was right.

The lesson: treat the prompt like you'd treat a dimensioned sketch. Every number you leave out is a number the AI guesses, and its guesses are trained on averages, not your specific assembly.

Step 6: add retry logic#

Production scripts need to handle failures without stopping. Here's the pattern I've settled on:

def generate_with_retry(prompt, output_path, max_attempts=2):
    for attempt in range(max_attempts):
        try:
            result = create_text_to_cad.sync(
                client=client,
                output_format="step",
                body=TextToCadCreateBody(prompt=prompt),
            )
        except Exception as e:
            print(f"  Attempt {attempt + 1} request error: {e}")
            time.sleep(10)
            continue

        for _ in range(60):
            response = get_text_to_cad_part_for_user.sync(
                client=client,
                id=result.id,
            )
            if response.status in (
                ApiCallStatus.COMPLETED,
                ApiCallStatus.FAILED,
            ):
                break
            time.sleep(5)

        if response.status == ApiCallStatus.COMPLETED:
            for name, content in response.outputs.items():
                if name.endswith(".step"):
                    step_data = base64.b64decode(content).decode("utf-8")
                    with open(output_path, "w") as f:
                        f.write(step_data)
                    return True

        if attempt < max_attempts - 1:
            print(f"  Attempt {attempt + 1} failed, retrying...")
            time.sleep(5)

    return False

Two attempts is usually enough. The generation model is non-deterministic, so the same prompt can succeed on a retry even if it failed the first time. I haven't found that more than two retries helps, though. If it fails twice, the prompt usually needs rewriting.

Step 7: from script to something you'd actually maintain#

At this point you have all the pieces. The progression from here depends on what you're building. A few directions I've gone:

Reading part specs from a CSV or YAML file instead of hardcoding them. The text-to-CAD API Python post has a CSV example that plugs directly into this workflow.

Adding logging. I write a JSON log entry for each generation with the prompt, request ID, timestamp, success/failure status, and output path. Three weeks later, when I'm wondering why a particular STEP file looks wrong, the log tells me what prompt produced it.

Running as a cron job. My current setup checks a YAML file for new entries twice a day, generates any parts that don't already have STEP files in the output folder, and sends me a Slack message with the results. About 80 lines of Python total. It's the most useful automation I've built in the past six months that doesn't involve a database.

Validating the output. After saving the STEP file, I open it with a STEP parser (pythonOCC or cadquery can do this) and check that the solid is valid, has non-zero volume, and has a bounding box that roughly matches the expected dimensions. This catches the occasional garbage output before it pollutes the project folder.

What this doesn't cover#

Assemblies. The API generates one part per request. If you need multiple parts that fit together, you're generating each one separately and assembling in your CAD tool.

Iterative refinement. The API doesn't have a "modify the last part I generated" endpoint. Each request starts fresh. Zoo's web UI supports conversational refinement, but the API is fire-and-forget.

KCL output. Zoo is building a code-based CAD language called KCL, and the API has a kcl option that returns KCL code alongside the geometry. I've experimented with this but haven't found a production use for it yet. The code is interesting to read and theoretically editable, but the tooling around KCL is still early.

For more context on KCL, Zoo's broader toolset, and how the text-to-CAD model performs on various part types, see the Zoo text-to-CAD review.

The honest verdict on this workflow#

I've been using this API-based workflow for about three months now, mostly for generating fixture brackets and mounting plates, the kind of parts that are boring to model but too specific to reuse from a library. The API saves me real time on those parts. Not because the output is perfect, it's not, and I still open every STEP file and check the dimensions. But starting from a generated solid instead of a blank sketch cuts the boring parts out of my CAD day, and the boring parts are what make you stop caring about quality.

The workflow from curl to production script took me an afternoon to build and has been stable since. The SDK is good enough that you don't fight it. The API is good enough that it succeeds most of the time. The output is good enough that it's useful as starting geometry.

"Good enough" is doing a lot of work in that paragraph, and I mean it precisely. This is a tool that gets you 70 to 80 percent of the way there on simple parts, and that last 20 to 30 percent is still your job. Whether that's a bargain or a trap depends on how many simple parts you generate and how high your tolerance is for checking the AI's work. For me, it's been a bargain. The KittyCAD Python SDK documentation has everything else you'd need beyond what I've covered here.

Newsletter

Get new TexoCAD thoughts in your inbox

New articles, product updates, and practical ideas on Text-to-CAD, AI CAD, and CAD workflows.

No spam. Unsubscribe anytime.