Simplio3D

Simplio3D API v3.4

Build powerful headless 3D CPQ applications with the Simplio3D RESTful API. Full control over option blocks, conditional logic, pricing, contact forms, 3D viewer, animations, and more.

Getting Started

The Simplio3D API is organized around REST. It accepts JSON-encoded request bodies, returns JSON-encoded responses, and uses standard HTTP response codes and JWT-based authentication.

Base URL: https://your-project.supabase.co/functions/v1/make-server-0532dd87

Quick Example

// Get all projects
const response = await fetch(BASE_URL + '/projects', {
  headers: {
    'Authorization': 'Bearer YOUR_ACCESS_TOKEN',
    'Content-Type': 'application/json'
  }
});

const data = await response.json();
console.log(data.projects);

Available Endpoints

Authentication

Sign up, sign in, sessions

4 endpoints

Projects

Full CRUD + model uploads

7 endpoints

Option Blocks

Configurator blocks CRUD + reorder

7 endpoints

Conditional Logic

Rules management + evaluation

4 endpoints

Pricing & CPQ

Price blocks, formulas, calculate

6 endpoints

Contact Forms

Form fields CRUD + submissions

5 endpoints

3D Viewer

Embed config, state, screenshots

5 endpoints

Animations

Animation blocks CRUD

4 endpoints

Project Settings

Branding, lighting, camera

3 endpoints

Assets

Upload, manage, download

6 endpoints

Materials

PBR materials CRUD

5 endpoints

Categories

Asset organization

5 endpoints

Team

Invite, update, remove members

4 endpoints

Sharing

Public links & settings

5 endpoints

Email

SMTP test, logs, resend

5 endpoints

Quote Submissions

Submit, list, manage quotes

4 endpoints

Headless Architecture

Build your own UI with full API access to option blocks, conditional logic, pricing formulas, and 3D viewer state. Perfect for custom storefronts.

Secure by Default

JWT-based authentication with Supabase Auth. All mutating endpoints require a valid access token.

Authentication

The Simplio3D API uses JWT tokens for authentication. All API requests must include a valid access token.

Sign Up

POST/auth/signup
const response = await fetch(BASE_URL + '/auth/signup', {
  method: 'POST',
  headers: { 'Content-Type': 'application/json' },
  body: JSON.stringify({
    email: '[email protected]',
    password: 'securePassword123',
    name: 'John Doe'
  })
});

const data = await response.json();
// { "success": true, "userId": "...", "message": "Account created" }

Sign In

import { createClient } from '@supabase/supabase-js';

const supabase = createClient(SUPABASE_URL, SUPABASE_ANON_KEY);

const { data: { session }, error } = await supabase.auth.signInWithPassword({
  email: '[email protected]',
  password: 'securePassword123'
});

// Use session.access_token for all API calls
const accessToken = session.access_token;

Get Session

GET/auth/session
const response = await fetch(BASE_URL + '/auth/session', {
  headers: { 'Authorization': 'Bearer ' + accessToken }
});

const data = await response.json();
// { "success": true, "userId": "...", "email": "[email protected]" }

Using Access Tokens

Include the access token in the Authorization header for all authenticated requests:

Authorization: Bearer YOUR_ACCESS_TOKEN

Projects

Manage 3D projects and configurations. Project types include configurator, viewer, cpq, ar-preview, and embedded.

List All Projects

GET/projects
const response = await fetch(BASE_URL + '/projects', {
  headers: { 'Authorization': 'Bearer ' + accessToken }
});

const data = await response.json();
// {
//   "success": true,
//   "projects": [
//     {
//       "id": "proj_123",
//       "name": "Modern Office Design",
//       "type": "configurator",
//       "thumbnail": "https://...",
//       "createdAt": "2026-02-25T10:00:00Z",
//       "updatedAt": "2026-03-01T14:30:00Z"
//     }
//   ]
// }

Create Project

POST/projects
const response = await fetch(BASE_URL + '/projects', {
  method: 'POST',
  headers: {
    'Authorization': 'Bearer ' + accessToken,
    'Content-Type': 'application/json'
  },
  body: JSON.stringify({
    name: 'New Product Configuration',
    type: 'configurator' // 'configurator' | 'viewer' | 'cpq' | 'ar-preview' | 'embedded'
  })
});

const data = await response.json();
// { "success": true, "project": { ... } }

Get Project

GET/projects/:id

Large projects may return an optimized payload variant to stay within edge gateway limits. Always handle both full and compressed response shapes.

const response = await fetch(BASE_URL + '/projects/proj_123', {
  headers: { 'Authorization': 'Bearer ' + accessToken }
});
const data = await response.json();

// Variant A (full / normalized scene)
// data.project.sceneData

// Variant B (optimized)
// data.project.data.sceneDataCompressed + data.project.data.sceneDataEncoding
// OR data.project.data.sceneData + data.project.data.models + data.project.data.camera

Large-Project Response Variants

The API may return one of these variants for GET /projects/:id:

  • project.sceneData (fully expanded scene)
  • project.data.sceneDataCompressed + sceneDataEncoding=gzip-base64
  • project.data.sceneData with project.data.models and project.data.camera

Update Project

PUT/projects/:id
const response = await fetch(BASE_URL + '/projects/proj_123', {
  method: 'PUT',
  headers: {
    'Authorization': 'Bearer ' + accessToken,
    'Content-Type': 'application/json'
  },
  body: JSON.stringify({
    name: 'Updated Project Name',
    data: { settings: { autoRotate: true } }
  })
});

Duplicate Project

POST/projects/:id/clone

Creates an independent copy by deep-cloning the project's entire sceneData — all 3D models (with nested groups and parts), every option block and variant (hotspots, scenery, materials, …), the full pricing structure (price groups, 1D/2D tables, unique-price blocks, variables, and all SKU maps), the complete form, animations, conditional logic, and project settings. Asset-library models, materials, and textures are reused by reference (no extra storage cost). Per-project uploaded models, the AR USDZ file, and the thumbnail are copied to the new project's storage path and their stored paths rewritten so the copy is independent of the source. Share settings, WooCommerce / Shopify product links, and saved configurations are not carried over.
Plan gate: requires Pro or Enterprise. Starter callers receive 403.

const response = await fetch(BASE_URL + '/projects/proj_123/clone', {
  method: 'POST',
  headers: {
    'Authorization': 'Bearer ' + accessToken,
    'Content-Type': 'application/json'
  },
  body: JSON.stringify({
    name: 'My Configurator Copy'   // optional — defaults to "{source name} Copy"
  })
});

const data = await response.json();
// {
//   "success": true,
//   "project": { "id": "proj_new", "name": "My Configurator Copy", "type": "configurator", ... },
//   "warnings": { "failedModelCopies": [...], "message": "..." }   // present only on partial failure
// }

Delete Project

DELETE/projects/:id
const response = await fetch(BASE_URL + '/projects/proj_123', {
  method: 'DELETE',
  headers: { 'Authorization': 'Bearer ' + accessToken }
});

Option Blocks

Option blocks are the core configurator building blocks. Each block type represents a different UI control that drives 3D model changes, material swaps, visibility toggles, and more. Supports 15 block types for headless integration.

Supported Block Types

dropdown
select-material
thumbnail-selector
checkbox
toggle-switch
carousel
text-input
number-input
file-upload
pattern-designer
design-canvas
hotspot
fixed-material
section-header
scenery

Select Material — authoring modes

The select-material block exposes two authoring modes via a top-level materialSource field:

  • Manual (materialSource: 'manual', default) — variants are authored individually in dropdownVariants, each with its own target, material, and pricing. Supports Price Group / Variable / Price Table linking.
  • From Category (materialSource: 'category') — runtime variants are auto-populated from every material inmaterialCategoryId. The block uses a shared target that can span multiple objects — categoryTargetObjectIds (string[]) and per-model categoryTargetParts (Record<modelId, string[]>) — applied to every synthesized variant (legacy single-object categoryTargetObjectId /categoryTargetPartNames are still read as a fallback). Pricing flows through the standard Pricing system (Price Group / Variable / Price Table / Unique Price) — link a pricing block to the category-mode Select Material block and author one price per material. Variant IDs at runtime are auto:{materialId}.

Public share viewers fetch referenced category materials at first paint via GET /share/:projectId/:token/category-materials?ids=cat1,cat2, with GET /share/:projectId/:token/category-materials/:categoryId still available for a single category.

List Option Blocks

