Complete /v1 REST reference — endpoints, render settings, credit rates, errors
All requests require a Bearer token in the Authorization header:
Authorization: Bearer dxgl_sk_...
Tokens are created in Studio: open the avatar menu (top-right) → API → Keys → Create key. The raw token is shown once on creation — store it securely.
Server-to-server only. A dxgl_sk_ token is a full-account secret — never embed one in a web page or mobile app. The API grants no CORS access to browsers; call it from your backend. For public browser embeds, use the public share URLs of published renders instead.
A key can be bound to a team workspace at creation (the workspace picker in the Create-key form; requires the editor role or above in that workspace). The binding is fixed for the key's lifetime — the key is the workspace selection, so nothing else in your integration changes: every endpoint documented here simply operates on the bound workspace.
GET /v1/account shows that pool and names the workspace); GET /v1/purchases returns only ledger rows attributable to this workspace.403 forbidden.A key created without a workspace (or before workspace keys existed) operates on your personal workspace, exactly as before.
Success:
{
"data": { ... }
}
List (paginated):
{
"data": [ ... ],
"meta": { "total": 142, "offset": 0, "limit": 50 }
}
Cursor pagination (recommended for large catalogs). GET /v1/models and GET /v1/renders also support keyset pagination: pass cursor= (empty for the first page, or the previous response's meta.nextCursor), and the response's meta becomes { "limit": 50, "nextCursor": "..." | null } — walk until nextCursor is null. Cursor mode skips the per-page COUNT(*) and stays fast at any depth, unlike large offset values. Cursors are opaque; don't parse them. Both endpoints also accept updatedSince=<ISO 8601> (rows modified at/after that instant) for cheap delta polling — combine it with cursor mode to sync a big catalog incrementally.
Error:
{
"error": {
"code": "model_not_found",
"message": "Model not found",
"status": 404
}
}
IDs. All resource IDs are short opaque strings (e.g. Ab3kF9x2qL1m). A note
on naming: when an endpoint returns a resource it just created, the field is
named id (e.g. POST /v1/renders → { "id": ... }); when an endpoint returns
a related resource alongside a primary one, the related ID carries a qualified
name — uploading a model returns { "modelId": ..., "renderId": ... }, and a
render always refers to its model as modelId. So the render you create is id
on POST /v1/renders but renderId on POST /v1/models.
A model is an uploaded 3D file (GLB) or a converted 3D scan. Uploading a model always creates a render job: with renderSettings it's the billed render you specified; without, it's a free system preview (see below).
| Field | Type | Required | Description |
|---|---|---|---|
file |
File | Yes | .glb (glTF binary container) or .zip (OBJ+MTL+textures). A raw .gltf JSON file is rejected — pack it into a .glb first (all mainstream DCC tools and gltf-transform can). Max 1 GB. |
renderSettings |
JSON string | No | Render configuration (see below). Omitting it makes the upload's render a free system preview; providing it creates a billed render at the settings given. |
Response 201:
{
"data": {
"modelId": "Ab3kF9x2qL1m",
"renderId": "Xz7pQ4w8nR2k"
}
}
If the file's SHA-256 matches an existing upload, the existing model is reused and the response is 200 with duplicate: true (a fresh upload returns 201). With renderSettings, a new billed render is created for the existing model; without, its existing system preview is returned.
Free preview: An upload without renderSettings creates a free system preview render (no credits deducted) — a lightweight 540p turntable that doubles as a model integrity check: if the preview fails, the model likely has issues (broken geometry, missing textures, invalid glTF). The preview is the renderId in the response and carries isSystemPreview: true in render list/detail responses.
3D scan support: Upload a .zip containing one OBJ file with its MTL and texture files (JPG/PNG). One model per ZIP. The server converts to GLB automatically, with materials set to roughness 1.0 for a natural matte finish. The converted GLB is stored as your model and can be downloaded via GET /v1/models/:id/file. Ideal for photogrammetry and structured-light scan exports (Artec Studio, RealityCapture, Metashape, etc.).
curl -X POST https://api.dx.gl/v1/models/ingest \
-H "Authorization: Bearer dxgl_sk_..." \
-H "Content-Type: application/json" \
-d '{"url": "https://example.com/model.glb", "renderSettings": {"aspect": "16:9"}}'
Response 201: Same as upload (duplicates return 200 with duplicate: true).
Downloads the file from the URL (max 1 GB). URL ingest accepts GLB only. For OBJ scans, use the file upload endpoint with a ZIP.
Pull from your own S3/R2 bucket. Point url at a presigned GET URL from your bucket — no credentials leave your side. Presigned URLs sign the HTTP method, so DX.GL skips the availability HEAD probe for them and streams the object directly (the 1 GB cap still applies mid-download, and the file is GLB-validated on arrival). A common S3 default content-type (binary/octet-stream) is accepted. Example:
# 1. presign in your own infra (7-day max)
aws s3 presign s3://your-bucket/models/chair.glb --expires-in 3600
# 2. hand the URL to DX.GL
curl -X POST https://api.dx.gl/v1/models/ingest \
-H "Authorization: Bearer dxgl_sk_..." \
-H "Content-Type: application/json" \
-d '{"url": "https://your-bucket.s3.amazonaws.com/models/chair.glb?X-Amz-Signature=..."}'
Combined with GET /v1/renders/:id/download-url (presigned egress), this gives a bucket-to-bucket workflow where DX.GL never holds your storage credentials. The download aborts after 45 seconds without progress, with a 10-minute absolute ceiling — a timed-out download returns download_timeout (400). SHA-256 deduplication applies. URL ingest accepts GLB only. For OBJ scans, use the file upload endpoint with a ZIP.
For large models, flaky links, or bucket-to-bucket pipelines, skip the multipart POST and upload straight to storage:
create-upload-url mints a presigned PUT to a staging area (default 1 h, expires 60–604800 s). After the PUT succeeds, finalize registers the model — it runs the exact same pipeline as the multipart upload (GLB validation, SHA-256 dedup with duplicate: true reuse, free system preview or billed render per renderSettings) and responds identically ({ modelId, renderId }). Staged uploads are GLB-only (max 1 GB — oversized staged objects are deleted at finalize). A ticket whose URL expired before the PUT, or whose PUT never happened, finalizes to 404 not_found. Abandoned staging objects are garbage-collected; don't rely on staging as storage.
url_resp=$(curl -s -X POST https://api.dx.gl/v1/models/create-upload-url -H "Authorization: Bearer dxgl_sk_...")
curl -T chair.glb "$(echo "$url_resp" | jq -r .data.url)"
curl -s -X POST https://api.dx.gl/v1/models/finalize \
-H "Authorization: Bearer dxgl_sk_..." -H "Content-Type: application/json" \
-d "{\"ticket\": \"$(echo "$url_resp" | jq -r .data.ticket)\", \"filename\": \"chair.glb\"}"
| Param | Type | Default | Description |
|---|---|---|---|
limit |
integer | 50 | Max 100 |
offset |
integer | 0 | Pagination offset (offset mode) |
cursor |
string | — | Keyset pagination: empty for page 1, then meta.nextCursor (see Response Format) |
updatedSince |
string | — | ISO 8601 — only rows modified at/after this instant |
tags |
string | — | Comma-separated tags (OR filter) |
status |
string | active |
active, archived, or any (active + archived; each row carries its status). Deleted models are never listed. |
curl "https://api.dx.gl/v1/models?limit=10&tags=furniture" \
-H "Authorization: Bearer dxgl_sk_..."
Response 200:
{
"data": [
{
"id": "Ab3kF9x2qL1m",
"originalName": "chair.glb",
"title": "Ergonomic Chair",
"sku": "EC-1001",
"tags": ["furniture", "office"],
"fileSize": 4521984,
"sha256": "a1b2c3...",
"animationCount": 0,
"createdAt": "2026-02-15T20:00:00.000Z"
}
],
"meta": { "total": 42, "offset": 0, "limit": 50 }
}
Returns the model detail with all its renders:
curl https://api.dx.gl/v1/models/Ab3kF9x2qL1m \
-H "Authorization: Bearer dxgl_sk_..."
{
"data": {
"id": "Ab3kF9x2qL1m",
"originalName": "chair.glb",
"title": "Ergonomic Chair",
"sku": "EC-1001",
"tags": ["furniture", "office"],
"fileSize": 4521984,
"sha256": "a1b2c3...",
"animations": [
{ "name": "Walk", "duration": 1.933, "channels": 57 }
],
"meshNames": ["Body", "Cap", "Cap_1"],
"materialNames": ["woven", "rib"],
"createdAt": "2026-02-15T20:00:00.000Z",
"renders": [
{
"id": "Xz7pQ4w8nR2k",
"status": "done",
"renderSettings": { ... },
"createdAt": "2026-02-15T20:00:01.000Z",
"updatedAt": "2026-02-15T20:01:30.000Z"
}
]
}
}
| Field | Type | Description |
|---|---|---|
title |
string | Display title |
sku |
string | Product code |
tags |
string[] | Up to 20 tags (lowercased, trimmed) |
All fields are optional — only fields present in the body are changed (an omitted field is never touched; passing null or "" explicitly clears title/sku). A body with none of the three fields returns 400 bad_request.
curl -X PATCH https://api.dx.gl/v1/models/Ab3kF9x2qL1m \
-H "Authorization: Bearer dxgl_sk_..." \
-H "Content-Type: application/json" \
-d '{"title": "Ergonomic Chair Pro", "sku": "ECP-2001", "tags": ["furniture", "office"]}'
Archives the model. Archived models are hidden from the default list; enumerate them with GET /v1/models?status=archived (or status=any). Detail, metadata edits, file download, and delete keep working on archived models.
curl -X PATCH https://api.dx.gl/v1/models/Ab3kF9x2qL1m/archive \
-H "Authorization: Bearer dxgl_sk_..."
Restores an archived model back to active status.
Downloads the original GLB file. Returns Content-Type: model/gltf-binary with a Content-Disposition: attachment header.
curl -o chair.glb https://api.dx.gl/v1/models/Ab3kF9x2qL1m/file \
-H "Authorization: Bearer dxgl_sk_..."
{ "sha256": "a1b2c3d4..." }
Response 200:
{
"data": {
"exists": true,
"modelId": "Ab3kF9x2qL1m"
}
}
Pre-flight check to avoid uploading duplicate files. Compute the SHA-256 of your file locally, then call this endpoint before uploading.
Soft-deletes the model. Existing renders remain accessible until independently deleted.
A render is a video generated from a model. Creating a render queues it for processing by the render pipeline.
{
"modelId": "Ab3kF9x2qL1m",
"renderSettings": {
"quality": "share",
"aspect": "16:9",
"bgColor": "#ffffff",
"length": 6,
"animation": "Walk",
"animationSpeed": 1,
"shadows": true,
"reflector": false,
"easing": true
}
}
Pricing note — read before your first render. The
qualityfield is optional and defaults to"standard"(HD 1080p) at 4 credits per second when omitted — a default 6-second render with noqualityset costs 24 credits. Pass"share"(540p, 1 credit/sec) explicitly for the cheapest tier: a 6ssharerender costs 6 credits. The other tiers:"4k"(16 credits/sec) and"pro"(ProRes 4444 with alpha, 64 credits/sec). A video render's cost is always tier rate × length in seconds. The JSON enum values are frozen for API stability and do not match the app's display labels one-to-one. See Render Settings for all options.
Idempotent retries. Send an Idempotency-Key header (any stable string ≤ 80 chars — e.g. a UUID minted once per submit intent, or your own order/SKU reference). If a render with the same key and byte-identical renderSettings was created within the last 30 minutes and hasn't errored, the request replays: you get the original render back with deduped: true and HTTP 200, and nothing is charged. This makes timed-out-and-retried submissions safe. One caveat: the same key with different settings creates (and charges) a new render — the key does not lock settings. POST /v1/renders/batch accepts the same header — one key covers the whole batch, each item replays against its own prior row and comes back with deduped: true. (Independently of the header, an identical resubmit of the same model + settings within a few seconds is deduplicated automatically.)
Response 201 (200 with deduped: true on an idempotent replay):
{
"data": {
"id": "Xz7pQ4w8nR2k",
"modelId": "Ab3kF9x2qL1m",
"status": "pending",
"isPreview": false,
"renderSettings": { ... }
}
}
isPreview reports whether the render was funded from the account's free-credit pool (see Credits); on an idempotent replay the response also carries deduped: true.
| Param | Type | Description |
|---|---|---|
model |
string | Filter by model ID |
status |
string | Filter by status |
limit |
integer | Max 100, default 50 |
offset |
integer | Pagination offset (offset mode) |
cursor |
string | Keyset pagination: empty for page 1, then meta.nextCursor |
updatedSince |
string | ISO 8601 — only renders modified at/after this instant (status changes update it — ideal for completion polling across a big batch) |
{
"data": {
"id": "Xz7pQ4w8nR2k",
"modelId": "Ab3kF9x2qL1m",
"status": "done",
"renderSettings": { ... },
"fileSize": 2457600,
"errorMessage": null,
"failureCode": null,
"hasWebVariant": true,
"hasThumb": true,
"isPreview": false,
"isSystemPreview": false,
"unlockedAt": null,
"hlsStatus": "ready",
"hlsReady": true,
"datasetZipSize": null,
"createdAt": "2026-02-15T20:00:01.000Z",
"updatedAt": "2026-02-15T20:01:30.000Z"
}
}
| Field | Type | Description |
|---|---|---|
fileSize |
integer | Video file size in bytes (null until done) |
errorMessage |
string | Human-readable failure reason when status is error (first line of the worker's report) |
failureCode |
string | Machine-readable failure class when status is error, else null: model_download_failed, env_download_failed, renderer_crash, renderer_error (bad/unparseable model), timeout, display_lost, encode_failed, upload_failed, reaped (render node stopped responding). Branch on this, not on message text. |
hasWebVariant |
boolean | Whether a web-optimized MP4 is available |
hasThumb |
boolean | Whether a thumbnail video is available |
isPreview |
boolean | Whether this render was funded from the free-credit pool (legacy preview variant — see Credits) |
isSystemPreview |
boolean | Whether this is the free system preview created by a bare model upload |
unlockedAt |
string | Timestamp a preview render was unlocked (legacy — null for API-created renders), or null |
hlsStatus |
string | HLS packaging status: pending/processing/ready/failed/skipped |
hlsReady |
boolean | Whether HLS playback is ready (hlsStatus === "ready") |
datasetZipSize |
integer | Dataset ZIP size in bytes (dataset renders only; null otherwise) |
These same fields appear on each item in List Renders.
Dismisses an errored render (transitions error → failed). Useful for acknowledging errors programmatically.
Cancels a render that is still queued (pending) and refunds the credits it was charged, to the pool they came from. The response reports the refund:
{ "data": { "id": "Xz7pQ4w8nR2k", "cancelled": true, "refundedCredits": 24 } }
Once a worker has claimed the job (any status past pending), cancellation returns 409 render_started — the GPU time is being spent and the render will complete or error normally.
Permanently deletes a terminal render (done, error, failed) and its files (video, poster, thumbnail). Queued and in-flight renders return 409 render_in_flight — cancel a queued render instead (which refunds); an in-flight render must finish or error first. Deleting a completed render never refunds (the GPU time was spent).
{
"renders": [
{ "modelId": "Ab3kF9x2qL1m", "renderSettings": { "aspect": "16:9", "bgColor": "#ffffff" } },
{ "modelId": "Ab3kF9x2qL1m", "renderSettings": { "aspect": "1:1", "bgColor": "#000000" } },
{ "modelId": "Yz9mK3v7pN4j", "renderSettings": { "aspect": "16:9" } }
]
}
Maximum 100 renders per batch. Each item requires a modelId and optional renderSettings. All credits are deducted atomically — if there aren't enough credits, the entire batch fails.
Response 201:
{
"data": [
{ "id": "Xz7pQ4w8nR2k", "modelId": "Ab3kF9x2qL1m", "status": "pending", "isPreview": false },
{ "id": "Bc5nL2x9mQ3r", "modelId": "Ab3kF9x2qL1m", "status": "pending", "isPreview": false },
{ "id": "Wv8jR6t4kP1s", "modelId": "Yz9mK3v7pN4j", "status": "pending", "isPreview": false }
]
}
Renders for the same model are automatically grouped into a batch — the worker renders shared frames once and encodes variants in parallel.
Batch requests honor the Idempotency-Key header (see Create Render): one key covers the whole batch, and on a retried call each item that matches a prior row is returned with deduped: true instead of being re-charged.
Download the output files of a completed render.
Returns the video file. Supports Range headers for streaming.
Pass ?quality=web to get the lighter web-optimized variant (when available, see hasWebVariant).
Content-Type: video/mp4 (standard/4k) or video/quicktime (pro — ProRes .mov)
Returns the poster image (first frame). By default returns a small thumbnail suitable for previews. Pass ?quality=full to get the full-resolution PNG at the video's native dimensions (e.g. 1920×1080).
Content-Type: image/png
Returns a quarter-resolution MP4 thumbnail video. Supports Range headers.
Content-Type: video/mp4
Downloads all render assets as a single ZIP file. The zip is streamed directly — no server-side buffering, suitable for large ProRes files. Contains:
video.mp4 or video.mov (main video)web.mp4 (web-optimized variant)poster.png (full-resolution poster)thumb.mp4 (thumbnail video)The response includes Cache-Control: immutable — renders never change after completion.
curl -o assets.zip https://api.dx.gl/v1/renders/Xz7pQ4w8nR2k/bundle \
-H "Authorization: Bearer dxgl_sk_..."
Returns the render's HLS packaging state and playback entry points:
{
"data": {
"id": "Xz7pQ4w8nR2k",
"status": "ready",
"masterUrl": "/v1/renders/Xz7pQ4w8nR2k/hls/master.m3u8",
"thumbnailsVttUrl": "/v1/renders/Xz7pQ4w8nR2k/hls/thumbs.vtt"
}
}
masterUrl/thumbnailsVttUrl are null until status is ready. The manifests, fMP4 segments, VTT thumbnail track, and sprite JPEGs are served under:
All HLS endpoints require the bearer token like every other /v1 route — they are for server-side consumption or proxying (the API grants no browser CORS). For direct browser playback, serve the flat MP4 from your own CDN or use published share URLs.
Returns a short-lived URL that downloads the asset directly from object storage, taking the API out of the data path — the efficient way to pull large files (a 4K ProRes bundle can be tens of GB) and to parallelize or resume downloads with your own tooling (aws s3 cp, curl -C -, etc.).
| Param | Description |
|---|---|
asset |
video (default), web, poster, full_poster, thumb, or dataset |
expires |
Link lifetime in seconds — default 3600, min 60, max 604800 (7 days) |
{ "data": { "url": "https://…?X-Amz-Signature=…", "asset": "video", "expiresIn": 3600 } }
The URL is an unauthenticated capability link until it expires — treat it as a secret and keep expires short. dataset requires the render to be done; asset endpoints 404 if that variant wasn't produced. (HLS manifests and the on-the-fly /bundle zip can't be presigned — use their streaming endpoints.)
Downloads the multi-view training dataset ZIP produced by a completed dataset
render (output: "dataset"). See Dataset Export
for the archive contents. The render must be done.
Content-Type: application/zip
curl -o dataset.zip https://api.dx.gl/v1/renders/Xz7pQ4w8nR2k/dataset \
-H "Authorization: Bearer dxgl_sk_..."
| Field | Type | Default | Description |
|---|---|---|---|
quality |
string | "standard" |
Quality tier: "share" (Standard, 540p H.264, 1 cr/sec), "standard" (HD, 1080p H.264, 4 cr/sec — the default when omitted), "4k" (4K H.264, 16 cr/sec), "pro" (ProRes 4444, 4K with alpha, 64 cr/sec). Display labels differ from JSON enum values; enums are frozen for API stability. |
effect |
string | "turntable" |
Camera path: "turntable" (360°), "hero-spin" (fast spin to front), "showcase" (look-around), "zoom-orbit" (360° + zoom), "reveal" (scale-up entrance), "keyframes" (custom fly-through, see keyframes/camera), "dataset" (vision training; flat cost per set by tier) |
sweepAngle |
number | — | Override sweep angle in degrees (hero-spin default 540, reveal default 180). Range: 10–1080. |
easeOutRatio |
number | — | Hero-spin deceleration fraction (default 0.6). Range: 0–1. |
amplitude |
number | — | Showcase swing angle in degrees (default 60). Range: 5–180. |
zoomStart |
number | — | Zoom-orbit starting radius multiplier (default 1.0). Range: 0.1–3. |
zoomEnd |
number | — | Zoom-orbit ending radius multiplier (default 0.7). Range: 0.1–3. |
scaleFrom |
number | — | Reveal starting model scale (default 0). Range: 0–1. |
vignette |
number | — | Vignette intensity (0 = none, 1 = full). Range: 0–1. |
aspect |
string | "16:9" |
Video aspect ratio: "16:9", "1:1", "9:16" |
bgColor |
string | "#ffffff" |
Background hex color, e.g. "#ff8800" (6-digit hex, must include #) |
bgImageId |
string | — | Overlay asset ID for background image (overrides bgColor). See Overlays. |
bgAlign |
string | "top-left" |
Crop anchor when background image doesn't match output aspect: "top-left", "top-center", "top-right", "center-left", "center", "center-right", "bottom-left", "bottom-center", "bottom-right" |
length |
number | 6 |
Video duration in seconds: greater than 0, up to 120 (ProRes 4444: up to 60; admin accounts uncapped). Decimal OK, e.g. 4.7. Credits = tier rate × seconds, 1-credit minimum. |
animation |
string | — | Animation clip name to play. Discover a model's clips via GET /v1/models/:id → animations ([{ name, duration, channels }], seconds); animationCount appears on list rows. Use "*" for the first clip. |
animationSpeed |
number | 1 |
Animation playback speed multiplier, −10 to 10, non-zero (e.g. 0.5 for half speed, 2 for double; negative values play the clip in reverse). |
shadows |
boolean | true |
Enable ground shadows |
reflector |
boolean | false |
Enable reflective ground plane |
easing |
boolean | true |
Ease in/out on turntable rotation |
strictCredits |
boolean | false |
Refuse the free-credit fallback: when paid credits are insufficient, fail with 402 no_credits instead of silently funding the render from the free pool and flagging it isPreview (a legacy path — current signups don't accrue free-pool credits, see Credits). Recommended for unattended pipelines. In a batch, one strict item that would fall to free credits fails the whole (atomic) batch. |
rotateY |
number | 0 |
Turntable camera starting angle in degrees (-180 to 180). Determines the initial camera direction before rotation begins. Does not rotate the model. |
tiltX |
number | 0 |
Model pitch in degrees (-180 to 180), applied in the yawed frame — see Model Orientation. Useful for angling products in portrait aspect. Bypasses shadow caching. |
panX |
number | 0 |
Model yaw in degrees (-180 to 180). Applied first, before tiltX and rollZ. |
rollZ |
number | 0 |
Model roll around the Z axis in degrees (-180 to 180). Applied last. |
zoom |
number | 1.0 |
Camera zoom factor (0.5 to 2.0): values above 1 move the camera closer, below 1 move it farther (distance = base ÷ zoom). |
output |
string | "video" |
"video" or "dataset" — datasets also take datasetQuality and coverage; see Dataset Export |
renderSettings accepts the full set of settings the Studio editor can
author — the same validator backs both surfaces. The fields above cover the
common cases; the following are also accepted (all optional). Invalid values
are rejected with invalid_settings.
Server-set keys.
previewandthumbOnlyare stamped by the server and rejected if sent. Note thatrenderSettingsobjects echoed back byGET /v1/renderscan contain a server-stampedpreview: true(free-pool-funded renders) — strip it before re-submitting those settings to a new render.
| Group | Fields |
|---|---|
| Filter / post | filter (none/sepia/bw/vivid/warm/cool/faded/noir/grain/toon), bloomEnabled, bloomStrength (0–2), dofEnabled, dofStrength (0–1), n8aoEnabled, n8aoIntensity (0.5–32), swayEnabled, swayIntensity (0–2) |
| Shadows & reflector | shadowMode (shader/flat/accumulated), shadowDarkness (0–5), shadowSoftness (0–1), reflectorMix (0–1), reflectorFade (0.05–5), reflectorDepthBlur (0–10) |
| Environment / IBL | envId (mint via POST /v1/assets/envs — a bad id hard-fails the render with a refund), envRotation (−180–180), envExposure (0.0156–4), envMode (grounded/lighting/floating), envOpacity (0–1), envBlur (0–1), envScale (0.25–4), envTint (hex), envTintStrength (0–1) |
| Lighting | spotlightIntensity (0–32), lightsIntensity (0–32), ambientIntensity (0–16), glossIntensity (0–16), keyLightColor (hex), fillLightColor (hex) |
| Model | hiddenMeshNames (string[], ≤512, each ≤256 chars) — see Mesh Exclusions |
| Background | bgGradient ({ stops: [{ color }] (2–8), angle: 0–359 } — both keys required when present) |
| Materials | materialEdits — object keyed by material name (≤256 keys, ≤32 KB serialized); each value takes color, roughness, metalness, opacity, alphaMode, emissive/emissiveIntensity, aoMapIntensity, envMapIntensity, side, flatShading, wireframe, normalScale, bumpScale, displacementScale, transmission, clearcoat/clearcoatRoughness, sheen/sheenRoughness/sheenColor, iridescence/iridescenceIOR, specularIntensity/specularColor, thickness, ior, attenuationColor, textureMaps toggles, mapAssetId (mint via POST /v1/assets/textures — a wrong id silently keeps the original map) |
| Decals | decals — array (≤64) of { id, imageAssetId, position:[x,y,z], normal:[x,y,z], euler:[x,y,z], size, imageAspect? (>0–100) } |
Keyframe camera (effect: "keyframes") |
keyframes (array 1–64 of { pose: { position, lookAt, fov }, dwell?, easing? } — all three pose fields required on every keyframe), camera ({ target, radius, theta, phi, fov } — all five required when present), startAngle/endAngle, startTilt/endTilt, startZoom/endZoom, startFov/endFov, easeInSeconds/easeOutSeconds, kfSegmentDuration, loop |
panX, tiltX, and rollZ rotate the model; rotateY offsets where the camera starts its orbit — the two are independent. The three model rotations compose in a fixed order — yaw first, then pitch in the yawed frame, then roll:
panX — yaw around the vertical axis (turn the model left/right)tiltX — pitch around the yawed X axis (lean it forward/back)rollZ — roll around the resulting Z axiszoom scales the camera distance: 2.0 = twice as close, 0.5 = twice as far (distance = base ÷ zoom).
curl -X POST https://api.dx.gl/v1/renders \
-H "Authorization: Bearer dxgl_sk_..." \
-H "Content-Type: application/json" \
-d '{
"modelId": "Ab3kF9x2qL1m",
"renderSettings": {
"quality": "share",
"panX": 35,
"tiltX": -12,
"rotateY": 90,
"zoom": 1.3
}
}'
This yaws the model 35°, leans it toward the camera by 12°, starts the turntable orbit a quarter-turn around, and moves the camera 30% closer. A common product-shot recipe: panX to face the label, a small negative tiltX for a heroic angle, zoom 1.2–1.5.
hiddenMeshNames hides named meshes for the duration of the render — useful for packaging variants, alternate trims, or scan scaffolding:
{
"modelId": "Ab3kF9x2qL1m",
"renderSettings": {
"quality": "share",
"hiddenMeshNames": ["Packaging", "Stand_Base", "Cap_Alt"]
}
}
Names must match the mesh names in the GLB exactly (case-sensitive). Unknown names are a silent no-op — the render succeeds without hiding anything — so verify names against your source file. Up to 512 names, each ≤256 characters. Hidden meshes are also excluded from ground shadows and reflections.
Discovering names:
GET /v1/models/:idreturnsmeshNames(the render-time mesh names, exactly ashiddenMeshNamesmatches them) andmaterialNames(the keysmaterialEditsmatches).nullmeans the model predates inventory extraction — re-upload, or ask us to run the backfill. Note mesh names are the runtime names (spaces become_, duplicate names gain_1/_2suffixes), which can differ from what your authoring tool shows.
| JSON value | Display label | Resolution | Codec | Alpha | Credits / sec | Output |
|---|---|---|---|---|---|---|
share |
Standard | 960×540 | H.264 MP4 | No | 1 | .mp4 |
standard |
HD (default when quality omitted) |
1920×1080 | H.264 MP4 | No | 4 | .mp4 |
4k |
4K | 3840×2160 | H.264 MP4 | No | 16 | .mp4 |
pro |
ProRes 4444 | 3840×2160 | ProRes 4444 | Yes | 64 | .mov |
The JSON enum values (
share/standard/4k/pro) are frozen for API stability. The display labels were renamed in a later UX pass; use the JSON values shown in the first column when making API calls.
pro renders always include an alpha channel — the bgColor is applied only to the web/thumbnail/poster variants. The .mov file has a transparent background for compositing in professional editors (DaVinci Resolve, After Effects, Final Cut Pro).
Video is billed per second — a render's cost is the tier rate above × its length in seconds. The default length is 6s, so a 6s share turntable = 6 credits, and a 6s render with quality omitted (= standard/HD) = 24 credits. All tiers render with GPU-accelerated 2× supersampling (SSAA) for anti-aliased edges.
Note: New accounts receive a 100-credit signup bonus. The bonus lands in the regular credit balance and behaves identically to purchased credits — no restrictions, any quality tier.
Set output: "dataset" with datasetQuality to generate a NeRF/3DGS-ready training dataset. Three tiers:
Tier (datasetQuality) |
Views | Resolution | Credits (flat) |
|---|---|---|---|
100x800 |
100 | 800×800 | 40 |
196x1024 |
196 | 1024×1024 | 160 |
400x2048 |
400 | 2048×2048 | 640 |
Use coverage: "hemisphere" (default) or coverage: "sphere" for full sphere viewpoints.
Output ZIP contains:
images/ — RGB PNG frames (composited on white background)depth/ — 8-bit grayscale depth PNGs (closer = darker, farther = lighter). Tight near/far planes maximize precision across the model's actual depth span.depth_16bit/ — 16-bit grayscale depth PNGs (65,536 levels of precision for surface reconstruction)normals/ — world-space normal map PNGsmasks/ — foreground/background alpha mask PNGstransforms.json — Camera intrinsics + per-frame 4×4 transform matrices (instant-ngp / nerfstudio format). Includes depth_near and depth_far for decoding: depth = pixel_value/255 * (depth_far - depth_near) + depth_nearoverview.webp — 4-quadrant contact sheet (10×10 grid of all views across RGB, depth, normals, masks)Quick start: Unzip the dataset and train a 3D Gaussian Splat with nerfstudio:
unzip dataset.zip -d mymodel
ns-train splatfacto --data ./mymodel \
--max-num-iterations 15000 \
--pipeline.model.sh-degree 3 \
--pipeline.model.background-color white
The background-color white flag is required because images are composited on a white background.
Renders progress through these statuses:
Any status can transition to error if the render fails. Poll for done or error; treat the intermediate statuses as opaque "in progress" states.
| Status | Description |
|---|---|
pending |
Queued, waiting for a worker to pick it up (cancellable) |
poster-processing |
Worker is rendering frames, poster extracted |
poster-done |
Recovery state: poster uploaded but the video pass was interrupted — a worker will resume it (not part of the happy path) |
video-processing |
Video being encoded (ffmpeg) |
done |
All assets ready (video, poster, thumbnail) |
error |
Render failed |
failed |
An errored render you dismissed via PATCH /v1/renders/:id/dismiss |
Overlay assets are reusable background (or foreground) images for renders. Upload once, reference by ID in render settings. System overlays (published by admins) are available to all users.
| Field | Type | Default | Description |
|---|---|---|---|
file |
file | — | Image file (PNG, JPEG, WebP; max 25 MB) |
layer |
string | "background" |
"background", "foreground", "decal" (referenced from decals[].imageAssetId), or "texture" (referenced from materialEdits.<name>.mapAssetId for base-color swaps). Any other value is rejected with 400 bad_request. |
name |
string | — | Display name (max 100 chars) |
description |
string | — | Short description (max 500 chars) |
alignment |
string | "top-left" |
Crop anchor when image doesn't match output aspect ratio |
opacity |
number | 1 |
Overlay opacity, 0–1 (background/foreground compositing) |
Response 200:
{ "data": { "id": "abc123DEF456", "layer": "background", "alignment": "top-left", "name": "My Gradient" } }
List items additionally carry fileSize, width, height, opacity, filename, isSystem, and createdAt.
Uploading requires the render scope (listing requires read). Each account can hold up to 100 overlay assets — at the cap, uploads return 429 limit_reached; delete unused assets to free slots.
Returns your overlay assets plus all published system overlays.
Deletes an overlay you own. Removes the image from storage.
Pass the overlay id as bgImageId in render settings:
{ "renderSettings": { "bgImageId": "abc123DEF456", "aspect": "16:9", "length": 6 } }
When bgImageId is set, bgColor is ignored. The image is scaled to cover the output dimensions and cropped from the bgAlign anchor (default: top-left), keeping logos safe across all aspect ratios.
Pre-flight for bulk pipelines: returns { "id": "...", "exists": true } if this exact source content already exists as an asset of that layer in the workspace (uploads record a content sha) — skip the upload and reuse the id instead of re-uploading into the 100-asset cap. layer defaults to background; texture works too.
Beyond overlays, renderSettings references two more asset kinds — both mintable over the API. Assets are workspace-scoped: they resolve in the workspace's renders (see Workspace-bound keys).
materialEdits.<name>.mapAssetId)POST returns 201 { "id": ... } — pass the id as materialEdits.<materialName>.mapAssetId to replace that material's base-color map. Re-POSTing identical bytes returns 200 with deduped: true and the existing id (content-sha dedup, per workspace). Quota: 100 textures / 500 MB per account; at the cap uploads return 429 limit_reached.
Failure mode: a wrong or missing
mapAssetIdsilently degrades — the render keeps the model's original map (deliberate stale-edit tolerance). Verify the id (it's echoed inrenderSettings) and the material name (GET /v1/models/:id→materialNames, exact match).
envId)Custom HDRI environments use the pre-encoded gainmap triple — the exact output of gainmap-js encodeAndCompress (the same encoder Studio runs in the browser): sdr (WebP), gainmap (WebP), metadata (JSON), plus an optional small thumb WebP. There is no server-side HDR→gainmap encoder — encode your .hdr/.exr with gainmap-js, or upload it through Studio's Environment panel and reuse the id. Parts are validated on upload (WebP magic, JSON metadata); caps: 48 MB per part, 20 environments / 200 MB per account (429 limit_reached at the cap).
POST returns 201 { "id": ... } — pass it as envId in render settings, with envMode/envRotation/envExposure etc. controlling how it's used.
Failure mode (asymmetric with textures): a bad
envIdhard-fails the render — the worker can't fetch the environment, the render errors withfailureCode: "env_download_failed", and the credits are refunded. Textures degrade silently; environments fail loudly.
Returns the authenticated user's account info and credit balance.
curl https://api.dx.gl/v1/account \
-H "Authorization: Bearer dxgl_sk_..."
{
"data": {
"id": "Ab3kF9x2qL1m",
"email": "[email protected]",
"credits": 100,
"paid": 100,
"free": 0,
"total": 100,
"scopes": ["read", "render"],
"workspace": null
}
}
| Field | Type | Description |
|---|---|---|
credits |
integer | Spendable credit balance (unchanged; equals paid) |
paid |
integer | Ordinary render credits — the signup bonus and any purchases land here |
free |
integer | Legacy free-credit grant type; normally 0 (accounts, including the signup bonus, are funded with ordinary paid credits) |
total |
integer | paid + free — matches what /quote reports as available |
scopes |
array | The scopes granted to the token making this request |
workspace |
object | The workspace this key is bound to ({ id, name }), or null for a personal key. For a bound key the balances above are the workspace owner's pool — the one that funds this key's renders. |
Estimate the credit cost for a set of renders before committing. Useful for agents and scripts that need to budget credits across multiple models.
{
"renders": [
{ "quality": "standard", "length": 12 },
{ "quality": "4k" },
{ "output": "dataset", "datasetQuality": "196x1024" }
]
}
Each item in the renders array accepts the cost-relevant render fields: quality, output, datasetQuality, and length (video duration in seconds; defaults to 6). Two legacy aliases are also honored: videoLength (equivalent to length) and effect: "dataset" (equivalent to output: "dataset"). Video cost = tier rate × length; datasets are flat per set.
Response 200:
{
"data": {
"creditsRequired": 304,
"creditsAvailable": 500,
"sufficient": true,
"breakdown": [
{ "quality": "standard", "length": 12, "credits": 48 },
{ "quality": "4k", "length": 6, "credits": 96 },
{ "datasetQuality": "196x1024", "credits": 160 }
]
}
}
| Field | Type | Description |
|---|---|---|
creditsRequired |
integer | Total credits needed for all renders |
creditsAvailable |
integer | Current credit balance (paid + free) |
sufficient |
boolean | Whether the account has enough credits |
breakdown |
array | Per-item credit cost |
Maximum 100 items per quote request.
curl -X POST https://api.dx.gl/v1/quote \
-H "Authorization: Bearer dxgl_sk_..." \
-H "Content-Type: application/json" \
-d '{"renders": [{"quality": "4k"}, {"quality": "4k"}, {"quality": "4k"}]}'
Read-only endpoints for discovering credit packs and reconciling spend. Purchasing itself happens in the web app (Stripe checkout), not over the API.
Lists the purchasable credit packs.
{
"data": [
{ "id": "…", "name": "1,000 credits", "credits": 1000, "priceCents": 7900, "stripePriceId": "price_…" }
]
}
Returns the caller's credit ledger (purchases, unlocks, refunds, grants), newest first, scoped to the token's account.
| Param | Type | Description |
|---|---|---|
limit |
integer | Max 100, default 50 |
offset |
integer | Pagination offset |
{
"data": [
{
"id": "…",
"createdAt": "2026-02-15T20:00:01.000Z",
"type": "purchase",
"credits": 1000,
"priceCents": 7900,
"source": "storefront",
"description": "1,000 credit pack",
"renderId": null
}
],
"meta": { "total": 12, "offset": 0, "limit": 50 }
}
type is one of purchase, unlock, render, refund, promo, referral,
admin_grant, signup_bonus. renderId is set for entries tied to a specific
render (e.g. an unlock), otherwise null.
No authentication required. Returns database connectivity status.
Video is charged per second of output (cost = rate × seconds); datasets are a flat cost per set:
| Quality | Credits / sec |
|---|---|
share |
1 |
standard |
4 |
4k |
16 |
pro |
64 |
New accounts include a 100-credit signup bonus, added to the regular credit balance — no restrictions, any quality tier. Credits never expire.
Batch renders deduct credits atomically based on the sum of all items' quality costs. If there aren't enough credits, the entire batch fails.
When credits are exhausted, render requests return 402 with error code no_credits.
Free-pool fallback — for unattended runs. Accounts can additionally hold a separate free credit pool (support grants and refunds of free-funded renders; current signups don't accrue it). If the paid balance can't cover a render but the free pool can, the render is funded from the free pool and flagged isPreview: true — the request still succeeds with HTTP 201. Unattended pipelines that want deterministic billing should either check isPreview on the response, or set strictCredits: true in renderSettings to hard-fail with 402 no_credits instead of falling back.
Requests are rate-limited per token account. When a limit is hit, the API
returns 429 with error code rate_limited and a Retry-After header (in
seconds) — honor it and retry. Current defaults (subject to tuning):
| Scope | Limit |
|---|---|
| All authenticated requests | 300 / minute |
POST /v1/models/ingest (URL fetches) |
20 / 10 minutes |
Batch endpoints make high throughput cheap within these limits — one
POST /v1/renders/batch call submits up to 100 renders.
Independent of request-rate limits, the following hard caps apply today:
| Limit | Value |
|---|---|
| Upload / ingest file size | 1 GB |
| Ingest download timeout | 45 s without progress (10 min absolute) |
| Overlay image size | 25 MB |
| Overlay assets per account | 100 |
Renders per batch (POST /v1/renders/batch) |
100 |
Items per quote (POST /v1/quote) |
100 |
List page size (limit) |
100 |
| Code | Status | Description |
|---|---|---|
unauthorized |
401 | Missing, invalid, or revoked token |
token_expired |
401 | Token has expired |
forbidden |
403 | Account suspended, or operating on a resource you don't own (e.g. deleting a system overlay) |
insufficient_scope |
403 | Token lacks required scope |
no_file |
400 | No file in upload request |
bad_request |
400 | Malformed request (e.g. no updatable fields in a PATCH, invalid overlay upload, invalid cursor/updatedSince, unknown asset param) |
invalid_format |
400 | File extension not accepted (.glb / .zip) |
invalid_url |
400 | Malformed URL |
invalid_url_scheme |
400 | Ingest URL must be http(s) |
url_error |
400 | Ingest URL could not be fetched |
download_timeout |
400 | Ingest download stalled (45 s) or exceeded the 10-minute ceiling |
url_required |
400 | Missing url field |
invalid_settings |
400 | Invalid renderSettings value |
invalid_length |
422 | Video length outside the allowed range for the tier |
model_id_required |
400 | Missing modelId field |
file_too_large |
400 | File exceeds the 1 GB limit |
no_credits |
402 | No render credits remaining |
model_not_found |
404 | Model does not exist |
render_not_found |
404 | Render does not exist |
dataset_not_found |
404 | Dataset ZIP not available (render not done or not a dataset render) |
not_found |
404 | Unknown endpoint, HLS asset not ready, or resource missing (e.g. overlay on DELETE) |
asset_not_found |
404 | Requested asset variant was not produced for this render (/download-url) |
rate_limited |
429 | Rate limit hit — honor the Retry-After header (see Rate Limits) |
render_started |
409 | Cancel refused — the worker already claimed the job |
render_in_flight |
409 | Delete refused — render is queued or in progress (cancel instead) |
renders_required |
400 | Missing or empty renders array |
too_many |
400 | Batch exceeds 100 renders |
upload_limit |
429 | Free upload limit reached |
limit_reached |
429 | Overlay asset cap (100) reached |
video_not_found |
404 | Video not yet available |
poster_not_found |
404 | Poster not yet available |
thumb_not_found |
404 | Thumbnail not yet available |
file_not_found |
404 | Model file not found in storage |
sha256_required |
400 | Missing sha256 field |
timeout |
408 | URL pre-flight (HEAD) timed out |
upload_failed |
varies | Upload could not be processed (message has detail) |
ingest_failed |
varies | URL ingest failed (message has detail) |
internal |
500 | Internal server error |
# Upload a GLB model and start rendering
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}'
# Upload a 3D scan (OBJ+MTL+textures in a ZIP)
curl -X POST https://api.dx.gl/v1/models \
-H "Authorization: Bearer dxgl_sk_..." \
-F "[email protected]" \
-F 'renderSettings={"aspect":"1:1","bgColor":"#ffffff","length":9}'
# Create a 4K render (16 credits/sec — default 6s = 96 credits)
curl -X POST https://api.dx.gl/v1/renders \
-H "Authorization: Bearer dxgl_sk_..." \
-H "Content-Type: application/json" \
-d '{"modelId":"Ab3kF9x2qL1m","renderSettings":{"quality":"4k","aspect":"16:9","bgColor":"#ffffff"}}'
# Create a Pro render (ProRes 4444 with alpha, 64 credits/sec — default 6s = 384 credits)
curl -X POST https://api.dx.gl/v1/renders \
-H "Authorization: Bearer dxgl_sk_..." \
-H "Content-Type: application/json" \
-d '{"modelId":"Ab3kF9x2qL1m","renderSettings":{"quality":"pro","aspect":"16:9","bgColor":"#000000"}}'
# Check render status
curl https://api.dx.gl/v1/renders/Xz7pQ4w8nR2k \
-H "Authorization: Bearer dxgl_sk_..."
# Download the video when done
curl -o chair.mp4 https://api.dx.gl/v1/renders/Xz7pQ4w8nR2k/video \
-H "Authorization: Bearer dxgl_sk_..."
# Download all assets as a zip
curl -o chair.zip https://api.dx.gl/v1/renders/Xz7pQ4w8nR2k/bundle \
-H "Authorization: Bearer dxgl_sk_..."
import requests, time
API = "https://api.dx.gl/v1"
HEADERS = {"Authorization": "Bearer dxgl_sk_..."}
# Upload
with open("chair.glb", "rb") as f:
r = requests.post(f"{API}/models", headers=HEADERS,
files={"file": f},
data={"renderSettings": '{"aspect":"16:9","length":6}'})
render_id = r.json()["data"]["renderId"]
# Poll
while True:
r = requests.get(f"{API}/renders/{render_id}", headers=HEADERS)
status = r.json()["data"]["status"]
if status == "done": break
if status == "error": raise Exception("Render failed")
time.sleep(5)
# Download
r = requests.get(f"{API}/renders/{render_id}/video", headers=HEADERS)
with open("chair.mp4", "wb") as f:
f.write(r.content)
const fs = require('fs');
const API = 'https://api.dx.gl/v1';
const headers = { 'Authorization': 'Bearer dxgl_sk_...' };
// Upload (Node 20+: openAsBlob pairs with the built-in fetch/FormData —
// a ReadStream would be stringified by the built-in FormData, not streamed)
const form = new FormData();
form.append('file', await fs.openAsBlob('chair.glb'), 'chair.glb');
form.append('renderSettings', JSON.stringify({ aspect: '16:9', length: 6 }));
const upload = await fetch(API + '/models', { method: 'POST', headers, body: form });
const renderId = (await upload.json()).data.renderId;
// Poll
let status;
do {
await new Promise(r => setTimeout(r, 5000));
const res = await fetch(API + '/renders/' + renderId, { headers });
status = (await res.json()).data.status;
} while (status !== 'done' && status !== 'error');
// Download
const video = await fetch(API + '/renders/' + renderId + '/video', { headers });
fs.writeFileSync('chair.mp4', Buffer.from(await video.arrayBuffer()));