Variable and digital products
Commerce products are not limited to a single shippable item. A Configurable / Variation Product sells one product with selectable options such as size or color, and Virtual Product and Downloadable Product cover items that never ship. This guide covers how those types are configured, how variations are saved, and how a product view template renders the result.
The Products templating page lists every product token. This page focuses on variation and digital-delivery data.
Product types
Choose the type when you create the product. The editor label and the stored product type differ for configurable products.
| Editor label | Stored type | Use it for |
|---|---|---|
| Simple Product | physical | One item with one SKU |
| Configurable / Variation Product | variable | One product with selectable options and one variation per combination |
| Virtual Product | virtual | Services, memberships, and appointments |
| Downloadable Product | downloadable | Digital files delivered by the storefront |
Virtual and downloadable are delivery flags as well as product types. Any product, including a configurable or physical one, can be marked Virtual product or Downloadable product in the Delivery area of the product editor, and a configurable product can mix shippable and non-shippable variations through per-variation delivery flags. Virtual and downloadable products are never shipped.
Configurable (variable) products
A configurable product begins with attributes: option groups such as size and color, each with the values a shopper can pick. The variation wizard turns every combination of those values into a variation you can price, stock, and illustrate separately.
Variation fields
| Field | What it holds |
|---|---|
| Title | The variation name. Use the main product title for all variations or a unique title per combination. |
| SKU | The stock-keeping unit for the variation. Required for every variation. |
| Price | The variation price. Falls back to the product price when you do not set one. |
| Quantity | Stock available for the variation. A variation can be unlimited instead, regardless of its qty. |
| Images | Images assigned to the variation, selected from the product media. |
| Enabled | Whether the variation is purchasable. |
| Delivery flags | Optional overrides that mark a variation virtual, downloadable, or non-shipping. |
| Downloads | Optional per-variation file list that overrides the product's downloads. |
Per-field modes
The wizard asks for a mode per field. "Same for all" uses the value from the main product, "per variation" lets you fill in each combination, and "skip" leaves the field for later.
| Field | Modes |
|---|---|
| Title | Main product title for all variations, or a unique title per variation |
| Images | One gallery for all variations, unique images per variation, or skip |
| Price | Main price for all variations, a unique price per variation, or skip |
| Quantity | Same quantity for all variations, a unique quantity per variation, unlimited for all, or skip |
SKU is always per variation and always required. Default SKUs are derived from the product SKU or slug with a numeric suffix, and you can replace them.
Unlimited quantity
The product editor has an Unlimited quantity toggle. When it is on, the product stores unlimited_stock: true and no stock checks or purchase caps apply: the tracked quantity is ignored.
A variation can be unlimited on its own even when the product is not. That variation stores unlimited: true with qty: null, and only that combination bypasses the stock checks. In the wizard, choose the unlimited quantity mode for all variations, or handle it per variation when you set a unique quantity.
Stock behavior is decided by these fields:
| Field | Meaning |
|---|---|
unlimited_stock | Product-level toggle. true disables stock checks and caps for the product and its variations. |
unlimited | Per-variation flag (stored with qty: null when true). Disables stock checks for that variation only. |
track_stock | When false, the product is also treated as unlimited. |
in_stock | When false, the product or variation is marked out of stock. |
CSV import: the unlimited_stock column (accepted aliases: unlimited, unlimited_qty, unlimited_quantity; values true/false or 1/0) turns the flag on. On a product row it stores unlimited_stock with qty: null; on a variation row in a grouped import it stores unlimited: true with qty: null for that variation.
How variations are saved
The wizard stages the attribute definitions and generated variations on the product form while you work. Finishing the wizard attaches them to the product, and saving the product persists them together, so there is no separate save step for variations. Reopening the product reads the variations back into the editor, where the wizard manages them.
Attributes beyond the variation axes
The product form can also carry project attributes that are not variation axes, so a configurable product can store values such as plugin_key or show_developer_portal next to its option groups. Add them in the product editor like any other attribute; they are saved with the product and never generate a variation.
The attribute definition, not the product, decides whether a value reaches the storefront. Each definition has a Product display setting (use_in_product). Only attributes whose definition has it enabled are included in {product.specifications_json} (and counted by {product.has_specifications} / {product.specifications_count}). A deleted or disabled attribute (enabled=0) is hidden from the editor and from specifications, even when an older product still stores a value for it.
Virtual and downloadable products
Virtual products
A virtual product has no physical form: a service, membership, or appointment. It is never shipped, so no shipping step or shipping copy applies at checkout.
Downloadable products
A downloadable product delivers files through a downloads list. The file editor appears whenever Downloadable product is on, whatever the stored product type is, so a physical or configurable product can offer files too. Each entry can point at an external URL or carry files uploaded in the product editor. Uploaded files are private: buyers receive only short-lived signed links through the download endpoints described below, never the stored file path.
Uploading files
Select Upload on an entry and choose one or more files. Uploaded files are stored privately under the project's uploads path, and the platform fills in the entry's stored source. Two bundle behaviors are supported:
- Upload a ZIP to deliver that archive as one file.
- Select several files and let the platform bundle them into a single ZIP; the entry sets
bundledtotrueand lists the members infiles.
Uploads are subject to server limits and checks:
- Up to 20 files, 200 MB per file, and 500 MB per upload.
- Server-executable or web-injected file types are rejected, including
.php,.html,.svg,.js,.sh, and.py. - An uploaded ZIP is validated by entry count and uncompressed size and is never extracted.
Every uploaded file and every generated bundle stores a SHA-256 checksum on the entry.
Each entry has:
| Field | Meaning |
|---|---|
id | Row identifier used to keep entries stable while editing |
title | Display name for the file |
url | Stored source for the entry: the platform path of an upload or an external URL. It is not a public buyer link. |
size | File size in bytes, or the bundle size for a bundled entry |
checksum | SHA-256 checksum of the file or bundle |
files | Member file names inside a bundled archive |
bundled | true when the platform packed several files into one ZIP |
max_downloads | Download limit enforced per entitlement |
expires_in_days | Validity window in days, enforced per entitlement |
External-URL entries keep pointing at their external file, but the manifest still only reveals them to entitled buyers.
Temporary share links
The product editor's file card can also generate a temporary share link for an uploaded file, for cases where someone needs the file without placing an order, such as sending a plugin build to a collaborator.
- The link points at
/__runtime/downloads/share?token=<64-hex>. When the project has a domain, the editor shows the absolute URL, ready to copy. - Expiry is bounded between 1 hour and 30 days and defaults to 24 hours.
- Max downloads is optional. Leave it empty for no limit within the expiry window.
- Revoke the link at any time from the same dialog.
Share links are admin-created and deliberately bypass the buyer entitlement check, so they work for visitors who do not own the product. The token is random and stored hashed, and opening the link streams the file with attachment headers while enforcing the expiry, the download limit, and revocation. The link is a bearer credential, so treat it like a secret: anyone who has it can download the file.
Buyer downloads
Buyer access is entitlement-gated. The storefront product page asks the platform what the current visitor may download:
GET /__runtime/downloads/manifest?product_id=123[&variant_id=45][&order_token=…]
The response says whether the visitor owns the product and which files are available:
{
"owned": true,
"reason": "ok",
"files": [
{
"id": 1,
"title": "Owner's manual",
"size": 2097152,
"max_downloads": 3,
"expires_in_days": 30,
"url": "https://example.com/__runtime/downloads/file?token=…"
},
{
"id": 2,
"title": "Starter kit",
"size": 15728640,
"max_downloads": 10,
"expires_in_days": 180,
"url": null
}
]
}
ownedis the entitlement decision;reasonis one ofok,guest,not_owned, orunknown_product.files[].urlis a short-lived signed link and is present when the visitor owns the product; it isnullwhen they do not. The link is re-checked and the entitlement limits are enforced when it is used.- Render
files[].urlonly. Never render the storedurlfrom the product configuration, and never build your own entitlement check; the platform enforces it throughGET /__runtime/downloads/file?token=….
Ownership comes from the order or subscription behind the visitor:
- A
paid,processing,complete, ortrialorder item grants access, matched against the product and, when given, the variation. - An active subscription to the product also grants access.
- Refunded, cancelled, and failed orders do not.
- Signed-in buyers are recognized from their session. Guests pass the order token that the order exposes as
order.download_tokenin theorder_tokenquery parameter.
max_downloads and expires_in_days are enforced per entitlement when the signed link is used: once the allowance is used up or the window passes, GET /__runtime/downloads/file?token=… refuses the download.
The built-in product view applies this automatically. It renders a signed anchor per available file, and when the visitor is a guest or does not own the product it shows the file list with a Sign in to download or Purchase to download notice instead of a link. When the manifest endpoint is not available (an older deployment), the page falls back to the stored public links only, and never exposes private /secure-downloads/ paths.
Delivery flags per variation
A variation can override the product's delivery flags, so one configurable product can mix shipped and digital variations. Virtual and downloadable items are excluded from shipment fulfillment regardless of the product's shipping setting.
Downloads per variation
A variation can also carry its own downloads list, which replaces the product's list for that combination. Use it when different options deliver different files, such as a platform-specific build per edition:
{
"title": "Pro / Windows",
"sku": "APP-PRO-WIN",
"attribute_values": { "edition": "Pro", "platform": "Windows" },
"downloads": [
{
"id": 21,
"title": "App installer (Windows)",
"url": "https://example.com/files/app-pro-windows.exe",
"size": 104857600,
"checksum": "9f2c1d...e1",
"max_downloads": 5,
"expires_in_days": 365
}
]
}
A variation without downloads inherits the product's files, and the storefront shows the selected variation's files when it has any, otherwise the product's.
Rendering variations and downloads
Variation and download tokens resolve on the product view page. Product collections expose summary fields only, so render the option picker from the product page itself, and load buyer download links from the download manifest instead of from the tokens.
| Token | Holds |
|---|---|
{product.product_type} | variable, virtual, or downloadable for these products |
{product.is_virtual} | true when the product is virtual, by type or flag |
{product.is_downloadable} | true when the product is downloadable, by type or flag |
{product.is_shippable} | false when the product is virtual or downloadable, or does not require shipping |
{product.variants_json} | JSON array of variations, each with its own delivery flags and optional downloads |
{product.downloads_json} | JSON array of product-level download entries with size, checksum, and bundle fields; the stored url is not a buyer link |
{product.specifications_json} | JSON array of {code,label,value} entries for attributes with Product display enabled |
Because the values are JSON, embed them in <script type="application/json"> blocks and read them from JavaScript, or output them directly where raw JSON is expected.
<script id="product-variants-json" type="application/json">{product.variants_json}</script>
<script id="product-downloads-json" type="application/json">{product.downloads_json}</script>
Variation payload
Each entry carries the variation's fields and the attribute values that identify it:
[
{
"title": "Small / Blue",
"sku": "SHIRT-S-BLUE",
"price": 24.0,
"qty": 12,
"media": ["/uploads/shirt-blue.jpg"],
"enabled": true,
"attribute_values": { "size": "Small", "color": "Blue" },
"is_virtual": false,
"is_downloadable": false
}
]
Use attribute_values to match the combination a shopper selects, and enabled to remove unavailable combinations from the picker. Delivery flags inside a variation override the product defaults.
Download payload
[
{
"id": 1,
"title": "Owner's manual",
"url": "https://example.com/files/manual.pdf",
"size": 2097152,
"checksum": "a1b2c3...f9",
"files": [],
"bundled": false,
"max_downloads": 3,
"expires_in_days": 30
},
{
"id": 2,
"title": "Starter kit",
"url": "/uploads/projects/12/commerce/downloads/starter-kit.zip",
"size": 15728640,
"checksum": "d4e5f6...0a",
"files": ["manual.pdf", "templates.docx", "colors.ase"],
"bundled": true,
"max_downloads": 10,
"expires_in_days": 180
}
]
This token payload is configuration data: a bundled entry's url points at the generated ZIP and lists its members in files, an uploaded single file or an external URL has bundled set to false and an empty files list, and a variation's downloads array uses the same entry shape. Do not render these stored URLs; use the download manifest to get the signed buyer links.
Delivery-aware markup
Conditional tokens keep shipping copy away from digital products:
{if product.is_downloadable}
<section>
<h2>Included downloads</h2>
<ul id="product-downloads"></ul>
</section>
{/if}
{if product.is_shippable}
<p>Ships within 2-3 business days.</p>
{/if}
Then fill the list from the download manifest, rendering only files whose signed url is present:
<script>
(function () {
var node = document.getElementById('product-downloads');
if (!node) return;
var manifestUrl = '/__runtime/downloads/manifest?product_id=' +
encodeURIComponent('{product.id}');
fetch(manifestUrl, { credentials: 'same-origin' })
.then(function (response) { return response.json(); })
.then(function (manifest) {
node.innerHTML = (manifest.files || []).map(function (file) {
if (!file.url) {
return '<li>' + file.title + ' (unavailable)</li>';
}
return '<li><a href="' + file.url + '" rel="nofollow">' + file.title + '</a></li>';
}).join('');
});
})();
</script>
The code sample uses the signed-in buyer's session. A guest reaches the same manifest with the order token, …&order_token={order.download_token}, using the order returned after checkout.

