Skip to content
DevelopersConfigurators (Beta)

Headless configurator SDK ​

Use a published configurator with your own controls, price and cart, and mount the iJewel 3D view in your page.

Use a configurator that you published in iJewel Drive with your own controls, your own price and your own cart. iJewel keeps the rules of the configurator (which options go together, which ring can be made), and its 3D view stays available: you mount it in an element of your page, or you bind an iJewel viewer that is in an iframe.

"Headless" means that the SDK draws nothing by itself. You read the choices from a session, draw them as you want (selects, swatches, cards), send each pick to the session, and add the session's configuration to your cart. The SDK runs in the browser, and the same calls run on your server to check a cart line: see Cart and Server.

SDK version

This page describes the configurator SDK 0.1.0-dev.2, a preview whose API can still change. The newest version is in latest-version.txt. Pin the version in your page.

Before you start ​

You need three things from the Drive:

Step 1 of 3
  1. A configurator folder that is published. Each publish makes a new revision; the SDK always opens the newest one.
  2. The folder's link access set to Anyone with the link. The SDK reads the folder without a sign-in, so a private folder gives E_NOT_FOUND.
  3. The Drive name and the folder id. Both are in the Link of the Embed dialog: https://ijewel3d.com/drive/files/<folder id>/view. The Drive name is drive for the shared Drive (ijewel3d.com/drive/…), or the name of your business workspace (ijewel3d.com/<name>/…).

For prices from iJewel's price service, your shop domain must also be on the configurator's list of allowed origins (Allowed origins for prices). Your own price function does not need this.

Install ​

The SDK is one file with no dependencies, released at https://releases.ijewel3d.com/libs/configurator-sdk/<version>/. The newest version number is in https://releases.ijewel3d.com/libs/configurator-sdk/latest-version.txt. The examples below use 0.1.0-dev.2.

Script tag. The file configurator-sdk.iife.js sets the global window.IjewelConfigurator.

html
<script src="https://releases.ijewel3d.com/libs/configurator-sdk/0.1.0-dev.2/configurator-sdk.iife.js"></script>
<script>
  const { open, mountViewer } = window.IjewelConfigurator
</script>

ES module. The file configurator-sdk.mjs is an ES module. Import it from the release URL, or copy it into your build.

js
import { open, mountViewer } from 'https://releases.ijewel3d.com/libs/configurator-sdk/0.1.0-dev.2/configurator-sdk.mjs'

npm. The release also has a package tarball, with TypeScript types:

sh
npm install https://releases.ijewel3d.com/libs/web/ijewel3d-configurator-sdk-0.1.0-dev.2.tgz
js
import { open, mountViewer } from '@ijewel3d/configurator-sdk'

The types are also at https://releases.ijewel3d.com/libs/configurator-sdk/<version>/types/api-runtime.d.ts.

Quickstart ​

This page opens a configurator, builds one select per choice, shows the price, mounts the 3D view and logs the cart line. Replace drive and <folder id> with your values.

html
<div id="ring-3d" style="width: 100%; max-width: 560px; aspect-ratio: 1"></div>
<div id="choices"></div>
<p id="price"></p>
<button id="add" type="button">Add to cart</button>

