Coffee subscription builder

Build your coffee subscription

Choose how often you want your deliveries, select your preferred grind type and coffees. We’ll help build a subscription that suits your routine.

Choose your delivery frequency

Choose how often you’d like your coffee delivered. You can choose bag sizes for each coffee in the next step.

Do you need your coffee ground?

Whole beans are best if you have a grinder. Choose a ground option if you need us to grind it for your brew method.

Choose your coffees

Select a bag size for each coffee, then add it to your subscription.

Il Caramello Blend

Il Caramello Blend

Chocolate, caramel, almond

A smooth, balanced coffee for everyday brewing.

Tall, Dark & Handsome Blend

Tall, Dark & Handsome Blend

Dark chocolate, buttery nougat

Velvety chocolate and smooth buttery nougat.

Guatemalan Single Origin

Guatemalan Single Origin

Roast hazelnut, chocolate, red apple

Single origin perfect balanced acidity and milk chocolate flavours

Papua New Guinean Single Origin

Papua New Guinean Single Origin

Dark chocolate, fig, spices

Full-bodied with medium acidity - equally delicious black or with milk

Decaf Coffee

Decaf Coffee

Creamy chocolate & malt

Glacial mountain water washed and chemical free

Review your subscription

Check your coffees, delivery frequency, subscription price and order details before adding to cart.

Your coffees

Create a flexible coffee subscription based on how much coffee you drink, how you brew and the grind type you need. Choose your preferred coffee, select your delivery frequency and enjoy freshly roasted coffee delivered on your schedule.

${ unlocked ? 'Nice — you’ve unlocked free shipping on this subscription order.' : `You’re ${formatMoney(remaining)} away from free shipping. Add another bag to unlock free shipping.` }
`; } function updateFreeShippingNudges() { updateFreeShippingElement(freeShippingNudge); updateFreeShippingElement(finalFreeShippingNudge); } function getRoutineGuidance() { const coffeesPerDelivery = getEstimatedCoffeesPerDelivery(); const coffeesPerDay = getEstimatedCoffeesPerDay(); const roundedPerDay = coffeesPerDay >= 1 ? coffeesPerDay.toFixed(1).replace('.0', '') : coffeesPerDay.toFixed(1); return `Approx. ${coffeesPerDelivery} coffees per delivery, or around ${roundedPerDay} coffees per day.`; } function updateContinueState() { nextButton.disabled = state.coffees.length === 0; } function setupLoopBundleDefaults() { const boxSizes = getBoxSizes(); const discounts = getDiscounts(); const total = getSelectedQuantityTotal(); const matchingBoxSize = boxSizes.find(box => { const min = Number(box.minSize || box.min || box.minimum || 0); const rawMax = Number(box.maxSize || box.max || box.maximum || 0); const max = rawMax === 0 ? 999 : rawMax; return total >= min && total <= max; }); const defaultBoxSize = matchingBoxSize || boxSizes.find(box => box.defaultSelected || box.isDefault) || boxSizes[0]; const defaultDiscount = discounts.find(discount => { const purchaseType = normalise(discount.purchaseType || discount.type || ''); return purchaseType.includes('subscription'); }) || discounts[0]; state.selectedBoxSizeId = getId(defaultBoxSize); state.selectedDiscountId = getId(defaultDiscount); console.log('Loop box sizes:', boxSizes); console.log('Loop discounts:', discounts); console.log('Selected boxSizeId:', state.selectedBoxSizeId); console.log('Selected discountId:', state.selectedDiscountId); } function renderReviewProducts() { if (!reviewProducts) return; if (!state.coffees.length) { reviewProducts.innerHTML = `
No coffees selected yet.
`; return; } const discountRate = getSubscriptionDiscountRate(); const discountPercent = Math.round(discountRate * 100); reviewProducts.innerHTML = state.coffees.map(coffee => { const quantity = getCleanQuantity(coffee.quantity); const regularUnitPrice = Number(coffee.price || 0); const subscriptionUnitPrice = regularUnitPrice * (1 - discountRate); const subscriptionLinePrice = subscriptionUnitPrice * quantity; return `
${ coffee.imageSrc ? `${escapeHtml(coffee.imageAlt || coffee.title)}` : `
Coffee
` }

${escapeHtml(coffee.title)}

${escapeHtml(coffee.variantTitle)}

