Last updated:

How to Fix Broken Upsell Logic in Your Subscription Portal

How to Fix Broken Upsell Logic in Your Subscription Portal

Allen Finn

Head of Marketing

@

Skio

Table of Contents

Book a Demo

Your subscription customers are clicking "add to order" and seeing products they already receive. Or worse — they're being pitched items that can't ship on their delivery schedule. The upsell module you spent three months building is showing a 2% attach rate because the recommendation logic treats recurring orders like one-time purchases.

Broken upsell logic stems from static product filtering that ignores subscription context — frequency, variant compatibility, and customer purchase history.

Why generic recommendation engines fail for subscriptions

Standard ecommerce recommendation engines assume one-time purchases. They're built to answer "what should this customer buy next?" not "what should we add to their recurring order?"

Generic recommendation engines fail for subscriptions because they treat recurring orders like one-time purchases, ignoring frequency compatibility and subscription-specific product catalogs.

Most platforms use collaborative filtering — "customers who bought X also bought Y" — without any subscription-aware filtering layer. The algorithm sees purchase correlation but can't distinguish between:

  • Products the customer bought once vs. products they receive every month

  • SKUs available for subscription vs. one-time-only items

  • Variants compatible with the customer's current delivery frequency vs. products that ship on different schedules

Customers see upsells for products already in their active subscription. They see one-time-only SKUs that can't be added to recurring orders. They see products that would require splitting their subscription into multiple shipments.

Your recommendation engine doesn't understand what a subscription is.

The technical symptom: your API calls return product recommendations without querying the customer's active subscription contract. You're filtering against purchase history (past orders) instead of subscription state (current recurring line items). Those are fundamentally different datasets.

Shopify's native product recommendation API won't save you. It's designed for one-time commerce. It doesn't know which products have selling plans attached. It can't filter by subscription frequency. It will confidently recommend a product the customer started subscribing to yesterday.

If you're using a headless commerce setup, the problem compounds — now you're stitching together product data from your commerce platform, subscription data from your subscription app, and recommendation logic from a third service. None of them speak the same language about what "compatible" means.

The technical anatomy of subscription upsell logic

Build three sequential filtering layers. Each layer answers a different question. Skip one and your recommendations break.

Effective subscription upsell logic requires three sequential filters: subscription eligibility, contextual relevance, and delivery compatibility.

Layer 1: Product eligibility filtering

Is this SKU available as a subscription? Not "does this product exist in our catalog" — does it have a selling plan association that matches the customer's current subscription type?

In Shopify's data model, this means checking sellingPlanAllocations on the product variant. If the variant doesn't have a selling plan that matches the customer's subscription contract, it's ineligible. Filter it out before you do anything else.

Most platforms store this as product metadata or tags. Skio uses selling plan groups to define which products can be added to which subscription types. If you're building custom logic, you need an API endpoint that returns subscription-eligible variants filtered by the customer's current selling plan ID.

Layer 2: Contextual ranking

Does this product make sense given what's already in the subscription?

Query their active subscription contract, extract the variant IDs of current line items, and remove those variants from your recommendation pool.

Go deeper: if a customer subscribes to "Coffee - Medium Roast - 12oz," don't show them "Coffee - Medium Roast - 16oz" unless you're explicitly offering an upgrade path. Filter at the product level, not just the variant level, when variants represent size or quantity differences rather than true product variety.

Contextual ranking also means respecting product relationships. If a customer subscribes to a coffee subscription, recommend complementary products (filters, mugs, flavor syrups) before recommending more coffee. Use product tags, collections, or manual merchandising rules to define these relationships.

Layer 3: Delivery compatibility check

Can this product ship on the same frequency as the customer's existing subscription without creating fulfillment conflicts?

If the customer has a 30-day subscription and you recommend a product that only ships every 60 days, what happens? Does your system automatically split the subscription into two separate orders? Does it force a frequency change? Does it just fail silently at checkout?

Most platforms don't handle this gracefully. You need explicit logic: either restrict recommendations to products that match the customer's current frequency, or flag frequency mismatches and present a clear upgrade path ("Add this item and change your delivery to every 60 days").

