Jewelry Configurator Live demo →

Embed API

Embed the 3D configurator on your storefront and drive it from your own UI.

There are two ways to use the configurator:

  1. Shopify theme block (no code) — add the Jewelry Configurator block to your product template and set the options in the theme editor. Section 1 below.
  2. Direct iframe (developers) — embed the iframe yourself and drive it from your own buttons via postMessage. Sections 2 onwards.

1. Shopify — each product's own model

Add the Jewelry Configurator block to your product template once. It then appears on every product page and works out which model belongs to the product the shopper is looking at, in this order — first hit wins:

  1. The jewelshop.model metafield — a product metafield holding the URL of a .glb. Always wins, and it is what we fill in for you when we produce your models.
  2. The product's own 3D model — a .glb you uploaded to the product in the Shopify admin (Products → your product → Add media). Nothing to configure at all.
  3. The sample ring — a product with neither keeps showing the demo ring chosen in the block settings, so a half-finished catalog never renders an empty box.

To add the metafield: Settings → Custom data → Products → Add definition, namespace and key jewelshop.model, type URL (single-line text also works). Then fill it in per product.

A model resolved this way is shown exactly as you authored it — your materials, textures and colours are left alone. Letting shoppers change the stone or the metal additionally needs a role manifest next to the model file (see Showing your own model), so those pieces have to be hosted alongside their manifest rather than uploaded as Shopify product media.

The block renders the viewer chromeless — 3D only, no buttons of ours. Your product page already has the shopper's option pickers between the title and the price; a second set of controls inside the frame would drive nothing purchasable. To make the viewer react to those pickers, drive it over postMessage — section 3.

2. Embedding the iframe

<iframe
  src="https://jewelshop.ai/demo/?embed=1"
  width="100%"
  height="550"
  style="border:none;"
  allowfullscreen></iframe>

URL parameters

