# DX.GL > Turn 3D models into product videos. DX.GL is a cloud rendering service that converts GLB/glTF 3D models and 3D scans (OBJ+MTL+textures) into high-quality turntable videos. It is built for e-commerce teams, agencies, 3D artists, and scanning professionals who need videos without 3D software. ## How It Works 1. Upload a GLB or glTF file, or a ZIP of an OBJ scan with textures (drag-and-drop, file picker, or URL import). 2. Configure render settings in a single unified panel: quality tier, camera motion effect, aspect ratio, background (color or image), video length, rotation and tilt, surface effects, and easing. 3. Submit a render (single or batch). Video is billed per second: Standard 540p (1 credit/sec), HD 1080p (4/sec), 4K (16/sec), ProRes 4444 (64/sec) — a default 6-second Standard turntable costs 6 credits. 4. Receive an MP4 video, a web-optimized variant, a thumbnail video, and a PNG poster image. ## Output Specifications - **Standard** (API: `share`): H.264 MP4, 960×540, 60 fps (1 credit/sec) - **HD** (API: `standard`): H.264 MP4, 1920×1080, 60 fps (4 credits/sec — the API default when `quality` is omitted) - **4K** (API: `4k`): H.264 MP4, 3840×2160, 60 fps (16 credits/sec) - **ProRes 4444** (API: `pro`): ProRes 4444 .mov, 3840×2160, 60 fps, with alpha channel (64 credits/sec) - **Web Variant:** H.264 MP4, lower bitrate — optimized for streaming - **Thumbnail Video:** H.264 MP4, quarter resolution — for hover previews - **Poster Image:** PNG, full resolution — static preview frame All tiers render with GPU-accelerated 2× supersampling (SSAA). ## Render Settings | Setting | Options | Default | |------------|------------------------------------------------------------|----------| | Effect | Turntable (360°), Hero Spin (decel to front), Showcase (look-around), Zoom Orbit (360° + zoom), Reveal (scale-up entrance), Keyframes (custom fly-through, API/Studio) | Turntable | | Aspect | 16:9, 1:1, 9:16 | 16:9 | | Background | Solid color (hex), or background image (system gradient or custom upload) | White | | Length | 6, 9, 15, 30 seconds, or custom (UI from 3s; API accepts any duration > 0 up to 120, ProRes max 60, decimal OK) | 6s | | Surface | Shadows, Reflection, or None (mutually exclusive) | Shadows | | Rotation | Yaw: −180° to 180° (turn). Pitch: −180° to 180° (forward/back lean). Roll: −180° to 180° (tilt). Camera Angle: orbit start position. | 0° | | Easing | On / Off | On | ## Features - **Unified render panel** — all settings in one view, no mode switching - **Rotation & tilt** — set a starting angle and forward/back tilt for each render; tilt-aware sizing prevents clipping - **Camera motion effects** — Turntable (classic 360°), Hero Spin (fast spin to front), Showcase (look-around), Zoom Orbit (360° + zoom), Reveal (scale-up entrance); all with tunable parameters via API - **Dataset export** — NeRF/3DGS-ready vision training datasets: Fibonacci hemisphere/sphere views (RGB on white), 8-bit + 16-bit depth maps, normal maps, alpha masks, transforms.json with depth_near/depth_far, and overview contact sheet. Flat cost per set: 100×800 = 40 credits, 196×1024 = 160, 400×2048 = 640 - **Animation playback** — render models with embedded animations (walk cycles, product reveals, mechanical motion); select clip, adjust speed, set cycles for seamless looping, auto-duration calculation; easing syncs with camera motion - **Real-time preview** — 3D preview panel with original PBR materials, ACES tone mapping, animation playback, and frame counter; updates live as you adjust settings - **Custom background images** — upload branded backgrounds (PNG/JPEG/WebP); logo-safe top-left alignment across all aspect ratios; system gradient presets included - **Batch mode** — toggle Batch on, build a list of setting variants, run them all at once across multiple models - **Video modal** — flippable card with front (playback, fullscreen) and back (QR code, downloads, notes, share/save) - **Model detail view** — Videos, Data (title/SKU/tags with autocomplete), and Manage tabs per model - **Archiving** — archive and restore models and videos without permanent deletion - **Bulk actions** — always-visible checkboxes; Share, Archive, or Delete selected videos - **Galleries** — bundle multiple videos into a single shareable link - **Tags** — organize models with tags; tags carry over to rendered videos; filter by tag on both tabs - **Export** — spreadsheet view with copy/save as Text, CSV, or JSON - **Support tickets** — create and track support requests from the portal - **Two-factor authentication** — TOTP-based 2FA with recovery codes - **OAuth** — sign in with Google or Microsoft - **Referrals** — earn credits when referred users make their first purchase - Shareable video pages (no login required for viewers) — share links, galleries, QR codes, embed HTML - For 3D artists: share models as video with clients, teams, and social media — no viewer needed for recipients (https://dx.gl/use-cases) - For fashion & apparel: 3D garment turntable videos for e-commerce, wholesale catalogs, and social media. Upload from Clo3D, Marvelous Designer, Browzwear (https://dx.gl/use-cases) - High-fidelity rendering — supports 8K textures, no downscaling - 3D scan support — upload OBJ+MTL+texture ZIPs from Artec Studio, RealityCapture, Metashape, etc. Auto-converted to GLB with roughness 1.0. The converted GLB is downloadable. - SHA-256 deduplication on uploads - RESTful API for programmatic access ## Datasets DX.GL publishes multi-view training datasets for NeRF, 3D Gaussian Splatting, and 3D reconstruction research. Datasets include calibrated camera poses, RGB images, depth maps (8-bit + 16-bit), normal maps, binary masks, and point clouds in nerfstudio-compatible format (transforms.json). - Objaverse-1K: https://huggingface.co/datasets/dxgl/objaverse-1k — 1,026 curated Objaverse objects, 196 views at 1024×1024, full sphere coverage, 6 modalities (RGB, depth 8+16bit, normals, masks, point clouds), CC0/CC-BY with full attribution - Polyhaven-10: https://huggingface.co/datasets/dxgl/multiview-datasets — 10 CC0 objects, 196 views at 1024×1024, full sphere coverage, pre-trained Gaussian Splats included - Collection page: https://dx.gl/datasets/polyhaven-10 - Browse all datasets: https://dx.gl/datasets - Interactive splat viewer: https://dx.gl/splat/index.html ## Pricing Every new account gets 100 free credits on signup — no watermarks, no restrictions, no credit card required. Video is billed per second: Standard 540p costs 1 credit/second, HD 1080p 4/second, 4K 16/second, ProRes 4444 64/second (so a 6-second turntable = 6 credits at Standard). Datasets are a flat 40, 160, or 640 credits per set. Credit packs: 100 credits ($9.90), 1,000 credits ($79), 10,000 credits ($490). See https://dx.gl/pricing for full details. ## API Full REST API at https://api.dx.gl/v1. Endpoints cover model upload (multipart, URL ingest with presigned S3/R2 URLs accepted, or staged presigned-PUT upload: create-upload-url → PUT → finalize), render creation and batch rendering (both honor an `Idempotency-Key` header for safe retries), status polling (incl. `updatedSince`/cursor delta polling), model content inventory (animation clips, mesh names, material names on model detail), asset download (streamed or presigned direct-from-bucket URLs), overlay assets (background/foreground/decal/texture layers + SHA-256 resolve), texture and HDRI-environment minting (/v1/assets/textures, /v1/assets/envs — feed materialEdits.mapAssetId and envId), cancel with refund, failure codes on errored renders, and cost estimation (quote). Authentication via Bearer token (`dxgl_sk_...`); keys can be workspace-bound (team scope, workspace-owner billing). ## MCP Server (Model Context Protocol) DX.GL provides an MCP server for AI agent integration. Agents in Claude Desktop, Windsurf, Cursor, and other MCP-compatible hosts can import 3D models (by URL or binary upload), create and cancel renders, check status, download assets (streamed or presigned), manage overlays, mint texture and HDRI-environment assets, and estimate costs — all via natural-language prompts. Available tools (37): `ingest_model`, `upload_model`, `list_models`, `get_model`, `get_model_file`, `get_model_file_url`, `check_hash`, `update_model`, `archive_model`, `unarchive_model`, `delete_model`, `create_render`, `create_batch_renders`, `list_renders`, `get_render`, `cancel_render`, `dismiss_render`, `delete_render`, `download_render`, `get_download_url`, `get_poster`, `get_thumb`, `download_bundle`, `download_dataset`, `get_hls`, `upload_overlay`, `list_overlays`, `delete_overlay`, `upload_texture`, `upload_env`, `list_assets`, `delete_asset`, `resolve_asset_sha`, `get_account`, `list_products`, `get_purchases`, `quote`. Developer guide with prompt examples and workflows: https://dx.gl/docs (Markdown: https://dx.gl/docs/markdown) Setup: add the MCP server URL (`https://mcp.dx.gl/`) and your DX.GL API key to your MCP client config. ## Documentation - API Reference: https://dx.gl/docs/api - API Reference (Markdown): https://dx.gl/docs/api/markdown - Developer Guide (REST + MCP): https://dx.gl/docs - Developer Guide (Markdown): https://dx.gl/docs/markdown - Full documentation (Markdown): https://dx.gl/llms-full.txt ## Links - Website: https://dx.gl - Sign Up: https://dx.gl/signup - Studio (app): https://dx.gl/studio - Portal (datasets): https://dx.gl/portal - Pricing: https://dx.gl/pricing - Blog: https://dx.gl/blog - FAQ: https://dx.gl/faq - About: https://dx.gl/about - Contact: https://dx.gl/contact - Privacy Policy: https://dx.gl/privacy - Terms of Service: https://dx.gl/terms - Refund Policy: https://dx.gl/refund-policy - Twitter / X: https://x.com/dxgl3d - YouTube: https://www.youtube.com/@dxgl3d - LinkedIn: https://www.linkedin.com/in/dxgl - Instagram: https://www.instagram.com/dxgl3d --- # Full Documentation ## Portal Guide # DX.GL Portal Guide How to use the DX.GL portal to create product videos from 3D models. --- ## Getting Started DX.GL turns 3D models into production-ready turntable videos. The workflow is: 1. Upload a GLB, glTF, or ZIP file 2. Configure render settings (aspect ratio, background, duration, surface effects) 3. Receive an MP4 video, a web-optimized variant, a thumbnail video, and a PNG poster image New accounts include **100 free credits on signup**. Video is billed per second (1 credit/sec at Standard 540p), so that's over 15 default 6-second turntables with no watermarks and no restrictions. Every upload also generates a free **system preview** — a 540p MP4 you can share with anyone via a link, and a way to verify your model loaded correctly before spending a credit. The portal has up to three top-level tabs (which tabs are visible depends on your active mode): - **Upload** — Quick capture (drop → preview → render) and batch import (file upload + URL import) - **Models** — Browse your model library, select models for rendering, configure render settings, and monitor the render queue - **Videos** — Browse, download, share, and export finished videos - **Datasets** — Browse, download, and export vision training datasets *(Data and Full modes only)* --- ## Portal Modes Portal modes focus the interface around a specific workflow, hiding options that aren't relevant to keep things simple. Click the mode label next to **DX.GL** in the top-left header to open the mode picker and switch modes. Your selection is saved in the browser. New accounts default to **Studio** mode, which exposes all creative features. Switch to a simpler mode (Social, Pro) if you want a more focused interface, or to Data if you're generating vision training datasets. | Mode | Best for | Tabs | Quality | Key features | |---|---|---|---|---| | **Social** | Quick turntable + share link | Upload, Models, Videos | Standard (540p) | Galleries | | **Pro** | High-volume e-commerce exports | Upload, Models, Videos | HD (1080p) | Batch, all camera paths, rotation, overlays, import, export URLs, API keys | | **Studio** *(default)* | Full creative control | Upload, Models, Videos | Standard, HD, 4K, ProRes 4444 | Everything in Pro + 4K/ProRes quality, Workshop, vignette, live preview, galleries, animation clips | | **Data** | Vision training datasets | Upload, Models, Datasets | Dataset tiers | Batch, hemisphere/sphere coverage, import, API keys | Features mentioned throughout this guide that are only available in certain modes are noted inline. --- ## Uploading Models The Upload top-level tab has two sub-tabs: **Capture** and **Import**. ### Capture Drop a single 3D model to see an instant 3D preview. Choose aspect ratio and background color, then click **Render video** to render a shareable Standard (540p) turntable video. Click the canvas to pause/play the spinning preview. This is the fastest path from file to video. ### File Upload (Import) Click **Import** in the Upload tab, then drag-and-drop or click to select GLB, glTF, or ZIP files. Multi-file uploads are supported — files queue automatically and upload sequentially. This adds models to your library for rendering with full settings control. #### Tags on Upload Below the drop zone, a tag input lets you apply tags to all files in the current upload batch. Type a tag and press Enter or click **+** to add it. Tags auto-complete from your existing tags. All files uploaded while those tags are set will receive them automatically. Click **Clear** to remove all tags. - Maximum file size: **100 MB** in the portal drop zone (the API accepts up to 1 GB via multipart, URL ingest, or staged presigned-PUT upload) - Supported formats: `.glb`, `.gltf`, `.zip` (OBJ+MTL+textures) - Files are SHA-256 hashed client-side for deduplication — identical files are detected before upload - Files over 90 MB use chunked upload (50 MB chunks) for reliability - Free accounts can upload up to 10 models; the cap grows by 1 for every credit ever purchased or granted. ### 3D Scan Upload To upload a 3D scan (e.g. from Artec Studio, RealityCapture, or Metashape), export as OBJ with textures, zip the folder, and upload the ZIP. Include exactly one .obj file per ZIP along with its .mtl and texture files. The server converts to GLB automatically with roughness set to 1.0 for a natural matte finish. The converted model enters the standard pipeline — you'll see a system preview within seconds. You can also download the converted GLB from the model detail view, giving you a clean GLB with embedded textures and correct PBR materials — no manual conversion needed. ### Import from URL *Available in Pro, Studio, Data, and Full modes.* In the Import sub-tab, expand the **Import from URL** section below the file drop zone. Paste one or more URLs to GLB files hosted elsewhere, one per line. The server downloads each file directly (max 1 GB; downloads abort after 45 seconds without progress). SHA-256 deduplication applies. URL import supports GLB/glTF only. For OBJ scans, upload a ZIP file via the drop zone above. ### After Upload Every uploaded model automatically receives a free system preview render (no credits deducted). This generates a thumbnail poster within seconds and serves as a model integrity check — if the preview fails, the model likely has issues with geometry, textures, or glTF structure. ### Model Detail View Click any model in the Models tab to open its detail view. The detail view shows the model poster (with hover video preview) and has five sub-tabs: - **Edit** — Edit the model's title, SKU, and tags. Tags have autocomplete suggestions drawn from all your existing tags. Click **Save** to persist changes. - **Videos** — All video renders for this model (grid or list view). Each render card has an archive button. Click a render to open the video modal. Dataset renders are excluded — they appear in the Datasets sub-tab. - **Datasets** — All dataset renders for this model. Each row shows status, date, and a download link with file size. Click a completed row to view the overview contact sheet. - **Source** — Embedded glTF metadata (read-only). Shows creator, license, copyright, source URL, original title, and generator when present in the GLB file. Source URLs and creator links are clickable. Sketchfab models include rich metadata here. - **Manage** — Archive or delete the model. Download the GLB (with file size shown). For scan-origin models (uploaded as OBJ ZIP), a **Fix Up Axis** button appears to correct Z/Y axis inversion. Actions in the detail header: **Download** (original GLB file), **Archive**, and **Render** (pre-selects this model and navigates to the Render sub-tab). ### Archiving Models and videos can be archived rather than permanently deleted. Archived items are hidden from default views but can be restored. Click the **archive toggle** button in the Models toolbar to switch to archived-only view, where you can inspect and unarchive models. ### Model Selection Model cards have always-visible checkboxes. Click the checkbox to select a model for rendering — clicking the card body still opens the detail view. When models are selected, a contextual strip appears between the toolbar and the model grid showing "N selected" with **Tag**, **Archive**, and **Clear** actions. Selected models feed directly into the Render sub-tab. Use `Shift+Click` for range selection. Press `S` to select/deselect all, or `Escape` to clear the selection. --- ## Render Settings The Models tab has three sub-tabs: **Library** (model browser with selection), **Render** (configure render options with inline review of selected models), and **Queue** (active render monitoring). Select models via checkboxes in Library, then switch to the Render sub-tab to configure settings: Settings are organized into collapsible groups: **Output**, **Motion**, **Appearance** (all expanded by default), and **Advanced** (collapsed). When Output is set to Dataset, Motion/Appearance/Advanced are hidden entirely since they don't apply to dataset renders. | Setting | Group | Options | Default | |---|---|---|---| | Type | Output | Video, Dataset | Video | | Quality | Output | Standard (540p, 1 credit/sec), HD (1080p, 4/sec), 4K (16/sec), ProRes 4444 (64/sec) | Standard | | Aspect | Output | 16:9, 1:1, 9:16 | 16:9 | | Camera path | Motion | Turntable, Hero Spin, Showcase, Zoom Orbit, Reveal | Turntable | | Animation | Motion | Clip selector (auto-shown only if the model has animation clips) | — | | Duration | Motion | 6, 9, 15, 30 seconds, or Custom (3–120, ProRes max 60, decimal OK) | 6s | | Easing | Motion | On / Off | On | | Background | Appearance | Preset (White, Cream, Navy, Black), Custom hex, or Image | White | | Surface | Appearance | Shadows, Reflection, or None | Shadows | | Vignette | Appearance | On / Off — subtle edge darkening for depth | On | | Rotate | Advanced | Yaw (Y): −180° to 180°, Pitch (X): −180° to 180°, Roll (Z): −180° to 180° | 0° / 0° / 0° | | Camera | Advanced | Angle: −180° to 180° (orbit start), Zoom: 0.5× to 2.0× (distance) | 0° / 1.0× | ### Quality The Quality selector in the Output group determines resolution, codec, and credit cost: - **Standard** — 540p H.264 MP4 (1 credit per second of video; a default 6s render = 6 credits). The default. Antialiased output that looks clean in messaging apps and quick shares. Available in all modes. - **HD** — 1080p H.264 MP4 (4 credits/sec). Best for web and social media. *(Pro, Studio, Full modes.)* - **4K** — 3840×2160 H.264 MP4 (16 credits/sec). For large screens and high-DPI displays. *(Studio and Full modes only.)* - **ProRes 4444** — 3840×2160 ProRes 4444 with alpha channel (64 credits/sec). For professional video editing and compositing. The `.mov` file has a transparent background; the `bgColor` is applied only to the web/thumbnail/poster variants. *(Studio and Full modes only.)* For Dataset output, a separate tier selector replaces the video quality buttons: 100×800 (40 credits), 196×1024 (160 credits), 400×2048 (640 credits) — flat per set. See [Datasets](#datasets--vision-training) below. All video quality tiers render with GPU-accelerated 2× supersampling (SSAA) for smooth, anti-aliased edges. ### Motion The **Motion** group contains everything related to how the camera moves and whether the model's own animation plays. - **Camera path** — The spatial trajectory the camera follows. Turntable is shown by default as the active option; the other four paths (Hero Spin, Showcase, Zoom Orbit, Reveal) are hidden behind a **More motions ▾** link. Expanding reveals all five; a **Less ▴** collapse link appears when you're on Turntable so you can tidy the UI back. If you load a render with an advanced path selected, the disclosure auto-expands. - **Animation** — If the selected model contains animation clips, an Animation row appears automatically inside the Motion group (no mode gating — purely driven by whether the GLB has clips). Choose a clip from the dropdown; Speed and Cycles rows reveal below it. The first clip is auto-selected when a model loads. - **Duration** — 6/9/15/30s presets or Custom. When an animation clip is active, duration is auto-calculated for seamless looping; manual edits override. - **Easing** — Smooth acceleration/deceleration of camera motion. Also governs animation playback timing so the two stay in sync. ### Custom Values Background and Length both support custom values. Click **Custom** to enter a hex color or a duration (3–120 seconds, ProRes max 60; admin accounts uncapped). Decimal values are supported (e.g. 4.7s) for precise animation loop alignment. Invalid values are highlighted and block submission. ### Surface Shadows and Reflection are mutually exclusive — selecting one disables the other. Choose **None** for a clean floating look with no ground effects. ### Advanced (Rotate & Camera framing) *Available in Pro, Studio, Data, and Full modes.* The **Advanced** group (collapsed by default) controls model orientation and camera framing: **Rotate** (Yaw / Pitch / Roll) — defines the model's pose: - **Yaw** (−180° to 180°) — swivels the model left/right (Y axis). Useful for showing a product from a slightly angled perspective. - **Pitch** (−90° to 90° in the portal UI; the API accepts −180° to 180°) — leans the model forward/backward (X axis). Useful for angling shoes, bottles, or packaging. Pitch bypasses shadow caching, so the first render with a new pitch value takes slightly longer. - **Roll** (−180° to 180°) — tilts the model sideways (Z axis). Useful for dynamic, non-upright compositions. Model rotations are applied in order: Yaw → Pitch → Roll using quaternion composition. The model is automatically re-scaled to fit the viewport after rotations are applied. **Camera** row combines Angle and Zoom on a single line: - **Angle** (−180° to 180°) — sets the turntable camera starting position. This determines which side the camera faces when the video begins, without changing the model's pose. Think of it as turning the turntable under the posed model. - **Zoom** (0.5× to 2.0×) — adjusts camera distance. Values above 1.0 zoom in (close-up), below 1.0 zoom out (wide). Default is 1.0 (auto-fit). Useful for detail shots like shoelaces or watch faces. A **Reset** link appears when any Rotate, Angle, or Zoom value differs from its default, returning all values to 0° / 1.0×. ### Animation details The Animation row inside the Motion group is shown automatically when the selected model contains clips (walk cycles, product reveals, mechanical motion). It is not gated by mode — any model with embedded animations exposes these controls regardless of which portal mode you're in. - **Clip selection** — Choose which animation clip to play. The first clip is auto-selected by default. - **Speed** — Adjust playback speed via slider or numeric input (0.25× to 4×). Useful for slowing down fast animations or speeding up slow reveals. - **Cycles** — Set how many complete loops of the animation play in the video. Duration is derived automatically: `cycles × clip duration / speed`. Decimal cycles (e.g. 1.5) are supported for partial loops. - **Auto-duration** — When you select an animation, the video length is automatically calculated for seamless looping. Manually editing the duration overrides this. - **Seamless loop indicator** — Shows when the cycle count produces an exact loop (no visible cut point). - **Easing sync** — Animation playback follows the camera easing curve, so acceleration/deceleration in the camera path is mirrored in the animation timing. ### Preview *Available in Studio and Full modes.* Toggle **Preview** on in the render settings to open a floating 3D preview panel. The panel shows a real-time animation with your model's original PBR materials, ACES filmic tone mapping (matching the renderer), and a ground grid. It updates live as you adjust settings. - **Drag** the panel anywhere on screen by its title bar - **Expand** to fullscreen via the expand button in the panel header - Click the canvas to **play/pause** the animation - **Frame counter** — Shows current frame, total frames, and elapsed/total time - **Animation playback** — If an animation clip is selected, it plays in the preview with correct speed and easing sync - The panel **animates smoothly** when you change aspect ratio - Closes automatically when you leave the Render sub-tab - GPU-accelerated — requires WebGL 2 support in your browser The preview uses the same tone mapping and materials as the final render. Shadows and reflections are not shown in preview for performance. ### Duplicate Detection If you submit a render with settings identical to an existing render for the same model, you'll see a warning. You can force the render if needed. --- ## Background Images *Image backgrounds are available in Pro, Studio, and Full modes.* Instead of a solid background color, you can use an image as the scene background — perfect for branded gradients, campaign visuals, or any custom backdrop. ### Color vs Image Mode In the render settings Background section, toggle between **Color** (solid hex, the default) and **Image**. The Image picker shows system backgrounds (gradients published by DX.GL) first, followed by your own uploads. ### Uploading Custom Backgrounds Click **Upload** in the Image picker to add your own background image. Supported formats: PNG, JPEG, WebP (max 10 MB). You can store up to 20 custom backgrounds. ### How Cropping Works Your background image is scaled to fully cover the output dimensions, then cropped based on the **alignment** setting. The default is top-left, meaning logos or branding placed there stay anchored regardless of aspect ratio. Choose from 9 anchor points (top-left, top-center, top-right, center-left, center, center-right, bottom-left, bottom-center, bottom-right) to control which part of the image is preserved when cropping for different aspect ratios. **Tip:** Use a square image (e.g. 1920×1920) for best results across all aspect ratios. ### Batch Compatibility Background images work with batch rendering. You can mix different backgrounds across batch variants — the GPU render pass is shared, and each variant gets its own background composited during video encoding. ### API Usage Upload backgrounds via the [Overlays API](https://dx.gl/docs/api), then pass the overlay ID as `bgImageId` in render settings. `POST /v1/overlays/resolve` with the file's SHA-256 skips re-uploading an asset you already have. --- ## Batch Rendering *Available in Pro, Studio, Data, and Full modes.* Batch rendering lets you create multiple videos efficiently. There are two approaches: ### Batch Mode Toggle **Batch** on in the Render sub-tab. This changes the workflow: 1. Configure a set of render settings (aspect, background, length, surface, easing) 2. Click **Add to Batch** to save that variant to the batch list 3. Repeat with different settings to build up multiple variants 4. Click **Run Batch** to submit all variants at once Total videos = variants × selected models. Each variant costs credits based on its quality tier and length (per second: 1 for Standard, 4 for HD, 16 for 4K, 64 for ProRes 4444). ### Multi-Model Selection Select multiple models in the Models tab using the always-visible checkboxes (click to toggle, `Shift+Click` for range, `S` for all). Selected models automatically carry over to the Render sub-tab. In single mode (Batch off), clicking **Render** applies the current settings to all selected models. In Batch mode, the batch list applies to all selected models. ### How It Works - Credits are deducted atomically — if you don't have enough for the full batch, nothing is deducted - The render worker automatically groups renders for the same model — GPU frames are rendered once and encoded into multiple variants in parallel - Each render in the batch appears independently in the queue and Videos tab --- ## Datasets & Vision Training Select **Dataset** in the Output selector to generate a NeRF/3DGS-ready training dataset instead of a video. Three tiers, flat cost per set: 100×800 (40 credits), 196×1024 (160 credits), 400×2048 (640 credits). Choose hemisphere or full sphere coverage. The 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 (nerfstudio / instant-ngp 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 showing all views across RGB, depth, normals, masks ### Datasets Tab The **Datasets** tab shows all completed dataset renders. It has two sub-tabs: - **Download** — List view with poster thumbnail, model name, date, tags, and download link with ZIP file size. Click a row to view the full overview contact sheet in a lightbox. Includes a search bar (matches name, title, SKU) and a tag filter toggle (same pill chip style as the Videos tab). - **JSON** — Exportable JSON array with download URLs for programmatic use. Each entry includes name, dataset URL, view count, resolution, format, and tags. Copy button for one-click clipboard. --- ## Videos & Downloads The **Videos** tab shows all completed video renders (excluding system previews and dataset renders). It has three sub-tabs: - **Browse** — Grid or list view of your videos (press `G` to toggle). Grid shows poster thumbnails with hover video preview; List shows compact rows with key metadata. Always-visible checkboxes on each card for selection. - **Galleries** — View and manage shared galleries. Create new galleries or delete existing ones. See [Sharing & Galleries](#sharing--galleries). *(Social, Studio, and Full modes only.)* - **Export** — Public asset URLs for each video, designed for embedding in Shopify, Amazon, and other platforms. No authentication required. Includes a collapsible JSON section with copy/save buttons. *(Pro, Studio, and Full modes only.)* ### Video Selection Video cards have always-visible checkboxes. Click the checkbox to select a video — clicking the card body opens the video modal. When videos are selected, a contextual strip appears above the grid with **Share**, **Archive**, and **Clear** actions. Use `Shift+Click` for range selection, `S` to select/deselect all, or `Escape` to clear. In list view, each row has a download icon with file size for downloading the asset bundle directly. Click the **archived toggle** (archive icon) in the toolbar to switch to archived-only view, where you can inspect and unarchive videos. ### Sorting & Grouping Click the sort toggle to switch between newest-first and oldest-first (saved in localStorage). Videos can be grouped by Model, Date, or None using the icon buttons in the toolbar. Groups are collapsible — use Expand All / Collapse All buttons (visible when grouping is active). ### Video Modal Click any video to open the modal. The modal is a flippable card with a front and back side. **Front Side:** - Video playback with play/pause (click video or button) - Fullscreen/expand toggle - Flip button — rotates the card to reveal the back side - **Download** button — downloads all assets as a zip bundle (shows estimated file size). System preview videos download the web MP4 directly instead of the full bundle. - Controls auto-fade while playing; reappear on hover or mouse movement - Legacy preview renders (pre-2026 accounts) show an **Unlock** button (costs credits to remove the watermark); new accounts no longer receive watermarked renders **Back Side:** - **QR Code** — scannable share link; tap QR to copy the URL - **Share / Save** — uses Web Share API on mobile, or downloads the video on desktop - **Downloads** — Full MP4, Web MP4 (if available), and Poster - **Note** — a free-text note field attached to the render (persisted) - **Edit mode** — toggle the edit switch to enter edit mode, where you can modify the note, toggle reshare permissions, then Save Click the back side background or press `Esc` to flip back to the front. Pressing `Esc` follows a layered exit order: exit edit mode → flip to front → collapse fullscreen → close modal. ### Output Files Each completed render produces: | File | Standard | HD | 4K | ProRes 4444 | |---|---|---|---|---| | Full Video | H.264 MP4 540p 60fps | H.264 MP4 1080p 60fps | H.264 MP4 4K 60fps | ProRes 4444 .mov 4K 60fps (with alpha) | | Web Video | H.264 MP4, lower bitrate — optimized for web embedding/streaming | | | | Thumbnail | H.264 MP4, quarter resolution — hover preview, social embeds | | | | Poster | PNG, full resolution — static preview frame | | | All assets can be downloaded individually or as a single zip bundle from the video modal. ### Render Queue Active renders appear in the persistent queue bar below the header. The queue polls every 3 seconds and shows status progression: ``` Queued → Rendering poster → Poster done → Encoding video → Done ``` Failed renders show an error indicator. Click any queue item to navigate to that model's detail view. --- ## Sharing & Galleries ### Share Links Every completed render has a public share page at `dx.gl/portal/v/{id}`. Anyone with the link can view the video without logging in. Use the Share button in the video modal or Export tab to copy the link. ### Galleries Check multiple videos in Browse, then click **Share** in the selection strip to create a gallery. If one video is selected, the share link is copied directly. If two or more are selected, a gallery creation modal opens where you can set an optional title. View and manage all galleries from the **Galleries** tab under Videos. Each gallery shows its title, video count, creation date, and actions to open, copy link, or delete. --- ## Tags & Organization Tags help organize models and filter both the Models and Videos tabs. ### Adding Tags Open a model's detail view, switch to the **Data** tab, and type tags in the tag input. The input offers autocomplete suggestions drawn from all your existing tags. Tags are automatically lowercased and trimmed. Maximum 20 tags per model. ### Filtering by Tag A tag filter bar appears above the model/video grid. Click tags to filter (OR logic — showing items matching any selected tag). The tag bar can be collapsed via the toggle button. Your filter visibility preference is saved in localStorage. ### Tag Carry-over Tags on a model automatically carry over to all videos rendered from that model, so you can filter videos by the same tags used to organize your models. ### Search Press `/` to focus search. Search matches model name, title, SKU, and tags. --- ## Export The **Export** sub-tab within the Videos tab shows a card for each video with public asset URLs — video, web video, and poster — ready for embedding in Shopify, Amazon, and other platforms. Each URL has a copy button. No authentication is required to access these URLs. The **JSON** sub-tab provides the same data as a copyable or downloadable JSON array for programmatic integration. --- ## Billing & Credits Video is billed **per second** by quality tier: Standard (540p) 1 credit/sec, HD (1080p) 4 credits/sec, 4K 16 credits/sec, ProRes 4444 64 credits/sec — so a default 6-second turntable costs 6 / 24 / 96 / 384 credits. Your credit balance is always visible in the utility strip below the tab bar. ### Credit Packs | Pack | Price | Per Credit | |---|---|---| | 100 credits | $9.90 | $0.099 | | 1,000 credits | $79 | $0.079 | | 10,000 credits | $490 | $0.049 | Payments are processed securely by Stripe. See [Pricing](https://dx.gl/pricing) for full details. ### Signup Bonus New accounts receive **100 credits on signup** — over 15 default 6-second Standard turntables. The bonus lands in your regular balance and behaves identically to purchased credits: no restrictions, any quality tier. Credits never expire. ### Credit Deduction Batch renders deduct all credits atomically — the batch fails entirely if you don't have enough. ### Promo Codes Promo codes can be redeemed from the Billing tab for bonus credits. ### Transaction History View all credit transactions (purchases, renders, refunds, promos, referrals) in the Billing tab. Paginated, 25 per page. --- ## Account & Security Access account settings from the avatar menu → **Settings**. ### Display Name Set a display name that appears instead of your email in the portal header and share pages. ### Password Change your password from the Account section of Settings. If you signed up via OAuth (Google/Microsoft), you can set a password to enable email+password login as an alternative. ### Two-Factor Authentication Enable 2FA from the Security section of Settings. DX.GL supports TOTP (time-based one-time passwords) compatible with any authenticator app (Google Authenticator, Authy, 1Password, etc.). When you enable 2FA, you'll receive recovery codes — store them securely. They allow login if you lose access to your authenticator. ### OAuth Login Sign up or log in with Google or Microsoft. OAuth accounts are automatically activated without email verification. If your OAuth email matches an existing account, the accounts are linked. ### Referrals Share your referral link (found in Settings → Referrals) to earn credits when referred users make their first purchase. --- ## Support Access the support system from the avatar menu → **Support**. You can: - View existing support tickets and their status - Create new tickets with a subject and message - Reply to ongoing conversations --- ## Workshop *Available in Studio and Full modes.* Press **W** or click **Workshop** in the utility strip to open a full-width 3D model viewer and inspector. Workshop lets you examine any GLB/glTF model in detail: - **Scene** — Hierarchy tree, mesh/material counts, file size breakdown - **Meshes** — Per-mesh vertex/face counts, bounding box, draw calls - **Materials** — PBR properties, texture previews, channel maps - **Transform** — Position, rotation, scale, world matrix - **Stats** — Total triangles, vertices, textures, draw calls, GPU memory estimate - **Source** — Embedded glTF metadata and extensions - **Warnings** — Potential issues (missing normals, oversized textures, unused materials) Use **Save GLB** to download an optimized GLB, or **Add to Library** to import the model directly into your DX.GL library. You can also open any model from Models in Workshop via the model detail **Manage** tab → "Open in Workshop". Press **Esc** to exit Workshop and return to the portal. --- ## API Keys *Available in Pro, Studio, Data, and Full modes.* Create API keys in [Studio](https://dx.gl/studio) → avatar menu → **API** → **Keys**. Keys allow programmatic access to the [REST API](https://dx.gl/docs/api) for uploading models, creating renders, and downloading assets. - Give each key a descriptive name and optionally set an expiration (in days) - A key can be bound to a **team workspace** at creation (editor role or above) — it then operates on that workspace, billing the workspace owner - The raw token (`dxgl_sk_...`) is shown once on creation — copy and store it securely - Keys can be revoked (and un-revoked) at any time - API tokens use Bearer authentication: `Authorization: Bearer dxgl_sk_...` See the [API Documentation](https://dx.gl/docs/api) for full endpoint reference. --- ## MCP / Agent Integration DX.GL provides a **Model Context Protocol (MCP)** server that lets AI agents import models, create renders, check costs, and download results via tool calls. Works with Windsurf, Claude Desktop, Cursor, and any MCP-compatible host. ### Available Tools 37 tools covering the full API surface — the highlights: | Tool | Description | |---|---| | `ingest_model` / `upload_model` | Import a 3D model from a URL, or upload a GLB from bytes | | `list_models` | List models with tag/status/pagination filters | | `get_model` | Model details, render history, and clip/mesh/material inventory | | `create_render` | Create a video or dataset render | | `create_batch_renders` | Create multiple renders atomically | | `get_render` / `list_renders` | Check status (poll until done) | | `cancel_render` | Cancel a still-queued render with refund | | `download_render` / `get_download_url` | Streamed or presigned download for any asset | | `upload_texture` / `upload_env` | Mint texture and HDRI-environment assets for material edits and `envId` | | `get_account` | Check credit balance (and which workspace a key is bound to) | | `quote` | Estimate credit cost before committing | The full 37-tool list lives in the [developer docs](https://dx.gl/docs). ### Connect Your Agent Add this to your MCP client configuration (Windsurf, Claude Desktop, etc.): ```json { "mcpServers": { "dxgl": { "serverUrl": "https://mcp.dx.gl/", "headers": { "Authorization": "Bearer dxgl_sk_..." } } } } ``` Replace `dxgl_sk_...` with your API key from the API Keys section above. All tools use the same credit system — use the `quote` tool to check costs before committing. ### Cost Estimation The [Quote endpoint](https://dx.gl/docs/api) (`POST /v1/quote`) lets agents estimate the credit cost for a set of renders before committing. Returns the total cost, per-item breakdown, current balance, and whether the account has enough credits. ### Full MCP Guides For prompt examples, workflows, and tips, see the dedicated guides: - [MCP for Video](https://dx.gl/docs/mcp-video) — automate turntable video production for e-commerce and marketing - [MCP for Datasets](https://dx.gl/docs/mcp-datasets) — automate multi-view dataset generation for NeRF, 3DGS, and 3D reconstruction --- ## Keyboard Shortcuts | Key | Action | |---|---| | `1` `2` `3` `4` | Switch tabs (Upload, Models, Videos, Datasets) | | `←` `→` | Cycle sub-tabs within current tab | | `/` | Focus search | | `S` | Select all / deselect all (Models or Videos tab) | | `A` | Select all (Models or Videos tab) | | `G` | Cycle view mode (Grid ↔ List) | | `Shift+Click` | Range select | | `W` | Toggle Workshop (3D model viewer) | | `Esc` | Clear selection, close modal, exit Workshop, navigate back | | `?` | Open in-app help | Shortcuts are disabled when typing in input fields. Modifier keys (Ctrl, Alt, Cmd) are passed through to the browser. --- ## API Documentation # DXGL Public API Base URL: `https://api.pinwing.ai/v1` — the original `https://api.dx.gl/v1` host remains fully supported: same API, same keys, either host in any example. ## Authentication All requests require a Bearer token in the `Authorization` header: ``` Authorization: Bearer dxgl_sk_... ``` Tokens are created in [Studio](https://dx.gl/studio): open the avatar menu (top-right) → **API** → **Keys** → **Create key**. The raw token is shown once on creation — store it securely. **Server-to-server only.** A `dxgl_sk_` token is a full-account secret — never embed one in a web page or mobile app. The API grants no CORS access to browsers; call it from your backend. For public browser embeds, use the public share URLs of published renders instead. ### 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. - **Scope**: models, renders, and overlay/texture assets are the bound workspace's — the whole team's, not just yours. Assets you upload land in the workspace and resolve in the team's renders. - **Billing**: renders are funded from the **workspace owner's** credit pool (`GET /v1/account` shows that pool and names the workspace); `GET /v1/purchases` returns only ledger rows attributable to this workspace. - **Lifecycle**: your membership is re-checked on every request — if you're removed from the workspace (or the workspace is deleted), the key stops working immediately with `403 forbidden`. A key created without a workspace (or before workspace keys existed) operates on your **personal** workspace, exactly as before. --- ## Response Format **Success:** ```json { "data": { ... } } ``` **List (paginated):** ```json { "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=` (rows modified at/after that instant) for cheap delta polling — combine it with cursor mode to sync a big catalog incrementally. **Error:** ```json { "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 Content-Type: multipart/form-data ``` | 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`: ```json { "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 Content-Type: application/json ``` ```bash curl -X POST https://api.pinwing.ai/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: ```bash # 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.pinwing.ai/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" } PUT (the GLB bytes — curl -T model.glb "") POST /v1/models/finalize { "ticket": "...", "filename": "chair.glb", "renderSettings": { ... } } ``` `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. ```bash url_resp=$(curl -s -X POST https://api.pinwing.ai/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.pinwing.ai/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](#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. | ```bash curl "https://api.pinwing.ai/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...", "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: ```bash curl https://api.pinwing.ai/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...", "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 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 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`. ```bash curl -X PATCH https://api.pinwing.ai/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. ```bash curl -X PATCH https://api.pinwing.ai/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://api.pinwing.ai/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 } } ``` > **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](#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): ```json { "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](#render-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 ``` ```json { "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](#render-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 `error` → `failed`). 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: ```json { "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 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", "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](#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 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.pinwing.ai/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: ```json { "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 GET /v1/models/:id/file-url?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) | ```json { "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](#dataset-export-vision-training) for the archive contents. The render must be `done`. **Content-Type:** `application/zip` ```bash curl -o dataset.zip https://api.pinwing.ai/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](#overlays). | | `bgAlign` | string | `"top-left"` | Crop anchor when background image doesn't match output aspect: `"top-left"`, `"top-center"`, `"top-right"`, `"center-left"`, `"center"`, `"center-right"`, `"bottom-left"`, `"bottom-center"`, `"bottom-right"` | | `length` | number | `6` | Video duration in seconds: greater than `0`, up to `120` (ProRes 4444: up to `60`; admin accounts uncapped). Decimal OK, e.g. `4.7`. Credits = tier rate × seconds, 1-credit minimum. | | `animation` | string | — | Animation clip name to play. Discover a model's clips via `GET /v1/models/:id` → `animations` (`[{ name, duration, channels }]`, seconds); `animationCount` appears on list rows. Use `"*"` for the first clip. | | `animationSpeed` | number | `1` | Animation playback speed multiplier, −10 to 10, non-zero (e.g. `0.5` for half speed, `2` for double; negative values play the clip in reverse). | | `shadows` | boolean | `true` | Enable ground shadows | | `reflector` | boolean | `false` | Enable reflective ground plane | | `easing` | boolean | `true` | Ease in/out on turntable rotation | | `strictCredits` | boolean | `false` | Refuse the free-credit fallback: when paid credits are insufficient, fail with `402 no_credits` instead of silently funding the render from the free pool and flagging it `isPreview` (a legacy path — current signups don't accrue free-pool credits, see [Credits](#render-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](#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. | | `scaleX` | number | `1` | Per-axis model scale X. `1` = original; a **negative value mirrors** that axis (chirality fix — see [Model Orientation](#model-orientation)); non-uniform values stretch the proportions; a uniform scale (all three equal) is a no-op (the frame refits). Magnitude 0.01–20. | | `scaleY` | number | `1` | Per-axis model scale Y (up/down). Same semantics as `scaleX`. | | `scaleZ` | number | `1` | Per-axis model scale Z (front/back). Same semantics as `scaleX`. | | `translateX` | number | `0` | Model position offset X (slide left/right) in normalized units (the model spans ~2). Range -20 to 20. | | `translateY` | number | `0` | Model position offset Y — a **positive value lifts** the model off the ground so it floats above its shadow. Range -20 to 20. | | `translateZ` | number | `0` | Model position offset Z (slide front/back) in normalized units. Range -20 to 20. | | `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](#dataset-export-vision-training) | ### 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`](#environments-envid) — 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](#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`](#textures-materialeditsnamemapassetid) — 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), placementScale? ([x,y,z] model scale at stamp time, each axis magnitude 0.01–20) }`. `placementScale` is the model scale when the decal was stamped: the decal bakes at that scale and the final model scale rides on top, so a non-uniform model scale **stretches** the sticker (matching Studio's preview). Omit it → the decal bakes at the final scale (undistorted). | | **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). `scaleX`/`scaleY`/`scaleZ` scale the **model** per axis (`1` = original): a **negative** value mirrors that axis — the fix for a model that loaded as its wrong-handed twin (e.g. a left shoe that should read as a right) — non-uniform values stretch its proportions, and a uniform scale is a visual no-op because the frame auto-refits. `translateX`/`translateY`/`translateZ` offset the model's position in normalized units (it spans ~2): a **positive `translateY` lifts** it off the ground to float above its shadow, while `translateX`/`translateZ` slide it (most visible on a keyframe path; on a turntable they read as an orbit-relative offset). ```bash curl -X POST https://api.pinwing.ai/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: ```json { "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: - `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 → video-processing → done ``` 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 Content-Type: multipart/form-data ``` | 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..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`:** ```json { "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 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. ### Resolve by SHA-256 ``` POST /v1/overlays/resolve { "sha256": "<64-char hex>", "layer": "decal" } ``` 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](#workspace-bound-keys)). ### Textures (`materialEdits..mapAssetId`) ``` POST /v1/assets/textures multipart: file (PNG/JPEG/WebP ≤ 25 MB), name? GET /v1/assets/textures DELETE /v1/assets/textures/:id ``` `POST` returns `201 { "id": ... }` — pass the id as `materialEdits..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/:id` → `materialNames`, exact match). ### Environments (`envId`) ``` POST /v1/assets/envs multipart: sdr, gainmap, metadata, thumb?, name?, width?, height? GET /v1/assets/envs DELETE /v1/assets/envs/:id ``` Custom HDRI environments use the **pre-encoded gainmap triple** — the exact output of [gainmap-js](https://github.com/MONOGRID/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. ```bash curl https://api.pinwing.ai/v1/account \ -H "Authorization: Bearer dxgl_sk_..." ``` ```json { "data": { "id": "Ab3kF9x2qL1m", "email": "user@example.com", "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 Content-Type: application/json ``` ```json { "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`: ```json { "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. ```bash curl -X POST https://api.pinwing.ai/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. ```json { "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 | ```json { "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](#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 ```bash # Upload a GLB model and start rendering curl -X POST https://api.pinwing.ai/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.pinwing.ai/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/sec — default 6s = 96 credits) curl -X POST https://api.pinwing.ai/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.pinwing.ai/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.pinwing.ai/v1/renders/Xz7pQ4w8nR2k \ -H "Authorization: Bearer dxgl_sk_..." # Download the video when done curl -o chair.mp4 https://api.pinwing.ai/v1/renders/Xz7pQ4w8nR2k/video \ -H "Authorization: Bearer dxgl_sk_..." # Download all assets as a zip curl -o chair.zip https://api.pinwing.ai/v1/renders/Xz7pQ4w8nR2k/bundle \ -H "Authorization: Bearer dxgl_sk_..." ``` ### Python Example ```python import requests, time API = "https://api.pinwing.ai/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://api.pinwing.ai/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())); ```