<script src="https://releases.ijewel3d.com/libs/configurator-sdk/0.1.0-dev.2/configurator-sdk.iife.js"></script>
<script>
(async () => {
  const { open, mountViewer } = window.IjewelConfigurator

  // 1. Open the published configurator: a session with the newest revision.
  const session = await open({ drive: 'drive', id: '<folder id>' })

  // 2. Build your controls from the session's axes (one axis = one choice, such as Shape or Metal).
  function render() {
    const box = document.getElementById('choices')
    box.replaceChildren()
    for (const axis of session.axes()) {
      if (!axis.active) continue                       // not used for this ring
      const select = document.createElement('select')
      for (const option of axis.options) {
        if (option.state === 'hidden') continue        // not offered with this ring
        const item = new Option(option.label, option.key, false, option.state === 'selected')
        item.disabled = option.state === 'blocked' || option.state === 'no-file'
        select.append(item)
      }
      // 3. Send each pick to the session.
      select.onchange = async () => {
        const result = await session.select(axis.key, select.value)
        if (!result.applied) render()                  // refused: show the selection as it is
      }
      const label = document.createElement('label')
      label.append(axis.label + ' ', select)
      box.append(label)
    }
  }

  // 4. Show the price of the configuration. `total` is in minor units (cents).
  const money = answer => new Intl.NumberFormat(undefined, {
    style: 'currency', currency: answer.currency, minimumFractionDigits: answer.minorUnits, maximumFractionDigits: answer.minorUnits,
  }).format(answer.total / 10 ** answer.minorUnits)
  async function showPrice() {
    const line = document.getElementById('price')
    if (!session.info().capabilities.includes('price')) return
    const state = session.stateString()
    try {
      const answer = await session.price()
      if (state !== session.stateString()) return       // the shopper picked again meanwhile
      line.textContent = 'total' in answer ? money(answer) : answer.onRequest ? 'Price on request' : ''
    } catch (error) {
      if (state === session.stateString()) line.textContent = `No price (${error.code})`
    }
  }

  // A pick on your page, or in the 3D view, changes the selection.
  session.on('selection:changed', () => { render(); showPrice() })
  render()
  showPrice()

  // 5. Mount iJewel's 3D view in your element. It follows the session both ways.
  mountViewer(document.getElementById('ring-3d'), session, { camera: 'fit' })
    .catch(error => console.warn('3D view:', error.code, error.message))

  // 6. Add to cart: the configuration is the cart line.
  document.getElementById('add').onclick = async () => {
    await session.price().catch(() => null)           // the line carries the price of this configuration
    const line = session.configuration()
    console.log('Add to cart', line)                   // send it to your cart
  }
})().catch(error => console.error('Configurator:', error.code, error.message))
</script>

Serve the page over http:// or https://, not from a file:// path. On localhost the price stays empty with E_PRICE, because iJewel's price service answers only allowed origins (Allowed origins for prices). Use your own price function while you develop (Your own price).

The session ​

open() returns a session. The session holds one configuration and changes it one pick at a time, in call order.

Axes and options. session.axes() returns the choices in the author's order. Each axis has key, label, kind (geometry, material, …), ordered (its options go from small to large, like carat), active and options. An axis with active: false is not used for the current ring (for example, a side-stone metal when the ring has no side stones); its reason is the author's text, when there is one. Each option has key, label, state and, when the author set them, description, icon (a URL), swatch (a CSS color), tags and reason.

stateWhat it meansWhat to show
selectedThe option of the current configuration.Selected.
availableA pick changes only this axis.A normal option.
adjustsA pick is possible, but other axes move so the ring can be made.A normal option, with a hint such as "changes other choices".
blockedA rule of the author forbids it with the current configuration.Disabled. reason says why, when the author wrote it.
no-fileThe author has no 3D model for it with the current configuration.Disabled.
hiddenThe author hides it with the current configuration.Do not show it.

session.options(axisKey) returns the options of one axis, and session.selection() returns { axisKey: optionKey } for every visible axis.

Picks. session.select(axisKey, optionKey) returns a promise of the result:

js
const result = await session.select('carat', '2-00-ct')
// { applied: true, moved: [{ axis: 'head', from: 'halo', to: '4-prong' }], deactivated: [] }
  • applied: false with refused ('blocked', 'hidden', 'no-file'): nothing changed.
  • moved: the other axes that changed so that the ring can be made. Tell the shopper, for example "Head changed to 4-prong".
  • deactivated: the axes that are now not used.
  • select(axis, option, { mode: 'strict' }) refuses an adjusts option instead of moving other axes.

session.selectMany({ shape: 'oval', carat: '1-00-ct' }) applies several picks as one change. session.reset() goes back to the author's default configuration. An unknown axis or option rejects with E_UNKNOWN_AXIS or E_UNKNOWN_OPTION.

State string. session.stateString() returns a short text, such as v2.r1.shape-round.carat-2-00-ct.…, that opens the same configuration again: open({ …, state }) or session.applyState(state). Store it in a link, a saved design or an order.

