agentleFS
Laic-parsiism709Laic-parsiism709/meshy-3d-agent/skills/meshy-3d-generation/SKILL.md

meshy-3d-generation

Skill5 starsChanged 8 months agoMITView on GitHub

Generate 3D models, textures, images, rig characters, and animate them using the Meshy AI API. Handles API key detection, setup, and all generation workflows via direct HTTP calls. Use when the user asks to create 3D models, convert text/images to 3D, texture models, rig or animate characters, or interact with the Meshy API.

  • Reads credentials
  • Installs packages

2 other projects use a version of this, 1 of them edited.

Adopted by

Most recently taken up by

  • Other versions2 projects use a version of this.

    Word for word · 0

    None found.

    Edited · 1

    1. Schepetkov/claude-skills-game-UE★ 221% same

    Wrote their own version · 1

    1. meshy-dev/meshy-3d-agent★ 107same name

    How matches are found. Word for word: the exact same text. Edited: shares passages with this file (“99% same” means 99% of its five-word phrases also appear here; 10% or more counts). Their own version: similar meaning, or, weakest, the same name. We can’t tell which project had it first. Projects are counted among the 200 most-starred candidates, and mentions are links to the repository we found, up to 50 per site, so both counts are a floor.

  • In this repository2 other files it gives its agents.

The file

Show its rules and text

We split this skill into the rules it gives an agent

17 rules. About 0 are common, 9 are distinctive, and 8 are very specific to this project.

  • ≈0

    Common · estimate

    Many files like this have a rule of this kind.

  • ≈9

    Distinctive · estimate

    This author's own choices. Worth a look.

  • 8

    Very specific

    Tied to their project's names, files or tools. Probably skip.

  • Common
  • Distinctive
  • Very specific
  1. Distinctive:

    When this skill is first activated in a session, inform the user:

    line 23
  2. Very specific:

    All downloaded files MUST go into a structured meshy_output/ directory in the current working directory.

    line 31
  3. Very specific:

    Use only standard POSIX tools in shell commands: Do NOT use rg (ripgrep), fd, or other non-standard CLI tools — they may not be installed.

    line 44
  4. Distinctive:

    Write the entire create → poll → download flow as ONE Python script and execute it in a single Bash call.

    line 59
  5. Very specific:

    Use python3 -u script.py (unbuffered) so progress output is visible in real time.

    line 60
  6. Distinctive:

    Before any API call, detect whether the environment is ready:

    line 67
  7. Very specific:

    Alternative (all platforms): Create a .env file in your project root:

    line 181
  8. Distinctive:

    CRITICAL: Before creating any task, present the user with a summary and get confirmation:

    line 190
  9. Distinctive:

    Wait for user confirmation before executing.

    line 216
  10. Very specific:

    All generation endpoints return {"result": "<task_id>"}, NOT the model.

    line 239
  11. Very specific:

    NEVER: read model_urls from the POST response.

    line 241
  12. Distinctive:

    Use this as the base for ALL generation workflows:

    line 245
  13. Distinctive:

    After task succeeds, report:

    line 558
  14. Distinctive:

    Thumbnail URL: if present

    line 563
  15. Very specific:

    PBR maps: Must set enable_pbr: true explicitly.

    line 595
  16. Very specific:

    Format availability: Check keys in model_urls before downloading — not all formats are always present.

    line 596
  17. Distinctive:

    Timestamps: All API timestamps are Unix epoch milliseconds.

    line 597

≈ “Common” is an estimate: at least 1 in 5 of the 350,495 skill files we read have a rule of the same kind, not the same words. “Very specific” is our guess from the words: it names a file, a command, a setting or the project itself.

Keep what fits and drop the rest. Your version, ready for your agents.
Files
SKILL.md

Meshy 3D Generation

Directly communicate with the Meshy AI API to generate 3D assets. This skill handles the complete lifecycle: environment setup, API key detection, task creation, polling, downloading, and chaining multi-step pipelines.

For full endpoint reference (all parameters, response schemas, error codes), read reference.md.


IMPORTANT: First-Use Session Notice

When this skill is first activated in a session, inform the user:

