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
}

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 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.

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.

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.