Combora

Combora — User manual

The complete guide to the bundles-and-discounts app for Shopify: the 8 offer types, every configuration option, what your shopper sees on the storefront, and a technical annex for developers. Every screenshot comes from Combora's public demo store — the same one you can try yourself — captured in August 2026, and the app screenshots show the interface in this manual's language.

1What Combora is

Combora creates bundles and automatic offers in your Shopify store: it groups products, applies the right discount in the cart and at checkout, and shows the offer in your theme through ready-made blocks. You configure the offer in the admin, and the discount is computed by a Shopify Function inside the checkout itself — the price charged is the one Shopify computes, not a script in the browser.

There are 8 bundle types. This table helps you choose:

TypeWhat it doesWhen to use it
Fixed bundleA closed set of products with a bundle price or a discount. Creates a one-click purchasable product in your catalog.Curated kits and sets: "the duo", "the starter kit".
Mix & matchThe shopper picks N items from a group and gets a discount."Pick 3 tees and save 20%".
Volume discountTiers: the more they buy, the more they save (2+ → 10%, 4+ → 20%…).Raising units per order of one product or group.
Buy X get Y (BOGO)Buy X units and the next Y come with a reward (free, % off or a fixed price)."Buy 2, get 1 free".
Gift with purchase (GWP)Adds a free gift automatically when the cart meets a condition."Surprise gift on orders over $50".
Build a boxThe shopper fills a fixed-size box; it is charged at a discount or at a box price.Boxes of 4, 6, 12… snacks, candles, beers.
Complements (FBT)"Frequently bought together": a main product suggests add-ons at a discount.Cross-sell on the product page.
ComboStacks two or more of the offers above into a single bundle; inside a combo they do add up.Campaigns: "volume + gift from $1,000".
The golden rule between different bundles: each cart item gets only the best offer — bundles never stack with one another. The single exception is the combo, whose sub-offers do add up among themselves. Full detail in section 14.
1 · You configure your offer in the Combora editor, then hit Publish 2 · One single published configuration the same source for everything that follows 🎨 Your theme — DISPLAYS the widgets paint the offer: bundle price, tiers, gift progress bar… (informational only — never charges) 🔒 Shopify checkout — CHARGES a Shopify Function computes the discount inside the checkout itself, with no scripts in the browser = the advertised price and the charged price ALWAYS match
Both paths of your offer come from the same published configuration: the theme displays it and the checkout charges it. By design they cannot count different things — the number-one complaint about this category of apps ("the discount didn't match at checkout") simply cannot happen here.

If you are torn between types, the app itself has a guide: under Create bundle, the link "Not sure which one?" opens a wizard, and every type has a "See how it works" panel with examples.

Which one should I use? modal with recommendations by goal
Not sure which one? The gallery wizard recommends a type based on your goal.
See how it works side panel for the Fixed bundle
Every type has its own "See how it works" panel with a concrete example.

2Getting started

2.1 What the app creates on install

Installing Combora changes nothing visible in your store. In the background the app prepares the data structures where it will publish your bundles (metaobjects and metafields prefixed with combora — the technical detail is in the developer annex) plus a single automatic discount named "Combora" under Discounts, which is what applies every price. Do not delete or hand-edit it: the app maintains it on its own.

2.2 Turn on the gift engine (app embed)

If you are going to use Gift with purchase (or combos with a gift), turn on the Combora Gift app embed: Online Store → Customize → Theme settings → App embeds → Combora Gift. It is the component that adds and removes the gift from the cart automatically. Without it the gift discount would still be correct, but the gift product would not add itself.

2.3 Add the blocks to your theme

Each bundle type is shown on the storefront through a theme block. You add them once under Online Store → Customize, in the matching template:

BlockTypes it showsWhere to add it
Combora bundleFixed bundle and Combo ("Available bundles")Product template
Combora mix & matchMix & match · Build a boxProduct template
Combora volume tableVolume · Buy X get YProduct template
Combora complementsComplementsProduct template (and cart)
Combora gift offerGift with purchase (the offer itself)Product template
Combora gift progressGift progress barCart template
Combora Gift (app embed)The engine that adds/removes the giftApp embeds (whole store)

You do not need to memorize this: every bundle editor has a "Where this shows up" card that names the right block, plus a "Preview on your live theme" link that opens the theme editor with that block already prepared for insertion into the right template.

Styling: how the widgets look is designed from the app, in the design studio (section 13): presets, element order and per-element styling, with a live preview. The block's own settings in the theme editor (accent color and corner radius) still work and are what wins with the "Theme editor" preset. What is configured only on the block is where it appears and the maximum number of offers to show in the product blocks (1–8, default 3).

3Tour of the app

The app lives under Apps → Combora. The menu has six entries — Home, Bundles, Insights, Plans, Help and Settings — plus the support chat, which is on every screen:

Home

The welcome screen and the pulse of your installation. While you are still setting up it shows a three-step checklist — create your first bundle, publish it and (if your offer is a gift with purchase) turn on the gift engine in your theme. The checklist is not decorative: each step ticks itself only when it actually happened in your store, the action button lives only on the step that is due, and if you later unpublish everything the checklist comes back reflecting reality. Once complete, Home celebrates with a direct link to view your store (you can dismiss the note whenever you want).

Below the checklist: your most recent bundles with their status, an attention band if a bundle needs review (for example because you archived a product it was made of — the app detects that on its own and flags it without unpublishing anything), and a strip with your current plan.

Home screen with the setup checklist
The Home tab with setup half done: step 1 (create) and step 3 (turn on the gift engine) are already done, and step 2 (publish) shows its action button. Below, the recent bundles with their real status.

Bundles

The list of all your bundles: search by name, filter by status (Published / Draft), per-row actions (duplicate, publish/move to draft, delete) and multi-select with bulk actions. Everything is created from here with Create bundle.

Bundle list with statuses and actions
The Bundles tab: each row shows type, status and last update (the date has a tooltip), plus the row actions. A bundle published in test mode adds the Test mode badge next to its status.

Insights

Performance metrics for your bundles over real orders: attributed revenue, % of orders with a bundle, savings delivered, average order value and a time chart. Detail in section 17.

Plans

Your current plan and the upgrade options. The Free plan allows 3 published bundles at a time; the paid plans have no limit (section 18).

Plans page
The Plans tab: your current plan at the top, the flat-price promise, and the Free ($0) · Pro ($14.99) · Plus ($39.99) comparison with what each one includes.

Help

Frequently asked questions and explanations of the concepts (arbitration between bundles, gifts, blocks).

Help page
The Help tab.

Diagnostics (a tab inside Help)

The health and support panel lives as a tab inside Help ("Guides | Diagnostics" selector at the top): live checks of the whole installation, the event log and the support report you copy when something goes wrong (section 19).

Support chat (floating bubble)

In the bottom-right corner of every screen there is a chat bubble 💬 to talk to support directly: conversations by topic, history, and a notice of whether support is available or away. Only what you write is shared — never your customers' data.

Settings

App language (7 languages), Widget design (the store-wide style template — section 13), manual discount sync and developer access.

Settings page
The Settings tab: app language, Widget design and discount sync. At the top, the informational notice that there is another automatic discount in the store.

4Anatomy of the editor

Every type shares the same editor; only the products and pricing section changes. A brand-new bundle looks like this:

Gallery of types when creating a bundle
Create bundle: the gallery of the 8 types, with the bulk-creation entry points at the top.
Empty editor with the Before publishing checklist
The editor just opened: the right rail shows the status and the "Before publishing or saving this bundle" checklist with what is missing; until it is complete, Publish and Save are disabled.

4.1 Basics

Bundle name
Internal. Only you see it; it is there so you can find it in your list.
Storefront title
What the shopper sees as the offer's name in the cart and at checkout. It is translatable. It is also the text of the discount line (trimmed to 100 characters).
Badge text
An optional short badge ("Best value") that your theme can show as a highlight.

4.2 Scheduling

Optional start and end dates. Empty = the offer goes live on publish and never expires. A published bundle outside its window stops applying the discount automatically and applies it again once it is back in window, with nothing for you to touch.

4.3 Products

Depending on the type you pick specific products (and, within each product, you can tick only some of its variants) or a collection. The "any product in the store" scope exists only in gift with purchase, as a condition. The picker is Shopify's standard one:

Shopify product picker
Shopify's standard product/variant picker inside the editor.
Collections: a bundle targeting a collection updates itself, at two speeds worth knowing: the price is always live — a product that joins the collection today already gets the discount at checkout, with no waiting — and the visible list in the widget (the options the shopper sees to choose from, and which product pages show the block) refreshes automatically every night, or instantly if you re-publish the bundle with the "Update bundle" button. A price different from the one charged is never advertised. Maximum 100 collections per picker.

4.4 Price

Each type has its own section (percentage, amount, fixed price, tiers, reward…). It is documented in each type's chapter. In every case the price charged is computed by Shopify at checkout from the published configuration: what the blocks show is informational.

4.5 Subscriptions

By default a shopper cannot buy a bundle item as a subscription (this protects the checkout from subscription pricing mixed with bundle discounts). In the types where it makes sense (mix & match, build a box and gift with purchase) there is an advanced opt-in, "Allow subscriptions in this bundle (advanced)"; turning it on makes the editor show a notice reminding you what it implies.

