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#
"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 |
|---|---|
|
As for blocks. Up to 10 surfaces per app. |
|
An HTTPS page on your server with a version or content hash in its path, such as |
|
|
|
How it first appears: |
|
How the surface opens. |
|
For inline surfaces, the height in pixels to reserve before your page loads, up to 2400, so the page doesn't jump. |
|
The commands the surface may send. See below. |
|
The page facts the surface receives. See below. |
|
Optional first paint shown while your page loads: |
|
Up to 4 static files from your origin the storefront may fetch early: |
Commands#
Action |
What it does |
|---|---|
|
Returns the shopper's cart. |
|
Adds lines: |
|
Changes quantities of lines your app added: |
|
Removes lines your app added, with the same shape. |
|
Takes the shopper straight to checkout with the lines you send, same payload as |
|
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 |
|---|---|---|
|
|
The kind of page, such as |
|
|
The shopper's language, country and currency. |
|
|
On product pages. |
|
|
The variant selected when the surface opened. |
|
|
The product's options and variants with prices in the shopper's currency and availability, so your surface can offer its own variant picker. |
|
|
Whether the product is a gift card. |
|
|
Whether the shopper is signed in. Never who they are. |
|
|
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:
curl -X POST https://api-store.tringify.com/api/2025-01/runtime/surface-launches/exchange \
-H "Authorization: Bearer LAUNCH_TOKEN"
{
"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_idis your app andaudienceis your client ID.subjectidentifies the shopper's browser session for your app.customer_subjectis 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-ancestorswith theancestor_originsso 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#
<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 |
|---|---|
|
Shows the surface and reports its height. |
|
Keeps reporting the element's height as it changes. |
|
Tells the storefront there is nothing to show, so it removes the space. |
|
Switches between |
|
Sends one of the surface's declared commands. Commands run one at a time, in order, and time out after 15 seconds. |
|
Shows a short message on the page. |
|
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-ancestorswith the exchange'sancestor_origins, so no other site can frame your page.Keep
urlversion-pinned: a new version of your page is a new release, which the store accepts as an update.Treat
subjectandcustomer_subjectas 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.