SDK Actions
SOME_CHECKOUT
Social media checkout works in the following way:
- A customer writes a comment on the live show on social media
- Sprii sends a message on direct messaging (DM) to the customer with a "Go to checkout" button
- The user clicks the link and ends on an landing page of your choice. The Sprii script needs to be loaded on that page
- 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.
(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:
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 workA 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.
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.
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
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
(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
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
(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
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
(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
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
(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
(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
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
(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
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
(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
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 }
>;