BitSoul3D Engine API
Everything the workspaces can do, from your own code: text and images to 3D, auto-rig and animation, PBR texturing, retopology, part splitting, format export and Gaussian splats. One API key, plain JSON over HTTPS, credits charged per job and refunded automatically when a job fails.
https://bitsoulhosting.com/marketplace/apiPOST /gen/create with an op and its fields. You get a task_id back within a second or two; credits are reserved up-front.GET /gen/task/:id every 3 seconds. progress climbs to 100 and status becomes done or failed.GET /gen/file/:id/:n streams each result file (GLB, FBX, PNG, USDZ, STL, .splat) from our servers, owner-only.The engine never holds a connection open while it works. Your client drives the waiting, so a job survives network blips, and the same key works from a build script, a game-engine plugin, a CI pipeline or a browser.
#Authentication
Every engine request carries your member API key in the X-API-Key header. There are no tokens to refresh and no OAuth dance.
X-API-Key: YOUR_API_KEY Content-Type: application/json User-Agent: MyStudioApp/1.0 ([email protected])
Where your key lives
- Sign in (or join free) on the account page.
- Open your dashboard. The API key row shows the key masked, with reveal and copy controls.
- Regenerate issues a new key and retires the old one immediately. Changing your password also rotates the key, so update any script or plugin that stores it.
Send a real User-Agent
Our edge blocks requests whose User-Agent is empty or looks like a generic scraper (the default strings of common command-line and scripting HTTP clients are on that list). Name your app, for example MyStudioApp/1.0 ([email protected]), and you will never see a 403. Browsers set this header for you.
Keep the key server-side
The key is as powerful as your login: it spends your credits and reads your generations. Put it in an environment variable or a secrets store, never in shipped client code or a public repository. If it leaks, regenerate it from the account page.
A missing or invalid key returns 401 with {"error": "..."}. Generations belong to the key that created them; another member's task id simply returns 404.
#Quick start: create, poll, download
The three calls below turn a sentence into a downloadable GLB. The same pattern applies to every operation: only the op and its fields change.
BASE=https://bitsoulhosting.com/marketplace/api KEY=your_api_key UA="MyStudioApp/1.0 ([email protected])" # 1. Create a job — credits are reserved now, refunded if it fails curl -s -A "$UA" -H "X-API-Key: $KEY" -H "Content-Type: application/json" \ -d '{"op":"text-to-3d","prompt":"a weathered bronze compass, game prop","quality":"standard","visibility":"private"}' \ "$BASE/gen/create" # → {"task_id":"6f1c2d…","cost":20,"status":"running"} # 2. Poll every 3 seconds until status is "done" or "failed" TASK=6f1c2d… while true; do BODY=$(curl -s -A "$UA" -H "X-API-Key: $KEY" "$BASE/gen/task/$TASK") echo "$BODY" | grep -q '"status":"running"' || break sleep 3 done echo "$BODY" # → {"task_id":"…","op":"text-to-3d","status":"done","progress":100,"cost":20, # "files":[{"name":"model.glb","file":"…","bytes":2481920,"mime":"model/gltf-binary"}]} # 3. Download file 0 curl -s -A "$UA" -H "X-API-Key: $KEY" -o compass.glb "$BASE/gen/file/$TASK/0"
// Node 18+ (built-in fetch). In a browser, drop the fs and User-Agent lines — the browser sets its own. import fs from 'node:fs/promises'; const BASE = 'https://bitsoulhosting.com/marketplace/api'; const H = { 'X-API-Key': process.env.BS3D_KEY, 'Content-Type': 'application/json', 'User-Agent': 'MyStudioApp/1.0 ([email protected])' }; const sleep = ms => new Promise(r => setTimeout(r, ms)); async function create(body) { for (let attempt = 0; attempt < 6; attempt++) { const r = await fetch(`${BASE}/gen/create`, { method: 'POST', headers: H, body: JSON.stringify(body) }); const j = await r.json(); if (r.ok) return j; // { task_id, cost, status: 'running' } if (r.status === 429 && j.retry_after) { await sleep(j.retry_after * 1000); continue; } // lane busy — nothing charged throw Object.assign(new Error(j.error || `HTTP ${r.status}`), { status: r.status, data: j }); } throw new Error('The engine stayed busy — try again in a minute.'); } async function waitFor(taskId) { let errors = 0; for (;;) { await sleep(3000); const r = await fetch(`${BASE}/gen/task/${taskId}`, { headers: H }); if (!r.ok) { if (++errors >= 20) throw new Error('Lost track of the job'); continue; } const t = await r.json(); errors = 0; if (t.status === 'done') return t; if (t.status === 'failed') throw new Error(t.error || 'Job failed — credits refunded'); console.log(`${t.progress}%`); } } async function download(taskId, n = 0) { const r = await fetch(`${BASE}/gen/file/${taskId}/${n}`, { headers: H }); if (!r.ok) throw new Error(`HTTP ${r.status}`); return Buffer.from(await r.arrayBuffer()); } const { task_id } = await create({ op: 'text-to-3d', prompt: 'a weathered bronze compass, game prop', quality: 'standard', visibility: 'private' }); const task = await waitFor(task_id); await fs.writeFile(task.files[0].name, await download(task_id, 0)); // model.glb
# Python 3.8+ with the requests package (pip install requests). # The explicit User-Agent matters: the package's default string is blocked at our edge. import os, time, requests BASE = 'https://bitsoulhosting.com/marketplace/api' H = {'X-API-Key': os.environ['BS3D_KEY'], 'User-Agent': 'MyStudioApp/1.0 ([email protected])'} def create(body): for _ in range(6): r = requests.post(f'{BASE}/gen/create', json=body, headers=H, timeout=60) j = r.json() if r.ok: return j # {'task_id': ..., 'cost': 20, 'status': 'running'} if r.status_code == 429 and j.get('retry_after'): time.sleep(j['retry_after']); continue # lane busy — nothing charged raise RuntimeError(f"{r.status_code}: {j.get('error')}") raise RuntimeError('The engine stayed busy — try again in a minute.') def wait_for(task_id): errors = 0 while True: time.sleep(3) r = requests.get(f'{BASE}/gen/task/{task_id}', headers=H, timeout=60) if not r.ok: errors += 1 if errors >= 20: raise RuntimeError('Lost track of the job') continue t = r.json(); errors = 0 if t['status'] == 'done': return t if t['status'] == 'failed': raise RuntimeError(t.get('error') or 'Job failed — credits refunded') print(f"{t.get('progress', 0)}%") def download(task_id, n=0): r = requests.get(f'{BASE}/gen/file/{task_id}/{n}', headers=H, timeout=300) r.raise_for_status() return r.content job = create({'op': 'text-to-3d', 'prompt': 'a weathered bronze compass, game prop', 'quality': 'standard', 'visibility': 'private'}) task = wait_for(job['task_id']) with open(task['files'][0]['name'], 'wb') as f: f.write(download(job['task_id'], 0)) # model.glb
Job runtimes vary from a few seconds for images to a few minutes for cinematic-quality 3D. Keep the 3-second interval; polling faster does not make the engine finish sooner and can trip the per-key poll limiter.
#Operations & costs
Costs are in engine credits and come straight from GET /api/gen/config, the same public endpoint the workspaces read. Your plan's monthly allowance is spent first, then purchased packs. rig-check and import are free.
| Op | What it makes | Credits | Result files |
|---|---|---|---|
| 3D generation | |||
text-to-3d | Text prompt to a textured 3D model | … | model.glb (model.fbx when quad) |
image-to-3d | One image to a 3D model | … | model.glb |
photo-scan | Two to four photos of one object to a model | … | model.glb |
splat | One photo to a Gaussian splat scene | … | scene.splat |
| Images | |||
text-to-image | Prompt to an image, priced by tier (see below) | … | image-1.png |
image-edit | Edit, restyle or fuse up to three reference images, priced by tier | … | image-1.png |
multiview | One image to front / left / back / right views | … | front.png left.png back.png right.png |
multiview-edit | Re-prompt one or more views of a turntable sheet | … | four view files again |
| Mesh tools | |||
texture | Paint full PBR textures onto a model | … | model.glb |
retopo | Retopology: smart (clean loops, optional quads) or basic (fast decimation) | … | model.glb (model.fbx when quad) |
ai-segment | Split a model into named parts | … | model.glb with separated parts |
smart-segment | Part split from an image or a model, with a mask and part names | … | parts.glb mask.png + output.parts[] |
complete | Close or rebuild the hidden geometry of split parts | … | model.glb |
restyle | Rebuild as a brick build, voxels, cells or a block world | … | model.glb |
convert | Export to USDZ, FBX, OBJ, STL, GLTF or 3MF | … | export.<ext> |
| Rig & motion | |||
rig-check | Is this model riggable, and as what? | … | no files — output.riggable, output.rig_type |
rig | Auto-rig: skeleton plus skin weights | … | model.glb or model.fbx |
retarget | Apply up to five motion presets to a rigged model | … | clip-1.glb … clip-5.glb |
| Utilities | |||
import | Register an uploaded model as a task so every tool above can use it | … | model.glb |
Image tiers
text-to-image and image-edit take a style tier. The tier sets the cost and which size values are allowed; 4K adds a size surcharge.
style | Tier | Credits | Sizes |
|---|---|---|---|
turbo | Fast | see config | 1K |
standard | Standard | see config | 2K |
pro | Pro | see config | 2K |
ultra | Ultra | see config | 2K, 4K |
Variable-cost operations
| Op | Mode | Credits | Notes |
|---|---|---|---|
retopo | mode: "smart" | … | face_limit see config; quad output up to see config faces |
retopo | mode: "basic" | … | face_limit see config |
complete | mode: "ai" | … | rebuilds hidden geometry |
complete | mode: "cap" | … | quick cap of open parts |
smart-segment | from an image | … | image_token or image_task |
smart-segment | from a model | … | input = one of your model tasks |
convert | with any advanced option | … | quad, symmetry, texture size or format, pivot, scale, orientation, preset, animation |
text-to-image / image-edit | size: "4K" | … | on top of the Ultra tier |
retarget | per preset | … | up to five presets per call |
ai-segment | semantic: true | … | on top of the base cost; labels parts by meaning |
3D quality ladder
The quality field on text-to-3d, image-to-3d and photo-scan moves the price up or down from the op's base cost. These steps are fixed and not listed in config; the cost in every create response is the final amount charged, so show that to your users.
quality | What you get | Credits |
|---|---|---|
draft | Geometry only, no textures, fastest preview | base − 10 (never below 5) |
standard | Textured model (default) | base |
pro | Detailed texture pass | base + 10 |
cinematic | Detailed textures and detailed geometry | base + 30 (base + 10 with game_ready) |
3D add-ons
Optional booleans on text-to-3d, image-to-3d and photo-scan. Each adds to the base cost; prices come from config.addons.
| Field | Effect | Credits |
|---|---|---|
quad | Quad-dominant topology; result arrives as model.fbx | … |
low_poly | Smart low-poly output for real-time use (face_limit is kept within 500–20000) | … |
parts | Generate as separate, named parts (geometry only, no textures; takes precedence over quad) | … |
ultra_texture | Ultra texture resolution | … |
pro_texture | Not a field on generation; this is the price of texture with quality: "pro" | … |
hd_geometry | Not a field; this is the geometry upgrade included in quality: "cinematic" | … |
game_ready uses a different pipeline
With game_ready: true the engine builds a clean low-polygon game mesh (about 5000 faces). On that pipeline quad, low_poly, parts and ultra_texture are ignored and not charged, and cinematic is priced as pro.
#Request bodies
POST /api/gen/create takes a JSON object with an op plus the fields below. Fields marked optional can be omitted. Unknown fields are ignored, so send only what is listed.
Chaining: input is always one of your task ids
Mesh, rig and motion operations work on an earlier result. Pass that result's task_id as input. Image operations accept either a fresh image_token from an upload or an image_task id of an image you generated. There are no other identifiers to juggle.
Common fields
- op
- One of the operation names in the table above. Required.
- visibility
"public"or"private"optional. Public results can appear in the community gallery. Generations default to public;import,splat,multiview,multiview-edit,smart-segment,convert,completeandretopodefault to private. You can change it later (see Visibility & delete).- seed
- Integer optional where listed. Same seed and inputs reproduce the same result as closely as the engine allows.
3D generation
| Op | Body fields |
|---|---|
text-to-3d | prompt (3–1200 chars) · optional: negative (≤255) · quality draft|standard|pro|cinematic · game_ready · seed · face_limit · quad · low_poly · parts · auto_size · ultra_texture · visibility |
image-to-3d | image_token or image_task · optional: quality · game_ready · seed · face_limit · quad · low_poly · parts · auto_size · ultra_texture · align_image · autofix · visibility |
photo-scan | views object with front, left, back, right image tokens (two to four), or multiview_task = one of your multiview / multiview-edit tasks · same add-ons as text-to-3d · visibility |
splat | image_token or image_task · optional: seed |
Images
| Op | Body fields |
|---|---|
text-to-image | prompt · style turbo|standard|pro|ultra · size "1K"|"2K"|"4K" · aspect (see list) · optional: negative · template · visibility |
image-edit | image_token or image_task · prompt (required unless a template is given) · style · size · aspect · optional: refs array of up to 3 image tokens · template · negative · visibility |
multiview | image_token or image_task · optional: visibility |
multiview-edit | input = one of your multiview tasks · prompts array of up to 4 { "prompt": "...", "view": "front"|"left"|"back"|"right" } |
- aspect
1:13:22:34:33:416:99:1621:94:55:4(live list:config.aspects)- template
- Prompt presets. For
text-to-image:asset_extraction(isolate subject),character_completion(complete character),t_pose(T-pose for rigging),variants(design variations),figure(full-body figure). Forimage-edit:t_pose,character_completion,3d_enhance(enhance for 3D),variants,figure. Live list:config.templates.
Mesh tools
| Op | Body fields |
|---|---|
texture | input · optional: prompt (≤400 chars; ignored when ref_image_token is set) · quality pro|ultra (omit for Standard) · style_image_token · ref_image_token · part_names array · seed · alignment original|geometry · pbr |
retopo | input · mode smart|basic · face_limit (ranges in the cost table; out-of-range values are clamped, not rejected) · optional: quad (smart only) · bake |
ai-segment | input · optional: semantic (labels parts by meaning; adds config.addons.semantic) · with semantic only: granularity simple|balanced|detailed · split |
smart-segment | one of image_token / image_task / input (a model task) · granularity coarse|medium|fine · optional: hint |
complete | input = one of your ai-segment tasks · mode ai|cap · optional: part_names array |
restyle | input · style one of lego (brick build) · voxel · voronoi (cells) · minecraft (block world) · optional: block_size |
convert | input · format USDZ|FBX|OBJ|STL|GLTF|3MF · optional (advanced, see cost table): print_ready · quad · symmetry · face_limit · texture_size 1024|2048|4096 · texture_format JPEG|PNG|WEBP · pivot_bottom · scale · orientation +x|-x|+y|-y · fbx_preset blender|3dsmax|mixamo (FBX only) · in_place · with_animation. print_ready applies to STL and 3MF. A rig or retarget result cannot be exported as OBJ, STL or 3MF (400); use FBX, GLTF or USDZ. |
Rig & motion
| Op | Body fields |
|---|---|
rig-check | input. Free. Result in output: { "riggable": true, "rig_type": "biped" } |
rig | input · rig_type biped|quadruped|hexapod|octopod|avian|serpentine|aquatic · spec native|mixamo (default mixamo, industry-standard humanoid bone naming) · out_format glb|fbx · library current|classic (classic is for biped rigs only and always uses native bone names) |
retarget | input = one of your rig tasks · animations array of 1–5 preset names · optional: in_place · out_format glb|fbx · bake |
import | model_token from POST /api/gen/upload-model · optional: visibility |
Motion presets: the current rig line ships preset:idle, preset:walk, preset:run, preset:jump, preset:dive, preset:climb, preset:slash, preset:shoot, preset:hurt, preset:fall, preset:turn plus non-biped gaits such as preset:quadruped:walk. Rigs built with library: "classic" use the 90+ humanoid preset:biped:* moves instead. Both lists are in config.animations and config.animations_classic.
#Responses & errors
Create
{ "task_id": "6f1c2d0a-…", "cost": 20, "status": "running" }cost is the final number of credits reserved for this job, after quality, add-ons, per-view or per-animation multipliers. It is the figure to show your users.
Task
{
"task_id": "6f1c2d0a-…",
"op": "text-to-3d",
"status": "running" | "done" | "failed",
"progress": 0–100,
"cost": 20,
"error": "…", // only when failed
"files": [ // empty until done
{ "name": "model.glb", "file": "6f1c2d0a-…-0.glb", "bytes": 2481920, "mime": "model/gltf-binary" }
],
"output": { … } // rig-check: { riggable, rig_type } · smart-segment: { parts: [names] }
}Download files by their index in files: GET /api/gen/file/:task_id/0, /1 and so on. Check files[0].name before you assume a GLB: quad-mesh results and FBX rigs arrive as .fbx, exports as export.usdz and similar, splats as scene.splat.
Error codes
| HTTP | Body | Meaning | Charged? |
|---|---|---|---|
400 | { error } | Validation: unknown op, missing prompt, bad token, out-of-range value. Fix the request. | No |
401 | { error } | Key missing, invalid or inactive. | No |
402 | { error, need, allowance_left, balance } | Not enough credits. need is the job's cost; allowance_left is what remains of this month's plan allowance; balance is purchased credits. Top up on Plans & credits. | No |
404 | { error } | No such task for this key, or the file index does not exist. | — |
429 | { error, retry_after, lane } | That engine lane is busy. Wait retry_after seconds, then send the same create again. | No |
429 | { error } (no retry_after) | You already have the maximum number of running jobs (limits.max_running), or you exceeded a per-key request limit. Wait for a job to finish; do not retry in a tight loop. | No |
502 | { error } | The engine could not process the request after it was accepted. | Charged, then refunded automatically |
503 | { error } | Engine at capacity or generation temporarily disabled. Try again in a few minutes. | No |
403 | HTML, from the edge | Blocked User-Agent (empty or generic scraper string). Set a real one and retry. | No |
A task that reaches status: "failed" after creation is refunded in full, automatically, to wherever the credits came from (allowance or balance). The error string is safe to show to your users.
#Polling rules
- Poll every 3 seconds per task. The per-key poll budget (
2400requests per 15 minutes) comfortably covers three jobs at that rate; tighter loops waste it and earn a429with"Slow down.". - Concurrency: at most
limits.max_runningjobs per key (live value: see config). A create beyond that returns429withoutretry_after; wait for a job to finish. - Lane busy (
429withretry_afterandlane): nothing was charged. Sleepretry_afterseconds (clamp to 3–120), then repeat the same create. Give up after about six attempts and tell the user the engine is busy. Lanes are per-op families; the live capacities are inconfig.lanes. 402: stop, showneedversusallowance_left + balance, and point the user at top-up. Retrying will not help.- Transient poll errors (network, a non-JSON body, a 5xx from the edge): keep polling. Abandon only after about twenty consecutive failures.
progress: 99means the engine has finished and we are copying the results to our servers. Keep polling; the next response usually carriesfiles.- Create limit: 40 creates per key per 15 minutes, 30 image uploads and 10 model uploads per 15 minutes. Batch work accordingly.
- Results stay available under your account until you delete them. There is no expiry to race.
async function runJob(body, { onWait, onProgress } = {}) { let created = null; for (let i = 0; i < 6 && !created; i++) { const r = await fetch(`${BASE}/gen/create`, { method: 'POST', headers: H, body: JSON.stringify(body) }); const j = await r.json().catch(() => ({})); if (r.ok) { created = j; break; } if (r.status === 429 && j.retry_after) { const wait = Math.min(120, Math.max(3, j.retry_after | 0)); onWait && onWait(wait, j.lane); await sleep(wait * 1000); continue; } throw Object.assign(new Error(j.error || `HTTP ${r.status}`), { status: r.status, data: j }); } if (!created) throw new Error('The engine stayed busy — please try again in a minute.'); let errors = 0; for (;;) { await sleep(3000); let t; try { const r = await fetch(`${BASE}/gen/task/${created.task_id}`, { headers: H }); if (!r.ok) throw 0; t = await r.json(); errors = 0; } catch { if (++errors >= 20) throw new Error('Lost track of that job.'); continue; } onProgress && onProgress(t.progress || 0, t); if (t.status === 'done') return t; if (t.status === 'failed') throw Object.assign(new Error(t.error || 'The job failed — you were not charged.'), { task: t }); } }
#Uploads
Uploads are raw bytes in the request body, not multipart forms. Each returns a token you pass to create. Tokens are single-purpose handles, so upload once and reuse the token across several jobs.
| Endpoint | Body | Limits | Returns |
|---|---|---|---|
POST /api/gen/upload | PNG or JPEG bytes; Content-Type: image/png or image/jpeg | up to see config MB · 30 per 15 min | { "image_token": "…" } |
POST /api/gen/upload-model | GLB bytes with Content-Type: model/gltf-binary; or FBX / OBJ / STL bytes as application/octet-stream with ?ext=fbx, obj or stl | up to see config MB · 10 per 15 min | { "model_token": "…" } |
Convert WebP or other formats to PNG before uploading. A model_token becomes usable by the mesh and rig tools after one free { "op": "import", "model_token": "…" } job, whose task_id you then pass as input.
# Image → image_token → image-to-3d curl -s -A "$UA" -H "X-API-Key: $KEY" -H "Content-Type: image/png" \ --data-binary @photo.png "$BASE/gen/upload" # → {"image_token":"…"} curl -s -A "$UA" -H "X-API-Key: $KEY" -H "Content-Type: application/json" \ -d '{"op":"image-to-3d","image_token":"…","quality":"standard"}' "$BASE/gen/create" # Model → model_token → import (free) → rig curl -s -A "$UA" -H "X-API-Key: $KEY" -H "Content-Type: model/gltf-binary" \ --data-binary @hero.glb "$BASE/gen/upload-model" # → {"model_token":"…"} # FBX / OBJ / STL: name the extension and send octet-stream curl -s -A "$UA" -H "X-API-Key: $KEY" -H "Content-Type: application/octet-stream" \ --data-binary @hero.fbx "$BASE/gen/upload-model?ext=fbx" curl -s -A "$UA" -H "X-API-Key: $KEY" -H "Content-Type: application/json" \ -d '{"op":"import","model_token":"…"}' "$BASE/gen/create" # poll that task to done, then: curl -s -A "$UA" -H "X-API-Key: $KEY" -H "Content-Type: application/json" \ -d '{"op":"rig","input":"IMPORT_TASK_ID","rig_type":"biped","spec":"native","out_format":"glb","library":"current"}' "$BASE/gen/create"
const AUTH = { 'X-API-Key': process.env.BS3D_KEY, 'User-Agent': 'MyStudioApp/1.0 ([email protected])' }; async function uploadImage(path) { // PNG or JPEG const bytes = await fs.readFile(path); const type = /\.jpe?g$/i.test(path) ? 'image/jpeg' : 'image/png'; const r = await fetch(`${BASE}/gen/upload`, { method: 'POST', headers: { ...AUTH, 'Content-Type': type }, body: bytes }); const j = await r.json(); if (!r.ok) throw new Error(j.error); return j.image_token; } async function uploadModel(path) { // GLB, or FBX / OBJ / STL via ?ext= const bytes = await fs.readFile(path); const ext = path.split('.').pop().toLowerCase(); const q = ext === 'glb' ? '' : `?ext=${ext}`; const r = await fetch(`${BASE}/gen/upload-model${q}`, { method: 'POST', headers: { ...AUTH, 'Content-Type': ext === 'glb' ? 'model/gltf-binary' : 'application/octet-stream' }, body: bytes }); const j = await r.json(); if (!r.ok) throw new Error(j.error); return j.model_token; } // Photo → 3D const image_token = await uploadImage('photo.png'); const model = await waitFor((await create({ op: 'image-to-3d', image_token, quality: 'standard' })).task_id); // Your own model → import (free) → rig const model_token = await uploadModel('hero.glb'); const imported = await waitFor((await create({ op: 'import', model_token })).task_id); const rigged = await waitFor((await create({ op: 'rig', input: imported.task_id, rig_type: 'biped', spec: 'native', out_format: 'glb', library: 'current' })).task_id);
def upload_image(path): # PNG or JPEG ctype = 'image/jpeg' if path.lower().endswith(('.jpg', '.jpeg')) else 'image/png' with open(path, 'rb') as f: r = requests.post(f'{BASE}/gen/upload', data=f.read(), headers={**H, 'Content-Type': ctype}, timeout=120) r.raise_for_status() return r.json()['image_token'] def upload_model(path): # GLB, or FBX / OBJ / STL via ?ext= ext = path.rsplit('.', 1)[-1].lower() url = f'{BASE}/gen/upload-model' + ('' if ext == 'glb' else f'?ext={ext}') ctype = 'model/gltf-binary' if ext == 'glb' else 'application/octet-stream' with open(path, 'rb') as f: r = requests.post(url, data=f.read(), headers={**H, 'Content-Type': ctype}, timeout=600) r.raise_for_status() return r.json()['model_token'] # Photo → 3D token = upload_image('photo.png') model = wait_for(create({'op': 'image-to-3d', 'image_token': token, 'quality': 'standard'})['task_id']) # Your own model → import (free) → rig model_token = upload_model('hero.glb') imported = wait_for(create({'op': 'import', 'model_token': model_token})['task_id']) rigged = wait_for(create({'op': 'rig', 'input': imported['task_id'], 'rig_type': 'biped', 'spec': 'native', 'out_format': 'glb', 'library': 'current'})['task_id'])
#Files & history
| Endpoint | Returns |
|---|---|
GET /api/gen/file/:task_id/:n | The bytes of files[n] with its mime as Content-Type (model/gltf-binary, image/png, application/octet-stream for FBX, USDZ, STL and .splat). Owner-only; 404 until the task is done. Cacheable privately for a day. |
GET /api/gen/thumb/:task_id | A small WebP preview (engine render for 3D results, a 320 px downscale for images). 404 when none exists, for example a convert or rig-check. Handy for pickers and history views. |
GET /api/gen/tasks | { "tasks": [ … ] }: your 50 most recent jobs, newest first. Each item is the task shape plus visibility, likes, has_thumb, created_at and params_json (a JSON string of your own inputs, such as the prompt and preset choices, for display). |
GET /api/gen/balance | { "credits": 120, "allowance": { "total": 300, "used": 80, "left": 220 }, "tier": "pro" }: purchased credits, this billing period's plan allowance, and your plan tier. Spendable total is credits + allowance.left. |
GET /api/gen/config | Public, no key needed: enabled, every op's cost, credit packs, motion presets, rig types, lane capacities, image tiers, aspects, templates, add-on prices, retopo / complete / segment / restyle pricing and limits. Read it at start-up instead of hardcoding numbers. |
const { tasks } = await (await fetch(`${BASE}/gen/tasks`, { headers: H })).json(); const models = tasks.filter(t => t.status === 'done' && t.files[0] && /\.glb$/.test(t.files[0].name)); for (const t of models) console.log(t.task_id, t.op, t.files[0].bytes, t.created_at);
#Visibility & delete
| Endpoint | Body | Returns |
|---|---|---|
POST /api/gen/task/:id/visibility | { "visibility": "public" | "private" } | { "ok": true, "visibility": "private" }. Public results may be shown in the community gallery with a non-identifying creator handle; private ones are yours alone. |
DELETE /api/gen/task/:id | none | { "ok": true }. Removes the task, every result file and its thumbnail. This is permanent and does not refund credits, so download first. |
# make a result private curl -s -A "$UA" -H "X-API-Key: $KEY" -H "Content-Type: application/json" \ -d '{"visibility":"private"}' "$BASE/gen/task/$TASK/visibility" # delete it for good curl -s -A "$UA" -H "X-API-Key: $KEY" -X DELETE "$BASE/gen/task/$TASK"
#Limits
Live values from config.limits and config.lanes. Request-rate limits are per API key unless noted.
| Limit | Value | Applies to |
|---|---|---|
| Running jobs per key | see config | create returns 429 beyond this |
| Image upload size | see config | upload, PNG or JPEG |
| Model upload size | see config | upload-model |
| Prompt length | see config | all prompts |
| Negative prompt length | see config | negative |
| Photo-scan views | see config | photo-scan / multiview-edit prompts |
| Reference images | see config | image-edit refs |
| Creates | 40 per 15 min | POST /api/gen/create |
| Uploads | 30 images, 10 models per 15 min | upload, upload-model |
| Polls | 2400 per 15 min | GET /api/gen/task/:id |
| Edge rate | 10 requests/s per IP, burst 20; 20 open connections per IP | everything under /marketplace/; over the limit answers 429 from the edge |
Engine lanes
Each op family runs in a lane with a fixed number of simultaneous slots across all members. When a lane is full, create answers 429 with retry_after and the lane name, and nothing is charged.
| Lane | Slots |
|---|---|
see config.lanes | |
#Chaining jobs
Pipelines are just sequential creates where each input is the previous task_id. Costs add up per step; check rig-check (free) before paying for a rig, and read config.ops to show the total up-front.
const cfg = await (await fetch(`${BASE}/gen/config`)).json(); const total = cfg.ops['text-to-3d'].cost + cfg.ops.rig.cost + cfg.ops.retarget.cost * 2; console.log(`This pipeline costs ${total} credits`); const model = await runJob({ op: 'text-to-3d', prompt: 'a stylised knight, T-pose, clean silhouette', quality: 'standard' }); const check = await runJob({ op: 'rig-check', input: model.task_id }); if (!check.output || !check.output.riggable) throw new Error('Not riggable — try a clearer pose'); const rig = await runJob({ op: 'rig', input: model.task_id, rig_type: check.output.rig_type || 'biped', spec: 'native', out_format: 'glb', library: 'current' }); const anim = await runJob({ op: 'retarget', input: rig.task_id, animations: ['preset:walk', 'preset:idle'], in_place: true }); // anim.files → clip-1.glb (walk), clip-2.glb (idle) for (let i = 0; i < anim.files.length; i++) await fs.writeFile(anim.files[i].name, await download(anim.task_id, i));
Other useful chains: text-to-image with template: "t_pose" → image-to-3d → rig; ai-segment → complete; image-to-3d → retopo → convert to USDZ for AR or STL for printing; any model → restyle for a voxel or brick-build variant.
#OpenAPI spec
The whole surface is described in an OpenAPI 3.1 document: every path above, the per-op request schemas with their enums, the task and error shapes, and the X-API-Key security scheme. Import it into any OpenAPI-compatible client generator, request explorer or AI coding assistant to get typed calls for free.
Costs are deliberately not baked into the spec; it points at GET /api/gen/config so generated clients always read the live numbers.
#MCP server Coming soon
A small open-source MCP server is in the works so AI coding agents and assistants that speak the Model Context Protocol can call the engine as tools: generate a model, rig it, animate it, retopologise it, make an image, all with the same API key and the same credits. Until it ships, the HTTP API above is complete on its own, and the OpenAPI document gives any agent the full contract today.
Want early access?
Open a ticket from your account page with the subject "MCP" and we will let you know the moment the package is published.