DX.GL

API Reference

Markdown Guide

Complete /v1 REST reference — endpoints, render settings, credit rates, errors

Contents
AuthenticationResponse FormatModelsRendersAssetsRender SettingsRender StatusOverlaysRender Assets (Textures & Environments)AccountQuoteBillingHealth CheckRender CreditsRate LimitsErrorsQuick Start

Authentication

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) → APIKeysCreate 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.

Workspace-bound keys

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.

A key created without a workspace (or before workspace keys existed) operates on your personal workspace, exactly as before.


Response Format

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.


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).

Upload Model

POST /v1/models

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.).

Ingest from URL

POST /v1/models/ingest

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.

Staged Upload (Presigned PUT)

For large models, flaky links, or bucket-to-bucket pipelines, skip the multipart POST and upload straight to storage:

POST /v1/models/create-upload-url → { "url", "ticket", "expiresIn", "maxBytes" }

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\"}"

List Models

GET /v1/models?limit=50&offset=0&tags=furniture

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 }
}

Get Model

GET /v1/models/:id

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"
      }
    ]
  }
}

Update Model

PATCH /v1/models/:id

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"]}'

Archive Model

PATCH /v1/models/:id/archive

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_..."

Unarchive Model

PATCH /v1/models/:id/unarchive

Restores an archived model back to active status.

Download Model File

GET /v1/models/:id/file

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_..."

Check Hash (Dedup Pre-flight)

POST /v1/models/check-hash

{ "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.

Delete Model

DELETE /v1/models/:id

Soft-deletes the model. Existing renders remain accessible until independently deleted.


Renders

A render is a video generated from a model. Creating a render queues it for processing by the render pipeline.

Create Render

POST /v1/renders

{
  "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 quality field is optional and defaults to "standard" (HD 1080p) at 4 credits per second when omitted — a default 6-second render with no quality set costs 24 credits. Pass "share" (540p, 1 credit/sec) explicitly for the cheapest tier: a 6s share render 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.

List Renders

GET /v1/renders?model=Ab3kF9x2qL1m&status=done&limit=50&offset=0

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)

Get Render

GET /v1/renders/:id

{
  "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.

Dismiss Render

PATCH /v1/renders/:id/dismiss

Dismisses an errored render (transitions errorfailed). Useful for acknowledging errors programmatically.

Cancel Render

POST /v1/renders/:id/cancel

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.

Delete Render

DELETE /v1/renders/:id

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).

Batch Render

POST /v1/renders/batch

{
  "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.


Assets

Download the output files of a completed render.

Video

GET /v1/renders/:id/video

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)

Poster

GET /v1/renders/:id/poster

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

Thumbnail

GET /v1/renders/:id/thumb

Returns a quarter-resolution MP4 thumbnail video. Supports Range headers.

Content-Type: video/mp4

Bundle (Zip Download)

GET /v1/renders/:id/bundle

Downloads all render assets as a single ZIP file. The zip is streamed directly — no server-side buffering, suitable for large ProRes files. Contains:

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_..."

HLS Streaming

GET /v1/renders/:id/hls

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:

GET /v1/renders/:id/hls/*

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.

Presigned Download URLs

GET /v1/renders/:id/download-url?asset=video&expires=3600

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.)

Dataset (Vision Training ZIP)

GET /v1/renders/:id/dataset

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_..."

Render Settings

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/:idanimations ([{ 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

Advanced (Studio) settings

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. preview and thumbOnly are stamped by the server and rejected if sent. Note that renderSettings objects echoed back by GET /v1/renders can contain a server-stamped preview: 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

Model Orientation

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:

  1. panX — yaw around the vertical axis (turn the model left/right)
  2. tiltX — pitch around the yawed X axis (lean it forward/back)
  3. rollZ — roll around the resulting Z axis

zoom 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.

Mesh Exclusions

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/:id returns meshNames (the render-time mesh names, exactly as hiddenMeshNames matches them) and materialNames (the keys materialEdits matches). null means 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/_2 suffixes), which can differ from what your authoring tool shows.

Quality Tiers

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.

Dataset Export (Vision Training)

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:

POST /v1/renders

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.


Render Status

Renders progress through these statuses:

pendingposter-processingvideo-processingdone

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

Overlays

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.

Upload Overlay

POST /v1/overlays

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.

List Overlays

GET /v1/overlays

Returns your overlay assets plus all published system overlays.

Delete Overlay

DELETE /v1/overlays/:id

Deletes an overlay you own. Removes the image from storage.

Using Overlays in Renders

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.

Resolve by SHA-256

POST /v1/overlays/resolve

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.


Render Assets (Textures & Environments)

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).

Textures (materialEdits.<name>.mapAssetId)

POST /v1/assets/textures multipart: file (PNG/JPEG/WebP ≤ 25 MB), name?

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 mapAssetId silently degrades — the render keeps the model's original map (deliberate stale-edit tolerance). Verify the id (it's echoed in renderSettings) and the material name (GET /v1/models/:idmaterialNames, exact match).

Environments (envId)

POST /v1/assets/envs multipart: sdr, gainmap, metadata, thumb?, name?, width?, height?

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 envId hard-fails the render — the worker can't fetch the environment, the render errors with failureCode: "env_download_failed", and the credits are refunded. Textures degrade silently; environments fail loudly.


Account

GET /v1/account

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.

Quote

Estimate the credit cost for a set of renders before committing. Useful for agents and scripts that need to budget credits across multiple models.

POST /v1/quote

{
  "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"}]}'

Billing

Read-only endpoints for discovering credit packs and reconciling spend. Purchasing itself happens in the web app (Stripe checkout), not over the API.

Products

GET /v1/products

Lists the purchasable credit packs.

{
  "data": [
    { "id": "…", "name": "1,000 credits", "credits": 1000, "priceCents": 7900, "stripePriceId": "price_…" }
  ]
}

Purchases

GET /v1/purchases?limit=50&offset=0

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.


Health Check

GET /v1/health

No authentication required. Returns database connectivity status.


Render Credits

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.


Rate Limits

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

Errors

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

Quick Start

# 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_..."

Python Example

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)

Node.js Example

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()));