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
// ── Instance lookup ────────────────────────────────────────────
first(): ConfiguratorInstance | null;
forElement(el: HTMLElement | string): ConfiguratorInstance | null;
// ── Cloud assets ──────────────────────────────────────────────
loadAsset(id: string, opts?: {
instance?: ConfiguratorInstance;
filename?: string;
}): Promise<AssetPayload>;
// ── Host integration ──────────────────────────────────────────
setModalAdapter(adapter?: ModalAdapter): void;
}
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. For the common single-configurator
case use Configento.first(); to resolve an instance from an arbitrary
descendant element (e.g. the host shop's <form id="product_addtocart_form">)
use Configento.forElement(elementOrSelector).
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.'.
Synchronous-read guarantee after ready. Once await Configento.ready
resolves, the following per-instance reads are guaranteed synchronous
and safe to call directly from onclick / event handlers (no
await required):
getValue,getState,getSelectionsgetPriceBreakdowngetAssetPayload,getHeaderValuegetSummary
Use this when you need to wire a host button (legacy Magento product form, custom modal, etc.) to read the current configuration without making the surrounding handler async.
No read-side caching. The write methods (selectValue, toggleValue,
setText, setQuantity, resetSelection) are synchronous: they
update the store in-place before returning. A read that follows in
the same turn observes the new value — you do not need to await
the next change event:
inst.setQuantity('extras', 'option_a', 3);
const header = inst.getHeaderValue(); // ← already reflects qty=3
One async caveat — webservice / productattribute. Calculated
Characteristics whose evaluator inputs come from a host-side webservice
call resolve asynchronously at mount time (one fetch round-trip after
the bundle boots). If a read fires before a webservice has answered,
that one calculated field shows null in evaluated and an empty
string in the asset payload — not stale data, a true gap. Typical
storefront flows are unaffected because mounts settle well before the
user clicks anything; if you read immediately after Configento.ready
in code and the configurator uses webservice fields, prefer one of:
await inst.finalize()— implicitly waits for everything the asset payload depends on,- await the first
changeevent, or - defer the read into the user-interaction phase.
Deep-link auto-restore (?dl=<uuid>). When the page URL carries
a saved-configuration share token as query parameter, the bundle
auto-restores the referenced asset into the first mounted
configurator once Configento.ready has resolved. Mirrors the
legacy contract — mail-share flows work without host-side wiring:
https://shop.example.com/product.html?dl=7ec4ea62-34e5-4015-a315-9d8028a52036
Mechanism: main.tsx reads window.location.search after
markReady(), dispatches Configento.loadAsset(<id>), then
first().restoreFromAssetJson(payload). Failure is quiet — a
missing / corrupt asset logs console.warn and the configurator
boots empty. Recognised query params (first match wins):
dl— legacy convention.configento_asset— new descriptive alias for hosts migrating.
To disable the auto-restore in host code that wants to control the timing itself (rare), strip the param before the bundle loads:
// Before <script src="…/configento.js" defer> executes:
const url = new URL(location.href);
url.searchParams.delete('dl');
history.replaceState(null, '', url);
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;
getSummary(opts?: {
format?: 'text' | 'html';
includePrices?: boolean;
separator?: string;
}): string;
// ── 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;
storeAsset(opts?: { filename?: string }): Promise<{ id: string; url: string }>;
// ── 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() ?? '';
getSummary(opts?): string
Render the visible-entry subset of the current configuration as a single
string for mail bodies, cart-line tooltips, and custom storefront
summary widgets. Pure read; does not validate, does not snapshot. Empty
selection → ''.
interface GetSummaryOpts {
/** `text` (default) joins entries with `separator`; `html` emits a
* `<dl><dt>Label</dt><dd>Value</dd>…</dl>` block. */
format?: 'text' | 'html';
/** Append `(<formattedPrice>)` after the value label when the entry
* carries `valuePrice`. Default false. */
includePrices?: boolean;
/** Joiner between entries for the text format. Default `'; '`. */
separator?: string;
}
// Plain text for a mail body line:
inst.getSummary()
// → 'Farbe: Rot; Material: Buche; Maße: 80 × 120 cm'
// HTML block to drop into a wrapped <dl> mail body:
inst.getSummary({ format: 'html', includePrices: true })
// → '<dl><dt>Farbe</dt><dd>Rot (12,50)</dd>…</dl>'
PDF rendering is not in getSummary — it lives in the server-side
Save-Button-Action pipeline (generate_pdf, send_email_with_pdf).
Use those actions when the configurator owner wants a PDF attachment;
mirroring the renderer client-side would drift on every styling change.
No new translation keys are introduced — labels and value labels come from the loaded Component, already localised by the bundle's locale picker.
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.
storeAsset(opts?): Promise<{ id, url }>
POST the current configuration as a JSON asset to the V3 admin's
storefront-asset endpoint (POST /api/v1/storefront/assets on
admin.configento.app) and resolve to { id, url }. id is the
share token (use it in mail-share URLs); url is the public Laravel
route URL of the stored JSON — recipients open it directly to fetch
the configuration back.
Auth: the bundle reuses the same x-jscloud-tok token it already
uses for cloud component reads. On 401 the call refreshes the token
via tokenEndpoint (resolveToken({forceRefresh: true})) and retries
once. The merchant's secure_key never reaches the browser.
Endpoint host: defaults to admin.configento.app; plugins override
via WebshopConfig.assetBaseUrl (Magento template injects it from the
preview-context payload). Decoupled from cloudBaseUrl — asset
storage lives on the admin (Laravel), separate from the V3 cloud
(Java backend) that serves component data.
interface StoreAssetOpts {
/** Reserved for forward-compat. The V3 endpoint stores one canonical
* payload per asset; filename slugs aren't part of the URL. The
* parameter is accepted for source-compatibility with the v1
* signature and currently ignored. */
filename?: string;
}
// Typical saved-configuration mail-link flow:
const { id } = await inst.storeAsset();
const shareUrl = new URL(window.location.href);
shareUrl.searchParams.set('asset', id);
mailBody.replace('{{share}}', shareUrl.toString());
The host plugin owns the share URL composition — the bundle only knows the admin‑side asset URL, not your product‑page URL pattern.
Failure modes — typed errors. Every failure surfaces as a
Promise‑reject with a ConfigentoCloudError (exposed on the public
surface as Configento.CloudError). The host discriminates on
err.code and reads err.retryable for the policy hint:
err.code |
When it fires | retryable |
|---|---|---|
NO_SELECTIONS |
storeAsset() called before any Characteristic is populated. |
false |
CONFIG_MISSING |
The mount has no WebshopConfig (admin-iframe preview, unit-test, or loadAsset with no instance mounted). |
false |
HTTP_4XX |
Admin returned 4xx (auth expired and refresh failed, payload too big, malformed body). err.status carries the code. |
false |
HTTP_5XX |
Admin returned 5xx. Transient — host can retry with backoff. err.status carries the code. |
true |
BAD_RESPONSE |
Admin returned 200 OK but the body wasn't a valid {id, url} envelope (malformed JSON, non-object). |
false |
NETWORK |
Browser-level fetch failure (offline, CORS, certificate, DNS). Transient. | true |
NOT_FOUND |
loadAsset(id) got 404. Asset doesn't exist (expired, never stored, or wrong customer context). |
false |
try {
const { id, url } = await inst.storeAsset();
} catch (err) {
if (!(err instanceof Configento.CloudError)) throw err; // unexpected
switch (err.code) {
case 'NO_SELECTIONS': return showInfo('Bitte erst konfigurieren.');
case 'CONFIG_MISSING': return reportSetupBug(err);
case 'NOT_FOUND': return showInfo('Konfiguration nicht mehr verfügbar.');
default:
if (err.retryable && attempts < 3) return retryWithBackoff();
return showError(`Cloud-Fehler (${err.code}): ${err.message}`);
}
}
.message strings stay word-identical to the previous untyped throws
('storeAsset: no selections to store', 'storeAsset failed: HTTP 503 at …', etc.), so any legacy regex-on-message code keeps working
alongside the new .code checks — Configento.CloudError extends Error.
The bundle does not auto-retry on 5xx — a duplicate upload would
produce a second share token and (for PDF-action flows that chain off
storeAsset) potentially a second mail / second metered token. The
retryable hint is for the host to act on, not a behaviour the bundle
implements itself.
Configento.loadAsset(id, opts?): Promise<AssetPayload>
Top-level (not per-instance) — fetch a previously stored asset by id
from the V3 admin's GET /api/v1/storefront/assets/{id} endpoint.
Unauthenticated by design — the UUID id is the share token, so
AskQuestion-mail recipients don't need credentials to open the link.
interface LoadAssetOpts {
/** Pick a specific instance when the page hosts several
* configurators. Defaults to `Configento.first()`. */
instance?: ConfiguratorInstance;
/** Reserved for forward-compat — the V3 endpoint addresses assets
* by UUID alone. Accepted for source-compatibility with v1. */
filename?: string;
}
// Typical mail-link landing flow:
const url = new URL(window.location.href);
const assetId = url.searchParams.get('asset');
if (assetId) {
await Configento.ready;
const payload = await Configento.loadAsset(assetId);
Configento.first().restoreFromAssetJson(payload);
}
Caller decides whether to call inst.restoreFromAssetJson(payload) —
loadAsset is a pure read (no side effects, no auto-restore).
Rejects with Configento.CloudError — see the typed-error table on
storeAsset() above for the full code list. The codes that can fire
on loadAsset specifically: CONFIG_MISSING (no instance mounted or
mounted instance has no WebshopConfig), NOT_FOUND (404), HTTP_5XX
/ NETWORK (retryable), HTTP_4XX, BAD_RESPONSE.
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. -
Legacy helper + cloud-asset globals: installs two namespaces that the saved-configuration / share-URL paths in older storefronts read from:
window.Justselling_Configento_App_Helper = { getConfigurator: () => Configento.first(), getTemplateId: () => Configento.first()?.componentId ?? null, getCustomerId: () => Configento.first()?.config?.customer ?? null, getBackendUrl: () => Configento.first()?.config?.cloudBaseUrl ?? null, }; window.Justselling_Configento_App_CloudAsset = { storeCloudAsset: (value, type, cb, filename) => { // routes onto inst.storeAsset({filename}), then invokes the // legacy success-callback signature (assetsId, folder, type, file) }, getAssets: () => [], };The install uses
??=per-method, so a host shop that has already defined either namespace stays authoritative — the compat layer fills in only what the host left empty. This means an existing Magento theme that callsJustselling_Configento_App_CloudAsset.storeCloudAsset(...)keeps working unchanged after the V3 rollout — no hard cut, no coordinated theme update day required.Migration caveats for theme code that does eventually move off the legacy namespaces:
- Success-callback shape changed: the legacy callback received
(assetsId, folder, type, file). V3 surfaces only{id, url}— the compat shim invokes the callback with(id, null, type, null). Theme code that readsfolderorfilemust migrate toinst.storeAsset()and consumeurl. getAssets()is a stub that returns[]. Legacy maintained a per-session asset list; V3's model is one asset perstoreAsset()call (fresh id each time). Theme code that readsgetAssets()must migrate to keeping its own list of returned{id, url}pairs.- Idempotent install: re-loading the compat bundle is safe (the
??=guards prevent double-install), but it does not overwrite globals the host shop defined first. If the shop ships its ownJustselling_Configento_App_Helper.getTemplateId(some custom Magento modules do), that host implementation wins.
- Success-callback shape changed: the legacy callback received
The compat layer is a no-op when none of the legacy globals exist — 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 call
inst.storeAsset()/Configento.first()/inst.on('change' | 'price' | …, cb)directly. The compat layer will stay around for the legacy surface but isn't where new APIs get added.
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.