Model Importer API
Process 3D models through a full pipeline: decimation, LoD generation, PBR texture creation, and Source 1 VMDL output. This is an asynchronous API with a submit...
# Model Importer API
Process 3D models through a full pipeline: decimation, LoD generation, PBR texture creation, and Source 1 VMDL output. This is an asynchronous API with a submit-then-poll pattern.
> **Queue API**: For unified queue management with position tracking, use the [Queue API](/wiki/tools-api/queue-api) with `tool=model`.
## Concurrency
Maximum **5 concurrent model jobs** per user. Additional requests return `429` until a slot opens.
## Step 1: Submit Job
```
POST /api/tools/model
```
Send as `multipart/form-data`.
### Parameters
| Field | Required | Description |
|---|---|---|
| **File** | | |
| `file` | Yes | 3D model file (FBX or OBJ). Max 100MB. |
| `format` | No | Input format: `fbx` (default) or `obj` |
| **Pipeline** | | |
| `pipeline` | No | `standard` (default) or `source1` (generates VMDL manifest) |
| `ratio` | No | Base decimation ratio. 0.01 - 1.0, default 0.5. Lower = fewer faces. |
| **LoD Options** | | |
| `lodLevels` | No | Additional LoD levels to generate. 0 - 5, default 0. |
| `lodFinalRatio` | No | Final lowest-detail LoD ratio. 0.001 - 1.0, default 0.15. |
| `lodMaxThreshold` | No | Switch distance for the furthest LoD. 1 - 500, default 50. |
| **PBR Textures** | | |
| `generatePbr` | No | `true` to generate normal/roughness/AO maps from uploaded color texture |
| `pbrNormalStrength` | No | 0.5 - 4.0, default 1.5 |
| `pbrRoughnessBase` | No | 100 - 240, default 180 |
| `pbrAoStrength` | No | 0 - 1.0, default 0.3 |
| **Textures** | | |
| `tex_{name}_{slot}` | No | Texture files. `name` = material name, `slot` = color, normal, roughness, metalness, emissive, ao |
| **Bone Mapping** | | |
| `skipBoneMapping` | No | `true` to process models with bones without remapping them |
| `mappingId` | No | Public mapping ID (e.g. `map_abc123xyz`) to use for bone remapping |
| `useMostPopularBoneMapping` | No | `true` to auto-select the highest-rated matching mapping |
| `autoMatchThreshold` | No | Minimum match percentage (50-100, default 80) to auto-apply a mapping |
| `skeletonTarget` | No | Target skeleton: `citizen` (default) or `human` |
### Response (202 Accepted)
```json
{
"ok": true,
"jobId": "a1b2c3d4e5f6",
"statusUrl": "/api/tools/model/a1b2c3d4e5f6",
"message": "Model processing started. Poll statusUrl for progress."
}
```
## Step 2: Poll Status
```
GET /api/tools/model/:jobId
```
Requires the same API key in the Authorization header.
### Response
```json
{
"jobId": "a1b2c3d4e5f6",
"status": "done",
"progress": [
"Starting model processing pipeline...",
"Decimating model (ratio: 0.5, format: fbx)...",
"LOD0 ready: 512 KB",
"Generating LOD1 (ratio: 0.1500, threshold: 50)...",
"LOD1 ready: 102 KB",
"Pipeline complete."
],
"result": {
"originalFaces": 15000,
"resultFaces": 7500,
"pipeline": "source1",
"lodLevels": 1,
"lods": [
{ "level": 1, "ratio": 0.15, "threshold": 50, "faces": 2250, "size": 102400 }
]
},
"downloads": {
"lod0": "/uploads/temp/123/abc_model_lod0.glb",
"lod1": "/uploads/temp/123/abc_model_lod1.glb",
"vmdl": "/uploads/temp/123/abc_model.vmdl",
"pbrTextures": "/uploads/temp/123/abc_pbr_textures.zip"
},
"downloadUrl": "/uploads/temp/123/abc_model_lod0.glb",
"pipeline": "source1",
"createdAt": 1711900000000,
"expiresAt": 1711903600000
}
```
### Status Values
| Status | Meaning |
|---|---|
| `queued` | Job accepted, waiting to start |
| `running` | Blender is processing the model |
| `done` | All files ready for download |
| `error` | Processing failed. Check `error` field. |
## Step 3: Download
Fetch each file from the `downloads` object with a simple GET request (no auth required for temp files):
```bash
curl -o model_lod0.glb https://sboxcool.com/uploads/temp/123/abc_model_lod0.glb
```
Download links expire after **1 hour**.
## Pipeline Modes
### Standard Pipeline
Default mode. Decimates the model and optionally generates LoDs. Output is the same format as input.
### Source 1 Pipeline
Set `pipeline=source1`. Does everything the standard pipeline does, plus generates a `.vmdl` manifest file with:
- `RenderMeshList` entries for each LoD level
- `LODGroupList` with switch thresholds
- Correct file references for s&box asset import
## Bone Mapping
Models with armatures/skeletons require bone name remapping to work with s&box characters. The API handles this in several ways:
### Automatic Matching
By default, if your model has bones, the API searches for public mappings that match your model's bone structure. If a mapping matches >= 80% of your bones, it's auto-applied.
### Explicit Mapping ID
You can specify a `mappingId` to use a specific bone mapping. Get mapping IDs from:
- The Model Importer UI at sboxcool.com/model-converter (click the clipboard icon next to any public mapping)
- API responses that include `availableMappings` when a mapping is needed
### Skip Mapping
Set `skipBoneMapping=true` to process models with bones without any remapping. Useful when:
- You want the original bone names preserved
- Your model already uses s&box-compatible bone names
- You'll handle remapping separately
### Response Fields
When processing models with bones, the response includes:
```json
{
"result": {
"bones": ["Hips", "Spine", "Chest", ...],
"boneCount": 65,
"boneMappingApplied": {
"publicId": "map_abc123xyz",
"name": "Mixamo to Citizen",
"author": "username",
"matchPercent": 95
}
}
}
```
### Error: Mapping Required
If your model has bones but no suitable mapping is found/specified:
```json
{
"status": "error",
"error": "Model has 65 bones but no mapping selected...",
"result": {
"bones": ["mixamorig:Hips", ...],
"availableMappings": [
{
"publicId": "map_abc123xyz",
"name": "Mixamo to Citizen",
"author": "username",
"matchPercent": 95,
"upvotes": 42,
"downvotes": 2
}
]
}
}
```
Use one of the suggested `publicId` values with `mappingId`, or create your own mapping in the web UI.
## LoD Generation
When `lodLevels` > 0, the API generates additional simplified versions of the model using an exponential decay curve. LoD (Level of Detail) lets the engine switch to lower-poly meshes when a model is farther away or smaller on screen, improving runtime performance. The final, lowest-detail LoD defaults to 15% of the original mesh. Preview that lowest LoD in the web importer before exporting or adopting similar settings in automation, because it is the most aggressively decimated mesh and can visibly damage silhouettes, textures, or rigging if set too low:
```
ratio[i] = lodFinalRatio ^ (i / lodLevels) ^ 1.7
```
Each LoD gets a switch threshold distributed linearly up to `lodMaxThreshold`.
## Example: cURL
```bash
# 1. Submit (with bone mapping)
JOB=$(curl -s -X POST https://api.sboxcool.com/tools/model \
-H "Authorization: Bearer YOUR_API_KEY" \
-F "[email protected]" \
-F "format=fbx" \
-F "pipeline=source1" \
-F "ratio=0.5" \
-F "lodLevels=2" \
-F "lodMaxThreshold=50" \
-F "mappingId=map_abc123xyz")
JOB_ID=$(echo $JOB | jq -r '.jobId')
# 2. Poll until done
while true; do
STATUS=$(curl -s -H "Authorization: Bearer YOUR_API_KEY" \
https://api.sboxcool.com/tools/model/$JOB_ID)
echo $STATUS | jq '.status, .progress[-1]'
[ "$(echo $STATUS | jq -r '.status')" = "done" ] && break
sleep 3
done
# 3. Download all files
for key in $(echo $STATUS | jq -r '.downloads | keys[]'); do
URL=$(echo $STATUS | jq -r ".downloads.$key")
curl -o "${key}.${URL##*.}" "https://sboxcool.com$URL"
done
```
## Example: Python
```python
import requests, time
headers = {"Authorization": "Bearer YOUR_API_KEY"}
# Submit with bone mapping
resp = requests.post("https://api.sboxcool.com/tools/model",
headers=headers,
files={"file": open("character.fbx", "rb")},
data={
"format": "fbx",
"pipeline": "source1",
"ratio": "0.5",
"lodLevels": "2",
# Use a specific mapping ID (get from web UI or error response)
"mappingId": "map_abc123xyz",
# Or auto-select most popular: "useMostPopularBoneMapping": "true",
# Or skip mapping entirely: "skipBoneMapping": "true",
})
job = resp.json()
# Poll
while True:
status = requests.get(
f"https://sboxcool.com{job['statusUrl']}",
headers=headers).json()
print(status["status"], status["progress"][-1] if status["progress"] else "")
if status["status"] in ("done", "error"):
break
time.sleep(3)
# Download
for key, url in status["downloads"].items():
data = requests.get(f"https://sboxcool.com{url}").content
ext = url.rsplit(".", 1)[-1]
with open(f"{key}.{ext}", "wb") as f:
f.write(data)
```