Hosted surfaces

Show an interactive page from your server in the storefront: declare it, open it with a signed launch, and talk to the cart through declared commands.

A hosted surface is a page on your server that the storefront shows in an isolated frame: inline in the page, or as a drawer, a modal or a sticky bar. Use one for anything the shopper interacts with, such as a size finder, a configurator or a booking form. A surface is opened by an app block in hosted_surface mode, which the merchant places in the theme editor; see App blocks.

Declare a surface#

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

How the surface opens. form_post (Signed first request in the Developer Portal): the storefront opens url with a POST carrying the signed launch token, so your server can check it and send the finished page in its first response. message (Page loads first): the storefront loads url, then hands the signed launch token to the page through the storefront bridge.

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, and must end in the extension of their type: .js or .mjs for script, .css for style; a file preloaded as the wrong type is refused when you save.

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.

Secure your surface#

  • Exchange the launch token on your server before you send anything shopper-specific, and use each token once.

  • Send Content-Security-Policy: frame-ancestors with the exchange's ancestor_origins, so no other site can frame your page.

  • Keep url version-pinned: a new version of your page is a new release, which the store accepts as an update.

  • Treat subject and customer_subject as pseudonymous identifiers for your app only; don't try to link them to the shopper's account.

  • Never put a Store API token in the page. Fetch store data from your server with the store's token.

Test#

Install the app on a development store, add the block that opens your surface to a product page in the theme editor, and open the product page. Check that the launch exchange works once and fails the second time, that each command you declared works and an undeclared one is refused, and that surface.empty() removes the space when there is nothing to show.