Extensions let your app run on a merchant’s storefront and inside their dashboard without the merchant editing any code. You declare them on an app version; once that version is approved, every store with your app installed gets them. Everything is delivered at runtime: there is no storefront republish, and uninstalling your app removes all of it immediately.

1. Upload a bundle

Blocks, embeds and pixels run one JavaScript file each. Upload it first:
  • JavaScript only (.js, UTF-8), 256 KiB maximum. Keep it small: it loads on the merchant’s storefront. Minify, and load heavy work lazily.
  • The URL is content-hashed and immutable (cached for a year). Uploading the same file again returns the same URL. A new build gets a new hash, so it cannot break a store still on the old version.

2. Declare extensions on a version

Rules, all checked when you create the version:
  • Up to 30 extensions per version. handle is ^[a-z0-9][a-z0-9-]{0,63}$ and unique within the version. name is 1–64 characters and is what the merchant sees.
  • target:
    • section for blocks (the default), or product_media for a block drawn over the product gallery (see Product media blocks).
    • head or body for embeds (default body).
    • product_details, order_details or customer_details for admin links and actions (required).
  • bundle_sha256 is required for blocks, embeds and pixels. It must be one of your own uploads. Admin links and actions take no bundle.
  • settings_schema uses the same setting objects as theme sections: text, textarea, richtext, number, range, checkbox, select, radio, color, url, product, collection and the rest of the theme setting types, plus header and paragraph for sidebar text. It allows up to 25 settings.
    • Ids must be unique. select and radio need options. A range needs min < max and a sensible step. Defaults must match their type.
    • Web pixels accept text settings only.
  • url (admin only, required) is a page path starting with / (not / itself), on your embedded_app_url’s origin, like every embedded-app path.
A version’s extensions are reviewed with the rest of the version. Only the approved version is live. Approving a new version replaces the previous version’s extensions on every store where the install moves to it, which is every install whose granted scopes already cover the new version’s required scopes. An install that must re-consent (pending_reauth) serves no extensions until the merchant re-authorizes.

Theme app blocks

The merchant adds your block from the theme editor’s block picker. It is offered in every section that accepts app blocks, which is any section whose schema lists { "type": "@app" }: in the default theme, Origin, the product information, featured product, cart and footer sections; in Silent Scale, those plus the article and newsletter sections. The block’s settings appear in the editor’s inspector, and their values are stored in the merchant’s theme. Your bundle registers one render function:
  • The bundle is loaded once per page, however many times the block is placed. render runs once per placement, and again (after your cleanup) when the merchant changes its settings.
  • Each block has its own error boundary. If your code throws, only your block disappears.
  • If the app is uninstalled, or the block’s extension no longer exists in your approved version, the block renders nothing. No empty box is shown.

The render context

productId, variantId, onVariantChange and addCartGuard exist only when the block sits in a product section, and activeMedia / onMediaChange only when that section draws a gallery. Anywhere else they are absent, so check for them before use:

App blocks in the product section

The product page’s main section accepts app blocks in its buy-box column, in the default theme (main-product) and in Origin (origin-main-product). A block renders at the position the merchant gives it among the section’s other blocks: above the price, between the variant picker and the Add to cart button, or anywhere else in the list. This is where a product customizer, a size guide or a personalisation form belongs.

Product media blocks

A block declared with "target": "product_media" is drawn over the active image of the product gallery, as an absolutely positioned layer the size of the image frame. It is not placed below the gallery. Use it to preview a customisation on the product photo: engraved text, a monogram, a chosen colour.
  • It is offered in the theme editor only on product sections, and it renders on the product gallery of the default theme, Origin and Silent Scale. ctx.target is "product_media".
  • root is full size over the active image: position: absolute; inset: 0. It moves with the gallery when the shopper switches images. Position your layer inside it with percentages so it follows the frame when the page is resized.
  • root has pointer-events: none, so taps and swipes go through to the gallery (zoom, carousel). Set pointer-events: auto on each interactive element you add, and only on those. In the theme editor the whole root is clickable so the merchant can select the block.
  • If the section draws no gallery (a product without media, or a section without one), the block renders nothing. It is never moved into the column instead.
  • root follows whichever frame is active. To draw over one slide only (your preview belongs on the first photo, not on the lifestyle shots), read ctx.activeMedia and subscribe to ctx.onMediaChange, and hide your layer on every other frame:
  • To bring the shopper back to your slide when they change an option while looking at another photo, call ctx.setActiveMedia(index) from your option handler:

