Skip to content

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 labelStored typeUse it for
Simple ProductphysicalOne item with one SKU
Configurable / Variation ProductvariableOne product with selectable options and one variation per combination
Virtual ProductvirtualServices, memberships, and appointments
Downloadable ProductdownloadableDigital 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

FieldWhat it holds
TitleThe variation name. Use the main product title for all variations or a unique title per combination.
SKUThe stock-keeping unit for the variation. Required for every variation.
PriceThe variation price. Falls back to the product price when you do not set one.
QuantityStock available for the variation. A variation can be unlimited instead, regardless of its qty.
ImagesImages assigned to the variation, selected from the product media.
EnabledWhether the variation is purchasable.
Delivery flagsOptional overrides that mark a variation virtual, downloadable, or non-shipping.
DownloadsOptional 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.

FieldModes
TitleMain product title for all variations, or a unique title per variation
ImagesOne gallery for all variations, unique images per variation, or skip
PriceMain price for all variations, a unique price per variation, or skip
QuantitySame 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:

FieldMeaning
unlimited_stockProduct-level toggle. true disables stock checks and caps for the product and its variations.
unlimitedPer-variation flag (stored with qty: null when true). Disables stock checks for that variation only.
track_stockWhen false, the product is also treated as unlimited.
in_stockWhen 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 bundled to true and lists the members in files.

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:

FieldMeaning
idRow identifier used to keep entries stable while editing
titleDisplay name for the file
urlStored source for the entry: the platform path of an upload or an external URL. It is not a public buyer link.
sizeFile size in bytes, or the bundle size for a bundled entry
checksumSHA-256 checksum of the file or bundle
filesMember file names inside a bundled archive
bundledtrue when the platform packed several files into one ZIP
max_downloadsDownload limit enforced per entitlement
expires_in_daysValidity 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:

text
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:

json
{
  "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
    }
  ]
}
  • owned is the entitlement decision; reason is one of ok, guest, not_owned, or unknown_product.
  • files[].url is a short-lived signed link and is present when the visitor owns the product; it is null when they do not. The link is re-checked and the entitlement limits are enforced when it is used.
  • Render files[].url only. Never render the stored url from the product configuration, and never build your own entitlement check; the platform enforces it through GET /__runtime/downloads/file?token=….

Ownership comes from the order or subscription behind the visitor:

  • A paid, processing, complete, or trial order 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_token in the order_token query 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:

json
{
  "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.

TokenHolds
{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.

html
<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:

json
[
  {
    "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

json
[
  {
    "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:

html
{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:

html
<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.