GET/projects/:id/option-blocks
const response = await fetch(BASE_URL + '/projects/proj_123/option-blocks', {
  headers: { 'Authorization': 'Bearer ' + accessToken }
});

const data = await response.json();
// {
//   "success": true,
//   "blocks": [
//     {
//       "id": "blk_001",
//       "type": "dropdown",
//       "name": "Frame Color",
//       "visible": true,
//       "enabled": true,
//       "dropdownVariants": [
//         {
//           "id": "var_a",
//           "label": "Black",
//           "value": "black",
//           "materialData": { "baseColor": "#1a1a1a", "metallic": 0.8, "roughness": 0.2 },
//           "targetPartNames": ["frame_body", "frame_legs"]
//         },
//         {
//           "id": "var_b",
//           "label": "Silver",
//           "value": "silver",
//           "materialData": { "baseColor": "#C0C0C0", "metallic": 0.95, "roughness": 0.1 },
//           "targetPartNames": ["frame_body", "frame_legs"]
//         }
//       ],
//       "defaultDropdownValue": "black"
//     },
//     {
//       "id": "blk_002",
//       "type": "thumbnail-selector",
//       "name": "Fabric Type",
//       "visible": true,
//       "enabled": true,
//       "thumbnailStyle": "grid",
//       "thumbnailSize": "60x60",
//       "thumbnailShape": "rounded",
//       "dropdownVariants": [
//         {
//           "id": "var_c",
//           "label": "Linen",
//           "value": "linen",
//           "thumbnailUrl": "https://...",
//           "materialData": { "baseColor": "#f0e6d3", "roughness": 0.9 }
//         }
//       ]
//     },
//     {
//       "id": "blk_003",
//       "type": "toggle-switch",
//       "name": "Armrests",
//       "visible": true,
//       "enabled": true,
//       "dropdownVariants": [
//         { "id": "var_d", "label": "Without", "value": "without",
//           "visibilityConfig": { "targetObjectIds": ["model_armrest"], "action": "hide" } },
//         { "id": "var_e", "label": "With Armrests", "value": "with",
//           "visibilityConfig": { "targetObjectIds": ["model_armrest"], "action": "show" } }
//       ]
//     }
//   ]
// }

Create Option Block

POST/projects/:id/option-blocks
// Create a Dropdown block with material-swapping variants
const response = await fetch(BASE_URL + '/projects/proj_123/option-blocks', {
  method: 'POST',
  headers: {
    'Authorization': 'Bearer ' + accessToken,
    'Content-Type': 'application/json'
  },
  body: JSON.stringify({
    type: 'dropdown',
    name: 'Wood Finish',
    dropdownVariants: [
      {
        id: crypto.randomUUID(),
        label: 'Oak',
        value: 'oak',
        targetPartNames: ['desk_top', 'desk_drawer'],
        materialData: {
          baseColor: '#8B6F47',
          metallic: 0.0,
          roughness: 0.8,
          textureMap: 'https://cdn.example.com/textures/oak.jpg',
          normalMap: 'https://cdn.example.com/textures/oak_normal.jpg'
        }
      },
      {
        id: crypto.randomUUID(),
        label: 'Walnut',
        value: 'walnut',
        targetPartNames: ['desk_top', 'desk_drawer'],
        materialData: {
          baseColor: '#5C3D2E',
          metallic: 0.0,
          roughness: 0.75,
          textureMap: 'https://cdn.example.com/textures/walnut.jpg'
        }
      }
    ],
    defaultDropdownValue: 'oak'
  })
});

// { "success": true, "block": { "id": "blk_new", ... } }

Create Checkbox Block

POST/projects/:id/option-blocks
// Checkbox — multiple selections, each toggling 3D object visibility
const response = await fetch(BASE_URL + '/projects/proj_123/option-blocks', {
  method: 'POST',
  headers: {
    'Authorization': 'Bearer ' + accessToken,
    'Content-Type': 'application/json'
  },
  body: JSON.stringify({
    type: 'checkbox',
    name: 'Optional Accessories',
    checkboxAlignment: 'vertical',
    showSelectAll: true,
    dropdownVariants: [
      {
        id: crypto.randomUUID(),
        label: 'Cup Holder',
        value: 'cup-holder',
        visibilityConfig: { targetObjectIds: ['model_cup_holder'], action: 'show' }
      },
      {
        id: crypto.randomUUID(),
        label: 'Monitor Stand',
        value: 'monitor-stand',
        visibilityConfig: { targetObjectIds: ['model_monitor_arm'], action: 'show' }
      },
      {
        id: crypto.randomUUID(),
        label: 'Cable Tray',
        value: 'cable-tray',
        visibilityConfig: { targetObjectIds: ['model_cable_tray'], action: 'show' }
      }
    ],
    defaultCheckboxValues: ['cup-holder']
  })
});

Create Carousel Block

POST/projects/:id/option-blocks
// Carousel — swipe/arrow navigation through variants
const response = await fetch(BASE_URL + '/projects/proj_123/option-blocks', {
  method: 'POST',
  headers: {
    'Authorization': 'Bearer ' + accessToken,
    'Content-Type': 'application/json'
  },
  body: JSON.stringify({
    type: 'carousel',
    name: 'Upholstery',
    carouselArrowStyle: 'filled',
    carouselAnimation: 'slide',
    carouselLoop: true,
    carouselShowCounter: true,
    carouselShowLabel: true,
    dropdownVariants: [
      { id: crypto.randomUUID(), label: 'Velvet Red', value: 'velvet-red',
        thumbnailUrl: 'https://...', materialData: { baseColor: '#8B0000', roughness: 0.9 } },
      { id: crypto.randomUUID(), label: 'Leather Brown', value: 'leather-brown',
        thumbnailUrl: 'https://...', materialData: { baseColor: '#654321', roughness: 0.6, metallic: 0.1 } }
    ]
  })
});

Create Text Input Block

POST/projects/:id/option-blocks
// Text Input — user-typed text rendered as a texture on a 3D part
const response = await fetch(BASE_URL + '/projects/proj_123/option-blocks', {
  method: 'POST',
  headers: {
    'Authorization': 'Bearer ' + accessToken,
    'Content-Type': 'application/json'
  },
  body: JSON.stringify({
    type: 'text-input',
    name: 'Engraving',
    textInputTargets: [{
      id: crypto.randomUUID(),
      label: 'Custom Text',
      targetPartName: 'engraving_plate',
      placeholder: 'Enter up to 20 characters',
      maxLength: 20,
      fontFamily: 'Roboto',
      fontSize: 48,
      fontColor: '#1a1a1a',
      textAlign: 'center',
      canvasWidth: 1024,
      canvasHeight: 256,
      backgroundColor: '',
      // Restrict consumer input. 'numbers' triggers the mobile numeric keypad
      // and strips any non-digit characters on every change. Use 'any' (or
      // omit the field) for unrestricted text. For decimal math / pricing,
      // use the dedicated number-input option block instead.
      contentType: 'numbers',
      // Let consumers swap the font, but only between these three.
      // Omit (or set to undefined) to expose every built-in font.
      allowConsumerFontFamily: true,
      consumerAllowedFonts: ['Roboto', 'Montserrat', 'Playfair Display']
    }],
    textInputShowLivePreview: true
  })
});

Create Hotspot Block

POST/projects/:id/option-blocks
// Hotspot — interactive 3D marker with popup content
const response = await fetch(BASE_URL + '/projects/proj_123/option-blocks', {
  method: 'POST',
  headers: {
    'Authorization': 'Bearer ' + accessToken,
    'Content-Type': 'application/json'
  },
  body: JSON.stringify({
    type: 'hotspot',
    name: 'Adjustable Mechanism',
    hotspotPosition: { x: 0.5, y: 1.2, z: 0.3 },
    hotspotTitle: 'Tilt Mechanism',
    hotspotDescription: 'Multi-angle tilt lock supports 5 positions from 90-135 degrees.',
    hotspotIcon: 'info',
    hotspotTrigger: 'click',
    hotspotAnimationStyle: 'pulse',
    hotspotImageUrl: 'https://cdn.example.com/mechanism.jpg',
    hotspotLinkUrl: 'https://example.com/specs',
    hotspotLinkLabel: 'View Specifications'
  })
});

Update Option Block

PUT/projects/:id/option-blocks/:blockId
const response = await fetch(BASE_URL + '/projects/proj_123/option-blocks/blk_001', {
  method: 'PUT',
  headers: {
    'Authorization': 'Bearer ' + accessToken,
    'Content-Type': 'application/json'
  },
  body: JSON.stringify({
    name: 'Updated Block Name',
    visible: false,
    dropdownVariants: [/* updated variants */]
  })
});

