This is the full developer documentation for Rigyd API # Rigyd API > Convert any 3D file, image, or text prompt into a physics-enabled SimReady USD asset over a simple REST API. ## What you can do [Section titled “What you can do”](#what-you-can-do) [3D → SimReady ](/conversions/3d-to-simready)Upload a .glb / .gltf / .fbx / .obj / .stl / .ply / .usd\[a|c|z] (or a .zip of those + textures) and get back physics-enabled USD. [2D → SimReady ](/conversions/2d-to-simready)Send 1 image (single-view) or 2-4 images (multiview) and Rigyd reconstructs a SimReady asset. [Text → SimReady ](/conversions/text-to-simready)Describe what you want (\`a red metal toolbox\`) and get a SimReady asset back. [Simulate ](/conversions/simulate)Run a demo or drop physics simulation against a completed conversion. Returns a video, GIF, and log. ## How it fits together [Section titled “How it fits together”](#how-it-fits-together) Every mutating call returns `202 Accepted` immediately with a job `id`. You then poll [`GET /api/conversions/:id`](/jobs/retrieve) until `status` is `completed` or `failed`, and download the result with [`GET /api/conversions/:id/result`](/jobs/download). ```text POST /api/conversions ─┐ POST /api/conversions/generate ─┼──► 202 { id, status: "queued" | "preprocessing" } POST /api/conversions/:id/simulate ─┘ │ │ poll ▼ GET /api/conversions/:id │ status === "completed" ▼ GET /api/conversions/:id/result?format=usd → streaming ZIP (USD + textures) ``` ## Where to start [Section titled “Where to start”](#where-to-start) [Quickstart ](/quickstart)Paste your key, run one curl, get a USD. [Authentication ](/authentication)The one header you need to send. [Job lifecycle ](/reference/job-lifecycle)Status enum, preprocessing, response shapes. [Errors ](/reference/errors)What every status code means. ## Account & billing [Section titled “Account & billing”](#account--billing) Sign up, mint API keys, and manage billing at **[app.rigyd.com](https://app.rigyd.com)**. Those flows are not part of the public API. # Authentication > Send Authorization Bearer rgyd_live_... on every request. Mint keys at app.rigyd.com. The Rigyd API authenticates every request with a single header: ```http Authorization: Bearer rgyd_live_<43-char-token> ``` That’s it. No OAuth, no signing, no expiring access tokens to refresh. ## Get a key [Section titled “Get a key”](#get-a-key) Mint an API key in **[app.rigyd.com](https://app.rigyd.com)** under **Settings → API Keys**. Click **Create key**, give it a name (e.g. `production-backend`), and optionally set an `expires_at` date. The plaintext token is shown **once** at creation time. Store it in a secret manager — Rigyd only keeps a SHA-256 hash, so we cannot recover it for you. Note Keys are scoped to the user that created them. Calls made with the key consume that user’s credits and inherit that user’s plan limits. Up to 10 active keys per user. ## Token shapes [Section titled “Token shapes”](#token-shapes) | Environment | Prefix | Example | | ----------- | ------------ | -------------------------------- | | Production | `rgyd_live_` | `rgyd_live_a1b2c3...` (43 chars) | | Other | `rgyd_test_` | `rgyd_test_a1b2c3...` (43 chars) | ## Use the key [Section titled “Use the key”](#use-the-key) * curl ```bash curl https://api.rigyd.com/api/conversions \ -H "Authorization: Bearer rgyd_live_..." ``` * JavaScript ```js await fetch('https://api.rigyd.com/api/conversions', { headers: { Authorization: `Bearer ${process.env.RIGYD_API_KEY}` }, }); ``` * Python ```python import os, requests requests.get( "https://api.rigyd.com/api/conversions", headers={"Authorization": f"Bearer {os.environ['RIGYD_API_KEY']}"}, ) ``` ## What happens if the key is bad [Section titled “What happens if the key is bad”](#what-happens-if-the-key-is-bad) | Situation | Status | Body | | ------------------------------- | ------ | ---------------------------------------- | | Missing `Authorization` header | `401` | `{ "error": "Authentication required" }` | | Unknown / revoked / expired key | `401` | `{ "error": "Invalid API key" }` | | Key valid but no credits left | `402` | `{ "error": "Insufficient credits" }` | See [Errors](/reference/errors) for the full table. ## Rotation and revocation [Section titled “Rotation and revocation”](#rotation-and-revocation) Both happen in **[app.rigyd.com](https://app.rigyd.com) → Settings → API Keys**: * **Rotate**: create a new key, ship it, then revoke the old one. * **Revoke immediately** if a key leaks — every authenticated request after the revoke call returns `401`. The Rigyd API does not currently expose key management programmatically by design — a leaked key cannot mint or revoke other keys. # Changelog > API changes that affect public consumers. The Rigyd API is currently versionless — backwards-incompatible changes will be announced here with at least 30 days’ notice. ## 2026-05 [Section titled “2026-05”](#2026-05) * **Initial public docs.** Documents the conversion surface (`POST /api/conversions`, `POST /api/conversions/generate`, `POST /api/conversions/:id/simulate`, plus the `/api/conversions[/...]` read endpoints). * **`llms.txt` / `llms-full.txt` published.** AI agents can ingest the full API surface in one fetch from `https://docs.rigyd.com/llms-full.txt`. ## Earlier [Section titled “Earlier”](#earlier) For pre-public-docs history, see internal release notes. # 2D → SimReady > Send 1 image (single-view) or 2-4 images (multiview) and Rigyd reconstructs a SimReady asset. 3 credits. Send one or more images of an object and Rigyd reconstructs it as a SimReady USD asset. | | | | ---------------- | -------------------------------------------------------------------- | | **Method** | `POST` | | **Path** | `/api/conversions/generate` | | **Content-type** | `multipart/form-data` | | **Job type** | `image_to_simready` (1 image) · `multiview_to_simready` (2-4 images) | | **Credits** | 3 | The same endpoint handles single-view and multiview based on the number of `images[]` you send. Provide multiple angles of the same object for sharper geometry. ## Request fields [Section titled “Request fields”](#request-fields) | Field | Type | Required | Notes | | ----------------- | ---------- | -------- | ------------------------------------------------- | | `images[]` | file (1-4) | yes | `.jpg`, `.jpeg`, `.png`, `.webp`. Max 10 MB each. | | `face_limit` | int | no | Cap output mesh face count. | | `model_version` | string | no | Override generation model (advanced). | | `threshold` | float | no | CoACD concavity threshold (advanced). | | `llm_provider` | string | no | Override material-identification LLM (advanced). | | `skip_validation` | boolean | no | Skip USD schema validation. Default `false`. | Caution Do not send a `prompt` field on the same request — that triggers a `422`. Use [Text → SimReady](/conversions/text-to-simready) for prompt-only generation. ## Examples [Section titled “Examples”](#examples) ### Single image [Section titled “Single image”](#single-image) * curl ```bash curl -X POST https://api.rigyd.com/api/conversions/generate \ -H "Authorization: Bearer rgyd_live_..." \ -F "images[]=@./toolbox-front.jpg" ``` * JavaScript ```js import fs from 'node:fs'; const form = new FormData(); form.append('images[]', new Blob([fs.readFileSync('./toolbox-front.jpg')]), 'toolbox-front.jpg'); const res = await fetch('https://api.rigyd.com/api/conversions/generate', { method: 'POST', headers: { Authorization: `Bearer ${process.env.RIGYD_API_KEY}` }, body: form, }); const { data } = await res.json(); ``` * Python ```python import os, requests with open("toolbox-front.jpg", "rb") as f: res = requests.post( "https://api.rigyd.com/api/conversions/generate", headers={"Authorization": f"Bearer {os.environ['RIGYD_API_KEY']}"}, files={"images[]": ("toolbox-front.jpg", f, "image/jpeg")}, ) job = res.json()["data"] ``` ### Multiview (2-4 images) [Section titled “Multiview (2-4 images)”](#multiview-2-4-images) * curl ```bash curl -X POST https://api.rigyd.com/api/conversions/generate \ -H "Authorization: Bearer rgyd_live_..." \ -F "images[]=@./front.jpg" \ -F "images[]=@./side.jpg" \ -F "images[]=@./back.jpg" ``` * JavaScript ```js import fs from 'node:fs'; const form = new FormData(); for (const name of ['front.jpg', 'side.jpg', 'back.jpg']) { form.append('images[]', new Blob([fs.readFileSync(name)]), name); } const res = await fetch('https://api.rigyd.com/api/conversions/generate', { method: 'POST', headers: { Authorization: `Bearer ${process.env.RIGYD_API_KEY}` }, body: form, }); ``` * Python ```python import os, requests files = [ ("images[]", (name, open(name, "rb"), "image/jpeg")) for name in ["front.jpg", "side.jpg", "back.jpg"] ] res = requests.post( "https://api.rigyd.com/api/conversions/generate", headers={"Authorization": f"Bearer {os.environ['RIGYD_API_KEY']}"}, files=files, ) ``` ## Response — `202 Accepted` [Section titled “Response — 202 Accepted”](#response--202-accepted) ```json { "data": { "id": "abc123...", "status": "queued", "filename": "toolbox-front.jpg", "progress": 0, "job_type": "multiview_to_simready", "credits_charged": 3, "createdAt": "2026-05-06T12:00:00.000Z" } } ``` `job_type` is `image_to_simready` for a single image and `multiview_to_simready` for 2-4 images. Both cost 3 credits. ## Tips [Section titled “Tips”](#tips) * **More views = sharper geometry.** Three to four images covering front / side / back / top yield noticeably better reconstructions than a single hero shot. * **Plain backgrounds help** — the generation model isolates the object before reconstruction. * **One object per call.** If your image contains multiple objects, the result is undefined. ## Next: [poll the job](/jobs/retrieve) → [download the USD](/jobs/download). [Section titled “Next: poll the job → download the USD.”](#next-poll-the-job--download-the-usd) # 3D → SimReady > Upload any 3D file and get back a physics-enabled USD asset. 1 credit per conversion. Upload a 3D model and Rigyd returns a SimReady USD asset — physics properties, collision meshes, and materials filled in automatically. | | | | ---------------- | --------------------- | | **Method** | `POST` | | **Path** | `/api/conversions` | | **Content-type** | `multipart/form-data` | | **Job type** | `glb_to_simready` | | **Credits** | 1 | ## Accepted formats [Section titled “Accepted formats”](#accepted-formats) `.glb`, `.gltf`, `.fbx`, `.obj`, `.stl`, `.ply`, `.usd`, `.usda`, `.usdc`, `.usdz`, and `.zip` (for multi-file inputs). `.glb` skips the preprocessing stage and is the fastest path. Anything else goes through Blender-based cleanup first — see [Supported formats](/reference/supported-formats) for the rules around ZIP archives, OBJ + MTL, GLTF + bin, and USD with references. ## Request fields [Section titled “Request fields”](#request-fields) | Field | Type | Required | Notes | | ----------------------- | ------------- | -------- | ---------------------------------------------------------------------------------------------------- | | `file` | file (upload) | yes | The 3D model. See accepted formats above. | | `target_triangle_count` | int | no | Decimate to this triangle count during preprocessing. `1000`–`1000000`. Useful for high-poly inputs. | | `threshold` | float | no | CoACD collision-decomposition concavity threshold (advanced). | | `llm_provider` | string | no | Override the LLM used for material identification (advanced). | | `skip_validation` | boolean | no | Skip USD schema validation. Default `false`. | ## Examples [Section titled “Examples”](#examples) * curl ```bash curl -X POST https://api.rigyd.com/api/conversions \ -H "Authorization: Bearer rgyd_live_..." \ -F "file=@./toolbox.glb" \ -F "target_triangle_count=50000" ``` * JavaScript ```js import fs from 'node:fs'; const form = new FormData(); form.append('file', new Blob([fs.readFileSync('./toolbox.glb')]), 'toolbox.glb'); form.append('target_triangle_count', '50000'); const res = await fetch('https://api.rigyd.com/api/conversions', { method: 'POST', headers: { Authorization: `Bearer ${process.env.RIGYD_API_KEY}` }, body: form, }); const { data } = await res.json(); ``` * Python ```python import os, requests with open("toolbox.glb", "rb") as f: res = requests.post( "https://api.rigyd.com/api/conversions", headers={"Authorization": f"Bearer {os.environ['RIGYD_API_KEY']}"}, files={"file": ("toolbox.glb", f, "model/gltf-binary")}, data={"target_triangle_count": 50000}, ) job = res.json()["data"] ``` ## Response — `202 Accepted` [Section titled “Response — 202 Accepted”](#response--202-accepted) ```json { "data": { "id": "abc123...", "physiq_job_id": "phy_...", "status": "queued", "filename": "toolbox.glb", "progress": 0, "job_type": "glb_to_simready", "credits_charged": 1, "createdAt": "2026-05-06T12:00:00.000Z" } } ``` For non-GLB inputs, `status` starts at `preprocessing` while the model is converted to a clean GLB, then transitions to `queued` → `running` → `completed`. Tip The credit is reserved atomically when the request is accepted and refunded automatically if the job ends in `failed`. You will not be double-charged on retries. ## Next: [poll the job](/jobs/retrieve) → [download the USD](/jobs/download). [Section titled “Next: poll the job → download the USD.”](#next-poll-the-job--download-the-usd) # Simulate > Run a physics simulation against a completed conversion. Returns a video, GIF, and log. Free. Run a physics simulation against a previously completed conversion. Useful for sanity-checking that mass, friction, and collision shapes look right before you import the asset into your simulator. | | | | ---------------- | ------------------------------- | | **Method** | `POST` | | **Path** | `/api/conversions/:id/simulate` | | **Content-type** | `application/json` | | **Job type** | `simulate_usd` | | **Credits** | 0 (free) | The `:id` is the `id` of a completed source job (any of [3D](/conversions/3d-to-simready), [2D](/conversions/2d-to-simready), [Text](/conversions/text-to-simready) → SimReady). You cannot simulate a simulation. ## Request fields [Section titled “Request fields”](#request-fields) | Field | Type | Required | Notes | | ------- | ------ | -------- | ----------------------------------------------------- | | `scene` | string | no | `"demo"` (default) or `"drop"`. Picks the test scene. | ## Examples [Section titled “Examples”](#examples) * curl ```bash curl -X POST https://api.rigyd.com/api/conversions/abc123.../simulate \ -H "Authorization: Bearer rgyd_live_..." \ -H "Content-Type: application/json" \ -d '{"scene":"drop"}' ``` * JavaScript ```js const res = await fetch( `https://api.rigyd.com/api/conversions/${jobId}/simulate`, { method: 'POST', headers: { Authorization: `Bearer ${process.env.RIGYD_API_KEY}`, 'Content-Type': 'application/json', }, body: JSON.stringify({ scene: 'drop' }), }, ); const { data } = await res.json(); ``` * Python ```python import os, requests res = requests.post( f"https://api.rigyd.com/api/conversions/{job_id}/simulate", headers={"Authorization": f"Bearer {os.environ['RIGYD_API_KEY']}"}, json={"scene": "drop"}, ) sim = res.json()["data"] ``` ## Response — `202 Accepted` [Section titled “Response — 202 Accepted”](#response--202-accepted) ```json { "data": { "id": "sim_xyz...", "status": "queued", "filename": "toolbox.glb", "progress": 0, "job_type": "simulate_usd", "credits_charged": 0, "source_job_id": "abc123...", "createdAt": "2026-05-06T12:05:00.000Z" } } ``` The simulation runs as its own job — poll [`GET /api/conversions/:id`](/jobs/retrieve) on the returned `id`. When `status` is `completed`, the response includes `output.sim_video`, `output.sim_gif`, and `output.sim_log` URLs. Note Simulations also appear nested under the source job’s `simulations[]` array when you fetch the parent. ## Errors specific to this endpoint [Section titled “Errors specific to this endpoint”](#errors-specific-to-this-endpoint) | Status | Body | When | | ------ | --------------------------------------------------------------- | -------------------------------------- | | `404` | `{ "error": "Conversion job not found" }` | Bad `id` or not your job | | `409` | `{ "error": "Source job must be completed before simulation" }` | Parent is still running / failed | | `422` | `{ "error": "Cannot simulate a simulation job" }` | `:id` was already a `simulate_usd` job | | `502` | `{ "error": "Simulation service unavailable: ..." }` | Upstream physics service is down | # Text → SimReady > Describe what you want and Rigyd generates a SimReady USD asset. 2 credits. Send a short text description and get back a SimReady USD asset. | | | | ---------------- | -------------------------------------------------------------- | | **Method** | `POST` | | **Path** | `/api/conversions/generate` | | **Content-type** | `multipart/form-data` (or `application/x-www-form-urlencoded`) | | **Job type** | `text_to_simready` | | **Credits** | 2 | ## Request fields [Section titled “Request fields”](#request-fields) | Field | Type | Required | Notes | | ----------------- | ------- | -------- | -------------------------------------------- | | `prompt` | string | yes | Up to 500 characters. | | `face_limit` | int | no | Cap output mesh face count. | | `model_version` | string | no | Override generation model (advanced). | | `threshold` | float | no | CoACD concavity threshold (advanced). | | `llm_provider` | string | no | Override material-identification LLM. | | `skip_validation` | boolean | no | Skip USD schema validation. Default `false`. | Caution Send `prompt` *or* `images[]`, never both — mixing the two returns `422`. For images, see [2D → SimReady](/conversions/2d-to-simready). ## Examples [Section titled “Examples”](#examples) * curl ```bash curl -X POST https://api.rigyd.com/api/conversions/generate \ -H "Authorization: Bearer rgyd_live_..." \ -F "prompt=a red metal toolbox with a black handle" ``` * JavaScript ```js const form = new FormData(); form.append('prompt', 'a red metal toolbox with a black handle'); const res = await fetch('https://api.rigyd.com/api/conversions/generate', { method: 'POST', headers: { Authorization: `Bearer ${process.env.RIGYD_API_KEY}` }, body: form, }); const { data } = await res.json(); ``` * Python ```python import os, requests res = requests.post( "https://api.rigyd.com/api/conversions/generate", headers={"Authorization": f"Bearer {os.environ['RIGYD_API_KEY']}"}, data={"prompt": "a red metal toolbox with a black handle"}, ) job = res.json()["data"] ``` ## Response — `202 Accepted` [Section titled “Response — 202 Accepted”](#response--202-accepted) ```json { "data": { "id": "abc123...", "status": "queued", "filename": "a red metal toolbox with a black handle", "progress": 0, "job_type": "text_to_simready", "credits_charged": 2, "createdAt": "2026-05-06T12:00:00.000Z" } } ``` The prompt is stored verbatim as `filename` so you can recognise the job in lists. The downloaded USD uses a slugified version (e.g. `a_red_metal_toolbox.usd`). ## Tips [Section titled “Tips”](#tips) * **Short, concrete prompts work best.** `"a red metal toolbox"` outperforms `"some kind of container, perhaps for tools, maybe red"`. * **Include material cues** (`metal`, `wood`, `plastic`) — they feed material identification. * **One object per prompt.** Compound scenes are not supported. ## Next: [poll the job](/jobs/retrieve) → [download the USD](/jobs/download). [Section titled “Next: poll the job → download the USD.”](#next-poll-the-job--download-the-usd) # Download result > Stream the SimReady USD (or MJCF) for a completed conversion as a ZIP. Download the result of a completed conversion as a ZIP archive. | | | | ----------- | ----------------------------- | | **Method** | `GET` | | **Path** | `/api/conversions/:id/result` | | **Returns** | streaming `application/zip` | ## Query parameters [Section titled “Query parameters”](#query-parameters) | Param | Values | Default | Notes | | -------- | ---------------------- | ------- | ----------------------------------------------------------------------- | | `format` | `usd` · `mjcf` · `all` | `all` | `usd` = USD + textures only. `mjcf` = MuJoCo XML package. `all` = both. | ## Examples [Section titled “Examples”](#examples) * curl ```bash curl -L "https://api.rigyd.com/api/conversions/abc123.../result?format=usd" \ -H "Authorization: Bearer rgyd_live_..." \ -o simready.zip ``` * JavaScript ```js import fs from 'node:fs'; import { Readable } from 'node:stream'; const res = await fetch( `https://api.rigyd.com/api/conversions/${jobId}/result?format=usd`, { headers: { Authorization: `Bearer ${process.env.RIGYD_API_KEY}` } }, ); if (!res.ok) throw new Error(`HTTP ${res.status}`); await new Promise((resolve, reject) => { Readable.fromWeb(res.body) .pipe(fs.createWriteStream('simready.zip')) .on('finish', resolve).on('error', reject); }); ``` * Python ```python import os, requests with requests.get( f"https://api.rigyd.com/api/conversions/{job_id}/result", headers={"Authorization": f"Bearer {os.environ['RIGYD_API_KEY']}"}, params={"format": "usd"}, stream=True, ) as res: res.raise_for_status() with open("simready.zip", "wb") as out: for chunk in res.iter_content(chunk_size=1024 * 1024): out.write(chunk) ``` ## ZIP contents [Section titled “ZIP contents”](#zip-contents) | `format` | Contents | | -------- | ----------------------------------------------------- | | `usd` | `*.usd` + `*.png` / `*.jpg` textures referenced by it | | `mjcf` | MuJoCo `*.xml` + meshes + textures | | `all` | Both, in separate sub-directories | ## Errors specific to this endpoint [Section titled “Errors specific to this endpoint”](#errors-specific-to-this-endpoint) | Status | Body | When | | ------ | ------------------------------------------------------------------ | -------------------------------------------------------------- | | `400` | `{ "error": "Invalid format. Expected one of: usd, mjcf, all" }` | Bad `format` value | | `404` | `{ "error": "Conversion job not found" }` | Bad `id` or not your job | | `404` | `{ "error": "MJCF package is not available for this conversion" }` | `format=mjcf` but the pipeline did not produce an MJCF package | | `409` | `{ "error": "Job not yet completed", "status": "running" }` | Job is still in flight | | `502` | upstream fetch failure | Underlying media store unreachable | Tip Result downloads are unauthenticated *after* the redirect — the URL is signed and short-lived. Don’t cache them long-term; re-call this endpoint when you need the result again. # List jobs > Paginated list of your conversion jobs, optionally filtered by job type. List your conversion jobs, newest first. | | | | ---------- | ------------------ | | **Method** | `GET` | | **Path** | `/api/conversions` | Simulation jobs are excluded by default — they are surfaced inline on each parent job’s `simulations[]` array. To include them explicitly, pass `job_type=simulate_usd`. ## Query parameters [Section titled “Query parameters”](#query-parameters) | Param | Type | Default | Notes | | ---------- | ------ | ------- | ------------------------------------------------------------------------------------------------------------------------------------ | | `page` | int | `1` | 1-indexed page number. | | `pageSize` | int | `25` | Max `100`. | | `job_type` | string | — | Comma-separated. One or more of `glb_to_simready`, `text_to_simready`, `image_to_simready`, `multiview_to_simready`, `simulate_usd`. | ## Examples [Section titled “Examples”](#examples) * curl ```bash curl "https://api.rigyd.com/api/conversions?page=1&pageSize=25" \ -H "Authorization: Bearer rgyd_live_..." # Only text and image jobs curl "https://api.rigyd.com/api/conversions?job_type=text_to_simready,image_to_simready" \ -H "Authorization: Bearer rgyd_live_..." ``` * JavaScript ```js const res = await fetch( 'https://api.rigyd.com/api/conversions?page=1&pageSize=25', { headers: { Authorization: `Bearer ${process.env.RIGYD_API_KEY}` } }, ); const { data, meta } = await res.json(); console.log(`${data.length} of ${meta.total} jobs`); ``` * Python ```python import os, requests res = requests.get( "https://api.rigyd.com/api/conversions", headers={"Authorization": f"Bearer {os.environ['RIGYD_API_KEY']}"}, params={"page": 1, "pageSize": 25}, ) body = res.json() jobs, meta = body["data"], body["meta"] ``` ## Response [Section titled “Response”](#response) ```json { "data": [ { "id": "abc123...", "physiq_job_id": "phy_...", "status": "completed", "filename": "toolbox.glb", "file_size_bytes": 1245678, "stage": "export", "progress": 100, "job_type": "glb_to_simready", "credits_charged": 1, "createdAt": "2026-05-06T12:00:00.000Z", "updatedAt": "2026-05-06T12:01:32.000Z" } ], "meta": { "page": 1, "pageSize": 25, "pageCount": 4, "total": 87 } } ``` This is the trimmed list shape — to get input/output URLs, the `preprocess` component, simulations, and the full report, fetch a single job with [`GET /api/conversions/:id`](/jobs/retrieve). # Pricing > Look up the current credit cost for each conversion type. Returns the credit cost for each conversion type. Costs are read from the API at request time so this endpoint is the source of truth — your client should call it instead of hard-coding numbers. | | | | ---------- | -------------------------- | | **Method** | `GET` | | **Path** | `/api/conversions/pricing` | ## Examples [Section titled “Examples”](#examples) * curl ```bash curl https://api.rigyd.com/api/conversions/pricing \ -H "Authorization: Bearer rgyd_live_..." ``` * JavaScript ```js const res = await fetch('https://api.rigyd.com/api/conversions/pricing', { headers: { Authorization: `Bearer ${process.env.RIGYD_API_KEY}` }, }); const { data } = await res.json(); // data.glb_to_simready === 1 ``` * Python ```python import os, requests pricing = requests.get( "https://api.rigyd.com/api/conversions/pricing", headers={"Authorization": f"Bearer {os.environ['RIGYD_API_KEY']}"}, ).json()["data"] ``` ## Response [Section titled “Response”](#response) ```json { "data": { "glb_to_simready": 1, "text_to_simready": 2, "image_to_simready": 3, "multiview_to_simready": 3, "simulate_usd": 0 } } ``` | Job type | Credits | Endpoint | | ----------------------- | ------- | ---------------------------------------------------------------------------- | | `glb_to_simready` | 1 | [`POST /api/conversions`](/conversions/3d-to-simready) | | `text_to_simready` | 2 | [`POST /api/conversions/generate`](/conversions/text-to-simready) (prompt) | | `image_to_simready` | 3 | [`POST /api/conversions/generate`](/conversions/2d-to-simready) (1 image) | | `multiview_to_simready` | 3 | [`POST /api/conversions/generate`](/conversions/2d-to-simready) (2-4 images) | | `simulate_usd` | 0 | [`POST /api/conversions/:id/simulate`](/conversions/simulate) | Costs are deducted atomically when a job is accepted (`202`) and refunded automatically when a job ends in `failed`. # Retrieve a job > Get the current status and full payload of a single conversion job. Fetch the current state of a single conversion job. This is what you poll after submitting. | | | | ---------- | ---------------------- | | **Method** | `GET` | | **Path** | `/api/conversions/:id` | The `:id` is the value returned in `data.id` from any conversion submission ([3D](/conversions/3d-to-simready), [2D](/conversions/2d-to-simready), [Text](/conversions/text-to-simready), [Simulate](/conversions/simulate)). The endpoint also synchronises the latest status from the upstream pipeline before returning, so you always get a fresh `progress`, `stage`, and `status`. ## Examples [Section titled “Examples”](#examples) * curl ```bash curl https://api.rigyd.com/api/conversions/abc123... \ -H "Authorization: Bearer rgyd_live_..." ``` * JavaScript ```js // Simple polling loop async function waitFor(jobId) { while (true) { const res = await fetch(`https://api.rigyd.com/api/conversions/${jobId}`, { headers: { Authorization: `Bearer ${process.env.RIGYD_API_KEY}` }, }); const { data } = await res.json(); if (data.status === 'completed') return data; if (data.status === 'failed') throw new Error(data.error || 'failed'); await new Promise((r) => setTimeout(r, 3000)); } } ``` * Python ```python import os, time, requests def wait_for(job_id): headers = {"Authorization": f"Bearer {os.environ['RIGYD_API_KEY']}"} while True: data = requests.get( f"https://api.rigyd.com/api/conversions/{job_id}", headers=headers, ).json()["data"] if data["status"] == "completed": return data if data["status"] == "failed": raise RuntimeError(data.get("error") or "failed") time.sleep(3) ``` ## Response [Section titled “Response”](#response) ```json { "data": { "id": "abc123...", "physiq_job_id": "phy_...", "status": "completed", "filename": "toolbox.glb", "file_size_bytes": 1245678, "stage": "export", "progress": 100, "error": null, "timing": { "queued_at": "2026-05-06T12:00:01.000Z", "started_at": "2026-05-06T12:00:05.000Z", "completed_at": "2026-05-06T12:01:32.000Z" }, "parameters": { "target_triangle_count": 50000 }, "report": { /* validation + pipeline metadata */ }, "job_type": "glb_to_simready", "credits_charged": 1, "input": { "model": { "url": "https://assets.rigyd.com/...", "name": "toolbox.glb" }, "images": [], "metadata": null }, "preprocess": { "status": "skipped", "steps": null, "started_at": null, "completed_at": null, "error": null, "input_stats": null, "telemetry": null, "intermediate_glb": null }, "output": { "model": { "url": "https://assets.rigyd.com/.../toolbox.usd", "name": "toolbox.usd", "size": 982341 }, "textures": [ { "url": "https://assets.rigyd.com/.../diffuse.png", "name": "diffuse.png" } ], "mjcf_package": { "url": "...", "name": "toolbox-mjcf.zip", "size": 1234 }, "sim_video": null, "sim_gif": null, "sim_log": null }, "source_job": null, "simulations": [], "createdAt": "2026-05-06T12:00:00.000Z", "updatedAt": "2026-05-06T12:01:32.000Z" } } ``` See [Job lifecycle](/reference/job-lifecycle) for the full status enum, the meaning of each field, and how `preprocess` relates to non-GLB inputs. Tip **Polling cadence**: every 3-5 seconds is plenty. Most jobs finish under two minutes. We do not currently rate-limit polling but you should still back off when not actively waiting on a result. ## When the job is done [Section titled “When the job is done”](#when-the-job-is-done) Continue to [Download result](/jobs/download). # Quickstart > Mint an API key, submit your first conversion, poll for completion, and download a SimReady USD — in five minutes. This guide takes you from zero to a downloaded SimReady USD in five minutes. 1. **Create an account and mint an API key.** Sign up at **[app.rigyd.com](https://app.rigyd.com)**, then go to **Settings → API Keys** and click **Create key**. Copy the `rgyd_live_...` token — you only see it once. Caution Treat the key like a password. It can spend your credits. 2. **Submit a conversion.** Send any 3D file you have. `glb` is the fastest path because it skips preprocessing. * curl ```bash curl -X POST https://api.rigyd.com/api/conversions \ -H "Authorization: Bearer rgyd_live_..." \ -F "file=@./model.glb" ``` * JavaScript ```js const form = new FormData(); form.append('file', new Blob([await fs.readFile('model.glb')]), 'model.glb'); const res = await fetch('https://api.rigyd.com/api/conversions', { method: 'POST', headers: { Authorization: 'Bearer rgyd_live_...' }, body: form, }); const { data } = await res.json(); console.log(data.id); // → "abc123..." ``` * Python ```python import requests with open("model.glb", "rb") as f: res = requests.post( "https://api.rigyd.com/api/conversions", headers={"Authorization": "Bearer rgyd_live_..."}, files={"file": ("model.glb", f, "model/gltf-binary")}, ) job_id = res.json()["data"]["id"] ``` You get back `202 Accepted`: ```json { "data": { "id": "abc123...", "status": "queued", "filename": "model.glb", "progress": 0, "job_type": "glb_to_simready", "credits_charged": 1, "createdAt": "2026-05-06T12:00:00.000Z" } } ``` 3. **Poll until it’s done.** ```bash curl https://api.rigyd.com/api/conversions/abc123... \ -H "Authorization: Bearer rgyd_live_..." ``` Status moves through `submitting → preprocessing → queued → running → completed | failed`. Most jobs finish in under two minutes. See [Job lifecycle](/reference/job-lifecycle) for the full payload shape. 4. **Download the result.** ```bash curl -L "https://api.rigyd.com/api/conversions/abc123.../result?format=usd" \ -H "Authorization: Bearer rgyd_live_..." \ -o simready.zip ``` The ZIP contains a `.usd` and its textures. Drop it into Isaac Sim, MuJoCo, or any USD-aware tool. ## Next steps [Section titled “Next steps”](#next-steps) * **Try the other modes**: [2D → SimReady](/conversions/2d-to-simready), [Text → SimReady](/conversions/text-to-simready), [Simulate](/conversions/simulate) * **Build a long-running integration**: read [Job lifecycle](/reference/job-lifecycle) for the full status flow and `preprocess` component shape * **Hand this site to your AI agent**: point it at [`https://docs.rigyd.com/llms-full.txt`](https://docs.rigyd.com/llms-full.txt) # Errors > Every status code the Rigyd API returns, with example bodies and how to react. Errors come back as JSON with an `error` field. Some endpoints add extra context (`status`, `details`). ```json { "error": "Insufficient credits" } ``` ## Status codes [Section titled “Status codes”](#status-codes) | Status | Meaning | Example body | | ------ | --------------------------------- | --------------------------------------------------------------------------- | | `400` | Bad request | `{ "error": "Unsupported format: .xyz. Allowed: glb, gltf, ..." }` | | `400` | Bad ZIP layout | `{ "error": "Archive must contain exactly one .obj/.gltf/.usd file" }` | | `400` | Invalid filename | `{ "error": "Invalid filename" }` | | `400` | Invalid `format` on download | `{ "error": "Invalid format. Expected one of: usd, mjcf, all" }` | | `401` | Missing / invalid auth | `{ "error": "Authentication required" }` · `{ "error": "Invalid API key" }` | | `402` | Out of credits | `{ "error": "Insufficient credits" }` | | `404` | Not found / not your job | `{ "error": "Conversion job not found" }` | | `404` | MJCF unavailable | `{ "error": "MJCF package is not available for this conversion" }` | | `409` | Wrong job state | `{ "error": "Job not yet completed", "status": "running" }` | | `409` | Source not completed for simulate | `{ "error": "Source job must be completed before simulation" }` | | `413` | Image too big | `{ "error": "Image too large (max 10MB): front.jpg" }` | | `422` | Conflicting fields on `/generate` | `{ "error": "Provide either a text prompt or image(s), not both" }` | | `422` | Missing both prompt and images | `{ "error": "Provide a text prompt or at least one image" }` | | `422` | Prompt too long | `{ "error": "Prompt must be 500 characters or less" }` | | `422` | Too many images | `{ "error": "Maximum 4 images are supported" }` | | `422` | Bad image format | `{ "error": "Unsupported image format: foo.tiff" }` | | `422` | Cannot simulate a simulation | `{ "error": "Cannot simulate a simulation job" }` | | `502` | Upstream pipeline unavailable | `{ "error": "Conversion service unavailable: ..." }` | | `502` | Generation service down | `{ "error": "Generation service unavailable: ..." }` | | `502` | Simulation service down | `{ "error": "Simulation service unavailable: ..." }` | ## Idempotency and credits [Section titled “Idempotency and credits”](#idempotency-and-credits) * **Credit reservation is atomic** — a `202 Accepted` means the credit is held, not yet spent. * **Failed jobs auto-refund** — there’s an internal `refund_applied_at` guard so retries don’t double-refund. * **`401` and `400` never charge** — the credit is only reserved after auth + format validation pass. ## How to react [Section titled “How to react”](#how-to-react) | You see… | Do this | | ------------- | --------------------------------------------------------------------------------------- | | `401` | Check the key. Was it revoked? Has it expired? Is the prefix correct (`rgyd_live_`)? | | `402` | Top up at [app.rigyd.com](https://app.rigyd.com). Show the user — don’t silently retry. | | `409` (poll) | Keep polling. The job isn’t done yet. | | `422` | Surface the message — it’s a request-shape issue and the fix is on the caller side. | | `502` / `5xx` | Retry with exponential backoff. The credit is auto-refunded if the job fails. | Note Rate limiting is not enforced today but may be added without bumping the API version. Plan for `429 Too Many Requests` with a `Retry-After` header — that’s the shape we’ll use when we ship it. # Job lifecycle > Status enum, preprocessing stages, and the full payload shape returned by GET /api/conversions/:id. Every conversion follows the same lifecycle, regardless of whether you submitted a 3D file, image, text prompt, or simulation request. ## Status flow [Section titled “Status flow”](#status-flow) ```text submitting ──► preprocessing ──► queued ──► running ──► completed └─► failed ``` | Status | Meaning | | --------------- | --------------------------------------------------------------------------------- | | `submitting` | Initial state — credit reserved, file being staged. Usually invisible to clients. | | `preprocessing` | Non-GLB input is being converted to a clean GLB on the makina-worker. | | `queued` | Submitted to the SimReady pipeline, waiting for a runner. | | `running` | Pipeline is actively processing (export → align → physics → collision → USD). | | `completed` | Done. `output.model.url` is ready. Credit was charged. | | `failed` | Something went wrong. `error` is populated. Credit is auto-refunded. | GLB inputs skip the `preprocessing` state and go straight to `queued`. ## Progress and stage [Section titled “Progress and stage”](#progress-and-stage) `progress` is `0`-`100`. `stage` is a free-form string identifying the current pipeline step (e.g. `"export"`, `"physics"`, `"collision_decompose"`). Don’t pattern-match on `stage` — treat it as a hint for human-readable progress UI. ## Preprocessing component [Section titled “Preprocessing component”](#preprocessing-component) When the input is non-GLB, the response includes a `preprocess` block: ```json { "preprocess": { "status": "completed", "steps": ["export", "align", "transform", "compress", "process"], "started_at": "2026-05-06T12:00:05.000Z", "completed_at": "2026-05-06T12:00:42.000Z", "error": null, "input_stats": { "vertex_count": 12345, "face_count": 6789, "...": "..." }, "telemetry": { "duration_ms": 37123, "...": "..." }, "intermediate_glb": { "url": "https://assets.rigyd.com/.../intermediate.glb", "name": "intermediate.glb" } } } ``` `preprocess.status` enum: `skipped` · `pending` · `running` · `completed` · `failed`. For GLB inputs `status === "skipped"` and the rest of the block is `null`. ## Full payload shape [Section titled “Full payload shape”](#full-payload-shape) ```jsonc { "data": { "id": "abc123...", "physiq_job_id": "phy_...", "status": "completed", "filename": "toolbox.glb", "file_size_bytes": 1245678, "stage": "export", "progress": 100, "error": null, "timing": { "queued_at": "...", "started_at": "...", "completed_at": "..." }, "parameters": { // Echo of fields you sent (target_triangle_count, prompt, etc.) }, "report": { // Pipeline + validation report (mass, friction, validation results, ...) }, "job_type": "glb_to_simready", "credits_charged": 1, "input": { "model": { "url": "...", "name": "toolbox.glb" }, // 3D submissions "images": [{ "url": "...", "name": "front.jpg" }], // Generate (images) "metadata": null // Set for ZIP uploads }, "preprocess": { /* see above */ }, "output": { "model": { "url": "...", "name": "toolbox.usd", "size": 982341 }, "textures": [{ "url": "...", "name": "diffuse.png" }], "mjcf_package": { "url": "...", "name": "toolbox-mjcf.zip", "size": 1234 }, "sim_video": null, // populated on simulate_usd jobs "sim_gif": null, "sim_log": null }, "source_job": null, // populated on simulate_usd jobs (points back at the parent) "simulations": [], // simulations[] of this job (children) "createdAt": "2026-05-06T12:00:00.000Z", "updatedAt": "2026-05-06T12:01:32.000Z" } } ``` Tip **Polling cadence**: 3-5 seconds while a job is `preprocessing` / `queued` / `running` is plenty. Most jobs finish in under two minutes. ## Refund guarantees [Section titled “Refund guarantees”](#refund-guarantees) A failed job is refunded **once** — Rigyd uses an internal `refund_applied_at` guard so retries don’t double-refund. You don’t need to track this client-side. Just check `credits_charged` on the final job payload. # Supported formats > Which 3D file formats Rigyd accepts, what triggers preprocessing, and how to package multi-file inputs as a ZIP. ## Input formats — 3D [Section titled “Input formats — 3D”](#input-formats--3d) | Extension | Preprocessing? | Notes | | --------- | -------------- | ------------------------------------------------------------- | | `.glb` | No (fastest) | Direct path to the SimReady pipeline. | | `.gltf` | Yes | Send as `.zip` if it has external `.bin` / textures. | | `.fbx` | Yes | Single-file binary FBX. | | `.obj` | Yes | Send as `.zip` to include the `.mtl` and textures. | | `.stl` | Yes | Geometry only — no materials. | | `.ply` | Yes | Geometry only. | | `.usd` | Yes | Single-file USD. | | `.usda` | Yes | ASCII USD. | | `.usdc` | Yes | Crate-binary USD. | | `.usdz` | Yes | USD-zipped (Pixar standard). | | `.zip` | Yes | Multi-file bundle. See [ZIP packaging](#zip-packaging) below. | When preprocessing runs, Rigyd uses Blender to convert the file to a clean GLB before submitting it to the SimReady pipeline. You don’t have to do anything — the API picks the right path based on the extension. ## Input formats — images (Generate) [Section titled “Input formats — images (Generate)”](#input-formats--images-generate) `.jpg`, `.jpeg`, `.png`, `.webp`. Max 10 MB per image. Up to 4 images per request. ## ZIP packaging [Section titled “ZIP packaging”](#zip-packaging) Use `.zip` when your model has companion files that must travel together: | Primary file | What goes in the ZIP | | ------------ | ----------------------------------------------------- | | `*.obj` | The `.obj` + its `.mtl` + every texture it references | | `*.gltf` | The `.gltf` + its `.bin` + every texture | | `*.usd[a]` | The `.usd` + every file referenced via `@./...@` | Rules: * The archive must contain **exactly one** primary file (`.obj` / `.gltf` / `.usd[a]`). * Texture / mesh references must be **relative** (`./diffuse.png`, not absolute paths). * The API validates the archive on upload — invalid layouts fail at `400` before any credit is reserved. Note When you send a ZIP, the response payload includes `input.metadata` describing the detected primary file, the texture set, and the file tree. See [`GET /api/conversions/:id`](/jobs/retrieve). ## File size limits [Section titled “File size limits”](#file-size-limits) 500 MB per upload (server-side nginx limit). For larger assets, decimate locally first.