All generated files will be saved to meshy_output/ in the current working directory. Each project gets its own folder ({YYYYMMDD_HHmmss}_{prompt}_{id}/) with model files, textures, thumbnails, and metadata. History is tracked in meshy_output/history.json.

This only needs to be said once per session, at the beginning.

IMPORTANT: File Organization

All downloaded files MUST go into a structured meshy_output/ directory in the current working directory. Do NOT scatter files randomly.

  • Each project gets its own folder: meshy_output/{YYYYMMDD_HHmmss}_{prompt_slug}_{task_id_prefix}/
  • For chained tasks (preview → refine → rig), reuse the same project_dir
  • Track tasks in metadata.json per project, and global history.json
  • Auto-download thumbnails alongside models

The Reusable Script Template below includes get_project_dir(), record_task(), and save_thumbnail() helpers.


IMPORTANT: Shell Command Rules

Use only standard POSIX tools in shell commands. Do NOT use rg (ripgrep), fd, or other non-standard CLI tools — they may not be installed. Use these standard alternatives instead:

Do NOT useUse instead
rggrep
fdfind
batcat
exa / ezals

IMPORTANT: Run Long Tasks Properly

Meshy generation tasks take 1–5 minutes. When running Python scripts that poll for completion:

  • Write the entire create → poll → download flow as ONE Python script and execute it in a single Bash call. Do NOT split into multiple commands. This keeps the API key, task IDs, and session in one process context.
  • Use python3 -u script.py (unbuffered) so progress output is visible in real time.
  • Be patient with long-running scripts — do NOT interrupt or kill them prematurely. Tasks at 99% for 30–120s is normal finalization, not a failure.

Step 0: Environment Detection (ALWAYS RUN FIRST)

Before any API call, detect whether the environment is ready:

echo "=== Meshy API Key Detection ==="

# 1. Check current env var
if [ -n "$MESHY_API_KEY" ]; then
  echo "ENV_VAR: FOUND (${MESHY_API_KEY:0:8}...)"
else
  echo "ENV_VAR: NOT_FOUND"
fi

# 2. Check .env files in workspace
for f in .env .env.local; do
  if [ -f "$f" ] && grep -q "MESHY_API_KEY" "$f" 2>/dev/null; then
    echo "DOTENV($f): FOUND"
    export $(grep "MESHY_API_KEY" "$f" | head -1)
  fi
done

# 3. Check shell profiles
for f in ~/.zshrc ~/.bashrc ~/.bash_profile ~/.profile; do
  if [ -f "$f" ] && grep -q "MESHY_API_KEY" "$f" 2>/dev/null; then
    echo "SHELL_PROFILE: FOUND in $f"
  fi
done

# 4. Final status
if [ -n "$MESHY_API_KEY" ]; then
  echo "READY: key=${MESHY_API_KEY:0:12}..."
else
  echo "READY: NO_KEY_FOUND"
fi

# 5. Python requests check
python3 -c "import requests; print('PYTHON_REQUESTS: OK')" 2>/dev/null || echo "PYTHON_REQUESTS: MISSING (run: pip install requests)"

echo "=== Detection Complete ==="

Decision After Detection

  • Key found → Proceed to Step 1.
  • Key NOT found → Go to Step 0a.
  • Python requests missing → Run pip install requests.

Step 0a: API Key Setup (Only If No Key Found)

Tell the user:

To use the Meshy API, you need an API key. Here's how to get one:

  1. Go to https://www.meshy.ai/settings/api
  2. Click "Create API Key", give it a name, and copy the key (it starts with msy_)
  3. The key is only shown once — save it somewhere safe

Note: API access requires a Pro plan or above. Free-tier accounts cannot create API keys. If you see "Please upgrade to a premium plan to create API tasks", you'll need to upgrade at https://www.meshy.ai/pricing first.

Once the user provides their key, set it and verify:

macOS (zsh):

export MESHY_API_KEY="msy_PASTE_KEY_HERE"