One-time add-ons bypass this logic entirely — they attach to the next order only and don't affect the recurring schedule. Skio's one-time upsell system handles this automatically by distinguishing between recurring line items and one-off additions.

Common failure modes (and how to diagnose them)

Symptom

Root Cause

How to Test

Fix

Customers see products they already subscribe to

No purchase history filter at subscription contract level

Check if API calls include existing subscription line items in the exclusion logic

Exclude variant IDs present in active subscriptions before rendering recommendations

Upsells show one-time-only products

No selling plan association filter

Inspect product metadata for subscription availability; attempt to add a one-time SKU to a test subscription

Filter product catalog by sellingPlanAllocations that match customer's subscription type

Recommended products have incompatible frequencies

No delivery schedule logic

Create test subscriptions at different frequencies (7-day, 30-day, 60-day) and verify recommendations respect those intervals

Restrict recommendations to products with matching frequency options or implement frequency change flow

Upsells ignore customer preferences (flavor, scent, size)

No variant attribute filtering

Review past purchase data for attribute patterns; check if recommendation logic reads variant metadata

Build affinity scoring based on variant-level attributes (flavor, scent) and past purchase patterns

Recommendations don't update after subscription changes

Static recommendation caching

Add a product to a test subscription, then immediately reload the upsell module

Trigger recommendation refresh on every subscription modification event

Out-of-stock products appear in upsells

No inventory policy integration

Recommend a product, then set its inventory to zero and reload

Filter recommendations by inventoryQuantity > 0 or respect your platform's inventory policy settings

The most common failure: showing customers products they already subscribe to. This happens because your recommendation logic queries historical order data instead of current subscription state. Past orders include one-time purchases mixed with subscription orders. You need to query the subscription contract directly.

Test this by creating a subscription with Product A, then checking what your upsell module shows. If Product A appears in the recommendations, your filtering logic is broken at Layer 2.

What you actually need from your subscription platform's API

You can't fix broken upsell logic with front-end hacks or manual product tagging. You need real-time access to subscription contract data.

To build accurate subscription upsells, your platform must expose real-time subscription contract data, selling plan associations, and variant-level compatibility rules via API.

Required API endpoints:

  1. Active subscription contracts per customer — not order history, not "subscription status" — the actual contract object with current line items, frequencies, next charge date, and selling plan associations

  2. Product catalog filtered by selling plan — the ability to query "show me all products available on selling plan X" without manually tagging products or maintaining a separate eligibility list

  3. Variant-level metadata — attributes, tags, inventory levels, and selling plan IDs at the variant level (not just product level) because subscription eligibility often varies by size, flavor, or quantity

What most platforms don't expose:

  • Subscription frequency at the contract level (they store it as an order attribute, not a contract property)

  • Add-on eligibility rules (which products can be added to which subscription types)

  • Variant compatibility matrices (which flavors or sizes are mutually exclusive)

