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.
Getting Started
Script Tag (Direct)
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) => {
const api = e.detail.viewer.getPluginByType('WeddingBandBuilder').controller;
buildUI(api);
});iframe
Load the iframe with ?ui=false and use postMessage to control everything:
<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
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);
});
}Material Swatches
function buildMetalSwatches(api) {
const metals = api.getAvailableMetals();
const finishes = api.getAvailableFinishes();
const container = document.getElementById('metals');
metals.forEach((metal) => {
const swatch = document.createElement('button');
swatch.className = 'swatch';
swatch.title = metal.name;
if (metal.thumbnail) {
swatch.style.backgroundImage = `url(${metal.thumbnail})`;
}
swatch.addEventListener('click', () => {
api.setMaterial(1, metal.id, finishes[0]?.id || 'Polished');
});
container.appendChild(swatch);
});
}Dimension Sliders
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
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
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
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)}`;
}Staying in Sync
When state changes externally (e.g., user switches bands), update your UI to match:
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:
// 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' },
});Save and Restore
// Save current configuration
const config = api.exportConfig();
localStorage.setItem('wbb-config', JSON.stringify(config));
// Restore saved configuration
const saved = localStorage.getItem('wbb-config');
if (saved) await api.importConfig(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({ config: 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:
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