# Verify
STATUS=$(curl -s -o /dev/null -w "%{http_code}" \
  -H "Authorization: Bearer $MESHY_API_KEY" \
  https://api.meshy.ai/openapi/v1/balance)

if [ "$STATUS" = "200" ]; then
  BALANCE=$(curl -s -H "Authorization: Bearer $MESHY_API_KEY" https://api.meshy.ai/openapi/v1/balance)
  echo "Key valid. $BALANCE"
  echo 'export MESHY_API_KEY="msy_PASTE_KEY_HERE"' >> ~/.zshrc
  echo "Persisted to ~/.zshrc"
else
  echo "Key invalid (HTTP $STATUS). Check the key and try again."
fi

Linux (bash):

export MESHY_API_KEY="msy_PASTE_KEY_HERE"

# Verify (same as above), then persist to ~/.bashrc
STATUS=$(curl -s -o /dev/null -w "%{http_code}" \
  -H "Authorization: Bearer $MESHY_API_KEY" \
  https://api.meshy.ai/openapi/v1/balance)

if [ "$STATUS" = "200" ]; then
  BALANCE=$(curl -s -H "Authorization: Bearer $MESHY_API_KEY" https://api.meshy.ai/openapi/v1/balance)
  echo "Key valid. $BALANCE"
  echo 'export MESHY_API_KEY="msy_PASTE_KEY_HERE"' >> ~/.bashrc
  echo "Persisted to ~/.bashrc"
else
  echo "Key invalid (HTTP $STATUS). Check the key and try again."
fi

Windows (PowerShell):

$env:MESHY_API_KEY = "msy_PASTE_KEY_HERE"

# Verify
$status = (Invoke-WebRequest -Uri "https://api.meshy.ai/openapi/v1/balance" -Headers @{Authorization="Bearer $env:MESHY_API_KEY"} -UseBasicParsing).StatusCode
if ($status -eq 200) {
    Write-Host "Key valid."
    # Persist permanently
    [System.Environment]::SetEnvironmentVariable("MESHY_API_KEY", $env:MESHY_API_KEY, "User")
    Write-Host "Persisted to user environment variables. Restart terminal to take effect."
} else {
    Write-Host "Key invalid (HTTP $status). Check the key and try again."
}

Alternative (all platforms): Create a .env file in your project root:

MESHY_API_KEY=msy_PASTE_KEY_HERE

Step 1: Confirm Plan With User Before Spending Credits

CRITICAL: Before creating any task, present the user with a summary and get confirmation:

I'll generate a 3D model of "<prompt>" using the following plan:

  1. Preview (mesh generation) — 20 credits
  2. Refine (texturing with PBR) — 10 credits
  3. Download as .glb

  Total cost: 30 credits
  Current balance: <N> credits

  Shall I proceed?

For multi-step pipelines (e.g., text-to-3d → rig → animate), present the FULL pipeline cost upfront:

StepAPICredits
PreviewText to 3D20
RefineText to 3D10
RigAuto-Rigging5
Total35

Note: Rigging automatically includes basic walking + running animations for free (in result.basic_animations). Only add Animate (3 credits) if the user needs a custom animation beyond walking/running.

Wait for user confirmation before executing.

Intent → API Mapping

User wants to...APIEndpointCredits
3D model from textText to 3DPOST /openapi/v2/text-to-3d20 + 10
3D model from one imageImage to 3DPOST /openapi/v1/image-to-3d20–30
3D model from multiple imagesMulti-Image to 3DPOST /openapi/v1/multi-image-to-3d20–30
New textures on existing modelRetexturePOST /openapi/v1/retexture10
Change mesh format/topologyRemeshPOST /openapi/v1/remesh5
Add skeleton to characterAuto-RiggingPOST /openapi/v1/rigging5 (includes walking + running)
Animate a rigged character (custom)AnimationPOST /openapi/v1/animations3
2D image from textText to ImagePOST /openapi/v1/text-to-image3–9
Transform a 2D imageImage to ImagePOST /openapi/v1/image-to-image3–9
Check credit balanceBalanceGET /openapi/v1/balance0

Step 2: Execute the Workflow

CRITICAL: Async Task Model

All generation endpoints return {"result": "<task_id>"}, NOT the model. You MUST poll.

NEVER read model_urls from the POST response.

Reusable Script Template

Use this as the base for ALL generation workflows:

#!/usr/bin/env python3
"""Meshy API task runner. Handles create → poll → download."""
import requests, time, os, sys

API_KEY = os.environ.get("MESHY_API_KEY", "")
if not API_KEY:
    sys.exit("ERROR: MESHY_API_KEY not set")

BASE = "https://api.meshy.ai"
HEADERS = {"Authorization": f"Bearer {API_KEY}"}
SESSION = requests.Session()
SESSION.trust_env = False  # bypass any system proxy settings

def create_task(endpoint, payload):
    resp = SESSION.post(f"{BASE}{endpoint}", headers=HEADERS, json=payload, timeout=30)
    if resp.status_code == 401:
        sys.exit("ERROR: Invalid API key (401)")
    if resp.status_code == 402:
        try:
            bal = SESSION.get(f"{BASE}/openapi/v1/balance", headers=HEADERS, timeout=10)
            balance = bal.json().get("balance", "unknown")
            sys.exit(f"ERROR: Insufficient credits (402). Current balance: {balance}. Top up at https://www.meshy.ai/pricing")
        except Exception:
            sys.exit("ERROR: Insufficient credits (402). Check balance at https://www.meshy.ai/pricing")
    if resp.status_code == 429:
        sys.exit("ERROR: Rate limited (429). Wait and retry.")
    resp.raise_for_status()
    task_id = resp.json()["result"]
    print(f"TASK_CREATED: {task_id}")
    return task_id

def poll_task(endpoint, task_id, timeout=600):
    """Poll task with exponential backoff (5s→30s, fixed 15s at 95%+)."""
    elapsed = 0
    delay = 5            # Initial delay: 5s
    max_delay = 30       # Cap: 30s
    backoff = 1.5        # Backoff multiplier
    finalize_delay = 15  # Fixed delay during finalization (95%+)
    poll_count = 0
    while elapsed < timeout:
        poll_count += 1
        resp = SESSION.get(f"{BASE}{endpoint}/{task_id}", headers=HEADERS, timeout=30)
        resp.raise_for_status()
        task = resp.json()
        status = task["status"]
        progress = task.get("progress", 0)
        filled = int(progress / 5)
        bar = f"[{'█' * filled}{'░' * (20 - filled)}] {progress}%"
        print(f"  {bar} — {status} ({elapsed}s, poll #{poll_count})", flush=True)
        if status == "SUCCEEDED":
            return task
        if status in ("FAILED", "CANCELED"):
            msg = task.get("task_error", {}).get("message", "Unknown")
            sys.exit(f"TASK_{status}: {msg}")
        current_delay = finalize_delay if progress >= 95 else delay
        time.sleep(current_delay)
        elapsed += current_delay
        if progress < 95:
            delay = min(delay * backoff, max_delay)
    sys.exit(f"TIMEOUT after {timeout}s ({poll_count} polls)")

def download(url, filepath):
    """Download a file to the given path (within a project directory)."""
    os.makedirs(os.path.dirname(filepath), exist_ok=True)
    print(f"Downloading {filepath}...", flush=True)
    resp = SESSION.get(url, timeout=300, stream=True)
    resp.raise_for_status()
    with open(filepath, "wb") as f:
        for chunk in resp.iter_content(chunk_size=8192):
            f.write(chunk)
    size_mb = os.path.getsize(filepath) / (1024 * 1024)
    print(f"DOWNLOADED: {filepath} ({size_mb:.1f} MB)")

# --- File organization helpers (see File Organization section above) ---
import re, json
from datetime import datetime

OUTPUT_ROOT = os.path.join(os.getcwd(), "meshy_output")
os.makedirs(OUTPUT_ROOT, exist_ok=True)
HISTORY_FILE = os.path.join(OUTPUT_ROOT, "history.json")

def get_project_dir(task_id, prompt="", task_type="model"):
    timestamp = datetime.now().strftime("%Y%m%d_%H%M%S")
    slug = re.sub(r'[^a-z0-9]+', '-', (prompt or task_type).lower())[:30].strip('-')
    folder = f"{timestamp}_{slug}_{task_id[:8]}"
    project_dir = os.path.join(OUTPUT_ROOT, folder)
    os.makedirs(project_dir, exist_ok=True)
    return project_dir

def record_task(project_dir, task_id, task_type, stage, prompt="", files=None):
    meta_path = os.path.join(project_dir, "metadata.json")
    if os.path.exists(meta_path):
        meta = json.load(open(meta_path))
    else:
        meta = {"project_name": prompt or task_type, "folder": os.path.basename(project_dir),
                "root_task_id": task_id, "created_at": datetime.now().isoformat(),
                "updated_at": datetime.now().isoformat(), "tasks": []}
    meta["tasks"].append({"task_id": task_id, "task_type": task_type, "stage": stage,
                          "files": files or [], "created_at": datetime.now().isoformat()})
    meta["updated_at"] = datetime.now().isoformat()
    json.dump(meta, open(meta_path, "w"), indent=2)
    # Update global history
    if os.path.exists(HISTORY_FILE):
        history = json.load(open(HISTORY_FILE))
    else:
        history = {"version": 1, "projects": []}
    folder = os.path.basename(project_dir)
    entry = next((p for p in history["projects"] if p["folder"] == folder), None)
    if entry:
        entry["task_count"] = len(meta["tasks"])
        entry["updated_at"] = meta["updated_at"]
    else:
        history["projects"].append({"folder": folder, "prompt": prompt, "task_type": task_type,
            "root_task_id": task_id, "created_at": meta["created_at"],
            "updated_at": meta["updated_at"], "task_count": len(meta["tasks"])})
    json.dump(history, open(HISTORY_FILE, "w"), indent=2)

def save_thumbnail(project_dir, url):
    path = os.path.join(project_dir, "thumbnail.png")
    if os.path.exists(path): return
    try:
        r = SESSION.get(url, timeout=15); r.raise_for_status()
        open(path, "wb").write(r.content)
    except Exception: pass

Text to 3D (Preview + Refine)

Append this to the template above and run as one script:

PROMPT = "USER_PROMPT"  # max 600 chars

# --- Preview ---
preview_id = create_task("/openapi/v2/text-to-3d", {
    "mode": "preview",
    "prompt": PROMPT,
    "ai_model": "latest",
    # "model_type": "standard",    # "standard" | "lowpoly"
    # "topology": "triangle",      # "triangle" | "quad"
    # "target_polycount": 30000,   # 100–300000
    # "should_remesh": False,
    # "symmetry_mode": "auto",     # "auto" | "on" | "off"
    # "pose_mode": "t-pose",       # "" | "a-pose" | "t-pose" (use "t-pose" if rigging/animating later)
})

task = poll_task("/openapi/v2/text-to-3d", preview_id)
project_dir = get_project_dir(preview_id, prompt=PROMPT)
download(task["model_urls"]["glb"], os.path.join(project_dir, "preview.glb"))
record_task(project_dir, preview_id, "text-to-3d", "preview", prompt=PROMPT, files=["preview.glb"])
if task.get("thumbnail_url"):
    save_thumbnail(project_dir, task["thumbnail_url"])

print(f"\nPREVIEW COMPLETE")
print(f"  Task ID: {preview_id}")
print(f"  Project: {project_dir}")
print(f"  Formats: {', '.join(task['model_urls'].keys())}")

# --- Refine ---
refine_id = create_task("/openapi/v2/text-to-3d", {
    "mode": "refine",
    "preview_task_id": preview_id,
    "enable_pbr": True,
    "ai_model": "latest",
    # "texture_prompt": "",
    # "remove_lighting": True,     # Remove baked lighting (meshy-6/latest only, default True)
})

task = poll_task("/openapi/v2/text-to-3d", refine_id)
download(task["model_urls"]["glb"], os.path.join(project_dir, "refined.glb"))
record_task(project_dir, refine_id, "text-to-3d", "refined", prompt=PROMPT, files=["refined.glb"])

print(f"\nREFINE COMPLETE")
print(f"  Task ID: {refine_id}")
print(f"  Project: {project_dir}")
print(f"  Formats: {', '.join(task['model_urls'].keys())}")

Refine compatibility: Only previews generated with meshy-5 or latest can be refined. meshy-6 previews do NOT support refine (API returns 400). If the user wants to refine later, always use meshy-5 or latest for the preview step.

Image to 3D

import base64

# For local files, convert to data URI:
# with open("photo.jpg", "rb") as f:
#     image_url = "data:image/jpeg;base64," + base64.b64encode(f.read()).decode()

task_id = create_task("/openapi/v1/image-to-3d", {
    "image_url": "IMAGE_URL_OR_DATA_URI",
    "should_texture": True,
    "enable_pbr": True,            # Default is False; set True for metallic/roughness/normal maps
    "ai_model": "latest",
    # "image_enhancement": True,   # Optimize input image (meshy-6/latest only, default True)
    # "remove_lighting": True,     # Remove baked lighting from texture (meshy-6/latest only, default True)
})

task = poll_task("/openapi/v1/image-to-3d", task_id)
download(task["model_urls"]["glb"], "model.glb")

Multi-Image to 3D

task_id = create_task("/openapi/v1/multi-image-to-3d", {
    "image_urls": ["URL_1", "URL_2", "URL_3"],  # 1–4 images
    "should_texture": True,
    "enable_pbr": True,            # Default is False; set True for metallic/roughness/normal maps
    "ai_model": "latest",
    # "image_enhancement": True,   # Optimize input images (meshy-6/latest only, default True)
    # "remove_lighting": True,     # Remove baked lighting from texture (meshy-6/latest only, default True)
})
task = poll_task("/openapi/v1/multi-image-to-3d", task_id)
download(task["model_urls"]["glb"], "model.glb")

Retexture

task_id = create_task("/openapi/v1/retexture", {
    "input_task_id": "PREVIOUS_TASK_ID",      # or "model_url": "URL"
    "text_style_prompt": "wooden texture",     # or "image_style_url": "URL"
    "enable_pbr": True,
    # "remove_lighting": True,     # Remove baked lighting (meshy-6/latest only, default True)
})
task = poll_task("/openapi/v1/retexture", task_id)
download(task["model_urls"]["glb"], "retextured.glb")

Remesh / Format Conversion

task_id = create_task("/openapi/v1/remesh", {
    "input_task_id": "TASK_ID",
    "target_formats": ["glb", "fbx", "obj"],
    "topology": "quad",
    "target_polycount": 10000,
})
task = poll_task("/openapi/v1/remesh", task_id)
for fmt, url in task["model_urls"].items():
    download(url, f"remeshed.{fmt}")

Auto-Rigging + Animation

IMPORTANT: When the user explicitly asks to rig or animate, the generation step (text-to-3d / image-to-3d) MUST use pose_mode: "t-pose" for best rigging results. If the model was already generated without t-pose, recommend regenerating with pose_mode: "t-pose" first.

Before rigging, verify the model's polygon count is under 300,000. The script should auto-check and block if exceeded:

# Pre-rig check: verify face count (MUST be ≤ 300,000)
source_endpoint = "/openapi/v2/text-to-3d"  # adjust to match the source task's endpoint
source_task_id = "TASK_ID"
check_resp = SESSION.get(f"{BASE}{source_endpoint}/{source_task_id}", headers=HEADERS, timeout=30)
check_resp.raise_for_status()
source = check_resp.json()
face_count = source.get("face_count", 0)
if face_count > 300000:
    print(f"ERROR: Model has {face_count:,} faces (limit: 300,000). Remesh first:")
    print(f"  create_task('/openapi/v1/remesh', {{'input_task_id': '{source_task_id}', 'target_polycount': 100000}})")
    sys.exit("Rigging blocked: face count too high")
# Rig (humanoid bipedal characters only, polycount must be ≤ 300,000)
rig_id = create_task("/openapi/v1/rigging", {
    "input_task_id": "TASK_ID",
    "height_meters": 1.7,
})
rig_task = poll_task("/openapi/v1/rigging", rig_id)
download(rig_task["result"]["rigged_character_glb_url"], "rigged.glb")

# Rigging automatically includes basic walking + running animations — download them:
download(rig_task["result"]["basic_animations"]["walking_glb_url"], "walking.glb")
download(rig_task["result"]["basic_animations"]["running_glb_url"], "running.glb")

# Only call meshy_animate if you need a CUSTOM animation beyond walking/running:
# anim_id = create_task("/openapi/v1/animations", {
#     "rig_task_id": rig_id,
#     "action_id": 1,  # from Animation Library
# })
# anim_task = poll_task("/openapi/v1/animations", anim_id)
# download(anim_task["result"]["animation_glb_url"], "animated.glb")

Text to Image / Image to Image

# Text to Image
task_id = create_task("/openapi/v1/text-to-image", {
    "ai_model": "nano-banana-pro",
    "prompt": "a futuristic spaceship",
})
task = poll_task("/openapi/v1/text-to-image", task_id)
# Result: task["image_url"]

# Image to Image
task_id = create_task("/openapi/v1/image-to-image", {
    "ai_model": "nano-banana-pro",
    "prompt": "make it look cyberpunk",
    "reference_image_urls": ["URL"],
})
task = poll_task("/openapi/v1/image-to-image", task_id)

Step 3: Report Results

After task succeeds, report:

  1. Downloaded file paths and sizes
  2. Task IDs (for follow-up operations like refine, rig, retexture)
  3. Available formats (list model_urls keys)
  4. Thumbnail URL if present
  5. Credits consumed and remaining balance (run balance check)
  6. Suggested next steps:
    • Preview done → "Want to refine (add textures)?"
    • Model done → "Want to rig this character for animation?"
    • Rigged → "Want to apply an animation?"
    • Any model → "Want to remesh / export to another format?"
    • Any model → "Want to 3D print this model?" (requires meshy-3d-printing skill)

Error Recovery

HTTP StatusMeaningAction
401Invalid API keyRe-run Step 0; ask user to check key
402Insufficient creditsAuto-query balance (GET /openapi/v1/balance), show current balance, link https://www.meshy.ai/pricing
422Cannot processExplain limitation (e.g., non-humanoid for rigging)
429Rate limitedAuto-retry after 5s (max 3 times)
5xxServer errorAuto-retry after 10s (once)

Task FAILED messages:

  • "The server is busy..." → retry with backoff (5s, 10s, 20s)
  • "Internal server error." → simplify prompt, retry once

Known Behaviors & Constraints

  • 99% progress stall: Tasks commonly sit at 99% for 30–120s during finalization. This is normal. Do NOT kill or restart.
  • CORS: API blocks browser requests. Always server-side.
  • Asset retention: Files deleted after 3 days (non-Enterprise). Download immediately.
  • PBR maps: Must set enable_pbr: true explicitly.
  • Format availability: Check keys in model_urls before downloading — not all formats are always present.
  • Timestamps: All API timestamps are Unix epoch milliseconds.
  • Large files: Refined models can be 50–200 MB. Use streaming downloads with timeouts.

Execution Checklist

  • Ran environment detection (Step 0)
  • API key present and verified
  • Presented cost summary and got user confirmation
  • Wrote complete workflow as single Python script
  • Ran script with python3 -u for unbuffered output
  • Reported file paths, formats, task IDs, and balance
  • Suggested next steps

Additional Resources

For the complete API endpoint reference including all parameters, response schemas, deprecated fields, and detailed error codes, read reference.md.

Download now (ZIP)

Does it work?

Reviews from people and from AI agents that ran it: what they used it for, and whether it worked.

No reviews yet

People and AI agents can review whether this works in practice. Say what you used it for, and whether it worked.

  • It worked
  • Partly
  • It didn't
What did you use it for? e.g. Reviewing pull requests in a Rails app

Using an agent? It can post a review for you after it has run the file: the MCP tool registry_write, action report. How to connect one.

Your trail

Files you open will collect here, so you can find them again.

Open two or more, and you can remix the rules you like from each into one list. Kept in this browser only.

Now do this for your own.

Bring your CLAUDE.md, AGENTS.md or skills. agentleFS shows which of your rules are common, which are yours and which only fit one project, and keeps one version your agents read everywhere.