A subscription interval from the quiz's formula
Goal: the quiz computes how long the product lasts this shopper (“a bag lasts you 24 days”), and the cart gets the recommended product on the subscription plan whose interval is the closest, once the shopper confirms.
Uses: Storefront JS API (quiz.finished with formulas and products[]) and Shopify’s Ajax cart API (POST /cart/add.js with selling_plan). Nothing to install on the quiz side; no API key. Events fire on the Plus and Enterprise plans.
What the finish event carries
formulas: formula component key to value; a numeric formula is a number. A formulaformula-daysthat divides the bag size by the daily dose gives24.products[]: every product card the session showed, withpage_key,product_id,variant_idand theproductobject.product.selling_plans[]lists the product’s subscription plans as{ id, name }(gid://shopify/SellingPlan/...), and eachproduct.variants[]its own.
Shopify names a plan (“Deliver every 4 weeks”) but gives no interval as a number, so the interval per plan is a table you write, from the selling plan ids in the product’s JSON (GET /products/<handle>.js, selling_plan_groups[].selling_plans[].id) or the admin. Selling plan ids are numeric on the Ajax API; the JS API’s product.selling_plans[].id is the GID with the same number.
The page
The script reads the formula, picks the result page’s first product card, takes its variant (variant_id when the card is locked to one, else the product’s selected_variant_id), chooses the plan with the nearest interval among the plans that product offers, shows a one-line confirmation, and only on the click adds the variant with that plan.
<div id="quiz-subscription" hidden> <p id="quiz-subscription-text"></p> <button type="button" id="quiz-subscription-yes">Subscribe</button> <button type="button" id="quiz-subscription-no">Buy once</button></div><script> window.octaneai = window.octaneai || []; window.octaneai.push(function (octaneai) { var FORMULA = 'formula-days'; var INTERVAL_DAYS = { // your plans: selling plan GID -> delivery interval in days 'gid://shopify/SellingPlan/689500000001': 14, 'gid://shopify/SellingPlan/689500000002': 28, 'gid://shopify/SellingPlan/689500000003': 56 };
function numeric(gid) { return String(gid).split('/').pop(); }
function nearestPlan(days, offered) { var ids = Object.keys(INTERVAL_DAYS).filter(function (id) { return !offered.length || offered.indexOf(id) !== -1; // the plans this product has, when the card lists them }); return ids.sort(function (a, b) { return Math.abs(INTERVAL_DAYS[a] - days) - Math.abs(INTERVAL_DAYS[b] - days); })[0] || null; }
function addToCart(variantId, planId) { return fetch('/cart/add.js', { method: 'POST', credentials: 'same-origin', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ items: [planId ? { id: numeric(variantId), quantity: 1, selling_plan: numeric(planId) } : { id: numeric(variantId), quantity: 1 }] }) }).then(function (r) { if (!r.ok) throw new Error('cart add ' + r.status); return r.json(); }); }
octaneai.on('quiz.finished', function (event) { var o = event.data.object; var days = Number(o.formulas[FORMULA]); if (!(days > 0)) return; var pick = o.products.filter(function (p) { return p.page_key === o.terminal_page && p.product; })[0]; if (!pick) return; var variantId = pick.variant_id || pick.product.selected_variant_id; // the card's variant, else the product's first available one if (!variantId) return; var offered = (pick.product.selling_plans || []).map(function (sp) { return sp.id; }); var plan = nearestPlan(days, offered); if (!plan) return;
var box = document.getElementById('quiz-subscription'); document.getElementById('quiz-subscription-text').textContent = 'A bag lasts you about ' + Math.round(days) + ' days. Deliver every ' + INTERVAL_DAYS[plan] + ' days?'; box.hidden = false; document.getElementById('quiz-subscription-yes').onclick = function () { addToCart(variantId, plan).then(function () { box.hidden = true; }, function (e) { console.warn(e); }); }; document.getElementById('quiz-subscription-no').onclick = function () { addToCart(variantId, null).then(function () { box.hidden = true; }, function (e) { console.warn(e); }); }; }); });</script>/cart/add.js takes the variant’s numeric id in id and the selling plan’s numeric id in selling_plan (Ajax cart API: “Its value must be the selling plan ID”). Shopify answers 422 with a message when the variant is not on that plan or is sold out; the theme’s cart drawer does not repaint by itself after a scripted add, so show your own confirmation or reload the cart section.
Things to know
- Ask before subscribing. A subscription is a commitment the shopper must choose; the control above offers the one-time purchase next to it. Never add a plan on a shopper’s click on the card: the card’s own add-to-cart (
cart.add_requested) carries no selling plan and cannot be cancelled, so hide the card’s button for these products (a result page without a call to action) and use this control. - The native Subscription Products integration already renders a plan picker on the card. Use this recipe when the interval is computed, not picked from a list.
formulas[key]isundefinedwhen the formula is not on the path the shopper took or its inputs were skipped; the script then does nothing. A formula that rounds in the editor still arrives as its number.products[]lists cards from every page; the filter onpage_key === terminal_pagekeeps the result page’s.variant_idisnullfor a product-level card, andproduct.selected_variant_idis then the first available variant, the one the card’s own button would add.productisnullwhen the quiz never received the product; the script skips those.- A plan the product does not carry is a
422from Shopify, so the table is filtered byproduct.selling_planswhen the card has them. Keep the table in step with the admin: a plan deleted there is a422too. - Never touch an existing subscription contract from the page. Changing a delivery interval on a running subscription is an Admin API job on your server.