Delete Option Block

DELETE/projects/:id/option-blocks/:blockId
const response = await fetch(BASE_URL + '/projects/proj_123/option-blocks/blk_001', {
  method: 'DELETE',
  headers: { 'Authorization': 'Bearer ' + accessToken }
});

Reorder Option Blocks

POST/projects/:id/option-blocks/reorder
const response = await fetch(BASE_URL + '/projects/proj_123/option-blocks/reorder', {
  method: 'POST',
  headers: {
    'Authorization': 'Bearer ' + accessToken,
    'Content-Type': 'application/json'
  },
  body: JSON.stringify({
    blockIds: ['blk_003', 'blk_001', 'blk_002'] // New order
  })
});

// { "success": true, "blocks": [/* blocks with updated order */] }

Conditional Logic

Define rules that dynamically show/hide option blocks or specific variants based on user selections. Rules evaluate at runtime using "all" (AND) or "any" (OR) logic operators.

How Conditional Logic Works

Hide rules: Block/variants start visible and become hidden when conditions match.

Show rules: Block/variants start hidden and become visible when conditions match.

Evaluation order: Hide rules first, then show rules can override.

Match types: is-selected and is-not-selected

Auto-apply on show: When a block transitions from hidden → visible (e.g. via a show / block rule), the default or previously-selected material from that block is automatically applied to the 3D viewport. This ensures the model always reflects the newly-visible block's material without requiring an explicit user click.

Add Conditional Rules to a Block

PUT/projects/:id/option-blocks/:blockId
// Hide the "Armrest Color" block when "Armrests" toggle is set to "without"
const response = await fetch(BASE_URL + '/projects/proj_123/option-blocks/blk_armrest_color', {
  method: 'PUT',
  headers: {
    'Authorization': 'Bearer ' + accessToken,
    'Content-Type': 'application/json'
  },
  body: JSON.stringify({
    conditionalRules: [
      {
        id: crypto.randomUUID(),
        action: 'hide',
        targetScope: 'block',
        targetVariantIds: [],
        operator: 'all',
        conditions: [
          {
            id: crypto.randomUUID(),
            sourceBlockId: 'blk_armrests',
            matchType: 'is-selected',
            sourceVariantIds: ['var_without_armrests']
          }
        ]
      }
    ]
  })
});

Variant-Level Conditional Logic

// Hide specific fabric options when "Outdoor" is selected in another block
const rules = [{
  id: crypto.randomUUID(),
  action: 'hide',
  targetScope: 'variants',
  targetVariantIds: ['var_silk', 'var_velvet'], // These fabrics are indoor-only
  operator: 'all',
  conditions: [{
    id: crypto.randomUUID(),
    sourceBlockId: 'blk_environment',
    matchType: 'is-selected',
    sourceVariantIds: ['var_outdoor']
  }]
}];

// Show premium materials only when "Enterprise" plan is detected
const showPremiumRule = {
  id: crypto.randomUUID(),
  action: 'show',
  targetScope: 'variants',
  targetVariantIds: ['var_carbon_fiber', 'var_titanium'],
  operator: 'any',
  conditions: [{
    id: crypto.randomUUID(),
    sourceBlockId: 'blk_tier',
    matchType: 'is-selected',
    sourceVariantIds: ['var_enterprise', 'var_premium']
  }]
};

Multi-Condition Rules (AND / OR)

// Show the "Heated Seats" option ONLY when BOTH conditions are met:
// 1. Vehicle type is "SUV" or "Sedan"
// 2. Climate package is selected
const rule = {
  id: crypto.randomUUID(),
  action: 'show',
  targetScope: 'block',
  targetVariantIds: [],
  operator: 'all', // ALL conditions must match (AND logic)
  conditions: [
    {
      id: crypto.randomUUID(),
      sourceBlockId: 'blk_vehicle_type',
      matchType: 'is-selected',
      sourceVariantIds: ['var_suv', 'var_sedan'] // ANY of these
    },
    {
      id: crypto.randomUUID(),
      sourceBlockId: 'blk_climate_pkg',
      matchType: 'is-selected',
      sourceVariantIds: ['var_climate_yes']
    }
  ]
};

3D Object Visibility Rules

Use targetScope: '3d' to show/hide 3D objects in the viewport without affecting the UI block. This lets you keep the option panel visible while controlling 3D representation independently.

// Hide the armrest 3D objects when "Minimalist" style is selected
const rule = {
  id: crypto.randomUUID(),
  action: 'hide',
  targetScope: '3d',
  target3dObjectNames: ['Armrest_Left', 'Armrest_Right'],
  targetVariantIds: [],
  operator: 'all',
  conditions: [{
    id: crypto.randomUUID(),
    sourceBlockId: 'blk_style',
    matchType: 'is-selected',
    sourceVariantIds: ['var_minimalist']
  }]
};

// A block can have BOTH a block-hide rule and a 3D-hide rule:
// Rule 1: Hide the block UI when condition X
// Rule 2: Hide 3D objects when condition Y

3D Visibility Rules

Use targetScope: '3d' with targetObjectIds to show/hide whole 3D objects, or targetScope: '3d-parts' with targetPartNames to show/hide individual mesh parts (sub-meshes) in the viewport.

// Hide decorative stitching and logo when "Plain" finish is selected
const rule = {
  id: crypto.randomUUID(),
  action: 'hide',
  targetScope: '3d-parts',
  target3dPartNames: ['stitching_detail', 'logo_emboss', 'button_caps'],
  targetVariantIds: [],
  operator: 'all',
  conditions: [{
    id: crypto.randomUUID(),
    sourceBlockId: 'blk_finish',
    matchType: 'is-selected',
    sourceVariantIds: ['var_plain']
  }]
};

// Use '3d' scope for whole objects, '3d-parts' for individual meshes.
// Both keep the UI block visible while controlling viewport visibility.

Evaluate Conditions (Runtime)

POST/projects/:id/option-blocks/evaluate

Evaluate all conditional logic rules against a set of consumer selections. Returns which blocks and variants are visible.

const response = await fetch(BASE_URL + '/projects/proj_123/option-blocks/evaluate', {
  method: 'POST',
  headers: {
    'Authorization': 'Bearer ' + accessToken,
    'Content-Type': 'application/json'
  },
  body: JSON.stringify({
    selections: {
      dropdownSelections: { 'blk_frame': 'black', 'blk_size': 'large' },
      selectMaterialSelections: { 'blk_fabric': 'linen' },
      checkboxSelections: { 'blk_accessories': ['cup-holder', 'monitor-stand'] },
      toggleSwitchSelections: { 'blk_armrests': 'with' },
      carouselSelections: {}
    }
  })
});

const data = await response.json();
// {
//   "success": true,
//   "results": [
//     { "blockId": "blk_001", "blockVisible": true, "hiddenVariantIds": [], "hidden3dObjectNames": [], "hidden3dPartNames": [] },
//     { "blockId": "blk_002", "blockVisible": true, "hiddenVariantIds": ["var_silk"], "hidden3dObjectNames": [], "hidden3dPartNames": [] },
//     { "blockId": "blk_armrest_color", "blockVisible": true, "hiddenVariantIds": [], "hidden3dObjectNames": ["armrest_left", "armrest_right"], "hidden3dPartNames": ["logo_emboss"] },
//     { "blockId": "blk_heated", "blockVisible": false, "hiddenVariantIds": [], "hidden3dObjectNames": [], "hidden3dPartNames": [] }
//   ]
// }

Pricing & CPQ

Configure pricing with base prices, price groups, lookup tables, unique modular prices, and variables. Build formulas that combine pricing blocks with mathematical operators. Calculate real-time prices based on user selections.

Pricing Block Types

base-price — Fixed starting price (one per project)
price-group — Logical folder for grouping pricing items
price-table — Lookup table mapping selections to price adjustments
unique-price — Enterprise Modular quantity lookup with per-column default fallback
variable — Named numeric parameter (e.g. width, quantity)

List Pricing Blocks

GET/projects/:id/pricing-blocks
const response = await fetch(BASE_URL + '/projects/proj_123/pricing-blocks', {
  headers: { 'Authorization': 'Bearer ' + accessToken }
});