Without these, you're building recommendation logic on incomplete data. You'll ship features that work in testing (because you manually curated the test cases) but break in production (because real customer subscriptions have edge cases your logic doesn't handle).

Skio's API exposes all of this via GraphQL. You can query a customer's active subscriptions with full line item detail, filter products by selling plan associations, and check delivery compatibility before rendering recommendations. The API returns subscription contracts as first-class objects, not derived from order history.

If your current platform doesn't expose this data, you're not just building recommendation logic — you're also building the data layer that makes recommendations possible. Budget 6-8 weeks of API work before you write a single line of recommendation code.

How Skio's Smart Upsell solves this without custom dev work

Smart Upsell applies all three filtering layers automatically. No API calls. No custom logic. No ongoing maintenance when your product catalog changes.

It reads the customer's active subscription (Layer 1), filters for compatible products (Layer 2), and ranks by purchase affinity (Layer 3). Merchants control the product pool via Shopify collections or product tags — the engine handles eligibility filtering, variant exclusions, and frequency matching.

Edge cases most dev teams miss:

  • Variant-level exclusions: If a customer subscribes to "Vanilla Protein Powder," Smart Upsell won't recommend "Vanilla Protein Powder" in a different size — it recommends complementary flavors or entirely different product categories

  • Frequency mismatches: Products that don't match the customer's delivery schedule are automatically filtered out (or flagged for frequency change if you enable that option)

  • Inventory constraints: Out-of-stock products never appear in recommendations, respecting your Shopify inventory policy settings

  • One-time vs. recurring logic: One-time add-ons are treated separately from recurring upsells — they attach to the next order only and don't affect the subscription frequency

Waterboy was showing customers products already in their subscriptions (classic Layer 2 failure). Upsell attach rate was 3%. After switching to Smart Upsell, they 4X'd add-on revenue in six months. Same customer base. Same product catalog. The only change: fixing the recommendation logic.

The problem wasn't lack of customer interest — broken filtering made the upsell experience feel spammy. When customers see contextually relevant recommendations (products that actually make sense with their current subscription), attach rates jump from 2-3% to 15-20%.

One-time add-ons work differently in Smart Upsell. These bypass frequency logic entirely because they're not recurring line items. Useful for seasonal products, samples, or limited-time offers that you want to attach to the next shipment without affecting the ongoing subscription. Skio's one-time upsell system handles the distinction automatically — merchants just tag products as one-time or recurring, and the logic adapts.

Building your own: a reference implementation

If you're building this in-house, here's the logic flow. Pseudocode-level detail — you'll need to adapt it to your platform's API structure.

Step 1: Fetch customer's active subscriptions

- subscription_id
- selling_plan_id
- line_items (array of variant_id + quantity)
- frequency_interval
- next_charge_date
- subscription_id
- selling_plan_id
- line_items (array of variant_id + quantity)
- frequency_interval
- next_charge_date
- subscription_id
- selling_plan_id
- line_items (array of variant_id + quantity)
- frequency_interval
- next_charge_date

You need the full contract object, not just subscription status. If your API returns "customer has 2 active subscriptions" without line item detail, you can't build Layer 2 filtering.

Step 2: Query product catalog for subscription-eligible SKUs

`GET /products?selling_plan_id={customer_selling_plan_id} Returns: Array of products where at least one variant has a selling plan allocation matching the customer's subscription type`

This filters out one-time-only products at the API level. If your platform doesn't support this query, you'll need to fetch the entire product catalog and filter client-side (slow, breaks at scale).

Step 3: Exclude products already in active subscriptions

`active_variant_ids = subscriptions.flatMap(sub => sub.line_items.map(item => item.variant_id)) eligible_products = eligible_products.filter(product => !product.variants.some(variant => active_variant_ids.includes(variant.id)) )`

Filter at the product level, not variant level, if variants represent size/quantity differences. A customer subscribing to "Coffee - 12oz" shouldn't see "Coffee - 16oz" unless you're explicitly offering an upgrade path.

Step 4: Filter for delivery compatibility

`customer_frequency = subscriptions[0].frequency_interval // assume single subscription for simplicity compatible_products = eligible_products.filter(product => product.variants.some(variant => variant.selling_plans.some(plan => plan.frequency_interval === customer_frequency) ) )`

If you support multiple subscriptions with different frequencies, this logic gets more complex. You need to decide: recommend products compatible with all subscriptions, or recommend products compatible with any subscription and handle frequency splitting?

Step 5: Rank by affinity

`ranked_products = compatible_products.sort((a, b) => { // Simple affinity scoring: // 1. Products in the same collection as current subscription items // 2. Products purchased together historically // 3. Products with matching tags (flavor profiles, product categories) return affinity_score(b) - affinity_score(a) })`

Affinity scoring is where you get creative. Use past purchase data, product tags, manual merchandising rules, or collaborative filtering on the eligible product set (not the entire catalog).

Step 6: Return top N results

`recommendations = ranked_products.slice(0, 4) // Render in customer portal`

Four recommendations is the sweet spot. More than that and customers get decision paralysis. Fewer and you're not showing enough variety.

Critical gotcha: You need to re-run this logic every time the customer modifies their subscription. Static recommendations go stale immediately. A customer who just added Product X shouldn't see Product X recommended 10 seconds later.

Implement this as a real-time API call triggered on subscription modification events, not a nightly batch job that pre-computes recommendations.

A custom subscription recommendation engine requires real-time filtering of subscription-eligible products, exclusion of active line items, and frequency-based compatibility checks.

Budget 4-6 weeks for the initial implementation, then ongoing maintenance as your product catalog evolves and edge cases emerge.

When to use rules-based logic vs. ML-powered recommendations

Rules-based filtering (the approach described above) works for most subscription brands. Deterministic, debuggable, doesn't require training data.

Rules-based filtering outperforms ML for most subscription brands because it enforces hard compatibility constraints that collaborative filtering models ignore.

ML-powered recommendations (collaborative filtering, neural networks) make sense at scale — 50K+ active subscribers with rich behavioral data. Below that threshold, you don't have enough signal to train a model that outperforms simple rules.

Why rules win for subscriptions:

  • Hard constraints matter more than soft preferences — a customer can't subscribe to a product that isn't subscription-eligible, no matter how much they might "like" it based on their purchase history

  • Subscription data is sparse — customers modify their subscriptions infrequently (every 3-6 months on average), so you don't have the interaction volume that collaborative filtering requires

  • Edge cases break ML models — a customer with a paused subscription shouldn't see recommendations at all, but a collaborative filtering model trained on active subscribers will still return results

If you do go ML, use a hybrid approach:

  1. Apply rules-based filtering to enforce subscription eligibility and compatibility (Layers 1-3)

  2. Apply ML ranking to the filtered result set (Layer 5 from the reference implementation)

This gives you the best of both worlds: hard constraints prevent broken recommendations, ML optimizes for conversion within the eligible set.

Make sure your training data includes subscription context. Standard ecommerce recommendation models won't transfer. You need features like:

  • Subscription duration (how long has the customer been subscribed?)

  • Churn risk (is this customer likely to cancel?)

  • Frequency preference (do they typically choose shorter or longer intervals?)

  • Product category affinity (do they add complementary products or stick to a single category?)

Without these, your ML model is just a fancy version of "customers who bought X also bought Y" — which doesn't understand subscriptions.

Most brands don't have this data at scale. If you're under 10K active subscribers, stick with rules-based logic. Faster to build, easier to debug, performs better because it respects the constraints that actually matter.

How to test your upsell logic before going live

Create test subscriptions with edge cases. Real customer subscriptions are messy — your logic needs to handle all of them.

Test subscription upsell logic by creating edge-case subscriptions and verifying that recommendations exclude active products, respect frequency constraints, and filter at the variant level.

Test case 1: Single-product subscription

Create a subscription with one product. Verify that product doesn't appear in recommendations. Verify that variants of that product (different sizes, flavors) are handled according to your filtering rules.

Test case 2: Multi-product bundle

Create a subscription with 3-4 products. Verify that none of those products appear in recommendations. Add a product via the upsell module, then verify it's excluded from future recommendations.

Test case 3: Paused subscription

Pause a subscription. Verify that recommendations either don't render at all (correct behavior) or show products that will be added when the subscription resumes (acceptable if you make that clear).

Test case 4: Subscription with one-time add-ons

Add a one-time product to a subscription. Verify that product appears in recommendations again after the next order ships (because it's not a recurring line item).

Test case 5: Frequency mismatches

Create a 30-day subscription. Verify that products only available on 60-day frequencies are either hidden or flagged as incompatible. Test what happens if the customer tries to add an incompatible product — does your system handle the frequency change gracefully?

Test case 6: Variant-level filtering

Create a subscription with "Chocolate Protein Powder." Verify that "Chocolate Protein Powder" in a different size is handled correctly (excluded if sizes are interchangeable, shown if you're offering an upgrade path). Verify that "Vanilla Protein Powder" is shown (different flavor = different product).

Load test with real data

Run your recommendation logic against 1,000 active subscriptions from production. Export the results to a spreadsheet. Manually review a random sample of 50 recommendations.

Look for false positives:

  • Products already in the customer's subscription

  • Products incompatible with the customer's frequency

  • Out-of-stock products

  • One-time-only products (unless you're intentionally showing those as one-time add-ons)

If your false positive rate is above 5%, your filtering logic has gaps. Debug before you ship.

Real-world impact: what fixing this actually unlocks

Waterboy had a 3% upsell attach rate. They were showing customers products already in their subscriptions. The recommendation logic was pulling from order history instead of subscription contracts.

After fixing the filtering logic (switching to Smart Upsell), they 4X'd add-on revenue in six months. Same customer base. Same product catalog. The only change: contextually relevant recommendations.

Fixing broken upsell logic can increase add-on attach rates from 2-3% to 15-20% by showing contextually relevant products instead of duplicates or ineligible SKUs.

The problem wasn't lack of customer interest — broken filtering made the experience feel spammy and irrelevant. When a customer sees a recommendation for a product they already receive every month, they lose trust in the upsell module. They stop checking it.

When recommendations are accurate (products that actually make sense given the customer's current subscription), the upsell module becomes useful. Customers actively use it to discover complementary products they didn't know you offered.

Your subscription portal should make it easier to expand an order than to place a separate one-time purchase.

The technical fix unlocks real revenue. Waterboy's case isn't an outlier. When you fix recommendation logic:

  • Customers add more products per subscription (AOV increases)

  • Customers discover products they wouldn't have found via browse (catalog penetration increases)

  • Support tickets decrease (customers aren't confused about why they're seeing irrelevant recommendations)

The operational impact matters too. When your upsell logic works correctly, you're not manually curating recommendations or responding to support tickets from confused customers. The system handles edge cases automatically.

FAQ

What's the difference between a recommendation engine and product filtering for subscriptions?

Product filtering enforces hard constraints — subscription eligibility, frequency compatibility, inventory availability. A recommendation engine ranks the filtered results by relevance. You need both. Filtering prevents broken logic (showing ineligible products). Ranking optimizes for conversion (showing the right eligible products first).

Can I use Shopify's native product recommendations for subscription upsells?

No. Shopify's recommendation API doesn't account for subscription context. It treats all purchases as one-time orders. You'll show customers products they already subscribe to or SKUs that aren't subscription-eligible. The API doesn't expose selling plan associations or subscription contract data.

How do I handle one-time add-ons vs. recurring upsells?

One-time add-ons attach to the next order only and bypass frequency logic. Recurring upsells become permanent subscription line items and must match the existing delivery frequency (or trigger a frequency change flow). Your recommendation engine needs to distinguish between these two product types and present them differently in the UI.

What API endpoints do I need to build custom subscription upsell logic?

You need: (1) customer's active subscription contracts with line items and frequency data, (2) product catalog filtered by selling plan associations, (3) variant-level metadata including subscription eligibility, inventory levels, and product attributes. If your platform doesn't expose these via API, you'll need to build a data layer before you can build recommendation logic.

How often should upsell recommendations refresh?

Every time the customer modifies their subscription — adds or removes products, changes frequency, pauses or resumes. Static recommendations go stale immediately. A customer who just added Product X shouldn't see Product X recommended 10 seconds later. Implement this as a real-time API call triggered on subscription modification events.

Do I need machine learning to personalize subscription upsells?

No. Rules-based filtering (subscription eligibility + purchase history + frequency matching) outperforms ML for most brands because it respects hard constraints. ML makes sense only at scale (50K+ active subscribers) with rich behavioral data. Below that threshold, deterministic rules are faster to build, easier to debug, and more accurate because they enforce compatibility constraints that ML models miss.

Broken upsell logic is fixable. The solution isn't better ML models or more training data — it's subscription-aware filtering that respects eligibility, purchase history, and delivery compatibility. Build it yourself using the reference implementation above, or use Skio's Smart Upsell to handle it automatically. Either way, fix it before you lose more revenue to irrelevant recommendations.

Suggested Blogs

This is what a Shopify subscription platform 
should feel like.
This is what a Shopify subscription platform 
should feel like.

Grow your business with the most powerful all-in-one subscription suite on the market.



Request an AI summary of Skio

Copyright © 2025 Skio. All rights reserved.

Grow your business with the most powerful all-in-one subscription suite on the market.

Request an AI summary of Skio

Copyright © 2025 Skio. All rights reserved.