Skip to content

SDK Actions ​

SOME_CHECKOUT ​

Social media checkout works in the following way:

  1. A customer writes a comment on the live show on social media
  2. Sprii sends a message on direct messaging (DM) to the customer with a "Go to checkout" button
  3. The user clicks the link and ends on a landing page of your choice. The Sprii script needs to be loaded on that page
  4. The Sprii SDK triggers the action SOME_CHECKOUT

Implementing SOME_CHECKOUT ​

The payload includes orderNo, campaignUid and pageUid. See Order tracking for using orderNo and Custom prices for using campaignUid / pageUid.

The snippets use example shop functions that you must implement. Checkout quantities represent the Sprii basket snapshot; make repeat visits safe by setting or upserting quantities rather than blindly adding the snapshot again.

js
(window.onSpriiReady = window.onSpriiReady || []).push((sdk) => {
  const setProductQuantityInCart = async (product) => {
    // Example route: implement it in your shop to SET the total quantity.
    const response = await fetch('/api/cart/quantity', {
      method: 'POST',
      headers: { 'Content-Type': 'application/json' },
      body: JSON.stringify({ productId: product.product_id, quantity: product.quantity }),
    });
    if (!response.ok) throw new Error('Could not update the shop basket');
  };

  const someCheckoutHandler = async (payload) => {
    for (const product of payload.products) {
      await setProductQuantityInCart(product);
    }
    // Tie the Sprii orderNo to the user's basket so Sprii can attribute
    // the resulting order back to the live show for conversion tracking.
    await connectSpriiOrderNoToBasket(payload.orderNo);
    return { status: sdk.actionStatuses.SUCCESS };
  };

  sdk.registerAction(sdk.actions.SOME_CHECKOUT, someCheckoutHandler);
});

Checkout results and navigation ​

Handler resultWidget scriptCheckout-only script
SUCCESSContinues to navigationContinues to navigation
PARTIAL with failedProductsContinues; failed products are logged in debug modeContinues
Returned FAILURE, a thrown handler, or no registered handlerLogs an error and stays on the landing pageContinues despite the failed or missing handler
Basket loading failsStops navigationStops navigation

There is no action timeout or automatic retry. Handle failures in your shop integration and show the shopper which products could not be added; Sprii does not turn the returned message into a shopper-facing error.

When navigation continues, a mini-cart request takes precedence, then redirect_url, then OPEN_CART. Redirect URLs may be relative or absolute; utm_ parameters from the landing URL are carried over when absent from the redirect URL.

Reporting progress ​

While SOME_CHECKOUT is running, Sprii shows a full-screen progress overlay so the customer knows their cart is being prepared. The overlay is mounted automatically when the action starts and removed once the handler returns — you only need to tell it how far along you are.

Two methods are available on sdk.progress:

ts
sdk.progress.set({ percent: number }); // 0..1
sdk.progress.set({ step: number }); // requires a prior plan()
sdk.progress.plan({ totalSteps, msPerStep }); // optional, for step-based work

A few rules to know:

  • The bar only moves forward — set() calls below the current value are ignored.
  • Between explicit set() calls, the bar continues to creep slowly on its own so it never appears frozen during a long-running fetch.
  • When your handler returns successfully, the bar smoothly fills to 100% and is removed before Sprii triggers the redirect. You don't need to push it to 1.0 yourself.
  • If you never call sdk.progress.*, the bar still shows and creeps gently — but the customer gets a much better experience if you tell it where you are.

Pattern 1 — Percent-based with phase budget ​

Best when your work has distinct phases of unknown duration (e.g. one fetch updates the cart, then a loop adds products, then a cleanup step). Allocate a slice of the 0–1 range to each phase and advance through it proportionally.