ParamValuesDefaultMeaning
embed1Required. Runs the chromeless embed (no landing page, no intro).
controls0 / 101 shows the built-in gem/metal/ring UI. Omit / 0 = clean viewer you drive yourself.
gemdiamond, ruby, emerald, blue_topaz, amberdiamondInitial gemstone.
metalsilver, gold, rose_goldsilverInitial metal.
ring0, 1, 20Initial ring style (demo set only — ignored when model is given).
modelURL of a .glbShow your own model instead of our demo rings. See below.
bgRRGGBB (hex, no #)Loading-screen background colour.
shopyour-store.myshopify.comLicense identity. The Shopify theme block sets this automatically; direct integrators need a license (contact us).

By default the embed is chromeless — just the rotating 3D viewer. Add &controls=1 only if you want our built-in switcher instead of your own UI.

Showing your own model

https://jewelshop.ai/demo/?embed=1&model=https://cdn.example.com/rings/solitaire.glb

The URL must be https, and the host must send CORS headers allowing jewelshop.ai to fetch it — otherwise the load fails and you receive a model-error event.

Your model is rendered with the materials you authored. We only take over a material when a role manifest tells us to: a small JSON file served next to the model, at the model's URL with .roles.json appended (solitaire.glbsolitaire.glb.roles.json):

{ "gem_nodes": ["Diamond_Round"], "metal_nodes": "*" }

No manifest means no recolouring — the piece is shown exactly as exported, which is what you want for a fixed showcase. Add one when the shopper should be able to change the stone or the metal.

3. Driving it from your own UI (postMessage)

Every message — both directions — carries source: "jewelshop". Ignore any message without it.

Handshake — wait for ready

The viewer posts ready as soon as it is listening, and again once the 3D renderer has finished loading. Wait for it before sending commands.

const frame = document.querySelector('iframe').contentWindow;

window.addEventListener('message', (e) => {
  if (e.data?.source !== 'jewelshop') return;
  if (e.data.kind === 'ready' && e.data.rendererLoaded) {
    // Safe to send commands now.
  }
});

Commands you send (host → viewer)

kindPayloadEffect
apply-preset{ preset: { gem, metal, ring } }Change gem/metal/ring (any subset). Replies preset-applied.
set-camera{ position:[x,y,z], target:[x,y,z], duration?, ease? }Move the camera to a fixed angle. Replies camera-set.
set-model{ url: 'https://…/piece.glb', roles? }Swap the displayed model — e.g. when the shopper picks a different product. Replies model-set immediately, then model-loaded or model-error.
set-controls{ visible: true | false }Show/hide the built-in UI at runtime. Replies controls-set.
pauseStop rendering + input (park a hidden viewer). Replies paused.
resumeResume rendering + input. Replies resumed.
embed-init{ preset? }Only needed if you embed WITHOUT ?embed=1; boots embed mode.
frame.postMessage(
  { source: 'jewelshop', kind: 'apply-preset', preset: { gem: 'ruby', metal: 'gold' } },
  '*'
);

Passing the role manifest inline

set-model takes an optional roles object — the same manifest that would otherwise be served as <model>.glb.roles.json:

frame.postMessage({
  source: 'jewelshop',
  kind: 'set-model',
  url: 'https://cdn.example.com/rings/solitaire.glb',
  roles: { gem_nodes: ['Diamond_Round'], metal_nodes: ['Band'] }
}, '*');

Use this when the model sits somewhere you cannot add files next to it — an S3 bucket, a CDN, Shopify media — while the node roles live in your own database. Nothing is fetched; the manifest goes straight to the renderer.

Anything else (a string, an array) is refused with model-error {reason:'invalid-roles'} rather than being coerced into something that might repaint the piece. And because loads are deduplicated by URL, the roles that arrive with the first load of a given URL are the ones that apply.

Events you receive (viewer → host)

kindWhen
readyViewer is listening (rendererLoaded:false), then again when the 3D scene is loaded (rendererLoaded:true).
preset-appliedAfter apply-preset. Carries the resulting { gem, metal, ring }.
camera-setAfter set-camera.
model-setThe set-model URL was accepted. Carries { url }. Loading has only just started.
model-loadedThe model finished loading and is on screen. Carries { url }.
model-errorThe model could not be shown — bad URL, unusable roles, 404, or the host refused the cross-origin fetch. Carries { url, reason }.
controls-setAfter set-controls. Carries { visible }.
paused / resumedAfter pause / resume.
close-requestThe user pressed Escape inside the viewer.

4. Fullscreen

The iframe already allows fullscreen. Wrap it and fullscreen the wrapper so your own buttons stay on top:

<div id="jc-wrap" style="position:relative">
  <iframe id="jc" src="https://jewelshop.ai/demo/?embed=1" allowfullscreen></iframe>
  <button style="position:absolute;top:12px;right:12px"
          onclick="document.getElementById('jc-wrap').requestFullscreen()">
    Fullscreen
  </button>
</div>

5. Example — "View in 3D" over a photo gallery

Show product photos; on click, swap in the 3D viewer and resume it. Hide it again and pause to save resources.

<div id="gallery"><!-- your photos --></div>
<iframe id="jc" src="https://jewelshop.ai/demo/?embed=1"
        style="display:none" allowfullscreen></iframe>
<button id="view3d">View in 3D</button>

<script>
  const frame = document.getElementById('jc');
  const send = (kind, extra = {}) =>
    frame.contentWindow.postMessage({ source: 'jewelshop', kind, ...extra }, '*');

  document.getElementById('view3d').onclick = () => {
    document.getElementById('gallery').style.display = 'none';
    frame.style.display = 'block';
    send('resume');
  };

  // Your own gem buttons, for example:
  document.querySelectorAll('[data-gem]').forEach((b) =>
    b.addEventListener('click', () =>
      send('apply-preset', { preset: { gem: b.dataset.gem } })));
</script>
Commands are display-only and safe to send cross-origin — they never touch billing or licensing. On a licensed Shopify store the theme block handles the shop identity and the license check automatically; you only need the postMessage layer for custom UI.