${formatMoney(subscriptionUnitPrice)} subscription price ${formatMoney(regularUnitPrice)} Save ${discountPercent}%
This delivery: ${formatMoney(subscriptionLinePrice)}
${quantity}
`; }).join(''); } function updateFinalSummary() { renderReviewProducts(); const coffeeList = state.coffees.length ? state.coffees.map(coffee => `${coffee.quantity} × ${coffee.title} — ${coffee.size}`).join(', ') : 'No coffees selected yet'; const totalGrams = getSelectedTotalGrams(); const routineGuidance = getRoutineGuidance(); finalSummary.innerHTML = ` Your coffee subscription:
Delivery: ${state.frequency}
Grind: ${state.grind}
Total coffee per delivery: ${formatCoffeeWeight(totalGrams)}
Coffees: ${escapeHtml(coffeeList)}

Estimated coffee:
${routineGuidance} `; updatePriceSummary(); updateFreeShippingNudges(); updateProgressState(); addToCartButton.disabled = !state.frequency || !state.sellingPlanShopifyId || state.coffees.length === 0; } function updatePriceSummary() { if (!priceSummary) return; const discountAmount = getDiscountAmount(); const total = getDiscountedTotal(); priceSummary.innerHTML = `
Subscription summary: You saved ${formatMoney(discountAmount)}! Recurring subscription total: ${formatMoney(total)}
`; } async function readLoopBundleDetails() { if (!state.loopBundleId) { throw new Error('Missing Loop Bundle ID.'); } const url = `https://api.loopsubscriptions.com/storefront/2023-10/bundle/${state.loopBundleId}`; const response = await fetch(url, { method: 'GET', headers: { 'accept': 'application/json' } }); const json = await response.json(); console.log('Loop bundle details response:', json); if (!response.ok || json.success === false) { throw new Error(json.message || 'Could not load Loop bundle details.'); } state.loopBundle = json.data || json; return state.loopBundle; } async function createLoopBundleTransaction() { setupLoopBundleDefaults(); if (!state.selectedBoxSizeId) { throw new Error('Loop boxSizeId was not found. Check your Loop bundle setup.'); } if (!state.selectedDiscountId) { throw new Error('Loop discountId was not found. Check your Loop bundle setup.'); } if (!state.sellingPlanShopifyId) { throw new Error('Selling plan was not found for the selected frequency.'); } if (!state.coffees.length) { throw new Error('Please select at least one coffee before adding to cart.'); } const selectedVariants = state.coffees.map(coffee => { return { title: coffee.title, variantShopifyId: Number(coffee.variantId), quantity: getCleanQuantity(coffee.quantity) }; }); const totalQuantity = selectedVariants.reduce((sum, item) => { return sum + Number(item.quantity || 0); }, 0); const payload = { boxSizeId: String(state.selectedBoxSizeId), discountId: String(state.selectedDiscountId), sellingPlanShopifyId: Number(state.sellingPlanShopifyId), quantity: totalQuantity, productVariants: selectedVariants.map(item => ({ shopifyId: Number(item.variantShopifyId), quantity: Number(item.quantity) })) }; console.log('GCB VERSION: loop-transaction-working-stable-drawer'); console.log('Create Loop transaction payload:', JSON.stringify(payload, null, 2)); const url = `https://api.loopsubscriptions.com/storefront/2023-10/bundle/${state.loopBundleId}/transaction`; const response = await fetch(url, { method: 'POST', headers: { accept: 'application/json', 'content-type': 'application/json' }, body: JSON.stringify(payload) }); const json = await response.json(); console.log('Loop transaction response:', JSON.stringify(json, null, 2)); if (!response.ok || json.success === false) { throw new Error(json.message || 'Could not create Loop bundle transaction.'); } const transactionId = json?.data?.txnId; if (!transactionId) { throw new Error('Loop transaction was created, but no txnId was returned.'); } return transactionId; } async function addBundleToCart(transactionId) { const items = state.coffees.map(coffee => { return { id: Number(coffee.variantId), quantity: getCleanQuantity(coffee.quantity), selling_plan: Number(state.sellingPlanShopifyId), properties: { _bundleId: String(transactionId), _bundleVariantId: String(state.loopBundleId), _bundleSource: 'guided_coffee_bundle', _bagSize: String(coffee.size), _grind: String(coffee.grind), _frequency: String(state.frequency) } }; }); const payload = { items, sections: [ 'cart-drawer', 'cart-icon-bubble' ], sections_url: window.location.pathname }; console.log('Shopify cart payload:', payload); const response = await fetch('/cart/add.js', { method: 'POST', headers: { accept: 'application/json', 'content-type': 'application/json' }, body: JSON.stringify(payload) }); const json = await response.json(); console.log('Shopify cart response:', json); if (!response.ok) { throw new Error(json.description || json.message || 'Could not add subscription bundle to cart.'); } return json; } async function openCartDrawer(cartAddResponse) { try { sessionStorage.setItem('gcb_open_cart_drawer_after_reload', 'true'); window.location.href = window.location.href; } catch (error) { console.error('Could not refresh cart drawer:', error); window.location.href='/cart'; } } function openCartDrawerAfterReload() { const shouldOpen = sessionStorage.getItem('gcb_open_cart_drawer_after_reload'); if (shouldOpen !== 'true') return; sessionStorage.removeItem('gcb_open_cart_drawer_after_reload'); setTimeout(() => { const cartDrawer = document.querySelector('cart-drawer') || document.querySelector('#CartDrawer') || document.querySelector('[data-cart-drawer]') || document.querySelector('.cart-drawer'); if (cartDrawer) { if (typeof cartDrawer.open === 'function') { cartDrawer.open(); return; } if (typeof cartDrawer.show === 'function') { cartDrawer.show(); return; } cartDrawer.classList.add('active', 'is-open', 'animate'); cartDrawer.setAttribute('open', ''); cartDrawer.removeAttribute('aria-hidden'); document.body.classList.add('overflow-hidden'); document.body.classList.add('cart-drawer-open'); return; } const cartDrawerTrigger = document.querySelector('[aria-controls="CartDrawer"]') || document.querySelector('[data-cart-drawer-toggle]') || document.querySelector('[data-cart-toggle]') || document.querySelector('a[href="/cart"]') || document.querySelector('button[name="cart"]'); if (cartDrawerTrigger) { cartDrawerTrigger.click(); } },1500); } root.addEventListener('click', async event => { const button = event.target.closest('button'); if (!button) return; if (button.dataset.progress) { hideStatus(); const requestedStep = Number(button.dataset.progress); if (!canNavigateToStep(requestedStep)) { if (requestedStep === 2) { showStatus('Choose your delivery frequency first.', 'error'); } else if (requestedStep === 3) { showStatus('Choose your delivery frequency and grind first.', 'error'); } else if (requestedStep === 4) { showStatus('Add at least one coffee before reviewing your subscription.', 'error'); } return; } if (requestedStep === 3) { updateAllProductCards(); } if (requestedStep === 4) { try { setupLoopBundleDefaults(); updateFinalSummary(); } catch (error) { showStatus(error.message, 'error'); return; } } setStep(requestedStep); return; } const currentStep = root.querySelector(`[data-step="${state.step}"]`); if (button.hasAttribute('data-back')) { hideStatus(); setStep(Math.max(1, state.step - 1)); return; } if (button.dataset.frequency) { hideStatus(); clearSelectedOptions(currentStep); button.classList.add('is-selected'); state.frequency = button.dataset.frequency; state.selectedDeliveryInterval = button.dataset.deliveryInterval; state.selectedDeliveryCount = button.dataset.deliveryCount; state.monthlyMultiplier = Number(button.dataset.monthlyMultiplier || 0); const plan = findSellingPlanByDelivery( button.dataset.deliveryInterval, button.dataset.deliveryCount ); const sellingPlanId = getShopifySellingPlanId(plan); if (!plan || !sellingPlanId) { state.selectedSellingPlan = null; state.sellingPlanShopifyId = null; showStatus( `Could not find a Loop selling plan matching ${state.frequency}.`, 'error' ); return; } state.selectedSellingPlan = plan; state.sellingPlanShopifyId = sellingPlanId; state.brew = ''; state.grind = ''; setStep(2); const grindStep = root.querySelector('[data-step="2"]'); if (grindStep) { grindStep.querySelectorAll('.gcb-option').forEach(option => { option.classList.remove('is-selected'); }); } return; } if (button.dataset.grind) { hideStatus(); clearSelectedOptions(currentStep); button.classList.add('is-selected'); state.brew = button.dataset.brew; state.grind = button.dataset.grind; remapSelectedCoffeesForCurrentGrind(); updateAllProductCards(); setStep(3); return; } if (button.hasAttribute('data-continue-grind')) { hideStatus(); if (!state.grind) { state.brew = 'I have a grinder'; state.grind = 'Whole beans'; } updateAllProductCards(); setStep(3); return; } if (button.dataset.cardSize) { hideStatus(); const card = button.closest('[data-product-card]'); if (!card) return; setCardSize(card, button.dataset.cardSize, button.dataset.cardGrams); return; } if (button.hasAttribute('data-select-coffee')) { hideStatus(); const card = button.closest('[data-product-card]'); setCoffeeQuantity(card, 1); return; } if (button.hasAttribute('data-increase-coffee')) { hideStatus(); const card = button.closest('[data-product-card]'); updateCoffeeQuantity(card, 1); return; } if (button.hasAttribute('data-decrease-coffee')) { hideStatus(); const card = button.closest('[data-product-card]'); updateCoffeeQuantity(card, -1); return; } if (button.hasAttribute('data-next')) { hideStatus(); try { setupLoopBundleDefaults(); updateFinalSummary(); setStep(4); } catch (error) { showStatus(error.message, 'error'); } return; } if (button.dataset.reviewIncrease) { hideStatus(); const coffee = getCoffeeByVariantId(button.dataset.reviewIncrease); if (coffee) { coffee.quantity = getCleanQuantity(coffee.quantity) + 1; const card = getCardByTitle(coffee.title); if (card) updateProductCardDisplay(card); updateFinalSummary(); } return; } if (button.dataset.reviewDecrease) { hideStatus(); const coffee = getCoffeeByVariantId(button.dataset.reviewDecrease); if (coffee) { coffee.quantity = getCleanQuantity(coffee.quantity) - 1; if (coffee.quantity <= 0) { state.coffees = state.coffees.filter(item => Number(item.variantId) !== Number(coffee.variantId)); } const card = getCardByTitle(coffee.title); if (card) updateProductCardDisplay(card); updateFinalSummary(); } return; } if (button.dataset.reviewRemove) { hideStatus(); const coffee = getCoffeeByVariantId(button.dataset.reviewRemove); if (coffee) { state.coffees = state.coffees.filter(item => Number(item.variantId) !== Number(coffee.variantId)); const card = getCardByTitle(coffee.title); if (card) updateProductCardDisplay(card); updateFinalSummary(); } return; } if (button.hasAttribute('data-add-more-coffee')) { hideStatus(); updateAllProductCards(); setStep(3); return; } if (button.hasAttribute('data-add-to-cart')) { hideStatus(); try { button.disabled = true; button.textContent = 'Adding...'; const transactionId = await createLoopBundleTransaction(); const cartAddResponse = await addBundleToCart(transactionId); showStatus('Subscription bundle added to cart.', 'success'); await openCartDrawer(cartAddResponse); button.disabled = false; button.textContent = 'Add subscription to cart'; } catch (error) { console.error(error); showStatus(error.message, 'error'); button.disabled = false; button.textContent = 'Add subscription to cart'; } } }); async function init() { try { showStatus('Loading subscription options...', ''); await readLoopBundleDetails(); hideStatus(); updateProgressState(); console.log('Guided coffee bundle ready'); console.log('Loop Bundle ID:', state.loopBundleId); console.log('Shopify domain:', state.myshopifyDomain); } catch (error) { console.error(error); showStatus(error.message, 'error'); } } init(); openCartDrawerAfterReload(); })();