4.6 The right rail (and two body sections)

Besides Save (and Save as draft on a bundle not yet published), with the Unsaved changes / Save / Discard bar at the top, the editor organizes the rest like this:

Status + action (rail)
Publish (or Update bundle if it is already published) and Move to draft. Publishing puts the offer live; moving to draft withdraws it while keeping the whole configuration.
Test mode (body)
Tick "Publish in test mode" to try the bundle without exposing it to your shoppers. The widgets show only in the theme editor — open the product template there and the bundle appears exactly where it will sit. They never show on your storefront, not even through a draft theme's preview link. While the bundle is in test mode the discount is computed only for customers tagged combora-test; for everyone else the bundle may as well not exist. To walk the full checkout, add that tag to your own test customer (Admin → Customers) and log in as them on the storefront. Untick the box and hit Update bundle and it goes live, applying to anyone who meets the conditions. In the list, a bundle in test mode carries the Test mode badge.
Catalog visibility (body)
Fixed bundles only: whether the bundle's parent product appears in your catalog or not (section 5).
Before publishing (rail)
The checklist that blocks publishing until the essentials are done (title, products, price…).
What your shopper sees (rail)
While the offer is still missing something, this card explains what is missing and previews the content. Once the offer is complete, the card gives way to the real widget preview under See preview. The final look depends on your theme.
Preview style (rail, only on bundles already created)
Names this bundle's active widget design and offers See preview (the widget at full size in a modal) and Customize design (this bundle's design studio: presets, element order and per-element styling — section 13). On a brand-new bundle it appears after the first save.
Where this shows up (rail)
The theme block that renders this type and the direct link to the theme editor.

A bundle with scheduling also shows its window badge (Scheduled / Active / Ended) and the Remove schedule button to go back to "active on publish".

4.7 Notices the editor can show

  • Overlap: if the bundle's products are already in other published bundles, a banner warns: "Only the best offer per line applies at checkout: bundles do not stack". It is not an error — it is information (see section 14).
  • No saving / price 0: in a fixed bundle with a bundle price, if the chosen price saves nothing (or is 0), the editor flags it before you publish.
  • Empty pool: a collection with no eligible products shows as a note in the preview and, if you try to publish, as an error with the explanation.

4.8 Editing a published bundle

When you edit a published bundle, your changes are saved without reaching your storefront. The editor shows your edit, your shoppers keep seeing the published version at the published price, and the bundle is flagged Unpublished changes in your list. Press “Update bundle” to publish them, or discard them from the notice in the editor. That way you can prepare changes calmly. "Move to draft" pulls the offer off the storefront immediately; publishing again restores it exactly as it was.

5Fixed bundle

A closed set of products sold as one unit. It is the only type that also creates a one-click purchasable product in your catalog: a "parent" product (a native Shopify bundle) whose components expand automatically at checkout.

5.1 Configuration

  1. Products: add the components (specific variants) and how many of each. Maximum 30 components per bundle.
  2. Price — two mutually exclusive modes:
    • Fixed bundle price: you type the total price (e.g. $1,299.00). The saving is computed against the sum of the components.
    • Apply a discount: a percentage or an amount off the sum of the components.
  3. Catalog visibility: fixed bundles are real Shopify products, so you decide whether the bundle also appears in your catalog or is only reachable from the widget:
    • Hidden — from the widget only (default): the bundle does not show in collections, in storefront search, or in recommendations. Its product page stays alive so the widget's button works.
    • Visible — a normal catalog product: it appears in collections and search like any other product.
  4. Publish. On publish, Combora creates (or updates) the parent product in your catalog — and, if the pack has no image yet, copies the first component's image onto it, in BOTH visibility modes (a hidden pack still shows up in the cart, the checkout and the order emails). It never overwrites an image you set; change it any time under Admin → Products.
Selling on POS? Choose Visible: a hidden bundle does not show at the point of sale either.
Test mode and Catalog visibility in the fixed bundle editor
Catalog visibility (fixed bundles only), right under Test mode. On the right, the rail with See preview and Customize design.
Fixed bundle editor: components and bundle price
The components with their quantity and the Price section in "Set a bundle price" mode ($1,299.00 for two boards that add up to $1,600.00). The other mode in the dropdown is "Apply a discount", with a percentage or an amount.
Published fixed bundle
After publishing: status Published and the button becomes "Update bundle".

5.2 What your shopper sees

Available bundles card on the product page
On any component's product page, the Combora bundle block shows the bundle card: the contents with their quantity as chips, the bundle price ($1,299.00) with the total struck through ($1,600.00), the saving as a badge (Save $301.00) and the button to the parent product.
Parent product page of the bundle
The parent product: the bundle's own purchasable page. Mind the price the theme paints — it is the sum of the components ($1,600.00); the bundle price is applied by the Function once it reaches the cart. You can edit its image and description like any other product.
Fixed bundle in the cart
In the cart the bundle is a single line: $1,600.00 → $1,299.00, with the bundle's badge. That is where the bundle price shows up.
Checkout with the bundle expanded into components
At checkout the bundle expands into its components, with the saving applied and labelled with the bundle's title.
Order completed page
A purchase genuinely completed in the demo store while this manual was written: $1,299.00 paid through the test gateway.

5.3 Details and limits of this type

  • Components must be variants with stock; the parent product inherits availability.
  • The parent product manages itself: moving the bundle to draft or deleting it withdraws it from the storefront (verified while writing this manual). Do not delete it by hand.
  • By default the parent is hidden from the catalog. Before that option existed it showed up in /collections/all as an image-less card; bundles you already had published were switched to "Hidden" automatically. If you prefer the old behavior, switch it to Visible.
  • Maximum 30 components. The fixed price is split across components with exact rounding (the sum of the parts always equals the total).
  • The fixed bundle also applies if the shopper adds the components separately in the exact quantities — the checkout groups and discounts them just the same.

6Mix & match

The shopper picks at least N items from a group and the whole group comes out discounted.

6.1 Configuration

  1. Products: the eligible group can be specific products — ticking, if you want, only some variants of each one inside the picker — or a collection (which updates itself).
  2. Quantity: the minimum number of items to qualify, and optionally a maximum (above the maximum, the extra items are paid at full price).
  3. Discount: a percentage or an amount, applied to every eligible item in the cart once the minimum is reached.
Mix and match editor
Mix & match editor: eligible group and minimum number of items.
Mix pricing section
The mix Discount section: percentage or amount.
The visual picker shows with every scope. With specific variants it renders exactly those checkboxes; with products or a collection, the Combora mix & match block resolves the list of options at publish time (and refreshes it every night, or right away if you re-publish — the two speeds explained in section 4.3). The shopper can also compose the bundle by adding products to the cart as usual: the discount kicks in on reaching the minimum, whether or not they used the picker.

6.2 What your shopper sees

Mix picker with the option list and the button disabled
The picker on the product page: the option list with its own scroll and the button disabled until the minimum is reached.
Mix picker with 3 selected and the button active
On reaching the minimum, the chosen options are marked with an accent ring and the CTA becomes active.
The bundle in this screenshot has a design of its own (section 13): the button was moved to the top and the progress counter is hidden. With the stock design, the counter sits above the list and the button below it.
Cart with the mix discount applied to every eligible line
In the cart, each eligible line shows the reduced price with the bundle's badge ($400.00 → $320.00 and so on, 20% off each). Measured in the demo store: three boards, $1,050.00 → $840.00. Another bundle can apply in the same cart at the same time, on its lines — see section 14.

6.3 Details of this type

  • A cart below the minimum is not blocked: the shopper pays full price until they qualify.
  • With a maximum set, the picker stops accepting ticks once the cap is reached, and in the cart only the first units up to the maximum are discounted.
  • Quantities count by units: 3 units of the same eligible product also qualify for a minimum of 3.

7Volume discount

Price tiers by quantity: "buy at least 2 → 10%, at least 4 → 20%". The tier is decided per product: the quantity of each eligible product in the cart determines its own tier, and the tier reached applies to that product's units. Example with "4+ → 20%": 4 units of the same board → 20% off all 4; but 2 units of one board + 2 of another → each product sits in the 2+ tier (10%), not the 4+ one. Quantities of different products do not add up with each other.

7.1 Configuration

  1. Products: specific products or a collection (same as mix). If you only want some variants of a product, tick them inside the picker when you choose it.
  2. Tiers: each tier is "Buy at least N" + a discount (percentage or amount). Add as many as you need with "Add tier" (up to 50).
Volume editor with two tiers
Two tiers: 2+ → 10% and 4+ → 20%. The editor itself spells it out: the quantity of each product in the cart decides its tier.

7.2 What your shopper sees

Volume table on the product page
The Combora volume table block on the product page: the tier ladder, one row per tier with the saving as an accent badge and the highest tier highlighted with a ring.
Cart with 4 units and 20% applied
With 4 units in the cart the 20% tier applies: $4,000.00 → $3,200.00, with the bundle's badge on the line.