js
const someCheckoutHandler = async (payload) => {
  // Plan a budget. Sum to <= 0.95; the final fill to 100% is handled by Sprii.
  const PHASE = {
    cartFetched: 0.1,
    attrsUpdated: 0.2,
    addsDone: 0.85,
    cleanupDone: 0.95,
  };

  sdk.progress.set({ percent: 0.02 });

  const cart = await getCart();
  sdk.progress.set({ percent: PHASE.cartFetched });

  await updateCartAttributes(payload.orderNo);
  sdk.progress.set({ percent: PHASE.attrsUpdated });

  // Loop with proportional advance through the adds budget.
  let added = 0;
  for (const product of payload.products) {
    await addProductToCart(product);
    added += 1;
    sdk.progress.set({
      percent:
        PHASE.attrsUpdated +
        (PHASE.addsDone - PHASE.attrsUpdated) * (added / payload.products.length),
    });
  }

  // Optional cleanup phase…
  sdk.progress.set({ percent: PHASE.cleanupDone });

  return { status: sdk.actionStatuses.SUCCESS };
};

Pattern 2 — Step-based with plan() ​

Best when you know upfront how many discrete operations you have and roughly how long each takes. Sprii will project the bar forward at the expected pace between your set({ step }) calls, so the bar stays in motion even mid-step.

js
const someCheckoutHandler = async (payload) => {
  // Each cart-add fetch usually takes ~500ms.
  sdk.progress.plan({
    totalSteps: payload.products.length,
    msPerStep: 500,
  });

  let done = 0;
  for (const product of payload.products) {
    await addProductToCart(product);
    done += 1;
    sdk.progress.set({ step: done });
  }

  return { status: sdk.actionStatuses.SUCCESS };
};

You can also mix the two: call sdk.progress.set({ percent }) for prep/cleanup phases and sdk.progress.plan(...) + set({ step }) for the body of the work. Each plan() call resets the step counter so you can declare a new phase.

Payload ​

ts
type SoMeCheckoutActionHandler = (payload: {
  orderNo?: string;
  campaignUid: string;
  pageUid: string;
  products: Array<{
    product_id: string;
    quantity: number;
    sku?: string;
    custom_attributes?: { [x: string]: unknown };
  }>;
  redirect_url?: string;
}) => Promise<
  | {
      status: 'PARTIAL';
      message: string;
      failedProducts: Array<{
        product: {
          product_id: string;
          quantity: number;
          sku?: string;
          custom_attributes?: { [x: string]: unknown };
        };
        errorMessage: string;
      }>;
    }
  | { status: 'SUCCESS'; message?: string }
  | { status: 'FAILURE'; message: string }
>;

OPEN_CART ​

OPEN_CART opens the webshop basket:

  • After social checkout, it is the fallback when no mini-cart request or redirect_url is provided.
  • In the player, when using the webshop basket without full sync, the basket icon calls it. With isFullSync: true, the icon opens the player's basket panel; its “Go to shopping cart” button calls OPEN_CART.
  • Sprii checkout and product-link mode do not use this webshop-basket flow.

Implementing OPEN_CART ​

js
(window.onSpriiReady = window.onSpriiReady || []).push((sdk) => {
  sdk.registerAction(sdk.actions.OPEN_CART, async (payload) => {
    document.querySelector('div.cart-icon')?.click();
    return { status: sdk.actionStatuses.SUCCESS };
  });
});

payload ​

ts
type OpenCartActionHandler = (payload: {
  orderNo?: string;
}) => Promise<{ status: 'SUCCESS'; message?: string } | { status: 'FAILURE'; message: string }>;

GET_CART_CONTENTS ​

When you want the e-commerce basket to be shown inside the player, you must implement GET_CART_CONTENTS This action is called in the following scenarios

  • The player is opened / maximised
  • After modifying the basket

With isFullSync: true, return the complete webshop basket. product_id must match the product or variant identifier used by the integration; do not substitute a display name. With isFullSync: false, the player displays its own Sprii reservations instead of the returned product list. A FAILURE response does not update the player basket.

Implementing GET_CART_CONTENTS ​

