Skip to content

Headless API

Build your own fully custom interface for the Wedding Band Builder. Set hideWbbUi: true in Mini Viewer's options to hide the built-in panel and drive everything through the API.

Wedding Band Builder with a custom-designed interface

Getting Started

Script Tag (Direct)

javascript
let api;
let wbbPlugin;

new ijewelViewer.Viewer(document.getElementById('viewer'), {
  name: 'Wedding Band Builder',
  version: 'v5',
  basePath: 'https://your-cdn.com/wbb-assets/',
  plugins: {
    WeddingBandBuilder: {
      manifestUrl: 'wedding-band-project.json',
    },
  },
}, {
  showCard: false, showSwitchNode: false, showUiButtons: false,
  showConfigurator: false, showZoomButtons: false, enableZoom: true,
  hideWbbUi: true,
});

window.addEventListener('ijewel-viewer-ready', (e) => {
  wbbPlugin = e.detail.viewer.getPluginByType('WeddingBandBuilder');
  api = wbbPlugin.controller;
  buildUI(api);
});

iframe

Load the iframe with ?ui=false and use postMessage to control everything:

html
<iframe src="wbb-viewer.html?ui=false" style="width: 100%; height: 500px; border: none;"></iframe>

The same API methods are available via postMessage. See iframe Integration for the protocol.

Building Controls

Each control follows the same pattern: read options from the catalog, create your UI, call an API setter on interaction.

Profile Selector

javascript
function buildProfileSelector(api) {
  const profiles = api.getAvailableProfiles();
  const container = document.getElementById('profiles');

  profiles.forEach((profile) => {
    const btn = document.createElement('button');
    btn.textContent = profile.name;
    if (profile.thumbnail) {
      btn.style.backgroundImage = `url(${profile.thumbnail})`;
    }
    btn.addEventListener('click', () => api.setProfile(profile.index));
    container.appendChild(btn);
  });
}
Profile buttons showing ring cross section silhouettes

Material Swatches

javascript
function buildMaterialSwatches(api) {
  const materials = api.getAvailableMaterials('band');
  const container = document.getElementById('metals');

  materials.forEach((material) => {
    const swatch = document.createElement('button');
    swatch.className = 'swatch';
    swatch.title = material.name;
    if (material.iconUrl) {
      swatch.style.backgroundImage = `url(${material.iconUrl})`;
    } else if (material.swatch) {
      swatch.style.backgroundColor = material.swatch;
    }
    swatch.addEventListener('click', async () => {
      const variants = api.getAvailableVariants(material.id);
      const variant = variants.find(v => v.id === material.defaultVariant)
        || variants[0];
      const finishes = api.getAvailableFinishesFor(material.id)
        .filter(f => !variant?.files || variant.files[f.id]);
      const finish = finishes.find(f => f.id === material.defaultFinish)
        || finishes[0];

      await api.setMaterialRef(1, {
        base: material.id,
        variant: variant?.id,
        finish: finish?.id,
      });
    });
    container.appendChild(swatch);
  });
}
Circular metal swatches with the selected one highlighted

Outside and Inside Features

Inlays, overlays and the sleeve need a circular band on a plain vertical division. Gate the whole control group on areOutsideFeaturesAvailable(), and re-check it after any division change.

javascript
function buildOutsideControls(api) {
  const panel = document.getElementById('outside');
  panel.hidden = !api.areOutsideFeaturesAvailable();

  // Wood and marble are inlay-only in most catalogs, so ask for the role.
  const materials = api.getAvailableMaterials('inlay');

  // One centred stripe. Index 0 adds the first inlay and edits it afterwards.
  document.getElementById('inlay-width').addEventListener('input', async (e) => {
    const bounds = api.getInlayBounds(0);
    const widthMm = Math.min(Number(e.target.value), bounds.widthMax);
    await api.setInlay(0, { centerZ: 0, widthMm, metal: materials[0].id });
  });

  // A rim overlay. rimCoverage says how far it wraps the flat side face:
  // 1 covers it, 0 leaves it as base metal, 0.5 splits it.
  document.getElementById('overlay-left').addEventListener('change', async (e) => {
    if (!e.target.checked) return api.removeOverlay('left');
    await api.setOverlay('left', {
      widthMm: Math.min(1, api.getOverlayWidthMax('left')),
      metal: materials[0].id,
      rimCoverage: 1,
    });
  });

  // The bore in a second material.
  document.getElementById('sleeve').addEventListener('change', async (e) => {
    await api.setSleeve({ enabled: e.target.checked, metal: 'yellow' });
  });
}

Every setter clamps its own values, so a slider cannot push two features into each other. Read getInlayBounds(index) and getOverlayWidthMax(side) to bound the controls before the user drags them, and call outsideFeaturesFit() when you apply state you did not build yourself.

Change several features at once with one rebuild. Applying the setters in sequence rebuilds the band each time, so the ring flickers through the intermediate states.

javascript
await api.setOutsideFeatures({
  inlays: [{ centerZ: 0, widthMm: 1.5, metal: 'wood' }],
  overlays: [{ side: 'right', widthMm: 1, metal: 'white', rimCoverage: 0.5 }],
  sleeve: { enabled: true, metal: 'yellow', full: true },
});

Pass sleeve: null to clear the sleeve. The inlays:changed, overlays:changed and sleeve:changed events report every result, including a clear.