7.3 Details of this type

  • Only the highest tier reached applies (tiers do not add up).
  • Tiers can mix value types (some %, some amount).
  • A fixed discount amount is split across the lines proportionally and exactly.

8Buy X get Y (BOGO)

Buy X units and the next Y units come with a reward. The reward can be free (100%), a percentage, a discount amount or a fixed price per unit.

8.1 Configuration

BOGO editor — basics
The BOGO editor: the product the offer runs on and, on the right, the rail with the status and the preview. If those products were also in another bundle, an overlap notice would sit at the top — informational, not blocking.
BOGO editor — quantity to buy, to get and the reward
Quantity to buy = 2, quantity to get = 1 and a 100% reward ("buy 2, get 1 free"). The reward selector offers percentage, amount or fixed price.

8.2 What your shopper sees

Cart with 3 units and 1 free
With 3 units ($300.00 each): $900.00 → $600.00 — one unit comes out free, with the "Buy 2, get 1 free" badge on the line.

8.3 Details of this type

  • The offer repeats in whole blocks: with "2+1", 6 units = 2 free; 5 units = 1 free.
  • The theme block that renders it is Combora volume table (it presents "buy X get Y" as a savings table).
  • The "fixed price" reward charges exactly that price for each rewarded unit.

9Gift with purchase (GWP)

Adds a gift product (at $0.00) automatically when the cart meets the condition, and removes it if it stops meeting it. It is the most automated type: the Combora Gift app embed manages the gift without the shopper doing anything.

🛒 The cart changes add, remove, quantities… Condition met? spend · units · both · exact combination yes 🎁 Gift in it adds itself, at $0.00, flagged as a gift no ✋ Gift out it removes itself once the condition breaks If the gift runs out, the offer stops being advertised (never promise what you don't have)
The automatic gift cycle: on every cart change the engine re-evaluates the condition and adds or removes the gift with neither you nor the shopper doing anything. And if the gift runs out of stock, the offer card hides itself.

9.1 Configuration

  1. The gift: a specific variant that gets added when the shopper qualifies.
  2. Condition to unlock — four options:
    • By minimum spend: the cart reaches an amount (e.g. $1,000).
    • By number of items: it reaches N units.
    • Spend and quantity (both): both are required.
    • By carrying a combination of products: a specific list of items (from 2 to 5; one unit of each) that must all be in the cart.
  3. What counts toward the condition: any product in the store, specific products, or collections.
GWP editor with the minimum-spend condition and the qualifying products
The editor: the chosen gift (a $20.00 ski wax), the "By minimum spend" condition with the threshold at $1,000, and "What counts toward the condition" narrowed to one specific board, so only that board's units fill the bar. Choose Any product instead and the editor adds the checkbox "Show this gift offer on every product page".

9.2 What your shopper sees

Gift progress bar below the threshold
The Combora gift progress block's bar is a card with a gift icon: it says how much is left ($650.00 more on a cart holding one $350.00 board, so the bar sits at 35%) and fills up as the cart grows.
Gift auto-added at $0.00
On reaching the threshold, the gift adds itself at $0.00 with the bundle's badge, and the bar celebrates it with the accent ring. Measured in the demo store: three of those boards make $1,050.00, cross the $1,000 threshold, and the $20.00 ski wax comes in free — $1,070.00 → $1,050.00. If the cart drops below the threshold, the gift removes itself.

The bar has four states and, from the empty cart on, it names the gift and what qualifies: before the shopper starts it states the rule ("spend $1,000 on that board and get the ski wax free"), part-way there it says what is still missing, on crossing the threshold it announces the gift by name, and if the shopper takes the gift out by hand it says so instead of sitting on "applying". When more than three products qualify the bar says eligible products and adds a "Which products count?" disclosure that lists them.

You can write the bar's copy yourself. The gift editor has a Cart bar message section with three optional fields — empty cart, part-way there, gift unlocked. Leave a field empty and that state keeps the built-in copy; fill it and your text replaces it, with placeholders you can drop in: {{ amount }} (what is left), {{ qty }} (items left), {{ scope }} (the qualifying products), {{ gift }} (the gift's name) and {{ threshold }} (the full threshold). The editor previews the result with your own bundle's figures, and refuses to publish an unknown placeholder or a message over 160 characters.

