Skip to content

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 formula formula-days that divides the bag size by the daily dose gives 24.
  • products[]: every product card the session showed, with page_key, product_id, variant_id and the product object. product.selling_plans[] lists the product’s subscription plans as { id, name } (gid://shopify/SellingPlan/...), and each product.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] is undefined when 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 on page_key === terminal_page keeps the result page’s. variant_id is null for a product-level card, and product.selected_variant_id is then the first available variant, the one the card’s own button would add. product is null when the quiz never received the product; the script skips those.
  • A plan the product does not carry is a 422 from Shopify, so the table is filtered by product.selling_plans when the card has them. Keep the table in step with the admin: a plan deleted there is a 422 too.
  • 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.