A wood centre inlay and a marble left rim overlay applied to the band

Dimension Sliders

javascript
function buildDimensionSliders(api) {
  const widthSlider = document.getElementById('width-slider');
  const widthLabel = document.getElementById('width-value');

  const widthLimits = api.getLimits().width;
  widthSlider.min = widthLimits.min;
  widthSlider.max = widthLimits.max;
  widthSlider.step = widthLimits.step || 0.05;

  const dims = api.getDimensions();
  widthSlider.value = dims.widthMm;
  widthLabel.textContent = `×${dims.widthMm.toFixed(2)}`;

  widthSlider.addEventListener('input', (e) => {
    const multiplier = parseFloat(e.target.value);
    api.setWidthMultiplier(multiplier);
    widthLabel.textContent = `×${multiplier.toFixed(2)}`;
  });
}

Diamond Controls

javascript
function buildDiamondControls(api) {
  const types = api.getAvailableSettingTypes();
  const container = document.getElementById('diamonds');

  // "No diamonds" button
  const noneBtn = document.createElement('button');
  noneBtn.textContent = 'No Diamonds';
  noneBtn.addEventListener('click', () => api.setDiamonds(null));
  container.appendChild(noneBtn);

  // Setting type buttons
  types.filter(type => type.id !== 'none').forEach((type) => {
    const btn = document.createElement('button');
    btn.textContent = type.name;
    btn.addEventListener('click', () => api.setDiamonds({ settingType: type.id }));
    container.appendChild(btn);
  });
}

Ring Size

javascript
function buildRingSizeControl(api) {
  const slider = document.getElementById('ring-size');
  const label = document.getElementById('ring-size-value');

  // Ring size: inner diameter in mm, snapping to standard half-sizes
  slider.min = 14.04;  // US 3
  slider.max = 22.32;  // US 13
  slider.step = 0.4;   // ~half-size increments

  slider.addEventListener('input', (e) => {
    const diamMm = parseFloat(e.target.value);
    api.setRingSize(diamMm / 2);  // API takes radius
    label.textContent = `${diamMm.toFixed(1)} mm`;
  });
}

Live Price Display

javascript
function buildPriceDisplay(api) {
  const el = document.getElementById('price');

  api.events.on('price:updated', (data) => {
    const p = data.pricing;
    let text = `$${p.totalUsd.toFixed(2)}`;
    if (p.diamonds) {
      text += ` (${p.diamonds.count} diamonds, ${p.diamonds.totalCarats.toFixed(2)}ct)`;
    }
    el.textContent = text;
  });

  // Show initial price
  const price = api.getPrice();
  if (price) el.textContent = `$${price.totalUsd.toFixed(2)}`;
}
A live price display updating from the price:updated event, with an Add to Cart button

Staying in Sync

When state changes externally (e.g., user switches bands), update your UI to match:

javascript
api.events.on('band:switched', (data) => {
  const snapshot = api.getSnapshot(data.to);
  updateProfileHighlight(snapshot.profile.index);
  updateMetalHighlight(snapshot.materials.slots[0].metal);
  updateWidthSlider(snapshot.dimensions.widthMm);
});

api.events.on('build:started', () => showSpinner());
api.events.on('build:complete', () => hideSpinner());

TIP

When the user switches bands (her / his), all your controls should update to reflect the new band's state. Call getSnapshot() to get the full configuration.

Batch Updates and Presets

Apply multiple changes in one geometry rebuild instead of triggering separate rebuilds:

javascript
// Apply a "Classic Gold" preset
await api.batch({
  profile: { name: 'D-Shape' },
  dimensions: { widthMm: 1.0, heightMm: 1.0 },
  materials: {
    partition: 1,
    slots: [{ slot: 1, metal: 'Yellow', finish: 'Polished' }],
  },
  diamonds: null,
  edge: { type: 'None' },
});

Styled preset cards: Classic Gold, Diamond Elegance, Two-Tone Modern

Save and Restore

javascript
// Save complete state, including variants and outside features
const config = api.toJSON();
localStorage.setItem('wbb-config', JSON.stringify(config));

// Restore saved configuration through the plugin
const saved = localStorage.getItem('wbb-config');
if (saved) await wbbPlugin.fromJSON(JSON.parse(saved));

// Send to backend for order processing
const herConfig = api.getSnapshot('her');
const herPrice = api.getPrice('her');
await fetch('/api/cart/add', {
  method: 'POST',
  headers: { 'Content-Type': 'application/json' },
  body: JSON.stringify({ state: config, summary: herConfig, price: herPrice }),
});

Design Tips

Keep it simple. Start with profile, metal, and width. You don't need to expose every API option.

Use thumbnails. Catalog methods return thumbnail URLs when available. Use them for visual selectors instead of text labels.

Show loading state. Profile changes require geometry computation. Listen for build:started / build:complete to show a spinner.

Debounce sliders. For continuous sliders, debounce to avoid excessive rebuilds:

javascript
let timer;
slider.addEventListener('input', (e) => {
  clearTimeout(timer);
  timer = setTimeout(() => api.setWidthMultiplier(parseFloat(e.target.value)), 50);
});

Test on mobile. Make sure your custom controls work on touch devices. The 3D viewer handles touch/pinch natively.

Next Steps

  • API Reference for complete method and event documentation
  • Pricing to configure the pricing engine
  • Measurements for ring size, diamond carat, and dimension details