9.3 Details and protections of this type

  • The gift itself does not count toward the qualifying spend.
  • The threshold is measured on the list price of what qualifies, not on what the shopper ends up paying, so another offer discounting those same lines never eats into it. The bar, the automatic gift and the checkout all read the same number.
  • If the shopper removes the gift by hand, the app respects that and does not add it back in that session; the cart bar says exactly that ("Gift removed") instead of sitting on "applying".
  • If you withdraw the offer (move to draft, end of the window) while carts are open that already had the gift, the engine removes that orphan line on its own as soon as the cart moves again — it never turns into a charge.
  • If the gift runs out of stock, the offer simply does not add it (it does not block the purchase).
  • Anti-charge protection: a checkout validation prevents paying for a gift that is flagged as a gift but arrived with a price — the shopper never pays by mistake for an item marked as a gift. The validation acts only at checkout, never while browsing.
  • When "what counts" is collections, the app resolves the collection's products at publish time (and refreshes them every night, or right away if you re-publish — the two speeds from section 4.3), so the progress bar and the automatic gift work just like with specific products. The one exception: a huge collection (more than ~150 products) cannot be predicted from the browser — the bar is not shown for that offer, but the gift and the price are still correct (the server decides them).
  • A gift whose "what counts" is any product in the store can be advertised with the Combora gift offer block on every product page: when you pick that scope, the editor shows the checkbox "Show this gift offer on every product page". Off (the default), the offer is only seen in the cart — the progress bar and the automatic gift work the same; on, it is also advertised with a card on every product page (the offer has no specific products to tick, so the block reads it from the store's published index). The deliberate exception is still the gift product's own page: a prize is not a hook.

10Build a box

The shopper fills a fixed-size box (e.g. 3 items) with whatever they pick from a group. Each complete box is charged at a discount or at a closed box price.

10.1 Configuration

Build a box editor with a box price
Box size = 3 over the five variants of one board, priced with "Set a box price" = $1,749.00 (from $2,100.00, so $351.00 off — $583.00 per item). The other mode in the dropdown is a plain percentage or amount. A cart below the size is not blocked: full price is paid until the box is complete.

10.2 What your shopper sees

It uses the same Combora mix & match block and the same picker with a counter as section 6: "select 3", a progress counter, a CTA with the quantity. The difference is the math: here boxes are charged per complete box — with a box of 3, six items are two boxes; seven items are two boxes plus one item at full price.

10.3 Details of this type

  • Eligible group: specific products (with their variants, if you tick only some) or a collection (the visual picker shows with every scope, as in mix — with collections the list is resolved at publish time and refreshed every night).
  • The box price is split across the items exactly; with 0-decimal currencies (JPY) or 3-decimal ones (BHD) the split respects the currency.
  • It supports the subscriptions opt-in (section 4.5).

11Complements (frequently bought together)

A main product suggests complementary products; if the shopper takes the set, they get a discount. It is the classic "frequently bought together" on the product page.

11.1 Configuration

Complements editor with main product and suggestions
The editor: the main product (the anchor where the offer shows), the complementary products and the discount (25%). The rail shows the widget preview with its CTA.

11.2 What your shopper sees

Complements block on the main product page
On the main product's page, the Combora complements block: the product itself marked as "This item", the complements with a checkbox and a price, the saving ("Save 25%") and the CTA that names the selection ("Add 3 to cart"). Items are separated by "+" dividers, and selected rows are marked with an accent ring. Measured in the demo store with the three of them in the cart: $800.00 → $600.00 ($500.00 → $375.00, $180.00 → $135.00, $120.00 → $90.00).

11.3 Details of this type

  • The discount applies once the main product and at least one complement are in the cart; it covers the set's lines.
  • The block can also be added to the cart template as a final cross-sell nudge.
  • Out-of-stock complements appear disabled with their notice, and are never pre-ticked.

12Combo

The advanced type: it stacks two or more sub-offers (of any of the other types) into a single bundle. The key difference from publishing separate bundles: inside a combo the sub-offers add up — the shopper can get the volume discount and the gift at the same time.

12.1 Configuration

Combo editor with two sub-offers ready
The combo editor: each sub-offer is an accordion row with its status (Ready). The preview summarizes the offers and the budget used ("Uses 1 of 50 product entries").

Each row expands in place with the chevron, and the sub-offer is edited inline: its own products, its own tiers or reward. Each sub-offer only affects its own products.

12.2 What your shopper sees

A combo is not advertised as a card in the Combora bundle block: that block lists fixed bundles, which have one price and one button, and a combo has neither. Each sub-offer is promoted by its own block instead — the volume table for a volume sub-offer, the gift bar for a gift one — and the combo shows its full effect in the cart, where every part acts with its own badge: the volume discount on its lines and the auto-added gift at $0.00.

Measured in the demo store on a combo that stacks "2 or more of this board → 15% off" with "a free beanie once you carry $900.00 of it": two boards go from $1,040.00 to $850.00 — the 15% and the beanie at $0.00 — and three go from $1,540.00 to $1,275.00. Two separate bundles would never do both on the same line; inside a combo the sub-offers add up (section 14).

12.3 Details of this type

  • Sub-offers add up inside the combo, but the combo competes as a whole against the other bundles under the best-offer-per-line rule.
  • The gift in a GWP sub-offer uses the same auto-add engine as the gift type.
  • Size limit: the combo's flattened set of products cannot exceed 50 entries (the editor shows it: "Uses X of 50").
  • The editor has a "How combos work" modal with a guided example, and each sub-offer defines "Products this offer applies to" — a sub-offer with no specific products shows a warning because it will not be able to price anything.
  • A BOGO sub-offer inside a combo offers a percentage or amount reward (the "fixed price per unit" is exclusive to the standalone BOGO).

13Designing the widget

Combora's blocks ship with a look that fits any theme, but you can design them from the app, without touching CSS: pick a style, reorder and hide elements, and adjust colors, sizes and shapes element by element. All of it with a preview that uses the same rendering engine as your storefront, so what you see is what gets published.

The design is appearance only: it never changes the price. You can tweak it knowing it does not touch a single rule of the offer.

13.1 Two places where you design

WhereScopeWhen to use it
Settings → Widget design → Edit widget template The store-wide template: every bundle inherits it. The normal case. You define your style once and every bundle follows it.
Bundle editor → Customize design That one bundle only. What you set here wins over the template, field by field. A bundle that needs to stand out (a campaign, a different color).

A bundle that has not touched anything shows the notice "This bundle follows your store's widget template". As soon as you pick a preset there, that bundle goes its own way.

Design studio: the store-wide widget template
Settings → Widget design: the styles on the left, the environment simulator on the right and, below, the widget preview. The Preview widget selector at the top changes which widget type you are looking at.
Saving the template re-syncs your published bundles so the new style reaches the storefront: it can take a minute to show. The panel itself tells you how many bundles will be re-synced.

13.2 The styles (presets)

Store template
Only in a bundle's design: it follows whatever you defined in Settings. It is the starting value. If there is no template yet, the block's settings in the theme editor apply.
Theme editor
The block's own settings in your theme editor win (accent, corners, style mode). This is the classic behavior, the one you had before the studio existed.
Combora
Combora's polished look: a card with its own background, borders and shadows. Consistent in any theme.
Like the theme
Minimal decoration: your theme's typography and colors dominate. For themes with a strong personality.
Unstyled
Clean markup, no decoration, so your developer can write their own CSS (theme editor → custom CSS). The hooks are the .combora-w class and the --combora-* variables.
Custom
Opens the controls below: layout, global style and per-element fine tuning.

13.3 Layout: reorder and hide

With the Custom preset, Layout appears: a list of the widget's elements that you can drag (or move with the arrows) to change their order, plus an eye icon to hide the optional ones.

The elements depend on the bundle type, because each type draws a different widget:

Bundle typeElements you can reorder
Fixed bundle · ComboTitle · Included products · Price · Saving · Button
Mix & match · Build a boxTitle · Progress counter · Option list · Note · Button
Volume discountTitle · Tier ladder
Buy X get YTitle · Offer rule
Gift with purchaseTitle · Gift · Unlock condition
ComplementsTitle · Item list · Saving · Button
What the shopper needs cannot be hidden. The pickers, the item lists, the tier ladder, the gift and the buttons deliberately have no eye icon: without them the widget would say nothing. Reordering them you can do.
Custom preset: layout, global style and per-element fine tuning
With the Custom preset: Layout (drag the rows, the eye hides the optional ones), Global style and Per-element fine tuning. The preview on the right updates with every change.

13.4 Global style and fine tuning

Global style is what changes the whole widget at once: accent color, card background, text color, corners (sharp / soft / round), shadow (none / soft / medium) and density.

Per-element fine tuning opens each element separately — title, price, saving, badge, button, card, thumbnails, labels — with whatever makes sense for each: size, weight, color, uppercase, padding, shape, border color, struck-through price color… Every control has a "Back to theme" to undo just that adjustment, and at the bottom there is a "Reset all customization".

  • If you do not define the Saving, it follows the Badge style.
  • The button's text color cannot be chosen: it is derived automatically by contrast against the background you pick, so it can never end up illegible.
  • Values are bounded to sensible ranges: you cannot type a size that breaks the widget on mobile.

13.5 The preview (and what it is NOT)

On the right there is a panel marked "Preview only". The difference matters:

  • Left = your design. What you adjust there is saved and goes to the storefront.
  • Right = the lens. Preview background (light / dark / a custom color), base text size, slot width (a narrow product column vs. a wide section) and an approximation of your typography. It is there to check that your design holds up in different places. It changes nothing in your store or in your design.

And in the bundle editor's rail, the "Preview style" card tells you which design is active ("Active design: Combora") and offers two buttons: See preview, which opens the widget at full size in a modal ("This is how it will look in your store"), and Customize design, which enters this bundle's studio. The real typography and background come from your theme, so for the final sign-off look at the widget in the theme editor.

The preview uses the same code as the storefront, but the theme is yours: if your theme defines very distinctive fonts or backgrounds, the final result may look different from the preview. That is the only margin.

14Combining bundles with each other

You can publish as many bundles as your plan allows, even over the same products. The rules of coexistence are fixed and predictable:

  1. The best offer per line. For each cart item, Shopify applies the offer that discounts that item the most. Two bundles never add up on the same line.
  2. Different bundles can coexist in the same cart — each one on its own lines. A mix discount on the boards and a spend-based gift can apply at the same time because they act on different lines.
  3. On a tie, the oldest bundle wins (the first one you created).
  4. The exception is the combo: its sub-offers do add up among themselves (section 12).

What about discounts that aren't Combora's? The rule above governs Combora packs among themselves. With third-party discounts Shopify is in charge, and we verified this on a live store: two product discounts (a Combora pack and an automatic discount or product code of your own) never add up on the same item — Shopify applies only the larger of the two, even when both are marked combinable; they can coexist on different lines of the same order. An order discount (say a −10% code on the whole order) does subtract afterwards, on top of the already discounted prices — Shopify's standard behavior with any app.

Cart with a mix and match discount on every eligible line
Every line an offer covers carries its discount and its badge — here the 20% mix on three boards, $1,050.00 → $840.00. A different bundle whose products are also in this cart applies to its lines, in the same cart, without the two ever meeting on one line.
The overlap notice in the editor exists for this: when you publish a bundle whose products are already in other bundles, it reminds you that at checkout only the best offer per line will apply. Publish without fear — the shopper will never see a double discount or a wrong price.

A real numeric example (verified in the store)

CartCandidate offersResult
3 × Cascade Freeride ($300.00)BOGO 2+1 free$900.00 → $600.00 (one unit free)
4 × Oxygen ($1,000.00)Volume 4+ → 20%$4,000.00 → $3,200.00
2 × OxygenVolume 2+ → 10% (the 4+ tier is not reached)$2,000.00 → $1,800.00
3 eligible boardsMix & match, minimum 3 → 20%$1,050.00 → $840.00 ($400.00 → $320.00 and so on)
3 × the board a gift is tied to ($1,050.00)Gift over $1,000 of that boardThe $20.00 ski wax joins at $0.00: $1,070.00 → $1,050.00
2 × the board of the comboInside one combo: volume 15% and a giftBoth apply: $1,040.00 → $850.00

In the demo store one product deliberately belongs to two bundles: the board that is a component of the fixed bundle is also the subject of the volume one. On its own it takes whatever volume tier its quantity earns; together with the fixed bundle's other component, in the exact quantities, the fixed bundle becomes a candidate too and the line keeps the better of the two. Same product, two bundles, one price per line — never both.

15Bulk creation

From the Create bundle gallery there are two flows for creating many bundles at once. Both create drafts: nothing reaches the storefront until you review and publish.

15.1 Generate from a collection

  1. Pick the collection.
  2. Pick the template: "One multipack per product" (a fixed bundle of N units for each product) or "A single bundle for the whole collection".
  3. Configure units and discount, then hit Create drafts.
Generator configured with a collection and a template
The generator: the multipack template, 3 units per pack and 15% off. The collection is picked with Shopify's own picker through Select collection.

What each template actually creates

One multipack per product walks the collection and creates one draft fixed bundle per product: N units of that product's first variant (position order), with the discount you chose applied to the sum. Each draft is a normal fixed bundle — open it to change the variant, switch to a closed pack price, add a badge… — and when you publish it, it becomes its own one-click purchasable product, like any fixed bundle (chapter 5).

A single bundle for the whole collection creates exactly one draft of the flexible type you pick (volume, mix & match, build a box or buy X get Y) whose eligible pool is the collection itself. Membership follows your catalog: the pool is resolved when you publish and refreshed nightly (or immediately on re-publish), so products you add to the collection later join the offer without touching the bundle.

Products are skipped, and the result banner tells you, when they are not Active in your catalog, when they have no purchasable variant — and when they are Combora pack products themselves: a generated pack never nests another pack.

Worked examples

The same screen, seven configurations, and exactly what each one produces:

ConfigurationWhat is created
Collection of 12 products · multipack · 3 units · 15%12 drafts, one per product, named "Product — pack of 3". Each contains 3× the product's first variant and charges (3 × price) − 15% at checkout once published.
Same, but discount = fixed amount 512 drafts where each pack charges (3 × price) − 5 in your store's currency. If a product is so cheap that the discount swallows the price, review that draft before publishing.
Collection of 9 where one product is a Combora pack and one is not Active7 drafts; the banner reports the skipped ones and the reason for each.
Re-running the exact same configuration next monthOnly the products new to the collection get a draft; every product that already has an identical generated pack is reported as a duplicate and skipped. Re-running is always safe.
Collection of 250 products · multipackThe first 100 drafts (the per-run cap) plus a notice; publish or delete some and run it again for the rest.
Single-bundle template · volume · min. quantity 3 · 15%One draft "Volume discount of {collection}": 3 or more units of the same variant get 15% off — each product in the collection ladders on its own (volume counts per line, chapter 7). To reward mixing different products, use mix & match instead: its minimum counts across the whole selection.
Single-bundle template · buy X get Y · buy 2, get 1 · reward 100%One draft: for every 2 units bought from the collection, the next unit is free. A 50% reward would make it half price instead.
Limit: up to 100 drafts per generation. Drafts are free on every plan — the Free plan's 3-live-bundles limit applies when you publish, not when you generate.

15.2 Import from a CSV

For large catalogs or for migrating from another app. The page includes the full format reference, a downloadable template with one example per type and an export button (it downloads all your bundles in the same format, ready to re-import into another store).

CSV import page with the column reference
The import page: columns type, title, discount_kind, discount_value, components, collection, min_qty, min_items, slots, buy_qty, get_qty. You can upload a file or paste the CSV directly.
  • Importable types: fixed, volume, mix_and_match, build_a_box, bogo (the types with a gift, an anchor or sub-offers are created in the editor).
  • References by product or collection handle, or by Shopify GID.
  • In components (fixed only): handle:quantity separated by |.
  • Up to 200 rows per import; a bigger file is trimmed and it tells you.
Import with per-row errors
With a faulty CSV nothing is dropped silently: every failed row is reported with its reason (no components, discount out of range, unknown type).
List filtered by drafts after the bulk flows
The drafts created by both flows, filtering the list by the Draft status. Publish them one by one or with the bulk actions.

16Day-to-day management

16.1 The list

Search by name, filter by status and, on each row: Duplicate (creates a draft copy), Publish / Move to draft and Delete. The date on each row is the last update (hover for the tooltip).

16.2 Bulk actions

Bulk action bar with 10 selected
Selecting rows (or Select all) brings up the bulk bar: Publish · Move to draft · Delete.
Inline confirmation for bulk delete
Delete asks for explicit confirmation: Delete 10 bundles? … This cannot be undone. During the operation you see the progress (Processing 6 of 10…).

16.3 The life cycle of a bundle

Draft
Invisible to the storefront. Editable without limits.
Published
The offer is active (within its scheduling window). Editing changes nothing live until you press Update bundle.
Move to draft
Withdraws the offer immediately; the configuration is kept intact so you can republish.
Delete
Deletes the bundle and withdraws its discount (and the parent product, if it was a fixed bundle). It cannot be undone. Analytics for past orders is kept.

16.4 What Combora does on its own, while you sleep

Four automatic processes keep your store up to date with nothing for you to do. You do not have to configure or watch them — they are documented here so you know why things fix themselves:

⏰ every 15 min scheduling windows 💳 03:00 plan refresh 📦 04:00 collection pools 🧹 04:30 cleanup and self-healing
The four automatic processes (UTC). None of them needs configuration.
ProcessWhenWhat it does for you
Scheduling windowsevery 15 minActivates and deactivates scheduled bundles at their time (in your time zone) and keeps the public catalog current. A bundle running from the 1st to the 15th starts and ends on its own.
Plan refresh03:00Confirms your real plan with Shopify (upgrade, cancellation, end of trial) so limits and badge always reflect the truth.
Collection pools04:00Updates the visible list of bundles targeting a collection with the products that joined or left (the price for those products was already correct from the first moment — section 4.3).
Cleanup and self-healing04:30Deletes expired data (chats closed 90 days ago, technical logs), removes leftovers from deleted bundles, and repairs the public contract: if someone accidentally altered the data schemas developers use, or the plan badge, it restores them and records what happened.

17Insights

Insights with real orders
The Insights tab with real data from the demo store (7 / 30 / 90-day range): the seven metrics on top — here a single real order of $1,299.00, so a 100% attach rate and 1 of 1 bundle orders — and the three charts below.
MetricWhat it measures exactly
Attributed revenueThe revenue of the order lines that carried a Combora discount (not the order total).
% of orders with a bundleThe share of orders in the range that included at least one bundle.
Orders with a bundleThe literal fraction: orders with a bundle / total orders.
Savings deliveredThe total discount your bundles gave to shoppers.
Average order valueAcross all orders in the range.
Bundle order valueThe average of only the orders that carried a bundle — compare it with the previous one to see the effect of your offers.
Orders with a giftOrders that included a free gift (GWP or combo).

If you sell in several currencies, a currency selector sits next to the metrics: the money figures are shown in the chosen currency (the order counters are global).

Below the metrics there are three charts for the chosen range, all with one mark per day and a tooltip:

  • Orders with a bundle over time — bars, with the value printed above the bars that have data.
  • Attributed revenue per day — a line, to see the trend.
  • Savings delivered per day — bars: how much discount your bundles gave each day.
Attribution is recorded at the moment of the order: deleting a bundle later does not erase its history in Insights.

18Settings and plans

18.1 Settings

  • App language: 7 languages (English, Spanish, French, German, Italian, Portuguese, Dutch). It affects the admin; what your shopper sees follows the language of your theme/market.
  • Widget design: Edit widget template opens the store-wide design studio — the style every bundle inherits (section 13). Saving here re-syncs the published bundles.
  • Sync discounts: you do not need to press it — everything syncs on its own when you publish. It is there as a safety net: if a bundle ever stopped discounting, this regenerates the Shopify discount from all your published bundles. It is safe to use as often as you like, and if there was nothing to fix it says so: Everything was already up to date.
  • Developer access: the public data your theme can read (annex 22). The contract documentation is open on every plan.

18.2 Plans

Flat pricing: we never take a percentage of your sales, and the price does not depend on your Shopify plan — a Basic store and a Plus store pay the same.

  • Free ($0/month): up to 3 published bundles at a time (drafts do not count) with all 8 types included. When you try to publish the 4th, the app blocks it with the matching notice: move another one to draft or upgrade. On the free plan, the blocks show a discreet Powered by Combora badge.
  • Pro ($14.99/month): unlimited published bundles and no badge.
  • Plus ($39.99/month): everything in Pro, plus priority support, bulk CSV import/export (up to 200 bundles per file) and direct integration support for your developer (headless storefronts and custom themes).

Billing goes through Shopify (Managed Pricing) — it is managed and cancelled from the Plans page itself. The paid plans include a 7-day free trial, and if you cancel you keep the plan until the end of the cycle you already paid for. Founding stores (the first 100 installs) keep the full product free forever.

19Diagnostics and support

Diagnostics (the second tab of Help) is your first stop when something does not add up — and what support will ask for if you open a ticket. Combora automatically records, per store, everything that fails underneath (on the server and in your online store) even when the shopper never sees it, and this panel puts it within reach. It stores no customer data at all.

Diagnostics tab with health checks, log and report
The Diagnostics tab: health checks at the top, the event log with filters in the middle and the support report at the bottom.

19.1 Health checks

The live state of what support asks about first, verified against Shopify on every load:

CheckWhat it verifies
BundlesSync errors and published bundles with unpublished changes (you edited and did not press Update bundle).
Automatic discount in ShopifyThat the Combora discount exists and is ACTIVE — it is the one that charges the prices at checkout.
Published store indexThat the public data your theme blocks read is up to date (live bundles vs. expected).
Combora Gift app embedThat the gift engine is enabled in your theme.
Discount applied on recent ordersOrders from the last 7 days with a bundle: if ALL of them arrived without a discount, something is wrong at checkout.
Storefront error channelThat your storefront blocks can report failures here (date of the last event received).
Background jobsFailed nightly jobs.
Unknown is not an alarm: it means that check could not be verified right now (for example, the storefront channel before it receives its first event). A real problem is flagged in red.

19.2 Event log

Each entry is something that happened and was recorded: a failure while publishing, a gift Shopify refused to add (with the exact reason), a discount that collided with another, a webhook that failed… Filter by level (Info = expected but relevant, such as the plan limit; Warning; Error) and by area (publishing, discounts, plan and billing, storefront, sync…). An empty log is a good sign: it only fills up when something goes wrong. Events are kept for 30 days.

Privacy — what is logged and what is not. The log stores technical error codes only: the HTTP status, the affected bundle and variant, and the reason Shopify returned. Your customers' information is never logged: no IP addresses, no names, no emails, no browsing identifiers or cookies — a shopper who buys without trouble generates no log entry at all. There are also automatic caps (a daily maximum per store) and everything deletes itself after 30 days. If your store receives a data-erasure request (GDPR), this log contains nothing to erase, and uninstalling the app deletes it in full along with the rest of your data.

19.3 The support report

A single document with everything needed to diagnose your store in one pass: the health checks, your plan and configuration (without sensitive data), the publishing status of every bundle and the latest events. When you contact support, press Copy report (or Download as a file) and attach it to the ticket — with that, whoever helps you sees exactly what happened without asking for screenshots or access.

  • The report includes no customer data and no keys: only health, bundle configuration and technical events.
  • If the copy button does not work in your browser, the text is left selected — press Ctrl+C (Cmd+C on a Mac) — or use the download.
For developers: the storefront blocks report their failures to /apps/combora/log (an App Proxy signed by Shopify). It is an internal support channel with a closed catalog of codes and a daily cap — it is not part of the public contract in annex 22.

20Limits and important rules

The single reference for limits, verified against the app's code:

Limit / ruleValueWhat happens when you hit it
Published bundles (Free plan)3Publishing the 4th is blocked with a notice. Drafts do not count.
Components in a fixed bundle30The editor will not let you add more.
Products in a combo (flattened)50 entriesThe editor shows the usage (Uses X of 50) and blocks the excess.
Collections per picker100Validated on save.
Gift combination (GWP)2–5 itemsValidated on save.
Title as the discount message100 charactersThe text shown at checkout is trimmed (the full title is kept).
Generate from a collection100 draftsTrimmed, and it tells you.
CSV import200 rowsTrimmed, and it tells you to split the file.
Offers visible per block1–8 (default 3)Adjustable on each theme block; it prioritizes the offers that declare a price/saving.
Total published configuration≈19.8 KB (2 blocks of 9.9 KB)With a great many huge bundles at once, the newest ones would end up published with no active price until space frees up (an extreme case; the app prioritizes the oldest bundles and splits the configuration across two blocks automatically).

Money rules (always exact)

  • Percentages truncate the discount downward (it is never rounded in favor of charging more).
  • Amounts and fixed prices are split across lines with an exact remainder: the sum of the parts always equals the total.
  • Decimals depend on your store's currency (JPY with none, BHD with three…) — two decimals are never assumed.
  • The price charged is computed by Shopify at checkout from the published configuration. What the theme blocks show is informational.

Multi-currency (Shopify Markets)

If you sell in several currencies, there is nothing for you to configure: you define prices, thresholds and discounts in your store's currency, and Combora does the rest. A shopper buying in another currency sees the widget amounts converted to theirs (the gift threshold, the bundle price, each tier's saving), and at checkout the discount is converted at Shopify's exchange rate of the moment — the same one that converts your product prices. The golden rule holds: what the shopper sees advertised is exactly what the checkout charges them, in their currency.

