Storefront extensions

Add your app to a store's storefront with app blocks, hosted surfaces and app embeds.

An app adds to the storefront in three ways, all declared in tringify.app.json and frozen into each release. Changing a declaration reaches a store only when it installs or updates to a release that contains it.

Extension

What it is

Who places it

App block

A Vascula block the merchant adds to a theme section, which opens one of your hosted surfaces.

The merchant, in the theme editor, in sections that accept app blocks.

Hosted surface

A page on your server that the storefront shows in an isolated frame, inline, as a drawer, a modal or a sticky bar. It can read and change the cart only through commands you declare.

Opened by an app block.

App embed

Vascula markup on every page, or a tracking pixel.

The merchant turns it on under App embeds in the theme editor.

Storefront extensions never appear in checkout, and nothing an app does on the storefront can stop a shopper checking out.

App blocks#

json
"storefront_blocks": [
  {
    "key": "size-guide",
    "name": "Size guide",
    "surface_key": "size-guide",
    "source": "<p class=\"size-guide__label\">{{ settings.label }}</p>\n{% schema %}\n{\"name\": \"size-guide\", \"kind\": \"block\", \"settings\": [{\"id\": \"label\", \"type\": \"text\", \"default\": \"Find your size\"}]}\n{% endschema %}"
  }
]

Field

Meaning

key

Lowercase letters and digits joined by - or _, up to 40 characters. It identifies the block in themes, so keep it stable across releases.

name

What merchants see in the theme editor. Up to 80 characters.

surface_key

The hosted surface this block opens. It must be declared in the same release.

source

A complete Vascula block, up to 128 KB, with "kind": "block" in its schema. It may read only shop, routes, product, cart and collection through ctx_needs; never customer data. See Theme blocks.

An app can declare up to 10 blocks. Merchants can add them to any section whose schema sets accept_app_blocks. The storefront renders the block's markup and then mounts its surface directly after it.

Hosted surfaces#

json
"storefront_surfaces": [
  {
    "key": "size-guide",
    "name": "Size guide",
    "url": "https://example.app/surfaces/size-guide@1.4.0",
    "bridge_major": 1,
    "presentation": "inline",
    "launch_transport": "form_post",
    "height": 320,
    "actions": ["cart.add_lines"],
    "context": ["page.type", "product.id", "product.selected_variant_id"]
  }
]

Field

Meaning

key, name

As for blocks. Up to 10 surfaces per app.

url

An HTTPS page on your server with a version or content hash in its path, such as @1.4.0. A new version is a new release.

bridge_major

1.

presentation

How it first appears: inline, modal, drawer or sticky_bar. The page can change it later with surface.present().

launch_transport

form_post: the storefront posts the launch token to url when it opens the surface.

height

For inline surfaces, the height in pixels to reserve before your page loads, up to 2400, so the page doesn't jump. 0 reserves nothing.

actions

The commands the surface may send. See below.

context

The page facts the surface receives. See below.

poster

