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
- Up to 30 extensions per version.
handleis^[a-z0-9][a-z0-9-]{0,63}$and unique within the version.nameis 1–64 characters and is what the merchant sees. target:sectionfor blocks (the default), orproduct_mediafor a block drawn over the product gallery (see Product media blocks).headorbodyfor embeds (defaultbody).product_details,order_detailsorcustomer_detailsfor admin links and actions (required).
bundle_sha256is required for blocks, embeds and pixels. It must be one of your own uploads. Admin links and actions take no bundle.settings_schemauses the same setting objects as theme sections:text,textarea,richtext,number,range,checkbox,select,radio,color,url,product,collectionand the rest of the theme setting types, plusheaderandparagraphfor sidebar text. It allows up to 25 settings.- Ids must be unique.
selectandradioneedoptions. Arangeneedsmin<maxand a sensiblestep. Defaults must match their type. - Web pixels accept
textsettings only.
- Ids must be unique.
url(admin only, required) is a page path starting with/(not/itself), on yourembedded_app_url’s origin, like every embedded-app path.
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.
renderruns 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.targetis"product_media". rootis 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.roothaspointer-events: none, so taps and swipes go through to the gallery (zoom, carousel). Setpointer-events: autoon 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.
rootfollows whichever frame is active. To draw over one slide only (your preview belongs on the first photo, not on the lifestyle shots), readctx.activeMediaand subscribe toctx.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, theorders/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.
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.
actionis"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
setLinePropertiesfor the same click. - Answer with
true,undefinedor{ ok: true }to allow, andfalseor{ ok: false, message }to refuse.messageis 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:blockedevent fires onwindowwithdetail: { productId, action, message }. Handle it yourself (scroll to the field, highlight it) and callevent.preventDefault(). If no listener does, the storefront showsmessagein 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/createwebhook:line_items[].app_properties. - From the order API:
GET /api/v1/agent/v1/orders/{order_id}returnsitems[].appProperties({}when the line has none).
_ 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: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:
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)andbrowser.cookie.set(cookieString)browser.localStorageandbrowser.sessionStorage, each withgetItem,setItemandremoveItem
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.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×tamp=…&signature=….
prefixis one ofapps,a,communityortools. These four path prefixes are reserved on every PlatformDTC storefront.subpathis^[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
CookieandAuthorizationheaders are removed.X-Forwarded-Host(the domain the shopper used) andX-Forwarded-Forare added. - Your response’s status, body and
Content-Typeare passed through.Set-Cookieis removed. Redirects are passed to the browser, not followed. - Timeout is 10 seconds. After that the shopper gets a
504. Content-Type: application/liquidis not supported. PlatformDTC storefronts are not Liquid themes, and such a response returns501. Return HTML (or JSON) instead.
Verify the signature
The algorithm is Shopify’s, so existing Shopify app-proxy verification code works unchanged:- Remove
signaturefrom the query parameters. - For each remaining key, join its values with
,. Format each pair askey=value. - Sort the pairs, then concatenate them with no separator.
- Compute a hex HMAC-SHA256 of that string, keyed with your app’s
client_secret. Compare it tosignaturein constant time. - Reject requests whose
timestampis more than a few minutes old.
shop. Always check that
the data you return belongs to that shop.
Admin links and actions
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.