const data = await response.json();
// {
//   "success": true,
//   "blocks": [
//     { "id": "pb_1", "type": "base-price", "name": "Base Price",
//       "basePrice": 499.99, "baseCurrency": "USD", "enabled": true },
//     { "id": "pb_2", "type": "variable", "name": "Width",
//       "variableKey": "width", "variableDefaultValue": 120,
//       "variableMin": 80, "variableMax": 200, "variableStep": 10, "variableUnit": "cm" },
//     { "id": "pb_3", "type": "price-table", "name": "Material Upcharge",
//       "tableColumns": [
//         { "id": "col_1", "label": "Material", "type": "option-ref" },
//         { "id": "col_2", "label": "Price", "type": "number" }
//       ],
//       "tableRows": [
//         { "id": "row_1", "cells": { "col_1": "oak", "col_2": 0 }, "priceAdjustment": 0 },
//         { "id": "row_2", "cells": { "col_1": "walnut", "col_2": 89 }, "priceAdjustment": 89 },
//         { "id": "row_3", "cells": { "col_1": "marble", "col_2": 299 }, "priceAdjustment": 299 }
//       ]
//     },
//     { "id": "pb_4", "type": "unique-price", "name": "Connect Full Wall Unique Price",
//       "uniquePriceModularBlockId": "blk_roof_parts",
//       "uniquePriceVariantId": "mod_connect_full_wall",
//       "uniquePriceColumnOptionBlockId": "blk_treatment",
//       "uniquePriceQuantities": [1, 2],
//       "uniquePriceCellPrices": { "1::natural": 249, "2::natural": 699 },
//       "uniquePriceDefaultCellPrices": { "natural": 399, "painted": 549 }
//     }
//   ]
// }

Create Pricing Block

POST/projects/:id/pricing-blocks
// Create a variable for "Width" dimension
const response = await fetch(BASE_URL + '/projects/proj_123/pricing-blocks', {
  method: 'POST',
  headers: {
    'Authorization': 'Bearer ' + accessToken,
    'Content-Type': 'application/json'
  },
  body: JSON.stringify({
    type: 'variable',
    name: 'Width',
    variableKey: 'width',
    variableDefaultValue: 120,
    variableMin: 80,
    variableMax: 200,
    variableStep: 10,
    variableUnit: 'cm'
  })
});

Update Pricing Block

PUT/projects/:id/pricing-blocks/:blockId
// Update a price table with new rows
const response = await fetch(BASE_URL + '/projects/proj_123/pricing-blocks/pb_3', {
  method: 'PUT',
  headers: {
    'Authorization': 'Bearer ' + accessToken,
    'Content-Type': 'application/json'
  },
  body: JSON.stringify({
    tableRows: [
      { id: 'row_1', cells: { col_1: 'oak', col_2: 0 }, priceAdjustment: 0 },
      { id: 'row_2', cells: { col_1: 'walnut', col_2: 120 }, priceAdjustment: 120 },
      { id: 'row_new', cells: { col_1: 'carbon-fiber', col_2: 450 }, priceAdjustment: 450 }
    ]
  })
});

Set Pricing Formula

PUT/projects/:id/pricing-formula

Define the formula that combines pricing blocks. Uses token-based format with block references, numbers, and operators.

const response = await fetch(BASE_URL + '/projects/proj_123/pricing-formula', {
  method: 'PUT',
  headers: {
    'Authorization': 'Bearer ' + accessToken,
    'Content-Type': 'application/json'
  },
  body: JSON.stringify({
    formulaTokens: [
      { type: 'block', blockId: 'pb_1' },            // @Base_Price
      { type: 'operator', value: '+' },
      { type: 'block', blockId: 'pb_3' },            // @Material_Upcharge
      { type: 'operator', value: '+' },
      { type: 'paren', value: '(' },
      { type: 'block', blockId: 'pb_2' },            // @Width
      { type: 'operator', value: '*' },
      { type: 'number', value: '2.5' },              // per-cm cost
      { type: 'paren', value: ')' }
    ]
    // Human readable: @Base_Price + @Material_Upcharge + (@Width * 2.5)
  })
});

Calculate Price

POST/projects/:id/pricing-blocks/calculate

Calculate the final price given a set of option block selections and variable values.

const response = await fetch(BASE_URL + '/projects/proj_123/pricing-blocks/calculate', {
  method: 'POST',
  headers: {
    'Authorization': 'Bearer ' + accessToken,
    'Content-Type': 'application/json'
  },
  body: JSON.stringify({
    selections: {
      dropdownSelections: { 'blk_wood': 'walnut' },
      selectMaterialSelections: {},
      checkboxSelections: { 'blk_accessories': ['cup-holder'] },
      toggleSwitchSelections: { 'blk_armrests': 'with' },
      carouselSelections: {}
    },
    variables: {
      width: 160,
      quantity: 2
    }
  })
});

const data = await response.json();
// {
//   "success": true,
//   "basePrice": 499.99,
//   "adjustments": [
//     { "blockId": "pb_3", "blockName": "Material Upcharge", "amount": 120 },
//     { "blockId": "pb_2", "blockName": "Width", "amount": 400 }
//   ],
//   "totalPrice": 1019.99,
//   "currency": "USD",
//   "subtotal": 1019.99,
//   "tax": 203.998,
//   "totalWithTax": 1223.988,
//   "taxRate": 20,
//   "taxMode": "exclusive",
//   "taxLabel": "VAT",
//   "taxEnabled": true,
//   "formatted": "$1,223.99",
//   "formattedSubtotal": "$1,019.99",
//   "formattedTax": "$204.00"
// }

Contact Forms

Build contact/quote request forms with configurable fields. Supports name, email, phone, text, dropdowns, country pickers, legal acceptance, custom headers, and confirmation screens.

List Form Fields

GET/projects/:id/form-fields
const response = await fetch(BASE_URL + '/projects/proj_123/form-fields', {
  headers: { 'Authorization': 'Bearer ' + accessToken }
});

const data = await response.json();
// {
//   "success": true,
//   "fields": [
//     { "id": "ff_1", "type": "header-title", "label": "Header",
//       "headerText": "Request a Quote", "headerSubtext": "Fill in the details below.",
//       "headerAlignment": "center", "enabled": true, "required": false, "width": "full" },
//     { "id": "ff_2", "type": "name", "label": "Full Name", "placeholder": "Enter your name",
//       "required": true, "enabled": true, "width": "full" },
//     { "id": "ff_3", "type": "email", "label": "Email", "placeholder": "[email protected]",
//       "required": true, "enabled": true, "width": "half",
//       "validationPattern": "^[^\\s@]+@[^\\s@]+\\.[^\\s@]+
quot; }, // { "id": "ff_4", "type": "telephone", "label": "Phone", "width": "half", // "required": false, "enabled": true }, // { "id": "ff_5", "type": "dropdown", "label": "Industry", "width": "full", // "options": [ // { "id": "1", "label": "Architecture", "value": "architecture" }, // { "id": "2", "label": "Retail", "value": "retail" }, // { "id": "3", "label": "Manufacturing", "value": "manufacturing" } // ] }, // { "id": "ff_6", "type": "country-state-city", "label": "Location", // "includeState": true, "includeCity": true }, // { "id": "ff_7", "type": "text", "label": "Additional Notes", // "multiline": true, "rows": 4, "maxLength": 500 }, // { "id": "ff_8", "type": "acceptance", "label": "I agree to the Terms", // "acceptanceText": "By submitting this form...", "required": true }, // { "id": "ff_9", "type": "submit", "label": "Submit", // "submitLabel": "Submit Your Request", "submitStyle": "gradient" }, // { "id": "ff_10", "type": "confirmation", "label": "Confirmation", // "confirmationTitle": "Thank You!", // "confirmationMessage": "We will get back to you within 24-48 hours.", // "confirmationShowSummary": true, "confirmationShowBackButton": true } // ] // }

Create Form Field

POST/projects/:id/form-fields
// Add a dropdown field for "Budget Range"
const response = await fetch(BASE_URL + '/projects/proj_123/form-fields', {
  method: 'POST',
  headers: {
    'Authorization': 'Bearer ' + accessToken,
    'Content-Type': 'application/json'
  },
  body: JSON.stringify({
    type: 'dropdown',
    label: 'Budget Range',
    required: true,
    width: 'half',
    options: [
      { id: '1', label: 'Under $1,000', value: 'under-1000' },
      { id: '2', label: '$1,000 - $5,000', value: '1000-5000' },
      { id: '3', label: '$5,000 - $10,000', value: '5000-10000' },
      { id: '4', label: 'Over $10,000', value: 'over-10000' }
    ],
    allowOther: true
  })
});