Price. session.price() returns a promise of the price of the current configuration:

AnswerMeaning
{ total, currency, minorUnits, sku?, lines?, taxIncluded? }total is an integer in minor units: { total: 349900, currency: 'USD', minorUnits: 2 } is 3,499.00 USD.
{ onRequest: true, currency, minorUnits, sku? }The author sells this configuration on request.
{ sku }The configurator has SKUs but no prices.

Answers are kept for the configuration, so a second price() with no change asks nothing. A failed price from iJewel rejects with E_PRICE; an error of your own price function rejects as you threw it. A session with no price rejects with E_NO_PROVIDER; check session.info().capabilities.includes('price') first.

Recipes ​

Your own price ​

Pass a price function to open(). It gets the configuration and returns a price answer (above). The second argument has app, the iJewel price of the folder, so you can build on it; app is null when the folder has no price.

js
// Your own price, from your server.
const session = await open({
  drive: 'drive',
  id: '<folder id>',
  price: async query => {
    const res = await fetch('/api/ring-price', {
      method: 'POST',
      headers: { 'Content-Type': 'application/json' },
      body: JSON.stringify({ state: query.state, selection: query.selection }),
    })
    if (!res.ok) throw new Error(`price ${res.status}`)
    const { cents } = await res.json()
    return { total: cents, currency: 'EUR', minorUnits: 2 }
  },
})
js
// iJewel's price plus 10 %.
const session = await open({
  drive: 'drive',
  id: '<folder id>',
  price: async (query, { app }) => {
    if (!app) return { onRequest: true, currency: 'EUR', minorUnits: 2 }   // the folder has no price
    const answer = await app(query)
    if (!Number.isSafeInteger(answer.total)) return answer                 // on request, or a SKU only
    const { lines, ...rest } = answer                                      // the lines no longer add up
    return { ...rest, total: Math.round(answer.total * 1.1) }
  },
})

query has configurator, revision, state, selection (the visible axes), derived (axes the author derives), tags and specs. The session calls your function again after each pick. An answer that is not one of the three forms rejects with E_PRICE.

No price ​

js
const session = await open({ drive: 'drive', id: '<folder id>', price: false })
session.info().capabilities.includes('price')   // false
session.configuration().price                   // undefined

A folder with no price set up in the Drive also opens with no price.

A transparent 3D background over your page ​

background: 'transparent' shows the ring with no ground plane, on what is behind the element. A CSS color, such as '#f6f3ee', shows the ring on that color.

html
<div class="hero" style="background: linear-gradient(#fff, #efe7da)">
  <div id="ring-3d" style="width: 100%; height: 480px"></div>
</div>
<script>
  IjewelConfigurator.mountViewer(document.getElementById('ring-3d'), session, {
    background: 'transparent',
    camera: 'fit',
    autoRotate: true,
    buttons: false,
  })
</script>

Give the element a size: the view fills it. Change the options later with view.update({ … }).

The 3D view in an iframe: attachViewer ​