js
(window.onSpriiReady = window.onSpriiReady || []).push((sdk) => {
  sdk.registerAction('GET_CART_CONTENTS', async () => {
    try {
      const cart = await getCartContents();

      if (!cart) {
        return {
          status: sdk.actionStatuses.SUCCESS,
          contents: { productCount: 0, totalPrice: 0, isFullSync: true, products: [] },
        };
      }
      const contents = {
        productCount: cart.numberofproducts || 0,
        totalPrice: cart.totalpriceClean || 0,
        isFullSync: true,
        products: cart.lines
          ? cart.lines.map((line) => ({
              product_id: line.productid,
              quantity: line.quantity,
              name: line.name,
              price: line.unitPrice,
              totalPrice: line.totalPrice,
              url: line.url,
              photoUrl: line.image,
            }))
          : [],
      };

      return { status: sdk.actionStatuses.SUCCESS, contents };
    } catch (error) {
      return { status: sdk.actionStatuses.FAILURE, message: error.message };
    }
  });
});

Payload ​

ts
type GetCartContentsActionHandler = () => Promise<
  | {
      status: 'SUCCESS';
      message?: string;
      contents: {
        products: Array<{
          product_id: string;
          quantity: number;
          sku?: string;
          custom_attributes?: { [x: string]: unknown };
          name: string;
          price: number;
          totalPrice: number;
          photoUrl?: string;
          url?: string;
        }>;
        totalPrice: number;
        productCount: number;
        isFullSync: boolean;
      };
    }
  | { status: 'FAILURE'; message: string }
>;

UPDATE_CART_CONTENTS ​

When a user adds, removes or updates the content of the cart inside the player, this action is called.

The payload includes orderNo, campaignUid and pageUid. See Order tracking for using orderNo and Custom prices for using campaignUid / pageUid.

The current player sends one change per call. For added, quantity is the amount to add; for changed, it is the new absolute quantity; for removed, remove the line. Handle every entry in changes and serialize shop mutations if your cart API cannot handle concurrent requests.

Return changed: true after a successful mutation so Sprii requests fresh GET_CART_CONTENTS. Return structured errors on failure; the player closes its basket and shows an error when this list is non-empty. A thrown handler, changed: false without an error, or a failed follow-up cart read can leave the basket loading. Ensure the follow-up cart read succeeds and do not use a silent no-op as success.

Implementing UPDATE_CART_CONTENTS ​

js
(window.onSpriiReady = window.onSpriiReady || []).push((sdk) => {
  sdk.registerAction(sdk.actions.UPDATE_CART_CONTENTS, async (payload) => {
    const errors = [];
    let changed = false;

    for (const change of payload.changes) {
      try {
        switch (change.type) {
          case 'added':
            await addToCart(change.item.product_id, change.item.quantity);
            changed = true;
            break;
          case 'changed':
            await updateCart(change.item.product_id, change.item.quantity);
            changed = true;
            break;
          case 'removed':
            await updateCart(change.item.product_id, 0);
            changed = true;
            break;
        }
      } catch (error) {
        errors.push({
          item: change.item,
          type: change.type,
          errorMessage: error instanceof Error ? error.message : String(error),
        });
      }
    }
    // Tie the Sprii orderNo to the user's basket so Sprii can attribute
    // the resulting order back to the live show for conversion tracking.
    await connectSpriiOrderNoToBasket(payload.orderNo);
    if (errors && errors.length) {
      if (changed) {
        return {
          status: sdk.actionStatuses.PARTIAL,
          message: 'Some updates could not be made.',
          changed,
          errors,
        };
      }

      return {
        status: sdk.actionStatuses.FAILURE,
        message: 'Cart could not be updated',
        changed,
        errors,
      };
    }

    return {
      status: sdk.actionStatuses.SUCCESS,
      changed,
      errors,
    };
  });
});

Payload ​

