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:
| Type | What it does | When to use it |
|---|---|---|
| Fixed bundle | A 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 & match | The shopper picks N items from a group and gets a discount. | "Pick 3 tees and save 20%". |
| Volume discount | Tiers: 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 box | The 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. |
| Combo | Stacks two or more of the offers above into a single bundle; inside a combo they do add up. | Campaigns: "volume + gift from $1,000". |
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.
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:
| Block | Types it shows | Where to add it |
|---|---|---|
| Combora bundle | Fixed bundle and Combo ("Available bundles") | Product template |
| Combora mix & match | Mix & match · Build a box | Product template |
| Combora volume table | Volume · Buy X get Y | Product template |
| Combora complements | Complements | Product template (and cart) |
| Combora gift offer | Gift with purchase (the offer itself) | Product template |
| Combora gift progress | Gift progress bar | Cart template |
| Combora Gift (app embed) | The engine that adds/removes the gift | App 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.
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.
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.
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).
Help
Frequently asked questions and explanations of the concepts (arbitration between bundles, gifts, blocks).
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.
4Anatomy of the editor
Every type shares the same editor; only the products and pricing section changes. A brand-new bundle looks like this:
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:
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
- Products: add the components (specific variants) and how many of each. Maximum 30 components per bundle.
- 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.
- 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.
- 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.
5.2 What your shopper sees
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/allas 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
- 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).
- Quantity: the minimum number of items to qualify, and optionally a maximum (above the maximum, the extra items are paid at full price).
- Discount: a percentage or an amount, applied to every eligible item in the cart once the minimum is reached.
6.2 What your shopper sees
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
- 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.
- 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).
7.2 What your shopper sees
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
8.2 What your shopper sees
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.
9.1 Configuration
- The gift: a specific variant that gets added when the shopper qualifies.
- 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.
- What counts toward the condition: any product in the store, specific products, or collections.
9.2 What your shopper sees
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.
{{ 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
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
11.2 What your shopper sees
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
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.
13.1 Two places where you design
| Where | Scope | When 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.
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-wclass 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 type | Elements you can reorder |
|---|---|
| Fixed bundle · Combo | Title · Included products · Price · Saving · Button |
| Mix & match · Build a box | Title · Progress counter · Option list · Note · Button |
| Volume discount | Title · Tier ladder |
| Buy X get Y | Title · Offer rule |
| Gift with purchase | Title · Gift · Unlock condition |
| Complements | Title · Item list · Saving · Button |
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.
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:
- 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.
- 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.
- On a tie, the oldest bundle wins (the first one you created).
- 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.
A real numeric example (verified in the store)
| Cart | Candidate offers | Result |
|---|---|---|
| 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 × Oxygen | Volume 2+ → 10% (the 4+ tier is not reached) | $2,000.00 → $1,800.00 |
| 3 eligible boards | Mix & 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 board | The $20.00 ski wax joins at $0.00: $1,070.00 → $1,050.00 |
| 2 × the board of the combo | Inside one combo: volume 15% and a gift | Both 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
- Pick the collection.
- Pick the template: "One multipack per product" (a fixed bundle of N units for each product) or "A single bundle for the whole collection".
- Configure units and discount, then hit Create drafts.
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:
| Configuration | What 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 5 | 12 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 Active | 7 drafts; the banner reports the skipped ones and the reason for each. |
| Re-running the exact same configuration next month | Only 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 · multipack | The 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. |
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).
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:quantityseparated by|. - Up to 200 rows per import; a bigger file is trimmed and it tells you.
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
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:
| Process | When | What it does for you |
|---|---|---|
| Scheduling windows | every 15 min | Activates 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 refresh | 03:00 | Confirms your real plan with Shopify (upgrade, cancellation, end of trial) so limits and badge always reflect the truth. |
| Collection pools | 04:00 | Updates 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-healing | 04:30 | Deletes 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
| Metric | What it measures exactly |
|---|---|
| Attributed revenue | The revenue of the order lines that carried a Combora discount (not the order total). |
| % of orders with a bundle | The share of orders in the range that included at least one bundle. |
| Orders with a bundle | The literal fraction: orders with a bundle / total orders. |
| Savings delivered | The total discount your bundles gave to shoppers. |
| Average order value | Across all orders in the range. |
| Bundle order value | The 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 gift | Orders 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.
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.
19.1 Health checks
The live state of what support asks about first, verified against Shopify on every load:
| Check | What it verifies |
|---|---|
| Bundles | Sync errors and published bundles with unpublished changes (you edited and did not press Update bundle). |
| Automatic discount in Shopify | That the Combora discount exists and is ACTIVE — it is the one that charges the prices at checkout. |
| Published store index | That the public data your theme blocks read is up to date (live bundles vs. expected). |
| Combora Gift app embed | That the gift engine is enabled in your theme. |
| Discount applied on recent orders | Orders from the last 7 days with a bundle: if ALL of them arrived without a discount, something is wrong at checkout. |
| Storefront error channel | That your storefront blocks can report failures here (date of the last event received). |
| Background jobs | Failed nightly jobs. |
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.
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.
/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 / rule | Value | What happens when you hit it |
|---|---|---|
| Published bundles (Free plan) | 3 | Publishing the 4th is blocked with a notice. Drafts do not count. |
| Components in a fixed bundle | 30 | The editor will not let you add more. |
| Products in a combo (flattened) | 50 entries | The editor shows the usage (Uses X of 50) and blocks the excess. |
| Collections per picker | 100 | Validated on save. |
| Gift combination (GWP) | 2–5 items | Validated on save. |
| Title as the discount message | 100 characters | The text shown at checkout is trimmed (the full title is kept). |
| Generate from a collection | 100 drafts | Trimmed, and it tells you. |
| CSV import | 200 rows | Trimmed, and it tells you to split the file. |
| Offers visible per block | 1–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) andcombora_tier(each volume/threshold tier). All three withPUBLIC_READstorefront access, publishable and translatable. - Shop metafields:
combora.indexandcombora.manifest(JSON, public read), andcombora.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.
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.
all_pdp_gwps).22.2 What happens on publish / update / withdraw
| Event | Effect on the contract |
|---|---|
| Publish a bundle | Its 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 bundle | A 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 draft | The 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. |
| Delete | Its metaobjects are deleted, combora.bundles is cleaned on its products and, if it was a fixed bundle, the parent product is withdrawn. |
| Uninstall the app | The 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
| Field | Type | Translatable | Contents |
|---|---|---|---|
title | text | yes | Storefront title (required). |
description | rich text | yes | ⚠️ The definition exists but the app does not write this field yet: today it always arrives empty — render tolerant of blank. |
badge_label | text | yes | The badge (Best value). |
test | boolean | no | Test 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. |
style | json | no | Precompiled 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_label | text | yes | Button text (optional). |
disclaimer | rich text | yes | ⚠️ Same as description: defined but still without content — tolerate blank. |
bundle_type | text (choices) | no | One of: fixed · mix_and_match · volume · bogo · gwp · build_a_box · fbt · combo. |
min_items / max_items | integer | no | Mix (min/max) and build-a-box (size in min_items). |
threshold | integer | no | The GWP threshold in minor units (cents), always in the store's currency — see the multi-currency rule in 22.9. |
discount_value | json | no | {"kind":"percentage","bps":1500} (basis points: 1500 = 15%) or {"kind":"fixed_amount","amount":500} (minor units). |
config | json | no | Per-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). |
components | list of references | — | → combora_component (variant, quantity, role: component/option/anchor/gift/suggestion, position, label…). |
tiers | list of references | — | → combora_tier (min_qty, discount_value, position, tier_label…). |
product | product reference | no | Anchor product (the parent of a fixed bundle, the main product of an FBT). |
image | file reference | no | Optional bundle media. |
revision / contract_version | integer / text | no | Published revision and contract version (public.v1). |
combora_bundle entry under Content → Metaobjects: title, Active
status, deterministic handle combora-bundle-{id}.
discount_value as JSON in basis points, the component references,
the anchor product and contract_version = public.v1.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.indexand renders each one by handle. - Headless / Hydrogen: the same read over the Storefront API with per-market localization.
- Integrations (feeds, recommendation apps): the
manifestdeclares 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_valueor 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
positiondivided by 1000 (each sub-offer occupies a band of 1000) or byconfig.of[].index— never byrole, which can repeat. - Style: if you build your own UI, ignore
styleand write your CSS. If instead you want to respect the design the merchant chose in the app, copystyle.varsinto your root'sstyleattribute andstyle.mode_classas a class; the studio's Unstyled preset exists for exactly the opposite case (clean markup, your CSS, with.combora-wand the--combora-*variables as hooks). - Private ≠ contract: the discount's
$app:runtime,$app:fnvarsand$app:validationmetafields 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 bycart.currency.rate; in JS, byShopify.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
comboranamespace. 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.ratebefore 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.
| Date | What was done |
|---|---|
| 15 August 2026 | First 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 2026 | Review 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 2026 | Every 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 2026 | The 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 2026 | The 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.