Optional first paint shown while your page loads: host_cart (the shopper's cart, drawn by the storefront; needs cart.read) or vascula (your own Vascula template in poster_source, up to 8 KB, without scripts, frames, forms or styles).

preload

Up to 4 static files from your origin the storefront may fetch early: { "path": "/assets/app@1.4.0.js", "as": "script" } or "as": "style". Paths are root-relative and versioned.

Commands#

Action

What it does

cart.read

Returns the shopper's cart.

cart.add_lines

Adds lines: { "lines": [{ "product_id", "variant_id", "quantity", "properties" }] }.

cart.update_lines

Changes quantities of lines your app added: { "lines": [{ "line_id", "product_id", "variant_id", "quantity" }] }.

cart.remove_lines

Removes lines your app added, with the same shape.

checkout.buy_now

Takes the shopper straight to checkout with the lines you send, same payload as cart.add_lines. Their cart is left as it was.

checkout.begin

Takes the shopper to checkout with their cart.

cart.update_lines and cart.remove_lines refuse lines the shopper added themselves and lines another app added. Surfaces cannot apply discount codes or set a gift card recipient. Prices, stock and line property rules are checked by Tringify, and each response carries any user_errors to show the shopper.

Context#

Context

Field in the launch

Value

page.type

page_type

The kind of page, such as product.

localization.locale, localization.country, localization.currency

locale, country, currency

The shopper's language, country and currency.

product.id

product_id

On product pages.

product.selected_variant_id

variant_id

The variant selected when the surface opened.

product.variants

product_variants

The product's options and variants with prices in the shopper's currency and availability, so your surface can offer its own variant picker.

product.is_gift_card

product_is_gift_card

Whether the product is a gift card.

customer.logged_in

logged_in

Whether the shopper is signed in. Never who they are.

cart.item_count

cart_item_count

The number of items in the cart.

Declare only what the surface uses. When an update adds commands or context to a surface, or moves it to another origin, the store's owner approves the update before it takes effect.

Open a surface#

When a surface opens, the storefront posts a form to its url with a launch_token field: a signed token that names the store, your app, the surface and the context you declared, and expires after two minutes. Exchange it from your server before you return the page:

bash
curl -X POST https://api-store.tringify.com/api/2025-01/runtime/surface-launches/exchange \
  -H "Authorization: Bearer LAUNCH_TOKEN"
json
{
  "success": true,
  "data": {
    "store_id": "74e634ea-84ec-43b8-b66b-986214c2c5a3",
    "storefront_id": "1b2f6a43-0c8e-4d2b-9a7e-3f5d8c1e6b20",
    "app_id": "d6a26466-7903-4e2f-8fd7-7c59f3c8cd9a",
    "audience": ["YOUR_CLIENT_ID"],
    "surface_key": "size-guide",
    "surface_group_id": "5f0c2b1e-7a4d-4c9e-8b3f-2d6e1a9c4b70",
    "subject": "...",
    "customer_subject": "...",
    "bridge_major": 1,
    "actions": ["cart.add_lines"],
    "context": { "page_type": "product", "product_id": 1042, "variant_id": 2210 },
    "ancestor_origins": ["https://example-store.com"],
    "expires_at": "2026-10-05T10:02:00Z"
  }
}
  • A token can be exchanged once; a second exchange returns 401.

  • Check that app_id is your app and audience is your client ID.

  • subject identifies the shopper's browser session for your app. customer_subject is present when the shopper is signed in: an identifier for that customer that only your app receives, not their account ID or contact details.

  • Send Content-Security-Policy: frame-ancestors with the ancestor_origins so only that storefront can frame your page.

The exchange needs no Store API token. If your surface needs store data the launch doesn't carry, fetch it from your server with the store's token.

Talk to the storefront#

html
<script src="https://bridge.tringify.com/surface-bridge@1.js"></script>
<script>
  const surface = TringifySurface.init({
    onInit: (init) => {
      // init.actions, init.context, init.expires_at
      surface.ready();
    },
  });

  async function addToCart(variantId) {
    const result = await surface.command("cart.add_lines", {
      lines: [{ product_id: 1042, variant_id: variantId, quantity: 1 }],
    });
    if (result.success) surface.toast("Added to cart");
  }
</script>

Method

Use

surface.ready()

Shows the surface and reports its height.

surface.observe(element)

Keeps reporting the element's height as it changes.

surface.empty()

Tells the storefront there is nothing to show, so it removes the space.

surface.present(mode)

Switches between inline, drawer, fullscreen, sticky-bar and close.

surface.command(action, payload)

Sends one of the surface's declared commands. Commands run one at a time, in order, and time out after 15 seconds.

surface.toast(message)

Shows a short message on the page.

surface.scrollIntoView()

Scrolls the page to the surface.

App embeds#

json
"storefront_embeds": [
  {
    "key": "store-badge",
    "name": "Trust badge",
    "kind": "vascula",
    "surface": "global_body_end",
    "consent_category": "essential",
    "snippet": "<div class=\"trust-badge\">{{ settings.text }}</div>",
    "settings_schema": [{ "id": "text", "type": "text", "default": "Secure checkout" }]
  }
]

Field

Meaning

key, name

As for blocks. Up to 5 embeds per app.

kind

vascula: markup rendered on every page. pixel: a tracking pixel; see Event delivery.

surface

global_head, global_body_end or global_overlay. Pixels must use global_body_end. Vascula markup is always added at the end of the page.

consent_category

The consent the shopper must give before the embed is added to the page: essential, analytics, advertising, advertising_data, personalized_advertising or personalization. Pixels use analytics, advertising or personalized_advertising.

snippet

For vascula embeds: the markup, up to 32 KB, with no <script>.

settings_schema

Optional settings the merchant fills in when turning the embed on, with the same setting types as theme sections. The snippet reads them as settings.<id>.

Merchants turn embeds on per storefront, under App embeds in the theme editor. An embed is left out of the page until the shopper has given the consent it declares.

Personalized products#

To collect text from the shopper, such as an engraving, declare line properties; for uploaded artwork and print files, use line artifacts. See Line properties and App-owned line artifacts. Check what the shopper supplies in your surface before you add it to the cart: once a line is in the cart, checkout never waits on your app.

Test#

  • Install the app on a development store and add your block to a product page in the theme editor.

  • Under the app's Installs in the Developer Portal, check which release each store runs.

  • Under Webhooks, Download lifecycle examples gives sample line artifact events to test your handlers without real orders.