Configento V3 Frontend API — Configento Docs v3/docs/configento-v3-frontend-api.md

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.

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

  1. Drop a mount element with the data-configento attribute 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>
    
  2. Wait for the bundle to finish booting before touching the API:

    await window.Configento.ready;
    const inst = [...window.Configento.instances.values()][0];
    
  3. 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, getSelections
  • getPriceBreakdown
  • getAssetPayload, getHeaderValue
  • getSummary

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 change event, 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 found vs alt_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. setText on a radiobuttons Char) → 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

  1. data-configento-config attribute on the mount element — a JSON-encoded WebshopConfig. Most common for plugins that emit one script-tag per product.

    <div data-configento
         data-configento-config='{"componentId":"10738","customer":"32929","currency":"EUR"}'></div>
    
  2. 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. taxRatedata-configento-tax-rate). The exception is componentId which uses the existing data-component-id slot.

    <div data-configento
         data-component-id="10738"
         data-configento-customer="32929"
         data-configento-tax-rate="0.19"
         data-configento-locale="de_DE"></div>
    
  3. 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:

  1. Mount selector: if a legacy #configurator1 element exists without [data-configento], the compat layer adds the attribute in-place so the V3 bundle picks it up. Missing data-component-id is filled from window.CONFIGENTO_PRODUCT.getTemplateId().

  2. Custom events: subscribes to each instance's change event and re-emits the legacy events on document. Theme code listening for configentoUpdateOptionsInDomBefore / …After (and the matching header pair) keeps working:

    document.addEventListener('configentoUpdateOptionsInDomAfter', () => {
        // theme code that rehydrates a sub-DOM after every config change
    });
    

    The Before events fire synchronously, the After events fire on the next microtask — matching the legacy ordering where the engine's own render happened between the two.

  3. Price callback: forwards every price event to window.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.

  4. 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 calls Justselling_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 reads folder or file must migrate to inst.storeAsset() and consume url.
    • getAssets() is a stub that returns []. Legacy maintained a per-session asset list; V3's model is one asset per storeAsset() call (fresh id each time). Theme code that reads getAssets() 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 own Justselling_Configento_App_Helper.getTemplateId (some custom Magento modules do), that host implementation wins.

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.