Other rules

  • Subscriptions are blocked on bundle lines unless you explicitly opt in (section 4.5).
  • A GWP gift is not advertised on the gift product's own page (a prize is not a hook).
  • Bundles published outside their scheduling window apply no discount (they reactivate themselves).

21Troubleshooting

I cannot see the offer on my storefront
① Is the bundle Published (not a draft) and inside its date window? ② Did you add the right block to the template (the Where this shows up card in the editor)? ③ If you edited a published bundle, did you press Update bundle?
The gift does not add itself
① Is the Combora Gift app embed enabled? ② Does the cart meet the condition counting only what qualifies (the gift does not count)? ③ Did the shopper remove it by hand in this session? ④ Is the gift in stock?
The cart discount is not what I expected
Remember the rule: only the best offer per line. If two bundles compete for the same item, the one that discounts more wins (and on a tie, the older one). Adding up only happens inside a combo.
The app says: You have another automatic discount active
This is information, not an error. It means you have your own automatic product discounts in Shopify. Where a Combora bundle and one of those discounts land on the same product, Shopify applies only the better of the two — stacking two product discounts on the same item requires Shopify Plus. That is a Shopify rule, not a Combora one. Your bundles do combine with your order and shipping discounts, and with product discounts that land on other products. The notice names the specific overlapping discount so you know which one to look at.
I published and it says plan limit
The Free plan allows 3 published bundles at a time. Move another one to draft or upgrade.
The gift progress bar does not appear
It renders in the cart template (the Combora gift progress block) and only for offers whose criterion can be evaluated in the browser. With collections as what counts it works too, because the app resolves the collection's products when you publish; the one exception is a very large collection (more than ~150 products), where the bar is not shown even though the gift still works.
The checkbox picker does not appear in mix / box
The picker renders with any scope: with specific variants it shows exactly those checkboxes, and with products or a collection the list is resolved when you publish (and refreshed every night, or right away if you re-publish). If it is still empty, the eligible group has no purchasable products — check the preview note in the editor. Either way the shopper can add products as usual and the discount kicks in on qualifying.
Errors when importing a CSV
Every failed row is listed with its reason. Fix those rows and re-run with just them — the good rows were already created.
I suspect a discount was left orphaned
Settings → Sync discounts repairs the state. Never hand-edit the Combora discount in Shopify's Discounts section.

