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) under the **API** tab. 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.

**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": {
    "aspect": "16:9",
    "bgColor": "#ffffff",
    "length": 6,
    "shadows": true,
    "reflector": false,
    "easing": true
  }
}
```

**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 | `"standard"` | Quality tier: `"standard"` (1080p H.264), `"4k"` (4K H.264), `"pro"` (4K ProRes 4444 with alpha) |
| `effect` | string | `"turntable"` | Camera motion: `"turntable"` (360°), `"hero-spin"` (fast spin to front), `"showcase"` (look-around), `"zoom-orbit"` (360° + zoom), `"reveal"` (scale-up entrance), `"dataset"` (vision training, 1 credit) |
| `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. |
| `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 `#`) |
| `length` | integer | `6` | Video duration in seconds: `3` – `15` (integer) |
| `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` | Starting angle in degrees (-180 to 180). Rotates the model so a different side faces camera first. Negative = left, positive = right. |
| `tiltX` | integer | `0` | Forward/back tilt in degrees (-90 to 90). Useful for angling products in portrait aspect. Bypasses shadow caching. |

### Quality Tiers

| Tier | Resolution | Codec | Alpha | Credits | Output |
|---|---|---|---|---|---|
| `standard` | 1920×1080 | H.264 MP4 | No | 1 | `.mp4` |
| `4k` | 3840×2160 | H.264 MP4 | No | 4 | `.mp4` |
| `pro` | 3840×2160 | ProRes 4444 | Yes | 16 | `.mov` |

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:** The `preview` watermark is automatically applied when using free render credits (standard quality only). Free credits cannot be used for `4k` or `pro` renders.

### 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 | 1 |
| `196x1024` | 196 | 1024×1024 | 4 |
| `400x2048` | 400 | 2048×2048 | 16 |

Use `coverage: "hemisphere"` (default) or `coverage: "sphere"` for full sphere viewpoints.

Output ZIP contains:
- `images/` — RGB PNG frames (composited on white background)
- `depth/` — 100 8-bit grayscale depth PNGs (closer = darker, farther = lighter). Tight near/far planes maximize precision.
- `depth_16bit/` — 100 16-bit grayscale depth PNGs (65,536 levels of precision for surface reconstruction)
- `normals/` — 100 world-space normal map PNGs
- `masks/` — 100 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": { "effect": "dataset" } }
```

---

## 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": "[email protected]",
    "credits": 47
  }
}
```

---

## Health Check

```
GET /v1/health
```

No authentication required. Returns database connectivity status.

---

## Render Credits

Render cost depends on quality tier:

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

New accounts receive **2 free credits** — renders using free credits produce a "PREVIEW" watermark. Free credits only cover `standard` quality.

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 "[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 (4 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, 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":"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_..."
```