Update Form Field

PUT/projects/:id/form-fields/:fieldId
const response = await fetch(BASE_URL + '/projects/proj_123/form-fields/ff_2', {
  method: 'PUT',
  headers: {
    'Authorization': 'Bearer ' + accessToken,
    'Content-Type': 'application/json'
  },
  body: JSON.stringify({
    label: 'Your Full Name',
    placeholder: 'First and Last name',
    width: 'half'
  })
});

Delete Form Field

DELETE/projects/:id/form-fields/:fieldId
const response = await fetch(BASE_URL + '/projects/proj_123/form-fields/ff_5', {
  method: 'DELETE',
  headers: { 'Authorization': 'Bearer ' + accessToken }
});

Reorder Form Fields

POST/projects/:id/form-fields/reorder
const response = await fetch(BASE_URL + '/projects/proj_123/form-fields/reorder', {
  method: 'POST',
  headers: {
    'Authorization': 'Bearer ' + accessToken,
    'Content-Type': 'application/json'
  },
  body: JSON.stringify({
    fieldIds: ['ff_1', 'ff_2', 'ff_3', 'ff_4', 'ff_7', 'ff_5', 'ff_8', 'ff_9', 'ff_10']
  })
});

3D Viewer

Embed and control the 3D viewer programmatically. Configure camera, lighting, environment, manage viewer state, take screenshots, and respond to user interactions via callbacks.

Get Embed Configuration

GET/projects/:id/viewer/embed-config
const response = await fetch(BASE_URL + '/projects/proj_123/viewer/embed-config', {
  headers: { 'Authorization': 'Bearer ' + accessToken }
});

const data = await response.json();
// {
//   "success": true,
//   "config": {
//     "projectId": "proj_123",
//     "cameraPosition": [0, 2, 5],
//     "cameraTarget": [0, 0, 0],
//     "cameraFOV": 50,
//     "enableZoom": true,
//     "enablePan": true,
//     "enableAutoRotate": false,
//     "environmentPreset": "studio",
//     "lightIntensity": 1.0,
//     "ambientLightIntensity": 0.5,
//     "enableShadows": true,
//     "backgroundColor": "#f5f5f5",
//     "enableAR": false,
//     "loadingAnimation": "spinner",
//     "loadingMessage": "Loading 3D model..."
//   }
// }

Embed via iframe

The simplest way to embed a 3D viewer is via the share URL in an iframe.

<!-- Simple iframe embed -->
<iframe
  src="https://your-project.supabase.co/share/proj_123/SHARE_TOKEN"
  width="100%"
  height="600"
  frameborder="0"
  allow="xr-spatial-tracking"
  allowfullscreen
></iframe>

<!-- Embed with custom parameters -->
<iframe
  src="https://your-project.supabase.co/share/proj_123/SHARE_TOKEN?autoRotate=true&bg=%23ffffff"
  width="800"
  height="600"
  style="border: none; border-radius: 12px;"
></iframe>

Headless Viewer Integration

For fully custom UIs, use the headless API to build your own viewer with any 3D library (Three.js, Babylon.js, etc.).

// 1. Load project data with all option blocks and settings
const projectRes = await fetch(BASE_URL + '/projects/proj_123', {
  headers: { 'Authorization': 'Bearer ' + accessToken }
});
const { project } = await projectRes.json();

// 2. Load option blocks for the configurator UI
const blocksRes = await fetch(BASE_URL + '/projects/proj_123/option-blocks', {
  headers: { 'Authorization': 'Bearer ' + accessToken }
});
const { blocks } = await blocksRes.json();

// 3. Load pricing blocks for CPQ
const pricingRes = await fetch(BASE_URL + '/projects/proj_123/pricing-blocks', {
  headers: { 'Authorization': 'Bearer ' + accessToken }
});
const { blocks: pricingBlocks } = await pricingRes.json();

// 4. Build your custom UI with any framework
// - Render 3D scene using project.data.scene
// - Render option blocks as custom components
// - Apply material/visibility changes based on selections
// - Calculate prices in real-time

Get Viewer State

GET/projects/:id/viewer/state
const response = await fetch(BASE_URL + '/projects/proj_123/viewer/state', {
  headers: { 'Authorization': 'Bearer ' + accessToken }
});

const data = await response.json();
// {
//   "success": true,
//   "state": {
//     "loaded": true,
//     "selections": {
//       "dropdownSelections": { "blk_frame": "black" },
//       "selectMaterialSelections": { "blk_fabric": "linen" },
//       "checkboxSelections": { "blk_accessories": ["cup-holder"] },
//       "toggleSwitchSelections": { "blk_armrests": "with" },
//       "carouselSelections": {}
//     },
//     "visibleBlocks": ["blk_001", "blk_002", "blk_003"],
//     "currentPrice": { "totalPrice": 1019.99, "currency": "USD" },
//     "cameraPosition": [0, 2, 5],
//     "cameraTarget": [0, 0, 0]
//   }
// }

Take Screenshot

POST/projects/:id/viewer/screenshot
const response = await fetch(BASE_URL + '/projects/proj_123/viewer/screenshot', {
  method: 'POST',
  headers: {
    'Authorization': 'Bearer ' + accessToken,
    'Content-Type': 'application/json'
  },
  body: JSON.stringify({
    width: 1920,
    height: 1080,
    format: 'png',
    cameraPosition: [2, 3, 5],
    cameraTarget: [0, 0.5, 0]
  })
});

const data = await response.json();
// { "success": true, "url": "https://...signed-screenshot-url..." }

PostMessage API (iframe communication)

When embedding via iframe, use postMessage for two-way communication.

// Send commands to the embedded viewer
const iframe = document.getElementById('simplio3d-viewer');

// Set a selection
iframe.contentWindow.postMessage({
  type: 'simplio3d:setSelection',
  blockId: 'blk_frame',
  value: 'silver'
}, '*');

// Set camera position
iframe.contentWindow.postMessage({
  type: 'simplio3d:setCamera',
  position: [3, 2, 4],
  target: [0, 0, 0]
}, '*');

// Request a screenshot
iframe.contentWindow.postMessage({
  type: 'simplio3d:screenshot',
  format: 'png'
}, '*');

// Listen for events from the viewer
window.addEventListener('message', (event) => {
  if (event.data.type === 'simplio3d:selectionChanged') {
    console.log('Selection changed:', event.data.selections);
  }
  if (event.data.type === 'simplio3d:priceChanged') {
    console.log('Price:', event.data.totalPrice, event.data.currency);
  }
  if (event.data.type === 'simplio3d:screenshotReady') {
    console.log('Screenshot URL:', event.data.url);
  }
  if (event.data.type === 'simplio3d:formSubmitted') {
    console.log('Form data:', event.data.formData);
  }
});

Animations

Define looping animations applied to 3D objects for presentations. Supports 6 animation types with configurable speed, amplitude, easing, and loop modes.

Animation Types

move — Linear translation along an axis
rotation — Continuous rotation around an axis
float — Gentle floating motion (bobbing)
scale-pulse — Rhythmic scale breathing effect
swing — Pendulum-like swinging motion
orbit — Circular orbit around a center point

List Animation Blocks

GET/projects/:id/animation-blocks
const response = await fetch(BASE_URL + '/projects/proj_123/animation-blocks', {
  headers: { 'Authorization': 'Bearer ' + accessToken }
});

const data = await response.json();
// {
//   "success": true,
//   "blocks": [
//     {
//       "id": "anim_1",
//       "type": "rotation",
//       "name": "Turntable",
//       "enabled": true,
//       "targetObjectId": "all",
//       "targetPartNames": [],
//       "axis": "y",
//       "speed": 0.2,
//       "amplitude": 360,
//       "easing": "linear",
//       "loopMode": "loop",
//       "delay": 0
//     },
//     {
//       "id": "anim_2",
//       "type": "float",
//       "name": "Hover Effect",
//       "enabled": true,
//       "targetPartNames": ["product_body"],
//       "axis": "y",
//       "speed": 0.4,
//       "amplitude": 0.3,
//       "easing": "ease-in-out",
//       "loopMode": "ping-pong",
//       "delay": 0.5
//     }
//   ]
// }

Create Animation Block