22Developer annex: the public contract

Combora publishes your bundle data into your store as metaobjects and metafields under the combora namespace, publicly readable from the storefront. Any developer can build a completely custom UI by reading this data from Liquid or the Storefront API — without calling any of the app's APIs. This contract is versioned and additive-only (current version: public.v1): existing fields are never renamed or retyped.

22.1 What is created on install

The installation provisions the definitions (the schemas) — visible under Settings → Metafields and metaobjects and under Content → Metaobjects:

  • Metaobjects: combora_bundle (the bundle), combora_component (each component line) and combora_tier (each volume/threshold tier). All three with PUBLIC_READ storefront access, publishable and translatable.
  • Shop metafields: combora.index and combora.manifest (JSON, public read), and combora.all_pdp_gwps (list.metaobject_reference → combora_bundle): the store-wide gifts that opted into being advertised on every product page.
  • Product metafield: combora.bundles (list.metaobject_reference → combora_bundle) — the reverse index product → its bundles.
⚠️ A Liquid limitation with shop-level references: a shop list.metaobject_reference (such as all_pdp_gwps) does not resolve to metaobjects in theme Liquid (the product one does; shop JSON does too). To render those gifts in Liquid, read the index entries with "all_pdp": true and resolve each bundle by handle: metaobjects['combora_bundle'][entry.handle]. Over the Storefront GraphQL API the reference resolves normally.
Content → Metaobjects with the three Combora definitions
Content → Metaobjects: the three definitions, Added by Combora, with their entries.
Definition of the Combora bundles product metafield
The Combora bundles product metafield (Metaobject type), in use on the products that take part in bundles.
Shop metafield definitions index and manifest
The shop metafields Combora bundle index and Combora contract manifest (JSON); since August 2026 they are joined by Combora store-wide gift offers (all_pdp_gwps).

22.2 What happens on publish / update / withdraw

EventEffect on the contract
Publish a bundleIts metaobjects are created/updated (children first, parent after, with deterministic handles), combora.index and combora.manifest are regenerated, the combora.bundles metafield of every participating product is written, and the translations of the translatable fields are seeded (the languages enabled in your store out of the 7 the app ships; it never overwrites a translation you customized in Translate & Adapt). The index never announces a bundle whose write failed.
Update bundleA full idempotent re-projection: it rewrites the bundle's projection from the configuration (which is why it also works as a repair). The parent's reference list is rewritten in the same step — the storefront never sees a dangling reference — and any child metaobjects left behind are removed by the nightly sweep.
Move to draftThe metaobject moves to draft state (it stops resolving on the storefront), the bundle leaves the index and its price switches off. The configuration is kept.
DeleteIts metaobjects are deleted, combora.bundles is cleaned on its products and, if it was a fixed bundle, the parent product is withdrawn.
Uninstall the appThe combora metaobjects and metafields are owned by the store and stay (your theme does not break; the data is frozen in its last state). The private $app:* metafields are removed by Shopify automatically and the Combora discount goes inert. On reinstall, the app reuses and reconciles everything — nothing is duplicated.

22.3 combora_bundle field by field