App blocks inside a group

A Group block accepts app blocks (section target), so a merchant can frame your block with a titled panel: a “Personalized” box around a customizer in the product section, for example. A block inside a group in a product section gets the same productId, variantId, addCartGuard and media fields as one placed directly in the section. product_media blocks are placed on the product section itself, never inside a group.

Line item properties

An app can attach data to a cart line, the way Shopify line item properties work. The data travels with the line from the cart to checkout, is saved on the order line, and comes back in the order API, the orders/create webhook, the order CSV, the merchant’s order page, the packing slip and the order confirmation email. A product customizer uses it to send the shopper’s choices and a preview image with the order.

Payload

Line properties are an object keyed by your app handle. A line can carry data from several apps, and each app writes only under its own handle:
Rules, checked by the cart API. A line that breaks one is rejected with 422 invalid_app_properties:
  • Each top-level key must be the handle of an app installed and active on that store.
  • The whole object, serialised as compact JSON with sorted keys, is at most 8 KB (8192 bytes) per line. Store large data on your own server and keep only an id here.
  • No string anywhere may start with data:. Upload images and pass their URL.
  • A line with no app data behaves exactly as before.
Two lines of the same variant with different properties stay two separate lines, like two differently engraved mugs. Lines with the same variant and identical properties are merged and their quantities added.

Set properties from the storefront

Your theme block sets the properties for the product being viewed. Every Add to cart and Buy now button on the page reads them at the moment of the click and attaches them to the line.
setLineProperties checks the payload against the rules above, except whether the handle is installed, and throws a TypeError that names the field at fault. It also throws a TypeError when productId is not a non-empty string. Catch it during development rather than finding out at checkout. | window.PlatformDTC.cart.addGuard(productId, fn) | Registers a cart guard for the product. Returns an unsubscribe function. Throws a TypeError when productId is empty or fn is not a function. | The platformdtc:line-properties event fires on window after every setLineProperties call, with detail.productId. The theme’s Preview button listens for it and shows your previewImage as soon as you set it, so keep previewImage current as the shopper changes their choices. The storefront and checkout show the product’s own image first and switch to your previewImage only after it has loaded in the background with a real size (larger than 1×1). A URL that is still rendering is retried, bypassing the browser cache, for about 30 seconds; after that the product image stays. So a preview you render on first request never shows as an empty tile, and it can take a few seconds to appear. The platformdtc:cart:change event fires on window whenever the cart’s lines change. Its detail.lines has the same shape as getLines():
  • Namespace everything under your app handle. Writing under another app’s handle, or a handle that is not installed on the store, is rejected.
  • Put only what the shopper and merchant should read in properties. Everything else goes under a _ key.
  • Carts with line properties are sent to checkout through the cart API only. Cart permalinks cannot carry them. If the cart cannot be saved, the shopper sees an error rather than an order without their customisation.

Cart guards

A cart guard lets your app stop a product from being added to the cart or taken to checkout until the shopper has done what you need: filled in a required name, finished an upload, picked a design.
  • Every path runs it, first. Add to cart, Buy it now, the Apple Pay / Google Pay / PayPal / Link express buttons, quick add on a product card, bundles and Build-a-Box all run the product’s guards before anything else. They do not depend on button labels or markup. action is "add" for a cart add and "buy" for a direct hand-off to checkout.
  • Guards run before line properties are read. A guard can finish its work and call setLineProperties for the same click.
  • Answer with true, undefined or { ok: true } to allow, and false or { ok: false, message } to refuse. message is written for the shopper (at most 300 characters).
  • Synchronous is best. When every guard answers synchronously the click proceeds in the same tick. A guard may return a Promise instead. It gets 1.5 seconds.
  • It fails closed. A guard that throws, rejects, times out or returns anything else refuses the click, because an unchecked customisation is not a checked one.
  • A refusal adds no cart line and opens no checkout. The platformdtc:cart:blocked event fires on window with detail: { productId, action, message }. Handle it yourself (scroll to the field, highlight it) and call event.preventDefault(). If no listener does, the storefront shows message in its own notice.
  • Guards are per page. A guard registered on the product page does not follow the product to other pages, and lines already in the cart are never re-checked.