POST/projects/:id/animation-blocks
// Create an orbit animation for a satellite object
const response = await fetch(BASE_URL + '/projects/proj_123/animation-blocks', {
  method: 'POST',
  headers: {
    'Authorization': 'Bearer ' + accessToken,
    'Content-Type': 'application/json'
  },
  body: JSON.stringify({
    type: 'orbit',
    name: 'Orbiting Camera Target',
    targetPartNames: ['highlight_ring'],
    axis: 'y',
    speed: 0.15,
    amplitude: 1,
    easing: 'linear',
    loopMode: 'loop',
    orbitRadius: 2.5,
    orbitCenter: [0, 0, 0],
    orbitPlane: 'xz'
  })
});

Update Animation Block

PUT/projects/:id/animation-blocks/:blockId
const response = await fetch(BASE_URL + '/projects/proj_123/animation-blocks/anim_1', {
  method: 'PUT',
  headers: {
    'Authorization': 'Bearer ' + accessToken,
    'Content-Type': 'application/json'
  },
  body: JSON.stringify({
    speed: 0.1,
    enabled: false
  })
});

Delete Animation Block

DELETE/projects/:id/animation-blocks/:blockId
const response = await fetch(BASE_URL + '/projects/proj_123/animation-blocks/anim_2', {
  method: 'DELETE',
  headers: { 'Authorization': 'Bearer ' + accessToken }
});

Project Settings

Configure every aspect of a project: branding, lighting, camera, loading screens, layout, watermarks, email notifications, e-commerce integration, webhooks, and analytics.

Get Project Settings

GET/projects/:id/settings
const response = await fetch(BASE_URL + '/projects/proj_123/settings', {
  headers: { 'Authorization': 'Bearer ' + accessToken }
});

const data = await response.json();
// {
//   "success": true,
//   "settings": {
//     "enableAutoRotate": false,
//     "autoRotateSpeed": 1,
//     "cameraFOV": 50,
//     "enableZoom": true,
//     "enablePan": true,
//     "environmentPreset": "studio",
//     "lightIntensity": 1.0,
//     "enableShadows": true,
//     "viewportBackgroundColor": "#f5f5f5",
//     "sidebarBackgroundColor": "#ffffff",
//     "primaryAccentColor": "#6b7280",
//     "actionButtonBgColor": "#6b7280",
//     "actionButtonHoverBgColor": "#4b5563",
//     "actionButtonTextColor": "#ffffff",
//     "fontFamily": "system",
//     "sidebarWidth": 360,
//     "shareViewLayout": "default",
//     "enableWatermark": false,
//     "showPrice": true,
//     "enableForm": true,
//     "enableAR": false,
//     "emailEnabled": false,
//     "ecommerceEnabled": false,
//     "webhookUrl": "",
//     "googleAnalyticsId": ""
//   }
// }

Update Project Settings

PUT/projects/:id/settings
const response = await fetch(BASE_URL + '/projects/proj_123/settings', {
  method: 'PUT',
  headers: {
    'Authorization': 'Bearer ' + accessToken,
    'Content-Type': 'application/json'
  },
  body: JSON.stringify({
    // Branding
    viewportBackgroundColor: '#1a1a2e',
    sidebarBackgroundColor: '#16213e',
    primaryAccentColor: '#e94560',
    actionButtonBgColor: '#e94560',
    actionButtonHoverBgColor: '#c73450',
    actionButtonTextColor: '#ffffff',
    fontFamily: 'poppins',
    sidebarWidth: 400,
    compactMode: true,

    // Lighting
    environmentPreset: 'sunset',
    lightIntensity: 1.2,
    enableShadows: true,

    // Camera
    cameraFOV: 45,
    enableAutoRotate: true,
    autoRotateSpeed: 0.5,

    // Loading screen
    customLoadingAnimation: 'pulse',
    loadingMessage: 'Preparing your experience...',
    backgroundColor: '#1a1a2e',

    // Features
    enableAR: true,
    showPrice: true,
    enableForm: true,

    // Webhooks
    webhookUrl: 'https://your-server.com/webhook',
    webhookEvents: ['configuration_changed', 'quote_requested'],

    // Analytics
    googleAnalyticsId: 'G-XXXXXXXXXX'
  })
});

Configure Email Notifications

PUT/projects/:id/settings
const response = await fetch(BASE_URL + '/projects/proj_123/settings', {
  method: 'PUT',
  headers: {
    'Authorization': 'Bearer ' + accessToken,
    'Content-Type': 'application/json'
  },
  body: JSON.stringify({
    emailEnabled: true,
    smtpHost: 'smtp.example.com',
    smtpPort: 587,
    smtpEncryption: 'tls',
    smtpUsername: '[email protected]',
    emailFromAddress: '[email protected]',
    emailFromName: 'Simplio3D Configurator'
  })
});

Assets

Manage 3D models, textures, and graphics. Upload, organize, and retrieve assets with signed download URLs.

List Assets

GET/assets
// Get all assets (optionally filter by type)
const response = await fetch(BASE_URL + '/assets?type=3d', {
  headers: { 'Authorization': 'Bearer ' + accessToken }
});

Upload Asset

POST/assets
const formData = new FormData();
formData.append('file', fileInput.files[0]);
formData.append('name', 'Chair Model');
formData.append('type', '3d');
formData.append('category', 'furniture');

const response = await fetch(BASE_URL + '/assets', {
  method: 'POST',
  headers: { 'Authorization': 'Bearer ' + accessToken },
  body: formData
});

Get Asset

GET/assets/:assetId
const response = await fetch(BASE_URL + '/assets/asset_123', {
  headers: { 'Authorization': 'Bearer ' + accessToken }
});

Update Asset

PUT/assets/:assetId
const response = await fetch(BASE_URL + '/assets/asset_123', {
  method: 'PUT',
  headers: {
    'Authorization': 'Bearer ' + accessToken,
    'Content-Type': 'application/json'
  },
  body: JSON.stringify({ name: 'Renamed Asset', category: 'new-category' })
});

Delete Asset

DELETE/assets/:assetId
const response = await fetch(BASE_URL + '/assets/asset_123', {
  method: 'DELETE',
  headers: { 'Authorization': 'Bearer ' + accessToken }
});

Download Asset (Signed URL)

GET/assets/:assetId/download
const response = await fetch(BASE_URL + '/assets/asset_123/download', {
  headers: { 'Authorization': 'Bearer ' + accessToken }
});

const data = await response.json();
// { "success": true, "url": "https://...signed-url..." }

Materials

Manage PBR materials with texture maps. Create realistic surface properties for your 3D models.

List Materials

GET/materials
const response = await fetch(BASE_URL + '/materials', {
  headers: { 'Authorization': 'Bearer ' + accessToken }
});

const data = await response.json();
// { "success": true, "materials": [{ "id": "mat_123", "name": "Brushed Metal",
//   "pbr": { "baseColor": "#808080", "metallic": 0.9, "roughness": 0.3 } }] }

Create Material

POST/materials
const response = await fetch(BASE_URL + '/materials', {
  method: 'POST',
  headers: {
    'Authorization': 'Bearer ' + accessToken,
    'Content-Type': 'application/json'
  },
  body: JSON.stringify({
    name: 'Oak Wood',
    category: 'wood',
    pbr: { baseColor: '#8B6F47', metallic: 0.0, roughness: 0.8 }
  })
});

Update Material

PUT/materials/:id
const response = await fetch(BASE_URL + '/materials/mat_123', {
  method: 'PUT',
  headers: {
    'Authorization': 'Bearer ' + accessToken,
    'Content-Type': 'application/json'
  },
  body: JSON.stringify({ pbr: { roughness: 0.5, metallic: 0.8 } })
});

Delete Material

DELETE/materials/:id
const response = await fetch(BASE_URL + '/materials/mat_123', {
  method: 'DELETE',
  headers: { 'Authorization': 'Bearer ' + accessToken }
});

Categories

Organize assets and materials into categories for better management and filtering.

List Categories

GET/categories
const response = await fetch(BASE_URL + '/categories/3d', {
  headers: { 'Authorization': 'Bearer ' + accessToken }
});

Create Category

POST/categories
const response = await fetch(BASE_URL + '/categories', {
  method: 'POST',
  headers: {
    'Authorization': 'Bearer ' + accessToken,
    'Content-Type': 'application/json'
  },
  body: JSON.stringify({ name: 'Furniture', assetType: '3d', color: '#2E8BCB' })
});

Update Category

PUT/categories/:id
const response = await fetch(BASE_URL + '/categories/cat_123', {
  method: 'PUT',
  headers: {
    'Authorization': 'Bearer ' + accessToken,
    'Content-Type': 'application/json'
  },
  body: JSON.stringify({ name: 'Updated Name', color: '#FF6600' })
});

