__HOST__The configurator is an embeddable 3D viewer. You drop it into a page with an
<iframe> and drive it with postMessage: when the shopper
changes an option, you send the new configuration and the model updates instantly — no reload.
Your page owns the UI and the configuration state; the viewer just renders what it's told and reports back.
postMessage call update live — copy them straight from there.Embed the /embed page. It is the bare viewer (no controls) — your page supplies the controls.
<iframe id="axton"
src="__HOST__/embed"
style="width:100%;height:600px;border:0"
title="Axton Station configurator"></iframe>
Set the initial configuration with URL parameters, so even a bare iframe renders the right station:
__HOST__/embed?robot=ax-r10kg&carousel=ax-fullsize-carousel
| Parameter | Meaning |
|---|---|
<slot> (e.g. robot, carousel) | option id selected for that slot |
accessories | comma-separated accessory option ids |
theme | light or dark (default dark) |
turntable | 0 to start with auto-rotate off (the viewer also shows a visible toggle button) |
The valid slot and option ids come from the catalog (section 4). They're managed in the admin and may change, so read them at runtime rather than hard-coding.
Post messages to the iframe's contentWindow. Updates are instant and never reload the frame. Omit a slot to leave it unchanged.
const viewer = document.getElementById('axton').contentWindow;
// change one or more parts
viewer.postMessage({
type: 'setConfig',
requestId: 1, // optional, echoed back on configApplied
config: { robot: 'ax-r10kg', carousel: 'ax-fullsize-carousel', accessories: [] }
}, '*');
| Message to the viewer | Effect |
|---|---|
{ type:'setConfig', config } | apply a configuration (swaps only what changed) |
{ type:'snapshot', requestId } | request a PNG of the current view (see §5) |
{ type:'frame' } | re-center / re-fit the camera |
{ type:'setTheme', theme } | background theme — 'light' or 'dark' |
{ type:'setTurntable', enabled } | enable/disable idle auto-rotate |
The viewer posts messages back to the parent window. Always check source === 'axton-viewer'.
window.addEventListener('message', (e) => {
if (e.data?.source !== 'axton-viewer') return;
switch (e.data.type) {
case 'ready': /* e.data.catalog — build your dropdowns from this */ break;
case 'loading': /* e.data.part — a part is streaming in */ break;
case 'configApplied': /* e.data.config, e.data.requestId — swap finished */ break;
case 'snapshot': /* e.data.dataUrl — a PNG data URL */ break;
case 'error': /* e.data.message */ break;
}
});
Fetch the catalog to discover the slots, their options, the defaults, and accessories — then render your own selectors. It updates automatically as parts are added in the admin, so don't hard-code option lists.
const catalog = await fetch('__HOST__/api/catalog').then(r => r.json());
// { name, units,
// slots: { robot: { label, default, options: { 'ax-r10kg': { label, model }, … } },
// carousel: { label, default, options: { … } } },
// accessories: { label, multiple, options: { … } } }
// (slot ids and options are admin-managed and can change — never hard-code them)
for (const [slotId, slot] of Object.entries(catalog.slots)) {
// make a <select> with slot.label and one <option> per id in slot.options
// (slot.default is the pre-selected id)
}
You also receive this same catalog object in the ready event, so a separate fetch is optional.
Ask for a PNG of the current view — handy for attaching the configured station to a quote or order.
viewer.postMessage({ type:'snapshot', requestId: 42 }, '*');
// then, in your message listener:
if (e.data.type === 'snapshot') {
const pngDataUrl = e.data.dataUrl; // e.g. put in an <img> or upload it
}
The snapshot is the 3D view only — the floor grid, the viewer's own controls (the auto-rotate toggle) and branding are never in the image, and it looks identical in light or dark mode, so it's clean to drop into a quote or order.
A minimal but complete page: it builds selectors from the catalog and drives the viewer.
<iframe id="axton" src="__HOST__/embed" style="width:100%;height:560px;border:0"></iframe>
<div id="controls"></div>
<script>
const viewer = document.getElementById('axton').contentWindow;
let catalog = null, reqId = 0;
function pushConfig() {
const config = { accessories: [] };
document.querySelectorAll('select[data-slot]').forEach(s => config[s.dataset.slot] = s.value);
viewer.postMessage({ type:'setConfig', requestId: ++reqId, config }, '*');
}
function buildControls() {
const c = document.getElementById('controls');
c.innerHTML = '';
for (const [slotId, slot] of Object.entries(catalog.slots)) {
const sel = document.createElement('select');
sel.dataset.slot = slotId;
for (const [id, o] of Object.entries(slot.options)) {
const opt = new Option(o.label, id, id === slot.default, id === slot.default);
sel.add(opt);
}
sel.addEventListener('change', pushConfig);
c.appendChild(sel);
}
}
window.addEventListener('message', (e) => {
if (e.data?.source !== 'axton-viewer') return;
if (e.data.type === 'ready') { catalog = e.data.catalog; buildControls(); }
});
</script>
| URL | What |
|---|---|
GET /embed | the embeddable viewer (iframe this) |
GET /demo | standalone demo with a live API-call preview (/ redirects here) |
GET /api/catalog | slots, options, defaults, accessories (JSON) |
GET /healthz | health check |
| type | fields |
|---|---|
setConfig | config, requestId? |
snapshot | requestId? |
frame | — |
setTheme | theme — 'light' | 'dark' |
setTurntable | enabled |
setLighting | lighting {intensity, tint} — optional; lighting is normally set in the admin |
| type | fields |
|---|---|
ready | catalog |
loading | part |
configApplied | config, requestId? |
snapshot | dataUrl |
error | message |
https://www.axtonrobotics.com) so they can be
added to ALLOWED_ORIGINS. From a non-allowlisted origin the iframe will be blocked by the browser.