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 an 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.

js


(window.onSpriiReady = window.onSpriiReady || []).push((sdk) => {
    ...
    const addProductToCart = async (product) => {
      await fetch(`shop-api?itemid=${product.product_id}&quantity=${product.quantity}`, {
        method: 'GET',
      });
    };

    const someCheckoutHandler = async (payload) => {
      for (const product of payload.products) {
        await addProductToCart(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);
    ...

});

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 forwardset() 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.10,
    attrsUpdated: 0.20,
    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

js
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

The OPEN_CART action is called in 2 scenarios

  • A user is doing SOME checkout
  • A user clicks "Go to basket" in the player

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

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

GET_CART_CONTENTS

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

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

Implementing GET_CART_CONTENTS

js
(window.onSpriiReady = window.onSpriiReady || []).push((sdk) => {
    ...
      sdk.registerAction('GET_CART_CONTENTS', async () => {
          try {
              const cart = 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,
                          unitname: orderlineLine.unitname,
                          url: line.url,
                          photoUrl: line.image,

                          })
                      )
                  : [],
              };

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


});

Payload

js
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.

Implementing UPDATE_CART_CONTENTS

js
(window.onSpriiReady = window.onSpriiReady || []).push((sdk) => {
    ...
      sdk.registerAction(sdk.actions.UPDATE_CART_CONTENTS, async (payload) => {
      const errors: ShoppingCartUpdateError[] = [];
      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 as Error).message,
          });
        }
      }
      // 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

js
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;
    }
>;

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

js
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

GET_USER_DATA must be implemented if the player should change currency, locale or other user specific settings

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,
            email: userInfo.email,
            name: userInfo.name,
          },
        };
      } catch (error) {
        return { status: 'SUCCESS' };
      }
    });
    ...


});

Payload

js
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.

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,
          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

js
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;
        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 }
>;