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 a 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.
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.
(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 result | Widget script | Checkout-only script |
|---|---|---|
SUCCESS | Continues to navigation | Continues to navigation |
PARTIAL with failedProducts | Continues; failed products are logged in debug mode | Continues |
Returned FAILURE, a thrown handler, or no registered handler | Logs an error and stays on the landing page | Continues despite the failed or missing handler |
| Basket loading fails | Stops navigation | Stops 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:
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.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.
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
OPEN_CART opens the webshop basket:
- After social checkout, it is the fallback when no mini-cart request or
redirect_urlis 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 callsOPEN_CART. - Sprii checkout and product-link mode do not use this webshop-basket flow.
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-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
(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
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
(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
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.
(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
(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
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
(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
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.
B2Cis the default whendisplayModeis omitted. The player showspriceas the current consumer price. A positivesuggestedRetailPricestrictly 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.B2Bshowspriceas the current wholesale price, with its price label. A positivesuggestedRetailPriceis 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.b2bRetailPriceis the recommended consumer resale price, shown as RRP. Do not put it insuggestedRetailPrice.retail_markupremains 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:
{ product_id: '123', displayMode: 'B2C', price: 104.95, suggestedRetailPrice: 139.95 }Wholesale example:
{ 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
(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
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 }
>;