Theme JavaScript or another Shopify app on the same page can read the live calculator and set option values. There is no API key — any script on the storefront can use this surface, same as other custom JavaScript (protect against XSS).
This page covers the events to listen for, how to read values (including quantity), and how to set a value. Use it from a theme script, another app’s script, or the browser console. If you are writing a Global script inside Calculator Editor → Placement & Styling → JavaScript, Appify CPO runs that script for you — see Custom JavaScript.
Attach listeners on document. The calculator dispatches these custom events:
| Event | When it fires | Typical use |
|---|---|---|
calc:ready |
The calculator has loaded | Start listening, or call window.CalcAI.setValue |
calc:change |
After every price/value update | Read current option values, quantity, and totals |
calc:customJsReady |
This calculator’s editor Custom JS has finished startup | Theme or third-party scripts that depend on those scripts |
How to listen — pass the event name and a function. The payload is on ev.detail:
document.addEventListener("calc:ready", (ev) => {
const { instanceId, calculatorId } = ev.detail || {};
console.log("Calculator ready", calculatorId, instanceId);
});
document.addEventListener("calc:change", (ev) => {
const { values, totals, external } = ev.detail || {};
if (external) return; // skip changes your own script just made
console.log("Values", values);
console.log("Total", totals && totals.total);
});
Add these listeners once (for example in a theme JS file or a Custom HTML / Custom Liquid section). You do not need to query a calculator element first.
If your theme script must wait until editor Custom JS has finished, listen for calc:customJsReady instead of calc:ready, or call window.CalcAI.isCustomJsReady(calculatorId). Do not listen for calc:customJsReady inside a Global script in the editor — that script is already running at that moment.
calc:change includes| Field | What it is |
|---|---|
values |
Current option values, keyed by each option’s Key (not the customer-facing label). Example: { WIDTH: 48, QUANTITY: 25, COLOR: "red" } |
totals |
Pricing snapshot. Common fields: total, subtotal, discountAmount, orderFees |
spec |
Calculator spec, including spec.options (id, key, type, label) |
summary |
Payload used by price-summary blocks, or null |
reason |
Why it fired — "recompute", "draw", or "external-set" |
external |
true when the change came from CalcAI.setValue / calcai:setValue |
instanceId |
Instance id (often null on a single-calculator page) |
calculatorId |
Calculator id |
productId |
Linked product id when present |
Find each option’s Key in Calculator Editor → Options & Pricing (the Key field on the option).
Quantity is a normal option. Read it from values using that option’s key. New quantity options usually use the key QUANTITY.
document.addEventListener("calc:change", (ev) => {
const values = ev.detail?.values || {};
const quantity = Number(values.QUANTITY);
if (!Number.isFinite(quantity)) return;
console.log("Quantity", quantity);
});
If you renamed the key, look up the quantity option by type, then read that key:
document.addEventListener("calc:change", (ev) => {
const detail = ev.detail || {};
const options = detail.spec?.options || [];
const qtyOpt = options.find((o) => String(o.type || "").toLowerCase() === "quantity");
if (!qtyOpt) return;
const quantity = Number(detail.values?.[qtyOpt.key]);
console.log("Quantity", quantity, "key", qtyOpt.key);
});
There is no separate getValue or getQuantity helper. Keep the last calc:change payload in a variable if another function needs the current quantity later.
Wait until the calculator is ready, then call window.CalcAI.setValue. optionId can be the option Key or the option id.
document.addEventListener("calc:ready", () => {
const out = window.CalcAI.setValue({
optionId: "WIDTH",
value: "48",
});
if (!out?.ok) console.warn(out?.code, out?.error);
});
You can also check typeof window.CalcAI?.setValue === "function" before calling. The function returns { ok: true } or { ok: false, error, code }.
The same update can be sent as an event. That path has no return value — use setValue when you need to handle errors:
document.dispatchEvent(
new CustomEvent("calcai:setValue", {
bubbles: false,
detail: { optionId: "WIDTH", value: "48" },
})
);
| Option types | value to send |
|---|---|
| Dropdown, radio, button, swatch, image swatch | String matching a choice value / key / id |
| Switch | Boolean, or "true" / "false" |
| Number, slider, quantity | Number (or numeric string) |
| Text, textarea, email, color, and similar | String |
| Checkbox group / multi swatch | Array of strings |
| Dimensions | Object (or JSON object string) |
| File upload, modal | Not supported (UNSUPPORTED_TYPE) |
Pass instanceId and/or calculatorId from calc:ready (or from calc:change) so the call targets the right calculator. On a page with only one calculator, those fields are often null and the API still resolves the only instance.
document.addEventListener("calc:ready", (ev) => {
const { instanceId, calculatorId } = ev.detail || {};
window.CalcAI.setValue({
optionId: "QUANTITY",
value: 10,
instanceId,
calculatorId,
});
});
If several calculators are present and you omit the ids, setValue returns AMBIGUOUS_INSTANCE.
When your script sets a value, the next calc:change has detail.external === true and reason === "external-set". Ignore those events if you are echoing state to analytics, a chat widget, or another setter.
OPTION_HIDDEN).setValue is unavailable (NO_CALC_INSTANCE). Standard theme blocks use the full renderer.Returned on { ok: false } from window.CalcAI.setValue:
| Code | Meaning |
|---|---|
NO_CALC_INSTANCE |
No registered calculator (not loaded yet, or fallback runtime) |
AMBIGUOUS_INSTANCE |
Several calculators on the page and ids were missing |
INVALID_PAYLOAD |
Missing or invalid optionId / detail |
UNKNOWN_OPTION |
optionId did not match an option id or key |
OPTION_HIDDEN |
Option is hidden by visibility rules |
INVALID_CHOICE |
Value is not an allowed choice |
UNSUPPORTED_TYPE |
Type cannot be set (file, modal) |
COERCION_FAILED |
Value could not be converted to the expected type |
Bulk stickers — log quantity and line total as the shopper changes the calculator
document.addEventListener("calc:change", (ev) => {
const detail = ev.detail || {};
if (detail.external) return;
const quantity = Number(detail.values?.QUANTITY);
const total = detail.totals?.total;
console.log("Stickers", quantity, "total", total);
});
Custom blinds — set width from your theme after the calculator is ready
document.addEventListener("calc:ready", () => {
const out = window.CalcAI?.setValue({
optionId: "WIDTH",
value: 36,
});
if (!out?.ok) console.warn(out?.code, out?.error);
});
Engraved jewelry — set quantity to 2, then ignore the echo
document.addEventListener("calc:change", (ev) => {
if (ev.detail?.external) return;
const qty = Number(ev.detail?.values?.QUANTITY);
console.log("Shopper quantity", qty);
});
document.addEventListener("calc:ready", () => {
window.CalcAI?.setValue({ optionId: "QUANTITY", value: 2 });
});
ctx change handlers