ts
type UpdateCartContentsActionHandler = (payload: {
  orderNo?: string;
  campaignUid?: string;
  pageUid?: string;
  changes: Array<{
    type: 'added' | 'changed' | 'removed';
    item: {
      product_id: string;
      quantity: number;
      sku?: string;
      custom_attributes?: { [x: string]: unknown };
    };
  }>;
}) => Promise<
  | { status: 'SUCCESS'; message?: string; changed: boolean }
  | {
      status: 'PARTIAL';
      message: string;
      errors: Array<{
        item: {
          product_id: string;
          quantity: number;
          sku?: string;
          custom_attributes?: { [x: string]: unknown };
        };
        errorMessage: string;
        type: 'added' | 'changed' | 'removed';
      }>;
      changed: boolean;
    }
  | {
      status: 'FAILURE';
      message: string;
      errors: Array<{
        item: {
          product_id: string;
          quantity: number;
          sku?: string;
          custom_attributes?: { [x: string]: unknown };
        };
        errorMessage: string;
        type: 'added' | 'changed' | 'removed';
      }>;
      changed: boolean;
    }
>;

REFRESH_MINICART ​

After the webshop basket changes, closing the player calls REFRESH_MINICART. Update your site's cart count and mini-cart contents and return SUCCESS to avoid a full page reload. If the handler is missing or returns a failure, Sprii reloads the page.

js
(window.onSpriiReady = window.onSpriiReady || []).push((sdk) => {
  sdk.registerAction(sdk.actions.REFRESH_MINICART, async () => {
    await refreshShopMiniCart(); // Implement using your shop's cart API/UI.
    return { status: sdk.actionStatuses.SUCCESS };
  });
});

GET_PRODUCT_REDIRECT_URL ​

GET_PRODUCT_REDIRECT_URL can be used for 2 purposes

  • Handle a custom redirect, like SPA redirect
  • Modify the default url to the product, based on the current user

Implementing GET_PRODUCT_REDIRECT_URL ​

Example 1: custom redirect

js
(window.onSpriiReady = window.onSpriiReady || []).push((sdk) => {
  sdk.registerAction(sdk.actions.GET_PRODUCT_REDIRECT_URL, async (payload) => {
    window.history.pushState(null, '', payload.productUrl);
    window.dispatchEvent(new PopStateEvent('popstate'));
    return { status: sdk.actionStatuses.SUCCESS, handled: true };
  });
});

Example 2: modify the default url

js
(window.onSpriiReady = window.onSpriiReady || []).push((sdk) => {
  sdk.registerAction(sdk.actions.GET_PRODUCT_REDIRECT_URL, async (payload) => {
    return {
      status: sdk.actionStatuses.SUCCESS,
      handled: false,
      url: getProductRedirectUrl(payload.productUrl),
    };
  });
});

Payload ​

ts
type GetProductRedirectUrlActionHandler = (payload: {
  productUrl: string;
  productData?: { id?: string; sku?: string | undefined };
}) => Promise<
  | {
      status: 'SUCCESS';
      message?: string;
      handled: false;
      url: string;
    }
  | { status: 'SUCCESS'; message?: string; handled: true }
  | { status: 'FAILURE'; message: string }
>;

GET_USER_DATA ​

Implement GET_USER_DATA to supply the player's currency and locale. Send only the fields your integration needs. Email, name and avatar are not needed for these settings. id can identify the viewer in Sprii tracking.

Implementing GET_USER_DATA ​

js
(window.onSpriiReady = window.onSpriiReady || []).push((sdk) => {
  sdk.registerAction(sdk.actions.GET_USER_DATA, async () => {
    try {
      const userInfo = await getUserInfo();
      if (!userInfo) {
        return { status: 'SUCCESS' };
      }
      return {
        status: 'SUCCESS',
        data: {
          id: `${userInfo.id}`,

          localeCode: userInfo.localeCode,
          currencyCode: userInfo.currencyCode,
        },
      };
    } catch (error) {
      return { status: 'SUCCESS' };
    }
  });
});

Payload ​

ts
type GetUserDataActionHandler = () => Promise<
  | {
      status: 'SUCCESS';
      message?: string;
      data?: {
        id?: string;
        avatar?: string;
        localeCode?: string;
        currencyCode?: string;
        email?: string;
        name?: string;
      };
    }
  | { status: 'FAILURE'; message: string }