FieldTypeTranslatableContents
titletextyesStorefront title (required).
descriptionrich textyes⚠️ The definition exists but the app does not write this field yet: today it always arrives empty — render tolerant of blank.
badge_labeltextyesThe badge (Best value).
testbooleannoTest mode. "true" ⇒ the widgets render this bundle only in the theme editor and in themes whose role is not the published one. Absent or "false" ⇒ live.
stylejsonnoPrecompiled widget style (design studio): {"v":1,"source":"app","vars":"--combora-…","mode_class":"combora-w--styled","model":{…}}. Copy vars into your root's style attribute and mode_class as a class. With "source":"block" (or the field absent) the block settings in the theme editor win.
cta_labeltextyesButton text (optional).
disclaimerrich textyes⚠️ Same as description: defined but still without content — tolerate blank.
bundle_typetext (choices)noOne of: fixed · mix_and_match · volume · bogo · gwp · build_a_box · fbt · combo.
min_items / max_itemsintegernoMix (min/max) and build-a-box (size in min_items).
thresholdintegernoThe GWP threshold in minor units (cents), always in the store's currency — see the multi-currency rule in 22.9.
discount_valuejsonno{"kind":"percentage","bps":1500} (basis points: 1500 = 15%) or {"kind":"fixed_amount","amount":500} (minor units).
configjsonnoPer-type scalars: price (fixed in price mode), buy_qty/get_qty (bogo), gift_variant/threshold/min_qty/combination/qualify (gwp — the full condition lives here: spend in minor units, minimum units, both, or a combination), slots/box_price (box), anchor_variant (fbt), eligible (the pool descriptor), of (combo).
componentslist of references—→ combora_component (variant, quantity, role: component/option/anchor/gift/suggestion, position, label…).
tierslist of references—→ combora_tier (min_qty, discount_value, position, tier_label…).
productproduct referencenoAnchor product (the parent of a fixed bundle, the main product of an FBT).
imagefile referencenoOptional bundle media.
revision / contract_versioninteger / textnoPublished revision and contract version (public.v1).
combora_bundle entry in the admin
A real combora_bundle entry under Content → Metaobjects: title, Active status, deterministic handle combora-bundle-{id}.
Fields discount_value, components, anchor, contract version
The same entry: discount_value as JSON in basis points, the component references, the anchor product and contract_version = public.v1.
Do not edit these entries by hand. They are a projection of the app's configuration. The nightly sync repairs the definitions (the schemas) and re-asserts the plan badge; the contents of a hand-edited entry are restored on that bundle's next publish/update — until then, whatever you wrote by hand stays as it is (and may not match what the app charges).

22.4 The eligible descriptor

The bundle's eligible pool, with the same shape across every type:

{ "mode": "all" | "products" | "collections" | "variants",
  "variants":   ["gid://shopify/ProductVariant/…"],
  "products":   ["gid://shopify/Product/…"],
  "collections":["gid://shopify/Collection/…"],
  "resolved_count": 12, "total_count": 12, "truncated": false }

Only the non-empty lists are included. The three resolution keys are informational (on collection pools they say how many members were materialized and whether the snapshot was trimmed) — you can ignore them. Mind the convention: the metaobject's config uses snake_case (gift_variant, min_qty), while the gwp object in the index uses camelCase (giftVariant, minQty) — the same data on two different surfaces.

Careful: the role:"option" children of a collection pool are a snapshot taken at publish time; live membership (and the price) is always resolved by the Function at checkout.

22.5 combora.index and combora.manifest

shop.metafields.combora.index — the routing table of published bundles (it exists so you never have to iterate metaobjects.combora_bundle.values, which Liquid silently trims to 50):

{ "schema": 1, "revision_highwater": 128, "generated_at": "…",
  "bundles": [
    { "id": "f3062319-9d6e-…", "handle": "combora-bundle-f3062319-9d6e-…", "type": "fixed",
      "gid": "gid://shopify/Metaobject/…", "status": "active", "revision": 42 }
  ] }