Delete Category

DELETE/categories/:id
const response = await fetch(BASE_URL + '/categories/cat_123', {
  method: 'DELETE',
  headers: { 'Authorization': 'Bearer ' + accessToken }
});

Team Management

Invite and manage team members with role-based access. Supported roles: admin, editor, viewer.

List Team Members

GET/team/members
const response = await fetch(BASE_URL + '/team/members', {
  headers: { 'Authorization': 'Bearer ' + accessToken }
});

const data = await response.json();
// { "success": true, "members": [{ "id": "mem_123", "email": "[email protected]",
//   "name": "Jane Smith", "role": "editor", "status": "active" }] }

Invite Team Member

POST/team/members
const response = await fetch(BASE_URL + '/team/members', {
  method: 'POST',
  headers: {
    'Authorization': 'Bearer ' + accessToken,
    'Content-Type': 'application/json'
  },
  body: JSON.stringify({
    email: '[email protected]',
    role: 'editor',
    name: 'John Doe'
  })
});

Update Team Member

PUT/team/members/:memberId
const response = await fetch(BASE_URL + '/team/members/mem_123', {
  method: 'PUT',
  headers: {
    'Authorization': 'Bearer ' + accessToken,
    'Content-Type': 'application/json'
  },
  body: JSON.stringify({ role: 'admin', status: 'active' })
});

Remove Team Member

DELETE/team/members/:memberId
const response = await fetch(BASE_URL + '/team/members/mem_123', {
  method: 'DELETE',
  headers: { 'Authorization': 'Bearer ' + accessToken }
});

Project Sharing

Generate public share links for projects. Supports password protection, expiry, and download permissions.

Get Share Configuration

GET/projects/:id/share
const response = await fetch(BASE_URL + '/projects/proj_123/share', {
  headers: { 'Authorization': 'Bearer ' + accessToken }
});

const data = await response.json();
// { "success": true, "shareConfig": { "enabled": true, "shareUrl": "https://...",
//   "viewCount": 42, "passwordProtected": false } }

Enable Sharing

POST/projects/:id/share/enable
const response = await fetch(BASE_URL + '/projects/proj_123/share/enable', {
  method: 'POST',
  headers: { 'Authorization': 'Bearer ' + accessToken }
});

Update Share Settings

PUT/projects/:id/share
const response = await fetch(BASE_URL + '/projects/proj_123/share', {
  method: 'PUT',
  headers: {
    'Authorization': 'Bearer ' + accessToken,
    'Content-Type': 'application/json'
  },
  body: JSON.stringify({
    allowDownload: true,
    passwordProtected: true,
    password: 'secret123',
    expiresAt: '2026-06-01T00:00:00Z'
  })
});

Regenerate Share Token

POST/projects/:id/share/regenerate
const response = await fetch(BASE_URL + '/projects/proj_123/share/regenerate', {
  method: 'POST',
  headers: { 'Authorization': 'Bearer ' + accessToken }
});

Access Shared Project (Public)

GET/share/:projectId/:token
const response = await fetch(BASE_URL + '/share/proj_123/abc123', {
  headers: {
    // Required when calling Supabase Edge Functions directly from browser clients
    'apikey': SUPABASE_ANON_KEY,
    'Authorization': 'Bearer ' + SUPABASE_ANON_KEY
  }
});
const data = await response.json();
// Handles large-project variants internally in the share viewer

Submit Shared Form (Async Processing)

POST/share/:projectId/:token/submit
const response = await fetch(BASE_URL + '/share/proj_123/abc123/submit', {
  method: 'POST',
  headers: {
    'Content-Type': 'application/json',
    'apikey': SUPABASE_ANON_KEY,
    'Authorization': 'Bearer ' + SUPABASE_ANON_KEY
  },
  body: JSON.stringify({
    formData: { name: 'Jane Smith', email: '[email protected]' },
    selectedOptions: {},
    configurationSummary: '...',
    screenshots: []
  })
});

const data = await response.json();
// Immediate acknowledgment:
// { "success": true, "requestId": "..." }
// Screenshots/PDF/emails/webhooks are processed asynchronously after response.

Async Submit Semantics

Shared-form submit endpoints persist the request first and respond immediately. Heavy side effects (screenshot upload, PDF generation, SMTP notifications, webhook dispatch) run in background tasks, so they are eventually consistent and may complete after the initial 200 response.

Email & Logs

Send test emails, view delivery logs, and resend failed emails. Emails use your own SMTP configuration.

Send Test Email

POST/email/test
const response = await fetch(BASE_URL + '/email/test', {
  method: 'POST',
  headers: {
    'Authorization': 'Bearer ' + accessToken,
    'Content-Type': 'application/json'
  },
  body: JSON.stringify({
    emailSettings: {
      smtpHost: 'smtp.example.com',
      smtpPort: 587,
      smtpEncryption: 'tls',
      smtpUsername: '[email protected]',
      smtpPassword: 'password',
      emailFromAddress: '[email protected]'
    },
    testRecipient: '[email protected]'
  })
});

Get Email Logs

GET/email/logs
const response = await fetch(BASE_URL + '/email/logs', {
  headers: { 'Authorization': 'Bearer ' + accessToken }
});
// Returns up to 200 logs. Auto-purged after 30 days.

Delete Email Log

DELETE/email/logs/:logId
const response = await fetch(BASE_URL + '/email/logs/log_123', {
  method: 'DELETE',
  headers: { 'Authorization': 'Bearer ' + accessToken }
});

Resend Failed Email

POST/email/logs/:logId/resend
// Max 3 resend attempts per log entry
const response = await fetch(BASE_URL + '/email/logs/log_123/resend', {
  method: 'POST',
  headers: { 'Authorization': 'Bearer ' + accessToken }
});

Bulk Delete Logs

POST/email/logs/bulk-delete
const response = await fetch(BASE_URL + '/email/logs/bulk-delete', {
  method: 'POST',
  headers: {
    'Authorization': 'Bearer ' + accessToken,
    'Content-Type': 'application/json'
  },
  body: JSON.stringify({ logIds: ['log_1', 'log_2', 'log_3'] })
});

API Tokens

Generate and manage API tokens for programmatic access. Tokens can be used as an alternative to JWT session tokens.

Generate API Token

POST/api-token
const response = await fetch(BASE_URL + '/api-token', {
  method: 'POST',
  headers: { 'Authorization': 'Bearer ' + accessToken }
});

const data = await response.json();
// { "success": true, "token": "550e8400-e29b-41d4-a716-446655440000" }

Get Current Token

GET/api-token
const response = await fetch(BASE_URL + '/api-token', {
  headers: { 'Authorization': 'Bearer ' + accessToken }
});

Regenerate Token

POST/api-token/regenerate
const response = await fetch(BASE_URL + '/api-token/regenerate', {
  method: 'POST',
  headers: { 'Authorization': 'Bearer ' + accessToken }
});

Token Security

API tokens provide full access to your account. Keep them secure, never commit them to version control, and regenerate immediately if compromised.

Quote Submissions

Submit and manage quote requests. Each submission captures the customer's form data, configuration selections, calculated pricing, and optional screenshot.

Submit a Quote (Public)

POST/projects/:id/quote-submissions

This endpoint can be used without authentication for public-facing configurators.

const response = await fetch(BASE_URL + '/projects/proj_123/quote-submissions', {
  method: 'POST',
  headers: { 'Content-Type': 'application/json' },
  body: JSON.stringify({
    formData: {
      name: 'Jane Smith',
      email: '[email protected]',
      phone: '+1 555 0123',
      message: 'Interested in bulk pricing for 50 units.'
    },
    configuration: {
      selections: {
        dropdownSelections: { 'blk_frame': 'black', 'blk_wood': 'walnut' },
        selectMaterialSelections: { 'blk_fabric': 'linen' },
        checkboxSelections: { 'blk_accessories': ['cup-holder', 'monitor-stand'] },
        toggleSwitchSelections: { 'blk_armrests': 'with' },
        carouselSelections: {}
      },
      variables: { width: 160, quantity: 50 },
      screenshotUrl: 'https://cdn.example.com/screenshots/config_abc.png'
    }
  })
});

const data = await response.json();
// {
//   "success": true,
//   "submission": {
//     "id": "qs_123",
//     "projectId": "proj_123",
//     "status": "new",
//     "customerName": "Jane Smith",
//     "customerEmail": "[email protected]",
//     "submittedAt": "2026-03-10T15:30:00Z"
//   }
// }

