Configento V3 Frontend API
Public JavaScript surface of the V3 storefront bundle. The current
release line is 3.0.0-alpha.N — the shape below is what's promised
for 3.0.0 stable; alpha versions may still tighten error wording
and add minor fields. Once 3.0.0 ships, semver applies and breaking
changes only bump the major.
- Online version: admin.configento.app/docs/configento-v3-frontend-api
- Bundle URL (production):
https://cloud.configento.app/configento-v3/configento.js - Bundle version at runtime:
window.Configento.version
This document describes what the bundle exposes on window.Configento and
on every mounted ConfiguratorInstance. It is the integration contract
for theme developers, plugin authors and any custom storefront code.
Quick start
-
Drop a mount element with the
data-configentoattribute somewhere on the page. The bundle scans for these on boot and mounts one configurator per element:<div data-configento data-component-id="8806"></div> <script src="https://cloud.configento.app/configento-v3/configento.js" defer></script> -
Wait for the bundle to finish booting before touching the API:
await window.Configento.ready; const inst = [...window.Configento.instances.values()][0]; -
Read state, subscribe to changes, or call
finalize()to validate + build the cart payload:const unsubscribe = inst.on('price', ({ total, currency, delta }) => { console.log(`total now ${total} ${currency} (Δ ${delta})`); }); document.querySelector('.add-to-cart').addEventListener('click', async () => { const result = await inst.finalize(); if (!result.ok) return showErrors(result.validationErrors); postToCart(result.payload, result.combinedImage); }, );
window.Configento — global surface
interface ConfiguratorPublicApi {
readonly version: string; // SemVer of the loaded bundle
readonly frontendVersionTag: string; // e.g. "v3"
readonly instances: ReadonlyMap<string, ConfiguratorInstance>;
readonly ready: Promise<void>; // resolves once every mount is ready
}
instances is keyed by the mount element's id attribute (which the
bundle auto-fills with configento-{hash} when the host element doesn't
have one). A typical loader uses [...Configento.instances.values()]
when it doesn't know the id in advance.
ready resolves AFTER every mounted instance has fetched its Component
and swapped its API methods from the throwing stub onto the live
implementation. Calling any instance method before ready throws
'Configento instance not ready — await Configento.ready first.'.
ConfiguratorInstance — per-mount surface
interface ConfiguratorInstance {
readonly id: string; // mount element id
readonly componentId: string | null; // template / component id
readonly element: HTMLElement; // the [data-configento] node
readonly config: WebshopConfig | null; // resolved mount-time config
// ── Read ────────────────────────────────────────────────────
getState(): ConfiguratorSnapshot;
getSelections(): SelectionsMap;
getValue(characteristic: CharacteristicRef): unknown;
getPriceBreakdown(): PriceSnapshot;
getAssetPayload(): AssetPayload | null;
getHeaderValue(): string | null;
// ── Write ───────────────────────────────────────────────────
selectValue(characteristic: CharacteristicRef, value: ValueRef | null): void;
toggleValue(characteristic: CharacteristicRef, value: ValueRef): void;
setText(characteristic: CharacteristicRef, value: string): void;
setQuantity(characteristic: CharacteristicRef, value: ValueRef, quantity: number): void;
resetSelection(characteristic: CharacteristicRef): void;
// CharacteristicRef = number (id) | string (alt_title)
// ValueRef = number (id) | string (Value.value)
// ── Action ──────────────────────────────────────────────────
finalize(): Promise<FinalizeResult>;
restoreFromAssetJson(payload: Record<string, unknown>): void;
// ── Events ──────────────────────────────────────────────────
on<E extends InstanceEventName>(event: E, cb: (p: InstanceEventMap[E]) => void): () => void;
onChange(cb: () => void): () => void; // @deprecated → use on('change', …)
}
Read methods
getState(): ConfiguratorSnapshot
Full read-only view onto the current configuration. Cheap to call — pure function over store state. Returned object is a fresh snapshot, not a live binding; call again to re-read.
interface ConfiguratorSnapshot {
selections: SelectionsMap; // user picks per Characteristic
evaluated: Record<number, number | string | null>; // calc outputs
hiddenCharacteristics: ReadonlyArray<number>; // rule-driven
blacklistedValues: ReadonlyArray<number>; // rule-driven
price: PriceSnapshot;
}
getValue(characteristic): unknown
Read a single Characteristic's current value in the same shape the expression engine sees it. Short-cut for the common case where you want one field and don't need the full state object.
| Characteristic type | Return value |
|---|---|
| single-pick | picked Value's value string (or id if value is empty) |
| multi-pick | array of value strings ([] when none picked) |
| text / area | the text string ('' when empty) |
| multiquantity | { [valueId]: qty } object ({} when none) |
expression / combi / matrixvalue / productattribute |
evaluator output (number / string / null) |
| Empty selection on input type | type-appropriate default (null / [] / '' / {}) |
inst.getValue('material') // → "holz"
inst.getValue('extras') // → ["sunroof", "roofbox"]
inst.getValue('customer_name') // → "Erika Mustermann"
inst.getValue('total_price') // → 1234.56 (expression evaluator output)
Accepts the same {@link CharacteristicRef} as the write methods —
numeric id OR alt_title. Throws on unknown ref.
getSelections(): SelectionsMap
Just the selections sub-tree of getState(). Shape:
type SelectionsMap = Readonly<Record<number, CharacteristicSelection>>;
type CharacteristicSelection =
| { kind: 'single'; valueId: number | null }
| { kind: 'multi'; valueIds: ReadonlyArray<number> }
| { kind: 'text'; value: string }
| { kind: 'quantities'; quantities: Record<number, number> };
Keyed by characteristic.id. Characteristics without direct user input
(calculated, package, UI-only) don't appear here.
getPriceBreakdown(): PriceSnapshot
interface PriceSnapshot {
total: number; // sum, in the configurator's base currency
currency: string; // ISO code, e.g. "EUR"
priceIncludesTax: boolean; // shop-side tax-inclusion flag
}
Recomputed every call — host code can poll it in render loops without worrying about staleness.
getAssetPayload(): AssetPayload | null
The same alt_title-keyed JSON shape finalize() builds, but WITHOUT
running validation, snapshotting the combined image, or surfacing
per-Characteristic errors. Returns null when no Characteristics are
populated yet.
Mirrors legacy configurator.js:generateHeaderValue minus the Base64
wrapping. Use getHeaderValue() if you need the encoded form.
interface AssetEntry {
value: string | string[] | number | null;
label: string;
valueLabel: string | string[];
valueId?: number | number[] | null;
valuePrice?: number | number[];
valueQty?: number | number[];
sku?: string;
choiceSku?: string | string[];
visibility: number;
}
type AssetPayload = Record<string, AssetEntry | string | undefined> & {
_component_title?: string;
_locale?: string;
_combined_image?: string; // only on finalize() result
};
getHeaderValue(): string | null
UTF-8-safe Base64 of getAssetPayload(). This is the value the legacy
storefront posted in the X-Configento HTTP header, and the value the
Shopify plugin embeds in saveSnapshot.payload. Returns null when no
Characteristic is populated yet.
Hosts that need to inject the snapshot into a form without going
through finalize() can read it directly:
form.querySelector('[name=configento_payload]').value = inst.getHeaderValue() ?? '';
Write methods
Every write method accepts the Characteristic by either its numeric
id OR its admin-authored alt_title (the same identifier the admin
uses in expression formulas — e.g. "Lamelle_01"). Values can be
referenced by id OR by their value field (the same string matrix
lookups compare against — e.g. "holz"). Mixing forms is fine; pick
whichever the host code already carries.
// All three calls produce the same store state:
inst.selectValue(75, 56); // id, id
inst.selectValue('transfer_trips', 'A-B-A'); // alt_title, value
inst.selectValue(75, 'A-B-A'); // id, value
Validation rules:
- Unknown Characteristic ref → throws with the ref kind included in
the message (
#42 not foundvsalt_title=oops not found) - Unknown Value ref → throws with the Characteristic context so the host can pinpoint typos fast
- Write incompatible with the Characteristic's type (e.g.
setTexton aradiobuttonsChar) → throws with actual + expected kinds - Calculated / display-only Characteristics (
expression,combi,matrixvalue,productattribute,webservice,package,fileupload,chart,savebutton,static) accept no writes; only the leaf input types do
selectValue(characteristic, value | null): void
Single-pick Characteristics (select / radiobuttons / single-pick
listimage). Pass null to clear.
inst.selectValue('material', 'holz'); // alt_title + value string
inst.selectValue(123, null); // clear by id
toggleValue(characteristic, value): void
Multi-pick Characteristics (multiselect / checkbox / multi-pick
listimage). Adds the value if it's not present, removes it if it is.
inst.toggleValue('extras', 'sunroof');
setText(characteristic, value: string): void
Free-entry text input (text / area). Passing an empty string is
fine — the store keeps the entry around so the UI can still show
"required" validation on submit.
inst.setText('customer_name', 'Erika Mustermann');
setQuantity(characteristic, value, quantity: number): void
Per-Value quantity for multiquantity Characteristics. Quantity is
clamped to ≥ 0 by the store.
inst.setQuantity('toppings', 'cheese', 2);
resetSelection(characteristic): void
Wipe a Characteristic's selection back to "none". Clears the
userTouched flag so rule-driven predefines repopulate the field on
the next render — exactly the same behaviour as the merchant clicking
"reset" in the UI.
inst.resetSelection('color');
Action methods
finalize(): Promise<FinalizeResult>
Run validation, build payload, snapshot the combined image. The canonical "user clicked add to cart" entry point.
interface FinalizeResult {
ok: boolean;
validationErrors: ReadonlyArray<ValidationError>;
payload?: AssetPayload; // only when ok === true
combinedImage?: string; // PNG data URL, opt-in per component
priceTotal: number;
summary: string; // ≤ 1024 chars, visible-text recap
}
interface ValidationError {
characteristicId: number;
altTitle: string; // same Characteristic, by its admin-authored name
code: // stable machine code, widened over time
| 'required' | 'pattern' | 'range' | 'length'
| 'numeric' | 'integer' | 'gt-zero' | 'gte-zero'
| 'email' | 'url' | string; // future codes parse as fallback string
message: string; // localised
}
On ok === false the instance also pushes errors into its store, so
inline error messages render on each Characteristic without the host
having to wire them up.
restoreFromAssetJson(payload): void
Inverse of getAssetPayload(). Re-seeds the store from a saved
snapshot — used when a customer revisits a saved configuration.
const saved = JSON.parse(localStorage.getItem('myConfig') ?? '{}');
inst.restoreFromAssetJson(saved);
Each restored Characteristic is marked as user-touched, so rule-driven predefines won't overwrite the restored pick on the next render.
Events
on<E>(event, cb): () => void
Typed multi-event subscription. Returns an unsubscribe function — call
it when the host UI unmounts to avoid leaks. One bad listener can't
take down the others (each callback is wrapped in a try/catch and any
throw is console.error'd).
| Event | Payload type | Fires when |
|---|---|---|
change |
ChangeEvent |
Any selection mutation (single/multi/text/quantity) |
price |
PriceEvent |
The total moved after a change (no-op deduped) |
finalize |
FinalizeResult |
finalize() resolved — ok true OR false |
error |
ErrorEvent |
Validation errors got surfaced into the store |
interface ChangeEvent { selections: SelectionsMap }
interface PriceEvent extends PriceSnapshot { delta: number }
interface ErrorEvent { errors: ReadonlyArray<ValidationError> }
// FinalizeResult — see finalize() docs above
inst.on('change', (e) => updateLivePreview(e.selections));
inst.on('price', (e) => {
console.log(`now ${e.total} ${e.currency} (Δ ${e.delta})`);
});
const offFinalize = inst.on('finalize', (r) => {
if (r.ok) sendToBackend(r.payload, r.combinedImage);
});
inst.on('error', (e) => {
showToast(`${e.errors.length} validation issue(s) — see fields`);
});
onChange(cb: () => void): () => void
Deprecated — use
on('change', cb)for the typed payload. Kept for back-compat with the original v3.0 surface; scheduled for removal in v4.0.
const unsub = inst.onChange(() => updateLivePreview(inst.getState()));
DOM contract
The bundle scans for elements matching [data-configento] and mounts
one configurator per match. Per element it reads:
| Attribute | Type | Required | Meaning |
|---|---|---|---|
data-configento |
flag | ✅ | marks the mount target |
id |
string | ❌ | becomes the instance id; auto-filled with configento-{hash} if absent |
data-component-id |
string | one-of | which Component to load (alternative: componentId inside data-configento-config) |
data-configento-config |
JSON | ❌ | full {@link WebshopConfig} object as a JSON string |
data-configento-{key} |
string | ❌ | individual {@link WebshopConfig} field — kebab-cased key, e.g. data-configento-tax-rate |
componentId must be supplied through one of the routes above (or
through window._configentoPreview in the admin preview path). If
none of them resolves a value, the bundle logs a warning and mounts
a placeholder.
Multiple [data-configento] elements on one page are supported. Each
mounts independently and registers under its own instance id. See the
WebshopConfig section below for the full list of
per-mount config keys.
WebshopConfig
Mount-time configuration the host plugin or theme hands to the bundle.
Lives on inst.config as a frozen read-only record — once the
instance is registered, nothing on this object changes for its
lifetime. Host code that needs to change config must re-mount.
interface WebshopConfig {
componentId: string; // required
customer?: string; // customer_no for cloud calls
token?: string; // cloud auth (per-customer Basic-Auth or similar)
cloudBaseUrl?: string; // defaults to whatever the bundle was built against
taxRate?: number | string; // host-shop effective tax rate
uiLocale?: string; // chrome strings ("Total", "Add to cart", …)
contentLocale?: string; // which title[locale] to pick from component data
currency?: string; // ISO code; defaults to component's
frontendVersion?: string; // "v3" — rarely overridden
}
Sources, in priority order
-
data-configento-configattribute on the mount element — a JSON-encodedWebshopConfig. Most common for plugins that emit one script-tag per product.<div data-configento data-configento-config='{"componentId":"10738","customer":"32929","currency":"EUR"}'></div> -
Individual
data-configento-{key}attributes on the mount element. Flatter and easier to read in static HTML. Each key from the WebshopConfig shape maps to a kebab-case attribute (e.g.taxRate→data-configento-tax-rate). The exception iscomponentIdwhich uses the existingdata-component-idslot.<div data-configento data-component-id="10738" data-configento-customer="32929" data-configento-tax-rate="0.19" data-configento-locale="de_DE"></div> -
window._configentoPreview— the admin-preview iframe path. New plugin integrations should use the data-attribute mechanism; this stays around for back-compat.
Sources merge in reverse order so JSON attribute > flat attrs >
preview context. Malformed JSON logs a console.warn and falls
through to the lower-priority sources instead of throwing.
Versioning + breaking changes
Configento.version follows SemVer. Patch releases are bug fixes only;
minor releases add API surfaces but don't break existing callers. Major
bumps may break callers — they ship with a migration guide.
The frontendVersionTag ("v3", "v4", …) is for backend dispatch — the
admin can roll individual components onto a newer engine without
moving every customer at once. Plugin authors should match against
this tag, not version, when their integration is engine-specific.
Compat layer — configento-compat.js
Optional 1-2 kB sidekick bundle for storefronts whose theme code still
references the legacy CONFIGENTO_PRODUCT global and the
configento*InDom* custom events. Load it BEFORE configento.js
(both with defer so order is deterministic):
<script src="https://cloud.configento.app/configento-v3/configento-compat.js" defer></script>
<script src="https://cloud.configento.app/configento-v3/configento.js" defer></script>
What it bridges:
-
Mount selector: if a legacy
#configurator1element exists without[data-configento], the compat layer adds the attribute in-place so the V3 bundle picks it up. Missingdata-component-idis filled fromwindow.CONFIGENTO_PRODUCT.getTemplateId(). -
Custom events: subscribes to each instance's
changeevent and re-emits the legacy events ondocument. Theme code listening forconfigentoUpdateOptionsInDomBefore/…After(and the matchingheaderpair) keeps working:document.addEventListener('configentoUpdateOptionsInDomAfter', () => { // theme code that rehydrates a sub-DOM after every config change });The
Beforeevents fire synchronously, theAfterevents fire on the next microtask — matching the legacy ordering where the engine's own render happened between the two. -
Price callback: forwards every
priceevent towindow.CONFIGENTO_PRODUCT.setPrice(total)when the host has it defined. Themes that mirrored the configurator's total into a sticky bar outside the bundle's DOM keep getting their number.
The compat layer is a no-op when neither #configurator1 nor
CONFIGENTO_PRODUCT exists — safe to include unconditionally on a
storefront where you're not 100% sure whether legacy code is still
present in a theme.
Don't use the compat layer for new integrations. It exists to stop existing theme code from going silent during the V3 rollout; new code should subscribe to
inst.on('change' | 'price' | …, cb)directly. The compat layer will stay around indefinitely but won't grow new bridges — legacy surface coverage is what it is today.
Migration from legacy CONFIGENTO_PRODUCT
Theme code that pre-dates V3 talks to a global CONFIGENTO_PRODUCT
object and listens for the configento*InDom* events. Two paths
forward:
Path A — load the compat layer (zero theme changes)
See the previous section. The shim keeps your theme working through the V3 rollout. Recommended for "deploy first, refactor later" cases.
Path B — port the theme to the new API
| Legacy | V3 |
|---|---|
CONFIGENTO_PRODUCT.getTemplateId() |
inst.componentId |
CONFIGENTO_PRODUCT.getCustomerId() |
inst.config?.customer |
CONFIGENTO_PRODUCT.getCloudBase() |
inst.config?.cloudBaseUrl |
CONFIGENTO_PRODUCT.getLocale() |
inst.config?.uiLocale |
CONFIGENTO_PRODUCT.getTaxRate() |
inst.config?.taxRate |
CONFIGENTO_PRODUCT.getStoreCurrencyCode() |
inst.config?.currency / inst.getPriceBreakdown().currency |
CONFIGENTO_PRODUCT.setPrice(total) (host hook) |
inst.on('price', ({ total }) => …) |
configentoUpdateOptionsInDomAfter event |
inst.on('change', cb) |
Submit-form hook (legacy webshop.js) |
await inst.finalize() → POST result.payload to your endpoint |
Mixed mode is supported: load the compat layer AND have new modules
use inst.on(…) directly. The two never fight.