Read them on the order

Checkout copies each line’s properties from the saved cart onto the order line. Values sent by the browser at payment are ignored, so what reaches the order is what the cart API validated. Read them:
  • In the orders/create webhook: line_items[].app_properties.
  • From the order API: GET /api/v1/agent/v1/orders/{order_id} returns items[].appProperties ({} when the line has none).
Private _ keys are included in both, so your backend can produce the item without a second lookup.

App embeds

An embed is a script loaded once on every storefront page, after the merchant turns it on in Theme editor → App embeds. Settings you declare are editable there too, and they reach your script like this:
The script is inserted async into <head> (target: "head") or at the end of <body>.

Web pixels

A web pixel observes storefront customer events without touching the page. Each enabled pixel runs in its own hidden <iframe sandbox="allow-scripts">. It has an opaque origin, so it can reach neither the page’s DOM nor its cookies directly. The API matches Shopify’s register:
Events use Shopify’s standard names: Every event has { id, clientId, name, seq, timestamp, type, context, data }. type is standard for the events above and custom for anything else the storefront tracks. context holds document, navigator and window snapshots. data carries the event’s commerce fields: value, currency, product ids, order id and so on. browser executes in the top frame, asynchronously:
  • browser.cookie.get(name) and browser.cookie.set(cookieString)
  • browser.localStorage and browser.sessionStorage, each with getItem, setItem and removeItem
Consent. When the shopper has declined tracking (window.dtcConsent === false), no events are delivered to any pixel.

App proxy

An app proxy serves your own pages on the merchant’s storefront domain. Examples are a reviews page, a store locator, or a JSON endpoint your theme block calls without CORS.
With that declaration, https://merchant.com/apps/reviews/product/42?page=2 is forwarded to https://reviews.example.com/proxy/product/42?page=2&shop=…&logged_in_customer_id=&path_prefix=/apps/reviews&timestamp=…&signature=….
  • prefix is one of apps, a, community or tools. These four path prefixes are reserved on every PlatformDTC storefront. subpath is ^[a-z0-9-]{1,30}$.
  • Added query parameters:
    • shop: the store’s permanent platform host (<store>.myshopquantum.ai). It stays the same when the merchant changes their custom domain.
    • path_prefix: /{prefix}/{subpath}.
    • timestamp: Unix seconds.
    • logged_in_customer_id: currently always empty.
    • signature.
  • Request Cookie and Authorization headers are removed. X-Forwarded-Host (the domain the shopper used) and X-Forwarded-For are added.
  • Your response’s status, body and Content-Type are passed through. Set-Cookie is removed. Redirects are passed to the browser, not followed.
  • Timeout is 10 seconds. After that the shopper gets a 504.
  • Content-Type: application/liquid is not supported. PlatformDTC storefronts are not Liquid themes, and such a response returns 501. Return HTML (or JSON) instead.

Verify the signature

The algorithm is Shopify’s, so existing Shopify app-proxy verification code works unchanged:
  1. Remove signature from the query parameters.
  2. For each remaining key, join its values with ,. Format each pair as key=value.
  3. Sort the pairs, then concatenate them with no separator.
  4. Compute a hex HMAC-SHA256 of that string, keyed with your app’s client_secret. Compare it to signature in constant time.
  5. Reject requests whose timestamp is more than a few minutes old.
The signature only proves the request came through PlatformDTC for that shop. Always check that the data you return belongs to that shop. admin_link and admin_action add an entry to the More actions menu of a product, order or customer page. Choosing it opens your embedded app inside the dashboard at url (on your embedded_app_url’s origin), with resource (product, order or customer) and id added to the query string, plus ext=<handle> so one page can serve several entries. Authenticate the request with the session token as for any embedded page.