When your page already embeds the iJewel viewer of the same folder (the iframe of the Drive's Embed dialog), bind it to your session. A pick on your page shows in the iframe, and a pick in the iframe comes back to your session.

html
<iframe id="viewer" src="<the iframe src from the Embed dialog>" width="100%" height="480" allow="fullscreen"></iframe>
<script>
  const iframe = document.getElementById('viewer')
  const detach = IjewelConfigurator.attachViewer(session, iframe, {
    origin: new URL(iframe.src).origin,                // messages go to this origin only
    onError: error => console.warn('viewer:', error.code, error.message),
  })
  // detach() stops both directions.
</script>

origin is the exact origin of the iframe's page, never '*'. The binding works only while the iframe shows the same configurator and revision as the session. Another folder gives onError with E_PARAMS. Another revision (the author published again after your page opened the session) gives E_REVISION: open the session again.

Listen to events ​

session.on(event, handler) returns a function that removes the handler.

EventDataWhen
selection:changed{ selection, moved, deactivated }The configuration changed: a pick on your page, in the 3D view, reset() or applyState().
price:changedthe price answerA new price is there for the current configuration. While a handler listens, the session asks for the price after each change.
error{ code, message }A price request failed, or a handler threw.
validation:warning{ field, provided, corrected }applyState() corrected a state of this revision.
state:migrated{ from, to, changes }applyState() moved a state of an older revision to the current one.
cart:requestedthe configurationsession.requestCart() priced the configuration and returns it.
readysession.info()At once: the session is open.
js
const off = session.on('price:changed', answer => console.log('price', answer))
session.on('error', ({ code, message }) => console.warn(code, message))
session.on('selection:changed', ({ moved }) => {
  for (const m of moved) console.log(`${m.axis} changed from ${m.from} to ${m.to}`)
})
off()   // stop listening to the price

Restore a saved state from a URL ​

Write the state string into the address after each change, and apply it when the page opens. Listen before you apply it, so you hear what was corrected.

js
const session = await open({ drive: 'drive', id: '<folder id>' })
session.on('state:migrated', ({ changes }) => console.log('the design moved to the newest revision', changes))
session.on('validation:warning', warning => console.log('corrected', warning))

const saved = new URL(location.href).searchParams.get('ring')
if (saved) {
  try {
    const result = await session.applyState(saved)
    if (!result.applied) console.log('The saved design was corrected (the events above say how)')
  } catch (error) {
    console.warn('not a state of this configurator:', error.code)   // E_STATE: keep the default ring
  }
}

session.on('selection:changed', () => {
  const url = new URL(location.href)
  url.searchParams.set('ring', session.stateString())
  history.replaceState(null, '', url)
})

A state of an older revision opens in the current revision: a retired option goes to its replacement or to the default, and a ring that can no longer be made goes to the nearest one that can. open({ …, state: saved }) does the same in one call, but you cannot hear the events of that first apply.

The 3D view: mountViewer ​

mountViewer(container, session, options) loads iJewel's viewer into container and returns a promise of the mounted view. The viewer shows the folder's scene (environment, camera, materials) and follows the session both ways. Its own configurator panel stays off: your page draws the controls.

OptionDefaultWhat it does
background'scene''scene': the folder's background and ground. 'transparent', or a CSS color such as '#f6f3ee': the ring with no ground plane.
camera'scene''scene': the folder's camera. 'fit': the ring fills the view.
autoRotatefalsetrue, or a speed (2 is about one turn in 30 s).
controlstrueThe shopper turns and zooms the ring.
zoom{}{ min, max } camera distances, or false: no zoom.
buttonstrueThe buttons over the ring: full screen, reset, rotate, Try-On.
tryOntrueThe Try-On button. The folder's Try-On setup and your domain's licence still apply.
loadingText'Loading the 3D view…'The text under the loader; '' shows none.
viewer, webgithe versions this SDK release was tested withA viewer or WebGI release, or the https URL of its bundle. A page that has its own keeps it. One version of each per page.
viewerOptions{}Other options of the iJewel viewer, passed as they are, such as { disableInteractionPrompt: true }.
onErrornoneHears viewer errors: E_SCENE (a file did not load), E_PARAMS or E_REVISION (the viewer shows another configurator or revision).

The mounted view has:

  • update(options): changes the options above on the running view, except viewer, webgi and project.
  • attach(session): binds another session of the same configurator, for example after open() with another price.
  • detach(): stops the binding; the view stays.
  • destroy(): stops the binding and removes the view from the page.
  • controller: the viewer's configurator, with on('load:complete', …) and getImage() for a picture of the ring.
  • viewer: the 3D viewer itself, for what the options do not cover.
js
const view = await mountViewer(document.getElementById('ring-3d'), session, { background: '#f6f3ee', camera: 'fit', tryOn: false })
view.update({ autoRotate: true })
const picture = await view.controller.getImage()   // a Blob, for the cart or an email

The view is optional. Without it, the session still gives the choices, the price and the cart line.

Error codes ​

Every error that the SDK gives is an Error with a code.

CodeWhereWhat it means
E_PARAMSopen, mountViewer, attachViewer, updateAn argument is wrong; the message says which. From a viewer: it shows another configurator.
E_NOT_FOUNDopen, loadPublishedThe Drive shows no public folder with that id: a wrong id or Drive name, or the folder is not shared with "Anyone with the link".
E_NOT_PUBLISHEDopen, loadPublishedThe folder has no published configurator. Publish it in the Drive.
E_UPSTREAMopen, loadPublished, mountViewerThe Drive did not answer within 10 s or answered with an error, a viewer script did not load, or the viewer did not start within 60 s.
E_SCHEMA, E_READER_TOO_OLDopen, loadPublishedThe configurator is in a format that this SDK version does not read. Use a newer SDK version.
E_INTEGRITYopen, loadPublishedThe published files do not match each other. Publish again.
E_UNKNOWN_AXIS, E_UNKNOWN_OPTIONselect, selectMany, optionsNo visible axis or option with that key.
E_STATEapplyState, open({ state })The text is not a state string of this configurator.
E_REVISIONviewer onErrorThe viewer shows another revision of the configurator. Open the session again.
E_PRICEprice, error eventThe price request failed: no answer in 10 s on each of two tries, an origin that is not allowed, a configuration that the price service cannot price, or an answer of the wrong form.
E_NO_PROVIDERpriceThe session has no price (price: false, or the folder has none).
E_SEARCH_LIMITselect, selectManyThe search for a ring that can be made took too long. Rare; pick again.
E_SCENEviewer onErrorA 3D file did not load.
js
try {
  const session = await open({ drive: 'drive', id: '<folder id>' })
} catch (error) {
  if (error.code === 'E_NOT_FOUND') showMessage('This configurator is not available.')
  else throw error
}

Limits ​

  • The SDK file is about 17 KB with gzip (the release limit is 24 KB). It holds no price model and no private data of the author.
  • open() waits at most 10 s for the Drive, and a request to iJewel's price service at most 10 s. A price request with no answer in 10 s goes once more before price() rejects.
  • The 3D view must start within 60 s. One viewer version and one WebGI version per page.
  • A price answer has at most 64 lines. A SKU has at most 64 characters.
  • iJewel's price service keeps the newest 20 revisions of a configurator. A page that holds an older revision gets E_PRICE: open the session again.
  • iJewel's price service has a daily request limit. Above it, price() rejects with E_PRICE. Browsers keep each price answer for 5 minutes, and the session asks again only when a choice that changes the price changes.
  • A state string is at most 2,048 characters for iJewel's price service.

Versioning ​

  • Each release has its own folder, libs/configurator-sdk/<version>/, and never changes. Pin a version in your page, and update it on purpose.
  • A version with -dev.N is a preview. Its API can change before the stable release.
  • The changes of each release are in https://releases.ijewel3d.com/libs/configurator-sdk/CHANGELOG.md.
  • Each SDK release names the viewer and WebGI versions that it was tested with (VIEWER_VERSION, WEBGI_VERSION). mountViewer loads those unless you set viewer and webgi.
  • The cart line has schema: "ijewel.configuration/2.0". A later 2.x adds fields and keeps the meaning of the fields above.
  • State strings stay valid when the author publishes again: option keys never change their meaning, and an older state moves to the current revision (Restore a saved state from a URL).

API summary ​

CallReturns
open({ drive, id, price, locale, state, fetch })A promise of the session. price: left out (iJewel's price), a function, or false.
session.axes(), session.options(axis), session.selection()The choices, their states, the current picks.
session.select(axis, option, { mode }), selectMany(picks), reset()A promise of the pick result.
session.price()A promise of the price answer.
session.configuration()The cart line.
session.requestCart()A promise of the cart line, priced; fires cart:requested.
session.stateString(), session.applyState(state)The state text; a promise of the pick result.
session.info(){ configuratorId, revision, capabilities, … }.
session.on(event, handler)The function that removes the handler.
mountViewer(container, session, options)A promise of the mounted view.
attachViewer(session, iframe, { origin, onError })detach().
loadPublished({ drive, id })A promise of the published folder, for verifyConfiguration and createSession.
verifyConfiguration(folder, line)A promise of { ok: true, configuration, migratedFrom? } or { ok: false, problems }.