dx.gl

API Documentation

HTML
# DXGL Public API

Base URL: `https://api.dx.gl/v1`

## Authentication

All requests require a Bearer token in the `Authorization` header:

```
Authorization: Bearer dxgl_sk_...
```

Tokens are created in the [Client Portal](https://dx.gl/portal) via the **API Keys** button in the utility strip. The raw token is shown once on creation — store it securely.

---

## Response Format

**Success:**
```json
{
  "data": { ... }
}
```

**List (paginated):**
```json
{
  "data": [ ... ],
  "meta": { "total": 142, "offset": 0, "limit": 50 }
}
```

**Error:**
```json
{
  "error": {
    "code": "model_not_found",
    "message": "Model not found",
    "status": 404
  }
}
```

---

## Models

A **model** is an uploaded 3D file (GLB/glTF) or a converted 3D scan. Uploading a model automatically creates a render job.

### Upload Model

```
POST /v1/models
Content-Type: multipart/form-data
```

| Field | Type | Required | Description |
|---|---|---|---|
| `file` | File | Yes | `.glb`, `.gltf`, or `.zip` (OBJ+MTL+textures) |
| `renderSettings` | JSON string | No | Render configuration (see below) |

**Response** `201`:
```json
{
  "data": {
    "modelId": "Ab3kF9x2qL1m",
    "renderId": "Xz7pQ4w8nR2k"
  }
}
```

If the file's SHA-256 matches an existing upload, a new render is created for the existing model and `duplicate: true` is returned.

**Instant preview:** Every upload automatically triggers a free system preview render (no credits deducted). The preview generates a lightweight thumbnail video within seconds, which also serves as a model integrity check — if the preview fails, the model likely has issues (broken geometry, missing textures, invalid glTF). Check the model's renders list for a render with `is_system_preview: true`.

**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
Content-Type: application/json
```

```bash
curl -X POST https://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.

Downloads the file from the URL (max 100 MB, 2-minute timeout). SHA-256 deduplication applies. URL ingest currently supports GLB/glTF only. For OBJ scans, use the file upload endpoint with a ZIP.

### 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 |
| `tags` | string | — | Comma-separated tags (OR filter) |

```bash
curl "https://dx.gl/v1/models?limit=10&tags=furniture" \
  -H "Authorization: Bearer dxgl_sk_..."
```

**Response** `200`:
```json
{
  "data": [
    {
      "id": "Ab3kF9x2qL1m",
      "originalName": "chair.glb",
      "title": "Ergonomic Chair",
      "sku": "EC-1001",
      "tags": ["furniture", "office"],
      "fileSize": 4521984,
      "sha256": "a1b2c3...",
      "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:

```bash
curl https://dx.gl/v1/models/Ab3kF9x2qL1m \
  -H "Authorization: Bearer dxgl_sk_..."
```

```json
{
  "data": {
    "id": "Ab3kF9x2qL1m",
    "originalName": "chair.glb",
    "title": "Ergonomic Chair",
    "sku": "EC-1001",
    "tags": ["furniture", "office"],
    "fileSize": 4521984,
    "sha256": "a1b2c3...",
    "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
Content-Type: application/json
```

| Field | Type | Description |
|---|---|---|
| `title` | string | Display title |
| `sku` | string | Product code |
| `tags` | string[] | Up to 20 tags (lowercased, trimmed) |

All fields are optional — only provided fields are changed.

```bash
curl -X PATCH https://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 default list queries but can be restored.

```bash
curl -X PATCH https://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.

```bash
curl -o chair.glb https://dx.gl/v1/models/Ab3kF9x2qL1m/file \
  -H "Authorization: Bearer dxgl_sk_..."
```

### Check Hash (Dedup Pre-flight)

```
POST /v1/models/check-hash
Content-Type: application/json
```

```json
{ "sha256": "a1b2c3d4..." }
```

**Response** `200`:
```json
{
  "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
Content-Type: application/json
```

```json
{
  "modelId": "Ab3kF9x2qL1m",
  "renderSettings": {
    "quality": "share",
    "aspect": "16:9",
    "bgColor": "#ffffff",
    "length": 6,
    "animation": "Walk",
    "animationSpeed": 1,
    "shadows": true,
    "reflector": false,
    "easing": true
  }
}
```

The `quality` field is optional (defaults to `"share"`, which is Standard 540p — 1 credit). Set to `"standard"` for HD 1080p H.264 (4 credits), `"4k"` for 4K H.264 (16 credits), or `"pro"` for 4K ProRes 4444 with alpha (64 credits). Note that the JSON enum values are kept for API stability; they do not match the portal's display labels one-to-one. See [Render Settings](#render-settings) for all options.

**Response** `201`:
```json
{
  "data": {
    "id": "Xz7pQ4w8nR2k",
    "modelId": "Ab3kF9x2qL1m",
    "status": "pending",
    "renderSettings": { ... }
  }
}
```

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

### Get Render

```
GET /v1/renders/:id
```

```json
{
  "data": {
    "id": "Xz7pQ4w8nR2k",
    "modelId": "Ab3kF9x2qL1m",
    "status": "done",
    "renderSettings": { ... },
    "fileSize": 2457600,
    "errorMessage": null,
    "hasWebVariant": true,
    "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 | Error detail when status is `error` |
| `hasWebVariant` | boolean | Whether a web-optimized MP4 is available |

### Dismiss Render

```
PATCH /v1/renders/:id/dismiss
```

Dismisses an errored render (transitions `error` → `failed`). Useful for acknowledging errors programmatically.

### Delete Render

```
DELETE /v1/renders/:id
```

Permanently deletes the render and its files (video, poster, thumbnail).

### Batch Render

```
POST /v1/renders/batch
Content-Type: application/json
```

```json
{
  "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`:
```json
{
  "data": [
    { "id": "Xz7pQ4w8nR2k", "modelId": "Ab3kF9x2qL1m", "status": "pending" },
    { "id": "Bc5nL2x9mQ3r", "modelId": "Ab3kF9x2qL1m", "status": "pending" },
    { "id": "Wv8jR6t4kP1s", "modelId": "Yz9mK3v7pN4j", "status": "pending" }
  ]
}
```

Renders for the same model are automatically grouped into a batch — the worker renders shared frames once and encodes variants in parallel.

---

## Assets

Download the output files of a completed render.

### Video

```
GET /v1/renders/:id/video
GET /v1/renders/:id/video?quality=web
```

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
GET /v1/renders/:id/poster?quality=full
```

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:

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

```bash
curl -o assets.zip https://api.dx.gl/v1/renders/Xz7pQ4w8nR2k/bundle \
  -H "Authorization: Bearer dxgl_sk_..."
```

---

## Render Settings

| Field | Type | Default | Description |
|---|---|---|---|
| `quality` | string | `"share"` | Quality tier: `"share"` (Standard, 540p H.264 — default), `"standard"` (HD, 1080p H.264), `"4k"` (4K H.264), `"pro"` (ProRes 4444, 4K with alpha). Portal 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), `"dataset"` (vision training, 4 credits) |
| `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](#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: `3` – `30` (decimal OK, e.g. `4.7`). Admin accounts: up to `600`. |
| `animation` | string | — | Animation clip name to play (from model's embedded animations). Use `"*"` for the first clip. |
| `animationSpeed` | number | `1` | Animation playback speed multiplier (e.g. `0.5` for half speed, `2` for double). |
| `shadows` | boolean | `true` | Enable ground shadows |
| `reflector` | boolean | `false` | Enable reflective ground plane |
| `easing` | boolean | `true` | Ease in/out on turntable rotation |
| `rotateY` | integer | `0` | Turntable camera starting angle in degrees (-180 to 180). Determines the initial camera direction before rotation begins. Does not rotate the model. |
| `tiltX` | integer | `0` | Forward/back tilt in degrees (-90 to 90). Useful for angling products in portrait aspect. Bypasses shadow caching. |
| `panX` | integer | `0` | Horizontal pan (yaw) of the model in degrees (-180 to 180). Applied after tilt. |
| `rollZ` | integer | `0` | Roll rotation of the model around Z axis in degrees (-180 to 180). |
| `zoom` | number | `1.0` | Camera distance multiplier (0.5 to 2.0). Values below 1 move closer, above 1 move farther. |

### Quality Tiers

| JSON value | Portal label | Resolution | Codec | Alpha | Credits | Output |
|---|---|---|---|---|---|---|
| `share` | Standard (default) | 960×540 | H.264 MP4 | No | 1 | `.mp4` |
| `standard` | HD | 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 portal's 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).

All tiers render with GPU-accelerated 2× supersampling (SSAA) for anti-aliased edges.

> **Note:** New accounts receive 10 free credits on signup. These credits behave identically to purchased credits — no watermarks, no restrictions, and they can be used on 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 |
|---|---|---|---|
| `100x800` | 100 | 800×800 | 4 |
| `196x1024` | 196 | 1024×1024 | 16 |
| `400x2048` | 400 | 2048×2048 | 64 |

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 PNGs
- `masks/` — foreground/background alpha mask PNGs
- `transforms.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_near`
- `overview.webp` — 4-quadrant contact sheet (10×10 grid of all views across RGB, depth, normals, masks)

```
POST /v1/renders
{ "modelId": "Ab3kF9x2qL1m", "renderSettings": { "output": "dataset", "datasetQuality": "100x800" } }
```

**Quick start:** Unzip the dataset and train a 3D Gaussian Splat with [nerfstudio](https://docs.nerf.studio/):

```bash
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:

```
pending → poster-processing → poster-done → video-processing → done
```

Any status can transition to `error` if the render fails.

| Status | Description |
|---|---|
| `pending` | Queued, waiting for a worker to pick it up |
| `poster-processing` | Worker is rendering frames, poster extracted |
| `poster-done` | Poster uploaded, video encoding in progress |
| `video-processing` | Video being encoded (ffmpeg) |
| `done` | All assets ready (video, poster, thumbnail) |
| `error` | Render failed |

---

## 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
Content-Type: multipart/form-data
```

| Field | Type | Default | Description |
|-------|------|---------|-------------|
| `file` | file | — | Image file (PNG, JPEG, WebP; max 10 MB) |
| `layer` | string | `"background"` | `"background"` or `"foreground"` |
| `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 |

**Response `200`:**
```json
{ "data": { "id": "abc123DEF456", "layer": "background", "alignment": "top-left", "name": "My Gradient" } }
```

### List Overlays

```
GET /v1/overlays
GET /v1/overlays?layer=background
```

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:

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

---

## Account

```
GET /v1/account
```

Returns the authenticated user's account info and credit balance.

```bash
curl https://dx.gl/v1/account \
  -H "Authorization: Bearer dxgl_sk_..."
```

```json
{
  "data": {
    "id": "Ab3kF9x2qL1m",
    "email": "user@example.com",
    "credits": 47
  }
}
```

---

## 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
Content-Type: application/json
```

```json
{
  "renders": [
    { "quality": "standard" },
    { "quality": "4k" },
    { "output": "dataset", "datasetQuality": "196x1024" }
  ]
}
```

Each item in the `renders` array accepts the same fields as render settings (`quality`, `output`, `datasetQuality`). Only cost-relevant fields are used.

**Response** `200`:
```json
{
  "data": {
    "creditsRequired": 36,
    "creditsAvailable": 47,
    "sufficient": true,
    "breakdown": [
      { "quality": "standard", "credits": 4 },
      { "quality": "4k", "credits": 16 },
      { "datasetQuality": "196x1024", "credits": 16 }
    ]
  }
}
```

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

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

---

## Health Check

```
GET /v1/health
```

No authentication required. Returns database connectivity status.

---

## Render Credits

Render cost depends on quality tier:

| Quality | Credits |
|---|---|
| `share` | 1 |
| `standard` | 4 |
| `4k` | 16 |
| `pro` | 64 |

New accounts include **10 free credits on signup**. These credits behave identically to purchased credits — no watermarks, no restrictions, and they work on 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`.

---

## Rate Limits

Not currently enforced. Planned for a future release.

---

## Errors

| Code | Status | Description |
|---|---|---|
| `unauthorized` | 401 | Missing, invalid, or revoked token |
| `token_expired` | 401 | Token has expired |
| `forbidden` | 403 | Account suspended |
| `insufficient_scope` | 403 | Token lacks required scope |
| `no_file` | 400 | No file in upload request |
| `invalid_format` | 400 | File is not .glb, .gltf, or .zip |
| `invalid_url` | 400 | Malformed URL |
| `url_required` | 400 | Missing `url` field |
| `invalid_settings` | 400 | Invalid renderSettings value |
| `model_id_required` | 400 | Missing `modelId` field |
| `file_too_large` | 400 | File exceeds 100 MB limit |
| `no_credits` | 402 | No render credits remaining |
| `model_not_found` | 404 | Model does not exist |
| `render_not_found` | 404 | Render does not exist |
| `renders_required` | 400 | Missing or empty renders array |
| `too_many` | 400 | Batch exceeds 100 renders |
| `upload_limit` | 429 | Free upload limit 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 download timed out |
| `internal` | 500 | Internal server error |

---

## Quick Start

```bash
# Upload a GLB model and start rendering
curl -X POST https://api.dx.gl/v1/models \
  -H "Authorization: Bearer dxgl_sk_..." \
  -F "file=@chair.glb" \
  -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 "file=@artec-scan.zip" \
  -F 'renderSettings={"aspect":"1:1","bgColor":"#ffffff","length":9}'

# Create a 4K render (16 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)
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

```python
import requests, time

API = "https://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

```javascript
const fs = require('fs');

const API = 'https://dx.gl/v1';
const headers = { 'Authorization': 'Bearer dxgl_sk_...' };

// Upload
const form = new FormData();
form.append('file', fs.createReadStream('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()));
```