The id is a text identifier, not a number. Only bundles that are published and inside their scheduling window are announced, so the index runs in step with the checkout price. Some entries carry extra fields: gwp (the gift offer, only on gift-type bundles), gwps (a combo's gift offers, in sub-offer order) and test (true if the bundle is in test mode: the storefront JS skips it on the published theme).

shop.metafields.combora.manifest — the contract descriptor a tool reads once: contract_version, schema_version, the bundle counter and the GIDs of the three definitions. It can carry powered_by_badge: true, which is what lights up the Powered by Combora badge in the blocks (only on Free-plan stores; elsewhere the field is omitted).

22.6 Reading it from Liquid

{%- comment -%} On a product page: the bundles for THIS product {%- endcomment -%}
{%- for bundle in product.metafields.combora.bundles.value -%}
  <h3>{{ bundle.title.value | escape }}</h3>
  {%- if bundle.badge_label.value != blank -%}
    <span class="badge">{{ bundle.badge_label.value | escape }}</span>
  {%- endif -%}
  <ul>
    {%- for component in bundle.components.value -%}
      {%- comment -%} label is optional: if missing, it falls back to the variant title and
          then to the product title. CAREFUL: if the product is NOT published to the Online
          Store channel, its reference does not resolve in Liquid and ALL the fallbacks
          return blank — guard the final name or you will render an empty "1× ". {%- endcomment -%}
      {%- assign name = component.label.value -%}
      {%- if name == blank -%}{%- assign name = component.variant.value.title -%}{%- endif -%}
      {%- if name == blank or name == 'Default Title' -%}{%- assign name = component.variant.value.product.title -%}{%- endif -%}
      {%- if name != blank -%}
        <li>{{ component.quantity.value }}× {{ name | escape }}</li>
      {%- endif -%}
    {%- endfor -%}
  </ul>
{%- endfor -%}

{%- comment -%} Or by direct handle, in any template {%- endcomment -%}
{%- assign bundle = metaobjects.combora_bundle['combora-bundle-{id}'] -%}

Both routes are bounded reads (the product's own list, or a direct handle) — never the global .values. Two Liquid gotchas that save you an afternoon: (1) a for over a list of references silently truncates at 50 — Combora's bundles never get there (the largest cap is 50 total entries), but do not iterate other people's collections assuming you see everything; (2) the optional fields (label, badge_label, cta_label) often arrive empty — guard with != blank as above.

22.7 Reading it from the Storefront API (localized)

query BundleByHandle @inContext(language: FR) {
  metaobject(handle: { type: "combora_bundle", handle: "combora-bundle-{id}" }) {
    title:       field(key: "title")       { value }
    badge_label: field(key: "badge_label") { value }
    components:  field(key: "components") {
      references(first: 25) {
        nodes { ... on Metaobject {
          label:    field(key: "label")    { value }
          quantity: field(key: "quantity") { value }
        } }
      }
    }
  }
}

@inContext returns the translations seeded by the app; anything untranslated falls back to the base language automatically.

22.8 What you can build with it

  • A completely custom bundle UI in the theme, without Combora's blocks, reading title, badge, components and saving from the metaobject.
  • A campaign landing page: a page that lists the active bundles from combora.index and renders each one by handle.
  • Headless / Hydrogen: the same read over the Storefront API with per-market localization.
  • Integrations (feeds, recommendation apps): the manifest declares the contract version and where everything lives.

22.9 Rules for the developer

  • The price belongs to the Function. The metaobject fields are for display: do not add up a combo's discount_value or recompute prices in the theme — the checkout is the authority.
  • Additive-only: new fields may appear; existing ones do not change. Write reads that tolerate unknown fields.
  • Combos: group the children by position divided by 1000 (each sub-offer occupies a band of 1000) or by config.of[].index — never by role, which can repeat.
  • Style: if you build your own UI, ignore style and write your CSS. If instead you want to respect the design the merchant chose in the app, copy style.vars into your root's style attribute and style.mode_class as a class; the studio's Unstyled preset exists for exactly the opposite case (clean markup, your CSS, with .combora-w and the --combora-* variables as hooks).
  • Private ≠ contract: the discount's $app:runtime, $app:fnvars and $app:validation metafields are internal to the Functions, have no storefront access, and their shape can change without notice. Do not build on them.
  • Multi-currency (Shopify Markets): every amount in the contract — config.price, threshold, fixed_amount.amount, box_price — is always in the store's currency, in minor units. If your store sells in several currencies, convert when presenting (in Liquid, multiply by cart.currency.rate; in JS, by Shopify.currency.rate) before formatting; rendering the raw number with the buyer's presentment currency shows a wrong price. The real checkout discount is computed by the app and is always correct — this rule is only about what YOUR widget displays.
  • Do not write into the combora namespace. Shopify gives you write access to it (it is your store), but the app regenerates it on every publish and a nightly review restores the fields that govern the plan. Anything of yours goes in your own namespace.
  • This annex is the complete contract reference for external developers. If something is missing and you need it, write to us through the app's chat or to support and we will document it.

23Developer annex: a widget per type, in Liquid

The previous annex describes what the contract contains; this one shows how to render every bundle type with it. All the code below reads only the public contract — no app API, none of Combora's own blocks — so you can paste it into a snippet or a Custom Liquid block on the product template and adapt the markup freely. Three rules apply to everything on this page:

  • The price belongs to the Function. Everything here is display; the amount actually charged is computed by Combora's Shopify Function at checkout. Never recompute a total and present it as the price.
  • Amounts are minor units in the store's currency. Multiply by cart.currency.rate before formatting, or a shopper browsing in another currency sees a wrong number.
  • Optional fields are often empty. Guard with != blank — including the component name after its fallbacks (see 22.6).

23.1 The skeleton every example plugs into

One card per bundle of the current product, with title, badge and the saving however the bundle expresses it (discount_value in basis points or minor units). Each type then renders its own body inside the case:

{%- liquid
  assign bundles = product.metafields.combora.bundles.value
  assign rate = cart.currency.rate | default: 1
-%}

{%- if bundles != blank -%}
  {%- for bundle in bundles -%}
    {%- liquid
      assign type = bundle.bundle_type.value
      assign dv = bundle.discount_value.value
      assign cfg = bundle.config.value
    -%}
    <article class="cx__card">
      <h3>{{ bundle.title.value | escape }}</h3>
      {%- if bundle.badge_label.value != blank -%}
        <span class="cx__badge">{{ bundle.badge_label.value | escape }}</span>
      {%- endif -%}

      {%- comment -%} The saving, however this bundle expresses it {%- endcomment -%}
      {%- if dv.kind == 'percentage' -%}
        <p>Save {{ dv.bps | divided_by: 100 }}%</p>
      {%- elsif dv.kind == 'fixed_amount' -%}
        <p>Save {{ dv.amount | times: rate | money }}</p>
      {%- endif -%}

      {%- case type -%}
        {%- comment -%} one block per type — sections 23.2 to 23.7 go here {%- endcomment -%}
      {%- endcase -%}
    </article>
  {%- endfor -%}
{%- endif -%}

23.2 Fixed bundle

List the role == 'component' children with the name fallback chain, show the bundle price when the bundle uses price mode (config.price, minor units), and link to the purchasable parent product:

{%- when 'fixed' -%}
  <ul>
    {%- for c in bundle.components.value -%}
      {%- if c.role.value == 'component' -%}
        {%- liquid
          assign name = c.label.value
          if name == blank
            assign name = c.variant.value.title
          endif
          if name == blank or name == 'Default Title'
            assign name = c.variant.value.product.title
          endif
        -%}
        {%- if name != blank -%}
          <li>{{ c.quantity.value }}× {{ name | escape }}</li>
        {%- endif -%}
      {%- endif -%}
    {%- endfor -%}
  </ul>
  {%- if cfg.price -%}
    <p class="cx__price">{{ cfg.price | times: rate | money }}</p>
  {%- endif -%}
  {%- if bundle.product.value != blank -%}
    <a href="{{ bundle.product.value.url }}">Buy this bundle</a>
  {%- endif -%}

A fixed bundle in price mode carries config.price and no discount_value; in discount mode it is the other way around — the skeleton's saving line already covers the second case.

In a single-variant product the variant title is Shopify's placeholder «Default Title» — the fallback chain skips it and uses the product title (that is what the extra condition is for).

23.3 Volume discount

The ladder lives in tiers (ordered by position), each tier with its own min_qty and discount_value:

{%- when 'volume' -%}
  <table>
    {%- for t in bundle.tiers.value -%}
      {%- assign tv = t.discount_value.value -%}
      <tr>
        <td>Buy {{ t.min_qty.value }}+</td>
        <td>
          {%- if tv.kind == 'percentage' -%}
            {{ tv.bps | divided_by: 100 }}% off
          {%- else -%}
            {{ tv.amount | times: rate | money }} off
          {%- endif -%}
        </td>
      </tr>
    {%- endfor -%}
  </table>

23.4 Buy X get Y

The rule is two scalars in config; a 100% percentage (bps == 10000) means the reward items are free:

{%- when 'bogo' -%}
  <p>Buy {{ cfg.buy_qty }}, get {{ cfg.get_qty }}
    {%- if dv.kind == 'percentage' and dv.bps == 10000 %} free{% else %} discounted{% endif -%}.</p>

23.5 Mix & match and build a box

Both are "reach a quantity" offers: mix reads the metaobject's min_items, the box reads config.slots (and config.box_price when the box charges a closed price). The eligible pool is in config.eligible (see 22.4):

{%- when 'mix_and_match' -%}
  <p>Pick {{ bundle.min_items.value }} or more from the selection.</p>
{%- when 'build_a_box' -%}
  <p>Fill a box of {{ cfg.slots }}.
    {%- if cfg.box_price %} Box price {{ cfg.box_price | times: rate | money }}.{% endif -%}</p>

23.6 Complements (FBT)

The components carry roles: the anchor is the main product, the suggestion children are the add-ons:

{%- when 'fbt' -%}
  <ul>
    {%- for c in bundle.components.value -%}
      {%- if c.role.value == 'suggestion' or c.role.value == 'anchor' -%}
        <li>
          {{ c.variant.value.product.title | default: c.label.value | escape }}
          {%- if c.role.value == 'anchor' %} <em>(this item)</em>{% endif -%}
        </li>
      {%- endif -%}
    {%- endfor -%}
  </ul>

23.7 Combo

A combo nests sub-offers. Group its children by position divided by 1000 (each sub-offer owns a band of 1000) or walk config.of[] — never group by role, which repeats across sub-offers:

{%- when 'combo' -%}
  <ol>
    {%- for sub in cfg.of -%}
      <li>{{ sub.type | replace: '_', ' ' }}</li>
    {%- endfor -%}
  </ol>

23.8 Store-wide gift offers, on any page

Gifts advertised on every product page live on a shop metafield, and theme Liquid does not resolve a shop-level metaobject list (22.1). Read the JSON index instead and resolve each bundle by handle:

{%- assign index = shop.metafields.combora.index.value -%}
{%- if index != blank -%}
  {%- for entry in index.bundles -%}
    {%- if entry.all_pdp and entry.status == 'active' -%}
      {%- assign gift = metaobjects['combora_bundle'][entry.handle] -%}
      {%- if gift != blank -%}
        <aside class="cx__gift">
          <strong>{{ gift.title.value | escape }}</strong>
          {%- assign gcfg = gift.config.value -%}
          {%- if gcfg.threshold -%}
            <span>Spend {{ gcfg.threshold | times: rate | money }} to unlock it.</span>
          {%- endif -%}
        </aside>
      {%- endif -%}
    {%- endif -%}
  {%- endfor -%}
{%- endif -%}

23.9 The complete snippet

All of the above assembled into one working file — skeleton, the six type bodies, the store-wide gifts and minimal CSS so it is legible out of the box: download combora-custom-bundle.liquid. Save it as snippets/combora-custom-bundle.liquid in your theme and render it from the product template with {% render 'combora-custom-bundle', product: product %}. GWP is not in the case on purpose: a gift is advertised (23.8 or the app's own blocks) and added automatically by the app — there is nothing for a product-card widget to sell.

24History of this edition

Every flow in this manual was tested in a real store. Here is the trail of when it was verified and what changed afterwards, so you know how current what you are reading is.

DateWhat was done
15 August 2026First complete edition, verified end to end on the development store app-bundle-6pofnlbj, with real test purchases (Bogus gateway). Two deviations were found (I-01: the pool qualifying for the gift excluded too much; I-02: CSV error reasons left untranslated) and both were fixed, with a regression test.
21 August 2026Review against the code and against the live app. Added section 13 (Designing the widget); the fixed bundle's catalog visibility (5.1); the three Insights charts; the another automatic discount is active notice in Troubleshooting; the store-wide gift on product pages (9.3); and in the annex, the test and style fields, plus powered_by_badge and the index's extra fields. The test mode copy was refined and the annex numbering corrected.
21 August 2026
(screenshots)
After the shopify app deploy that brought the widget redesign to the store, the storefront screenshots were retaken against the real store: the volume tier ladder, the + separators in complements, the mix & match picker, the combo card with its quantity chips, the gift bar (locked and unlocked), the volume, mix and BOGO carts, and the three fixed bundle ones (card, parent product and cart). Retaking that last one corrected a substantive error: the parent product's caption said its page shows its bundle price, when what the theme paints is the sum of the components — the bundle price is applied by the Function on reaching the cart.
24 August 2026Every screenshot was retaken against Combora's public demo store, which was built for the occasion with one published bundle per type and presentable names, and the text was adapted to the figures those screenshots show. The app-interface screenshots now exist in all 7 languages. Every price quoted from here on was measured against a real cart in that store, not calculated. The storefront runs Shopify's Horizon theme.
10 September 2026The demo store's whole catalogue was re-priced to round numbers and each type was moved onto products of its own, so every example measures one offer instead of the arbitration between several. The figures in this manual were then re-measured cart by cart, which is why the numbers differ from the entries above: the fixed bundle ($1,600.00 → $1,299.00), volume at 2 and at 4 units ($2,000.00 → $1,800.00 and $4,000.00 → $3,200.00), BOGO at 3 ($900.00 → $600.00), mix at 3 ($1,050.00 → $840.00), the box of 3 ($2,100.00 → $1,749.00), complements ($800.00 → $600.00), the gift over $1,000 ($1,070.00 → $1,050.00) and the combo at 2 units ($1,040.00 → $850.00). Two captions were describing behaviour that no longer exists and were corrected: a combo is not shown as a card on the product page (each sub-offer is promoted by its own block, and the combo shows in the cart), and the gift bar now names the gift and what qualifies in four states, with an optional message of your own per state (section 9.2). The gift threshold is measured on list price.
23 August 2026The manual became multilingual: it is now generated from one shared shell plus one content source per language, with a language switcher in the left panel and hreflang alternates. English is the canonical version. While translating, a contradiction was found and fixed in Troubleshooting: it claimed the checkbox picker only appears with specific variants, when 6.1 correctly documents that it appears with every scope.

The screenshots live in app/combora/public/manual/img/ in the repository: the app-interface ones under img/<language>/, one set per language, and the storefront and Shopify-admin ones at the root, because those follow your theme and your admin rather than the app's language. They are retaken from the demo store; see docs/manual/CAPTURAS.md for what state each one needs.