List Quote Submissions

GET/projects/:id/quote-submissions
const response = await fetch(BASE_URL + '/projects/proj_123/quote-submissions', {
  headers: { 'Authorization': 'Bearer ' + accessToken }
});

const data = await response.json();
// {
//   "success": true,
//   "submissions": [
//     {
//       "id": "qs_123",
//       "status": "new",
//       "customerName": "Jane Smith",
//       "customerEmail": "[email protected]",
//       "formData": { ... },
//       "configuration": { "selections": { ... }, "pricing": { "totalPrice": 1019.99 } },
//       "submittedAt": "2026-03-10T15:30:00Z"
//     }
//   ]
// }

Update Submission Status

PUT/projects/:id/quote-submissions/:submissionId
const response = await fetch(BASE_URL + '/projects/proj_123/quote-submissions/qs_123', {
  method: 'PUT',
  headers: {
    'Authorization': 'Bearer ' + accessToken,
    'Content-Type': 'application/json'
  },
  body: JSON.stringify({
    status: 'replied'  // 'new' | 'viewed' | 'replied' | 'archived'
  })
});

Delete Submission

DELETE/projects/:id/quote-submissions/:submissionId
const response = await fetch(BASE_URL + '/projects/proj_123/quote-submissions/qs_123', {
  method: 'DELETE',
  headers: { 'Authorization': 'Bearer ' + accessToken }
});

Webhooks

Receive real-time HTTP callbacks when events occur in your Simplio3D projects. Configure multiple webhooks per project with event filtering and HMAC-SHA256 signature verification.

Supported Event Types

quote.submittedquote.updatedquote.deletedoption.changedprice.calculatedform.submittedproject.updatedproject.publishedviewer.screenshotanimation.triggered

List Webhooks

GET/projects/:projectId/webhooks

Retrieve all webhook configurations for a project.

curl -X GET \
  https://api.simplio3d.com/v1/projects/proj_123/webhooks \
  -H "Authorization: Bearer YOUR_TOKEN"

Create / Update Webhook

PUT/projects/:projectId/webhooks/:webhookId

Create a new webhook or update an existing one. The webhook ID is idempotent — use the same ID to update.

curl -X PUT \
  https://api.simplio3d.com/v1/projects/proj_123/webhooks/wh_abc \
  -H "Authorization: Bearer YOUR_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{
    "name": "Order Notifications",
    "url": "https://your-server.com/webhook",
    "events": ["quote.submitted", "form.submitted"],
    "secret": "whsec_your_signing_secret",
    "enabled": true
  }'

Request Body

url (required) — HTTPS endpoint to receive events

events (required) — Array of event types to subscribe to

secret — Signing secret for HMAC-SHA256 verification

name — Display name for the webhook

enabled — Whether the webhook is active (default: true)

Delete Webhook

DELETE/projects/:projectId/webhooks/:webhookId
curl -X DELETE \
  https://api.simplio3d.com/v1/projects/proj_123/webhooks/wh_abc \
  -H "Authorization: Bearer YOUR_TOKEN"

Test Webhook

POST/projects/:projectId/webhooks/:webhookId/test

Send a test webhook.test event to verify connectivity. The response includes the HTTP status from your server.

curl -X POST \
  https://api.simplio3d.com/v1/projects/proj_123/webhooks/wh_abc/test \
  -H "Authorization: Bearer YOUR_TOKEN"

# Response:
# { "success": true, "test": { "statusCode": 200, "statusText": "OK", "delivered": true } }

Delivery Logs

GET/projects/:projectId/webhooks/:webhookId/deliveries

Returns up to the last 50 delivery attempts for a specific webhook.

curl -X GET \
  https://api.simplio3d.com/v1/projects/proj_123/webhooks/wh_abc/deliveries \
  -H "Authorization: Bearer YOUR_TOKEN"

# Response:
# { "success": true, "deliveries": [
#   { "webhookId": "wh_abc", "event": "quote.submitted",
#     "statusCode": 200, "delivered": true, "timestamp": "..." },
#   ...
# ] }

Trigger Webhooks

POST/projects/:projectId/webhooks/trigger

Manually fire an event to all matching webhooks. Useful for testing integrations or replaying events.

curl -X POST \
  https://api.simplio3d.com/v1/projects/proj_123/webhooks/trigger \
  -H "Authorization: Bearer YOUR_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{ "event": "quote.submitted", "data": { "quoteId": "q_456" } }'

Signature Verification

Each webhook delivery includes these headers for security:

X-Simplio3D-Event — Event type (e.g. quote.submitted)

X-Simplio3D-Delivery — Unique delivery UUID

X-Simplio3D-Signature — HMAC-SHA256 hex digest: sha256=...

// Node.js verification example
const crypto = require('crypto');

function verifySignature(payload, signature, secret) {
  const expected = 'sha256=' + crypto
    .createHmac('sha256', secret)
    .update(JSON.stringify(payload))
    .digest('hex');
  return crypto.timingSafeEqual(
    Buffer.from(signature), Buffer.from(expected)
  );
}

Augmented Reality (AR)

Enable AR experiences for your 3D configurator projects. Desktop users see a QR code, mobile users launch native AR viewers (Scene Viewer on Android, AR Quick Look on iOS).

Upload USDZ File (iOS)

Upload a .usdz file for native iOS AR Quick Look support. Without a USDZ file, iOS devices may fall back to a less reliable GLB-based AR experience.

POST/projects/:id/usdz
curl -X POST \
  ".../projects/PROJECT_ID/usdz" \
  -H "Authorization: Bearer YOUR_TOKEN" \
  -F "[email protected]"

# Response:
{
  "success": true,
  "usdzId": "uuid",
  "usdzPath": "userId/project-usdz/...",
  "fileName": "model.usdz"
}

Tip: Convert GLB to USDZ using Apple's Reality Converter (macOS), Blender's USDZ export, or online tools like glb-to-usdz.com.

Delete USDZ File

DELETE/projects/:id/usdz/:usdzId

Track AR Analytics Event

Record AR interaction events. This endpoint is public (no auth) and used via sendBeacon from the viewer. Fire-and-forget.

POST/ar-analytics
{
  "event": "ar_launch_ios",
  "projectId": "PROJECT_ID",
  "shareToken": "optional",
  "meta": { "source": "qr_code" }
}

// Valid event types:
// ar_dialog_open, ar_qr_copy, ar_qr_open_tab,
// ar_launch_ios, ar_launch_android,
// ar_auto_launch, ar_page_view

Get AR Analytics Summary

Fetch aggregated AR analytics for a project (requires auth).

GET/projects/:id/ar-analytics
// Response:
{
  "projectId": "...",
  "totalEvents": 142,
  "byEvent": {
    "ar_dialog_open": 58,
    "ar_launch_ios": 41,
    "ar_launch_android": 29,
    "ar_qr_copy": 14
  },
  "lastUpdated": "2026-03-17T...",
  "recentEvents": [...]
}

Public USDZ URL (Share Viewer)

Get a signed URL for a shared project's USDZ file. Used by the share viewer to launch iOS AR Quick Look with the USDZ file.

GET/share/:projectId/usdz

AR Project Settings

AR behavior is controlled via Project Settings fields. Set these when saving a project:

{
  "enableAR": true,           // Master toggle
  "arUsdzFileName": "...",    // iOS USDZ file name
  "arUsdzModelPath": "...",   // Storage path (set by upload)
  "arUsdzModelId": "...",     // Storage ID (set by upload)
  "arAutoLaunch": true,       // Auto-launch AR on ?ar=1
  "arAnalyticsEnabled": true  // Track AR events
}

API Changelog

Track all API changes, new endpoints, deprecations, and improvements across versions.

API Changelog

Track all changes, updates, and improvements

v3.4

patchLatest
April 8, 2026

✨ New Features

  • Large Project Transport + Async Submit Semantics: documented response variants for GET /projects/:id (full scene, gzip-base64 compressed scene payload, and split scene+models+camera payloads).
  • Sharing docs now include public form submit endpoint semantics: POST /share/:projectId/:token/submit returns immediate acknowledgment while screenshot/PDF/email/webhook side effects complete asynchronously.

⚡ Improvements

  • Project Sharing examples now show browser-safe Supabase Edge Function headers (apikey + bearer anon token) for direct public viewer/submit calls.

v3.3

minor
April 2, 2026

v3.2

minor
March 11, 2026

v3.1.0

minor
February 27, 2026

v3.0.0

major
January 15, 2026