REST API and MCP for turntable videos and multi-view datasets
You use a single API key (dxgl_sk_...) created in Studio → avatar menu → API → Keys. By default a key is personal: renders, uploads, and downloads bill your account. You handle user mapping and billing on your side however you want.
A key can also be bound to a team workspace at creation (the workspace picker in the Create-key form; requires the editor role or above). A bound key operates entirely on that workspace — the whole team's models and renders, billed to the workspace owner's credit pool — and your membership is re-checked on every request, so a removed member's key stops working immediately. GET /v1/account tells you which workspace a key is bound to.
The REST base URL is https://api.dx.gl/v1. Every request sends Authorization: Bearer dxgl_sk_... in the header.
| Path | Best for | How it works |
|---|---|---|
| REST API | Production pipelines, backend services | Direct HTTP calls from your server. Deterministic, testable, fully scriptable. |
| MCP (Model Context Protocol) | Prototyping, ad-hoc tasks, AI-powered workflows | AI agents call DX.GL tools via natural language. Great for internal tooling and rapid iteration. |
Both paths use the same underlying API and the same credit system. Many integrators start with MCP for prototyping and add direct REST calls for production.
Base URL: https://api.dx.gl/v1
All requests require Authorization: Bearer dxgl_sk_... in the header.
curl -X POST https://api.dx.gl/v1/models \
-H "Authorization: Bearer dxgl_sk_..." \
-F "[email protected]" \
-F 'renderSettings={"aspect":"16:9","bgColor":"#ffffff","length":6}'
Response:
{ "data": { "modelId": "Ab3kF9x2qL1m", "renderId": "Xz7pQ4w8nR2k" } }
The upload triggers a free system preview render automatically. If you pass renderSettings, a paid render is also queued.
curl -X POST https://api.dx.gl/v1/models/ingest \
-H "Authorization: Bearer dxgl_sk_..." \
-H "Content-Type: application/json" \
-d '{"url": "https://your-cdn.com/model.glb", "renderSettings": {"aspect":"16:9"}}'
Same response format. The server downloads the file directly (max 1 GB; the download aborts after 45 seconds without progress, 10-minute ceiling). The URL can be a presigned S3/R2 GET URL — pull directly from your own bucket, no credentials shared. SHA-256 deduplication prevents re-uploading identical files.
For large files, flaky links, or bucket-to-bucket pipelines, upload straight to storage instead of through the API:
# 1. mint a ticket + presigned PUT URL
curl -X POST https://api.dx.gl/v1/models/create-upload-url \
-H "Authorization: Bearer dxgl_sk_..."
# → { "data": { "url": "https://…", "ticket": "aB3kF9x2qL1m", "expiresIn": 3600, "maxBytes": 1073741824 } }
# 2. PUT the GLB (resumable with your own s3 tooling)
curl -T product.glb "<url>"
# 3. register it — same validation, dedup, and free preview as a direct upload
curl -X POST https://api.dx.gl/v1/models/finalize \
-H "Authorization: Bearer dxgl_sk_..." \
-H "Content-Type: application/json" \
-d '{"ticket": "aB3kF9x2qL1m", "filename": "product.glb"}'
The response is identical to a direct upload ({ modelId, renderId }). GLB only.
curl -X POST https://api.dx.gl/v1/renders \
-H "Authorization: Bearer dxgl_sk_..." \
-H "Content-Type: application/json" \
-d '{
"modelId": "Ab3kF9x2qL1m",
"renderSettings": {
"quality": "standard",
"aspect": "1:1",
"bgColor": "#000000",
"length": 9,
"shadows": true,
"easing": true
}
}'
Retries are safe: send an Idempotency-Key header (any stable string per submit intent) and a timed-out-and-retried POST replays the original render with deduped: true instead of charging twice. Works on batch too — one key covers the whole batch.
curl -X POST https://api.dx.gl/v1/renders/batch \
-H "Authorization: Bearer dxgl_sk_..." \
-H "Content-Type: application/json" \
-d '{
"renders": [
{ "modelId": "Ab3kF9x2qL1m", "renderSettings": { "aspect": "16:9", "bgColor": "#ffffff" } },
{ "modelId": "Ab3kF9x2qL1m", "renderSettings": { "aspect": "1:1", "bgColor": "#000000" } },
{ "modelId": "Ab3kF9x2qL1m", "renderSettings": { "aspect": "9:16", "bgColor": "#ffffff" } }
]
}'
Maximum 100 renders per batch. Credits deducted atomically. Same-model variants that share dimensions and scene settings (e.g. background-color variants at one aspect) render their GPU frames once and encode in parallel; variants that change the framing render separately.
curl https://api.dx.gl/v1/renders/Xz7pQ4w8nR2k \
-H "Authorization: Bearer dxgl_sk_..."
Status progression: pending → poster-processing → video-processing → done (any status can move to error; poster-done is a rare recovery state, not part of the happy path — poll for done or error and treat the rest as opaque "in progress"). Errored renders carry failureCode (machine-readable) alongside errorMessage.
# Full video
curl -o video.mp4 https://api.dx.gl/v1/renders/Xz7pQ4w8nR2k/video \
-H "Authorization: Bearer dxgl_sk_..."
# Web-optimized variant (lower bitrate, faster loading)
curl -o web.mp4 "https://api.dx.gl/v1/renders/Xz7pQ4w8nR2k/video?quality=web" \
-H "Authorization: Bearer dxgl_sk_..."
# Poster image (full resolution)
curl -o poster.png "https://api.dx.gl/v1/renders/Xz7pQ4w8nR2k/poster?quality=full" \
-H "Authorization: Bearer dxgl_sk_..."
# All assets as ZIP
curl -o assets.zip https://api.dx.gl/v1/renders/Xz7pQ4w8nR2k/bundle \
-H "Authorization: Bearer dxgl_sk_..."
All asset endpoints return Cache-Control: immutable — renders never change after completion. Safe to cache aggressively.
import requests, time, json
API = "https://api.dx.gl/v1"
HEADERS = {"Authorization": "Bearer dxgl_sk_..."}
def render_model(glb_path, settings):
"""Upload a model, render it, return the video URL."""
with open(glb_path, "rb") as f:
r = requests.post(f"{API}/models", headers=HEADERS,
files={"file": f},
data={"renderSettings": json.dumps(settings)})
data = r.json()["data"]
render_id = data["renderId"]
# Poll until done
while True:
r = requests.get(f"{API}/renders/{render_id}", headers=HEADERS)
status = r.json()["data"]["status"]
if status == "done":
return f"{API}/renders/{render_id}/video"
if status == "error":
raise Exception(f"Render failed: {r.json()['data'].get('errorMessage')}")
time.sleep(3)
# Usage
url = render_model("chair.glb", {"aspect": "16:9", "bgColor": "#ffffff", "length": 6})
const API = 'https://api.dx.gl/v1';
const headers = { 'Authorization': 'Bearer dxgl_sk_...' };
async function renderModel(filePath, settings) {
// Node 20+: openAsBlob pairs with the built-in fetch/FormData
// (a ReadStream would be stringified, not streamed)
const form = new FormData();
form.append('file', await require('fs').openAsBlob(filePath), 'model.glb');
form.append('renderSettings', JSON.stringify(settings));
const upload = await fetch(`${API}/models`, { method: 'POST', headers, body: form });
const { renderId } = (await upload.json()).data;
// Poll until done
while (true) {
await new Promise(r => setTimeout(r, 3000));
const res = await fetch(`${API}/renders/${renderId}`, { headers });
const { status, errorMessage } = (await res.json()).data;
if (status === 'done') return `${API}/renders/${renderId}/video`;
if (status === 'error') throw new Error(`Render failed: ${errorMessage}`);
}
}
Upload new SKUs nightly from your product database. Tag each upload with a client or campaign identifier for easy filtering.
for sku in new_skus:
r = requests.post(f"{API}/models/ingest", headers=HEADERS, json={
"url": sku["glb_url"],
"renderSettings": {"aspect": "16:9", "bgColor": "#ffffff", "length": 6}
})
model_id = r.json()["data"]["modelId"]
# Tag the model
requests.patch(f"{API}/models/{model_id}", headers=HEADERS, json={
"title": sku["name"],
"sku": sku["sku_code"],
"tags": ["client-acme", "fall-2026"]
})
Generate every combination of aspect ratio, background, and quality for each model:
variants = [
{"aspect": "16:9", "bgColor": "#ffffff", "shadows": True},
{"aspect": "1:1", "bgColor": "#000000", "shadows": True},
{"aspect": "9:16", "bgColor": "#ffffff", "reflector": True},
]
renders = []
for model_id in model_ids:
for v in variants:
renders.append({"modelId": model_id, "renderSettings": v})
# Submit all at once (max 100 per batch)
for batch in chunks(renders, 100):
r = requests.post(f"{API}/renders/batch", headers=HEADERS, json={"renders": batch})
Upload your client's branded background once, then reference it across all renders:
# Upload the background image
curl -X POST https://api.dx.gl/v1/overlays \
-H "Authorization: Bearer dxgl_sk_..." \
-F "[email protected]" \
-F "layer=background" \
-F "name=Acme Brand Gradient"
# Use it in renders
curl -X POST https://api.dx.gl/v1/renders \
-H "Authorization: Bearer dxgl_sk_..." \
-H "Content-Type: application/json" \
-d '{"modelId": "...", "renderSettings": {"bgImageId": "overlay_id_here", "aspect": "16:9"}}'
The image is scaled to cover the output dimensions and cropped from the bgAlign anchor (nine positions, default top-left), keeping logos safe across all aspect ratios. Before re-uploading an asset you already have, POST /v1/overlays/resolve with the file's SHA-256 returns the existing id.
Two more asset kinds slot into render settings the same way. Textures replace a material's base-color map — mint one, then key it to a material by name:
curl -X POST https://api.dx.gl/v1/assets/textures \
-H "Authorization: Bearer dxgl_sk_..." \
-F "[email protected]" -F "name=Red colorway"
# → { "data": { "id": "tex_id" } } (re-POSTing identical bytes returns the same id)
curl -X POST https://api.dx.gl/v1/renders \
-H "Authorization: Bearer dxgl_sk_..." \
-H "Content-Type: application/json" \
-d '{"modelId": "...", "renderSettings": {"materialEdits": {"woven": {"mapAssetId": "tex_id"}}}}'
Material and mesh names come from GET /v1/models/{id} — it returns materialNames (the keys materialEdits matches), meshNames (for hiddenMeshNames, which hides named meshes per render), and animations. Names are exact-match; a wrong mapAssetId silently keeps the original map, so verify against the inventory.
Environments are custom HDRIs for envId, uploaded as the pre-encoded gainmap triple (gainmap-js encodeAndCompress output — or upload through Studio's Environment panel and reuse the id). A bad envId fails the render loudly with a refund. POST /v1/assets/envs, GET /v1/assets/envs, quotas apply — full details in the API reference.
Tags organize models across clients, projects, and campaigns. Every model can have up to 20 tags (lowercased, trimmed).
# List all models for a specific client
curl "https://api.dx.gl/v1/models?tags=client-acme&limit=100" \
-H "Authorization: Bearer dxgl_sk_..."
# List models matching any of multiple tags (OR filter)
curl "https://api.dx.gl/v1/models?tags=client-acme,campaign-fall-2026" \
-H "Authorization: Bearer dxgl_sk_..."
Lists also take status=active|archived|any, keyset cursor pagination for large catalogs, and updatedSince=<ISO 8601> for cheap delta polling — poll renders with updatedSince + status=done to sweep completions across a big batch in one call.
To calculate spend per client, list their models, count their renders, and total each render's credit cost — video is billed per second by quality tier, datasets are flat per set. The quote endpoint can help estimate future costs.
curl https://api.dx.gl/v1/account \
-H "Authorization: Bearer dxgl_sk_..."
# Response: { "data": { "credits": 847, "paid": 847, "free": 0, "total": 847,
# "scopes": ["read", "render"], "workspace": null } }
For a workspace-bound key, workspace names the bound workspace and the balances are its owner's pool — the one that funds this key's renders.
curl -X POST https://api.dx.gl/v1/quote \
-H "Authorization: Bearer dxgl_sk_..." \
-H "Content-Type: application/json" \
-d '{"renders": [{"quality": "standard"}, {"quality": "standard"}, {"quality": "4k"}]}'
# Response: { "data": { "creditsRequired": 144, "creditsAvailable": 847, "sufficient": true } }
Always quote large batches before submitting. The quote is free and returns per-item breakdown, total cost, and whether you have enough credits. Batch renders deduct atomically — if you don't have enough credits, nothing is charged.
Credit packs are $9.90, $79, and $490. Build a simple dashboard that checks GET /v1/account periodically and alerts when credits drop below a threshold. Contact us for volume pricing at scale.
Every completed render produces four assets: the full video, a web-optimized variant, a thumbnail, and a poster.
Every completed render has a public share page at https://dx.gl/portal/v?id={renderId}. No authentication required for viewing. Useful for sharing with stakeholders or embedding in emails.
All asset endpoints support Range headers for streaming and return Cache-Control: immutable. Assets per render:
| Endpoint | Format | Use case |
|---|---|---|
GET /v1/renders/:id/video |
MP4 or MOV (Pro) | Full-quality download |
GET /v1/renders/:id/video?quality=web |
MP4 (lower bitrate) | Web embedding, streaming |
GET /v1/renders/:id/poster?quality=full |
PNG | Product page hero, social sharing |
GET /v1/renders/:id/poster |
PNG (thumbnail) | Grid previews, hover states |
GET /v1/renders/:id/thumb |
MP4 (quarter res) | Hover video preview |
GET /v1/renders/:id/bundle |
ZIP | Bulk download of all assets |
GET /v1/renders/:id/download-url |
Presigned URL | Large files (ProRes, dataset ZIPs) — no auth header on the download |
GET /v1/models/:id/file-url |
Presigned URL | Source GLB, direct from storage |
The web video variant is optimized for <video> tags:
<video autoplay loop muted playsinline>
<source src="https://api.dx.gl/v1/renders/{id}/video?quality=web" type="video/mp4">
</video>
The MCP server lets AI agents interact with DX.GL through tool calls. Add this to your MCP client configuration (Windsurf, Claude Desktop, Cursor, or any MCP-compatible host):
{
"mcpServers": {
"dxgl": {
"serverUrl": "https://mcp.dx.gl/",
"headers": {
"Authorization": "Bearer dxgl_sk_..."
}
}
}
}
Replace dxgl_sk_... with your API key from Studio → avatar menu → API → Keys.
Paste this into your AI agent to verify everything works:
List my models and tell me how many I have.
The agent will call list_models and return a summary. If this works, you're connected.
Render a single model in multiple formats (video):
Render model Ab3kF9x2qL1m in all three aspect ratios (16:9, 1:1, 9:16) with a white background, 9 seconds, shadows enabled.
The agent calls create_batch_renders with 3 variants and polls until all complete.
Full catalog in multiple variants (video):
I just uploaded 30 shoe models tagged "ss26". Render each one in 16:9 white, 1:1 black, and 9:16 with shadows. Quote the total cost first.
The agent will:
list_models with tags: "ss26" to find the 30 modelsquote with 90 renders (30 models × 3 variants) to show total costcreate_batch_renders to submit all 90 at onceGenerate a dataset for one model:
Generate a 196×1024 hemisphere dataset for model Ab3kF9x2qL1m.
The agent calls create_render with output: "dataset", datasetQuality: "196x1024", coverage: "hemisphere", then polls get_render until done, and returns the download URL.
Generate datasets for all models with a tag:
Generate 100×800 hemisphere datasets for every model tagged "validation-set". Quote the cost first and wait for my approval before proceeding.
The agent will:
list_models with tags: "validation-set" to find matching modelsquote with the dataset configuration to show the total credit costcreate_batch_renders to submit all datasets at onceget_render for each until complete37 tools covering the full API surface.
Models
| Tool | Description |
|---|---|
ingest_model |
Import a 3D model from a URL (public or presigned S3/R2) |
upload_model |
Upload a GLB from bytes — local file path (stdio) or base64 |
list_models |
List models — tag filter, updatedSince delta polling, cursor pagination |
get_model |
Get model details and all its renders |
update_model |
Update a model's title, SKU, and/or tags |
archive_model / unarchive_model |
Hide / restore a model in the library |
delete_model |
Soft-delete a model |
check_hash |
Check whether a SHA-256 already exists (dedup pre-flight) |
get_model_file |
Download URL for a model's original GLB |
get_model_file_url |
Presigned GLB URL — downloads direct from storage, no auth header |
Renders
| Tool | Description |
|---|---|
create_render |
Create a single render (video or dataset) |
create_batch_renders |
Create multiple renders atomically (max 100) |
list_renders |
List render jobs — status filter, updatedSince delta polling |
get_render |
Check render status (poll until done) |
cancel_render |
Cancel a still-queued render and refund its credits |
dismiss_render |
Acknowledge an errored render |
delete_render |
Permanently delete a finished render and its assets |
quote |
Estimate credit cost before committing |
Billing
| Tool | Description |
|---|---|
get_account |
Check credit balance |
list_products |
List purchasable credit packs |
get_purchases |
Credit ledger — purchases, spends, refunds |
Downloads
| Tool | Description |
|---|---|
download_render |
Get download URL for a completed render |
get_download_url |
Presigned URL for any render asset — downloads direct from storage, no auth header |
get_poster |
Poster image (PNG) URL |
get_thumb |
Thumbnail loop video (MP4) URL |
download_bundle |
ZIP of all assets for a completed render |
download_dataset |
ZIP of a dataset render's multi-view images |
get_hls |
HLS streaming status and playback URLs |
Overlays
| Tool | Description |
|---|---|
upload_overlay |
Upload a PNG/JPEG/WebP as a reusable overlay asset (background, foreground, decal, or texture layer) |
list_overlays |
List overlay assets — ids feed backgrounds, decals, material edits |
delete_overlay |
Delete an overlay asset you own |
Textures & Environments
| Tool | Description |
|---|---|
upload_texture |
Mint a base-color replacement texture — id feeds materialEdits.<material>.mapAssetId |
upload_env |
Upload a custom HDRI (pre-encoded gainmap triple) — id feeds envId |
list_assets |
List the workspace's texture or environment assets |
delete_asset |
Delete an owned texture/environment asset |
resolve_asset_sha |
Reuse an existing asset by content SHA-256 instead of re-uploading |
Team workspaces: there's no MCP workspace configuration — the key implies the workspace. A workspace-bound key (minted in Studio) makes every tool operate on that workspace, billing its owner.
Tag everything. Tags are the key to efficient batch operations. Tag models on upload or in Studio, then target them in prompts:
Render all models tagged "new-arrivals" in 16:9 white with shadows.
Quote first on large batches. The quote tool prevents surprises. Ask the agent to quote before any batch:
Quote 16:9 + 1:1 + 9:16 renders for my 80 shoe models, then proceed only if it's under 250 credits.
Atomic credit deduction. Batch renders deduct all credits at once. If you don't have enough for the full batch, nothing is charged — the batch fails cleanly and you can adjust.
Presigned URLs for large files. get_download_url (render assets) and get_model_file_url (source GLBs) return short-lived URLs that download directly from storage with no Authorization header — use them for ProRes videos, dataset ZIPs, or handing a download to another system.
| Tier (API value) | Label | Resolution | Codec | Alpha | Credits/sec | Best for |
|---|---|---|---|---|---|---|
share |
Standard | 960×540 | H.264 MP4 | No | 1 | Quick shares, web, messaging, social |
standard |
HD | 1920×1080 | H.264 MP4 | No | 4 | Web, social media, product pages |
4k |
4K | 3840×2160 | H.264 MP4 | No | 16 | In-store displays, kiosks, high-DPI screens |
pro |
ProRes 4444 | 3840×2160 | ProRes 4444 | Yes | 64 | Video editing, compositing, broadcast |
Video is charged per second of output based on quality tier.
| Tier | Views | Resolution | Credits | Best for |
|---|---|---|---|---|
100x800 |
100 | 800×800 | 40 | Quick experiments, proof-of-concept |
196x1024 |
196 | 1024×1024 | 160 | Production training, best quality-to-cost ratio |
400x2048 |
400 | 2048×2048 | 640 | Maximum fidelity, large-scale reconstruction |
Datasets are a flat cost per set.
Coverage
Output ZIP contents
Every dataset ZIP contains:
images/ — RGB PNG frames (composited on white background)depth/ — 8-bit grayscale depth PNGsdepth_16bit/ — 16-bit grayscale depth PNGs (65,536 levels)normals/ — world-space normal map PNGsmasks/ — foreground/background alpha maskstransforms.json — camera intrinsics + per-frame 4×4 transform matrices (nerfstudio / instant-ngp format)overview.webp — 4-quadrant contact sheetAfter downloading a dataset ZIP, you can train directly with nerfstudio (the --pipeline.model.background-color white flag is required, since images are composited on white):
unzip dataset.zip -d mymodel
ns-train splatfacto --data ./mymodel \
--max-num-iterations 15000 \
--pipeline.model.sh-degree 3 \
--pipeline.model.background-color white
| Field | Type | Default | Description |
|---|---|---|---|
quality |
string | "standard" |
"share" (540p, 1 credit/sec), "standard" (HD 1080p, 4 credits/sec — the default when omitted: a default 6s render with no quality set costs 24 credits), "4k" (16/sec), "pro" (4K ProRes with alpha, 64/sec) |
aspect |
string | "16:9" |
"16:9", "1:1", "9:16" |
bgColor |
string | "#ffffff" |
Any 6-digit hex (ignored when bgImageId is set) |
bgImageId |
string | — | Overlay asset ID for background image |
length |
number | 6 |
Duration in seconds — anything above 0 up to 120 (ProRes: 60), decimal OK e.g. 4.7. Billed per second, 1-credit minimum. |
animation |
string | — | Animation clip name (or "*" for first clip) — discover clips via GET /v1/models/{id} → animations |
animationSpeed |
number | 1 |
Animation speed multiplier (−10 to 10; negative plays in reverse) |
shadows |
boolean | true |
Ground shadows |
reflector |
boolean | false |
Reflective ground plane |
easing |
boolean | true |
Smooth rotation start/stop |
rotateY |
number | 0 |
Camera orbit start angle (−180° to 180°) — moves the camera, not the model |
panX |
number | 0 |
Model yaw (−180° to 180°), applied first |
tiltX |
number | 0 |
Model pitch (−180° to 180°), applied after yaw |
rollZ |
number | 0 |
Model roll (−180° to 180°), applied last |
zoom |
number | 1.0 |
Camera zoom (0.5–2.0; above 1 = closer) |
bgAlign |
string | "top-left" |
Background-image crop anchor (nine positions) |
hiddenMeshNames |
string[] | — | Meshes to hide, by exact runtime name (GET /v1/models/{id} → meshNames) |
materialEdits |
object | — | Per-material overrides keyed by material name — color, roughness, metalness, mapAssetId texture swaps, and more |
envId |
string | — | Custom HDRI environment (see Custom Materials & Environments) |
effect |
string | "turntable" |
Camera motion: "turntable", "hero-spin", "showcase", "zoom-orbit", "reveal", "keyframes" (custom fly-through) |
output |
string | "video" |
"video" or "dataset" |
datasetQuality |
string | — | "100x800", "196x1024", "400x2048" (when output is dataset) |
coverage |
string | "hemisphere" |
"hemisphere" or "sphere" (dataset only) |
This is the working set — the full surface (environment and lighting knobs, decals, keyframe camera paths, filters) is in the API reference, and everything the Studio editor can author is accepted here.
All errors return a consistent format:
{
"error": {
"code": "no_credits",
"message": "No render credits remaining",
"status": 402
}
}
Key error codes for integrators:
| Code | Status | Action |
|---|---|---|
unauthorized |
401 | Check API key |
no_credits |
402 | Purchase more credits or contact for volume pricing |
file_too_large |
400 | File exceeds 1 GB — reduce before upload |
invalid_format |
400 | Only .glb and .zip (OBJ+MTL) are supported — pack .gltf as .glb |
model_not_found |
404 | Model was deleted or ID is wrong |
too_many |
400 | Batch exceeds 100 renders — split into smaller batches |
upload_limit |
429 | Free upload limit reached — purchase credits to unlock |