>;

GET_PRODUCT_DATA_LIST ​

When users own prices must be shown in the player, GET_PRODUCT_DATA_LIST must be implemented. Only return the values that you want to be different from the default values.

Price display mode and comparison prices ​

Set displayMode on each product in the response to "B2C" or "B2B" (case-sensitive). This is a response field, not an action request parameter. The same product shape applies to GET_PRODUCT_DATA and GET_PRODUCT_DATA_LIST.

  • B2C is the default when displayMode is omitted. The player shows price as the current consumer price. A positive suggestedRetailPrice strictly higher than a positive current price is shown crossed out with no extra label. Equal, lower, missing or zero comparison prices are hidden. Mobile cards show neither price label; desktop lists and details retain the current-price / price-from label.
  • B2B shows price as the current wholesale price, with its price label. A positive suggestedRetailPrice is the full wholesale price before the customer's discount, labelled WSP and not crossed out. It is not treated as a consumer discount or filtered by the B2C greater-than rule.
  • b2bRetailPrice is the recommended consumer resale price, shown as RRP. Do not put it in suggestedRetailPrice. retail_markup remains Mark-up. These fields keep their existing presentation in both modes.

All price fields must use the same currency and tax basis as price. The mode only changes presentation; it does not change basket prices, taxes or currency. Campaign prices remain the fallback when no usable viewer price is returned, and hidden-price campaigns continue to hide all prices.

Consumer example:

js
{ product_id: '123', displayMode: 'B2C', price: 104.95, suggestedRetailPrice: 139.95 }

Wholesale example:

js
{ product_id: '123', displayMode: 'B2B', price: 150, suggestedRetailPrice: 200, b2bRetailPrice: 300, retail_markup: 2 }

Wholesale integrations must send B2B to get wholesale presentation; an omitted mode always means B2C. Integrations without a product-data handler do not need to add one just to declare the default.

Implementing GET_PRODUCT_DATA_LIST ​

js
(window.onSpriiReady = window.onSpriiReady || []).push((sdk) => {
  sdk.registerAction(sdk.actions.GET_PRODUCT_DATA_LIST, async (payload) => {
    try {
      // Extract product IDs from payload
      const productIds = payload.map((p) => p.product_id);

      // Make single batch request to fetch all products
      const productValues = await loadProductListData(productIds);

      // Map the response to the expected format
      const mappedProducts = productValues.products.map((product) => ({
        product_id: product.product_id,
        displayMode: 'B2C', // Use 'B2B' for wholesale prices.
        price: product.price,
        suggestedRetailPrice: product.suggestedRetailPrice,
        b2bRetailPrice: product.b2bRetailPrice,
        retail_markup: product.retail_markup,
        minPurchaseQty: product.minPurchaseQty,
        salable: product.salable,
        images: product.images,
        url: product.url,
        variants: product.variants,
        isFavorite: product.isFavorite,
        name: product.name,
        description: product.description,
      }));

      return { status: sdk.actionStatuses.SUCCESS, data: mappedProducts };
    } catch (error) {
      return {
        status: sdk.actionStatuses.SUCCESS,
        data: [],
      };
    }
  });
});

Payload ​

ts
type GetProductDataListActionHandler = (
  payload: Array<{
    product_id: string;
    sku?: string;
    custom_attributes?: { [x: string]: unknown };
    url?: string;
    eventUid?: string;
  }>
) => Promise<
  | {
      status: 'SUCCESS';
      message?: string;
      data: Array<{
        product_id: string;
        minPurchaseQty?: number;
        displayMode?: 'B2B' | 'B2C'; // Omitted means B2C.
        price: number;
        suggestedRetailPrice?: number;
        b2bRetailPrice?: number;
        retail_markup?: number;
        salable?: boolean;
        images?: Array<string>;
        url?: string;
        variants?: Array<{ id: string; price?: number }>;
        isFavorite?: boolean;
        name?: string;
        description?: string;
      }>;
    }
  | { status: 'FAILURE'; message: string }
>;