Subscription FAQS

How does subscription work?

Choose how often you want coffee delivered, select whether you want whole beans or ground coffee, then build your subscription by choosing your favourite coffees and bag sizes. Your subscription will be delivered and charged based on the frequency you select.

Can I choose different bag sizes in one subscription?

Yes. You can mix bag sizes in the same subscription. For example, you can choose one coffee as a 1kg bag and another coffee as a 250g or 500g bag.

Can I change the coffees in my subscription later?

Yes. You can change products anytime from your customer portal. You can also top up your order, skip, pause, delay or bring forward deliveries.

Is there a minimum subscription period?

Yes. There is a minimum of 3 orders before cancellation. You can still skip, pause, delay, bring forward, top up or change products from the start.

Can I skip or pause a delivery?

Yes. You can skip, pause, delay or bring forward deliveries anytime from your customer portal.

Can I get my coffee ground?

Yes. You can choose whole beans or select a grind option for your brew method, including espresso machine, stovetop, plunger, AeroPress, filter/drip or cold brew.

How many coffees are in each bag?

As a guide, a 250g bag makes around 15 coffees, a 500g bag makes around 31 coffees, and a 1kg bag makes around 62 coffees. This is based on approximately 16g of coffee per cup.

Do subscriptions qualify for free shipping?

Yes. Subscription orders over $99 qualify for free shipping. The builder will show your progress toward free shipping before checkout.

What is included with my coffee subscription?

Your subscription includes flexible coffee deliveries, 10% off, access to manage your subscription through the customer portal, and free access to our Kickstart Your Coffee Online Course.

Will I be charged monthly?

You’ll be charged based on the delivery frequency you choose. For example, if you choose delivery every 2 weeks, your subscription follows that selected frequency.

Person placing a 'Grounds of Origin' coffee bag into a box for delivery