captain-shipping-protection-sdk
v0.1.0
Published
Framework-independent SDK for adding Captain Shipping Protection to a Shopify headless storefront.
Readme
Captain Shipping Protection SDK
Framework-independent SDK for adding Captain Shipping Protection to a Shopify headless storefront.
Use this package when your storefront does not use React, when you already have your own widget UI, or when you want direct control over the integration. It can be used with Vanilla JavaScript, Vue, Svelte, Solid, Angular, or any other browser-based frontend.
The SDK:
- Loads the Captain configuration for a Shopify store.
- Calculates the correct protection price and variant for the current cart.
- Detects whether the cart already contains Captain's protection product.
- Applies merchant eligibility and exclusion rules.
- Returns plain data so your application can render any UI.
- Includes TypeScript declarations.
The SDK does not:
- Render a widget.
- Store the shopper's checked state.
- Add or remove Shopify cart lines.
- Redirect the shopper to checkout.
Those responsibilities stay in your storefront.
Requirements
- A Captain Shipping Protection configuration for the target Shopify store.
- A browser with
fetchand Web Crypto support. - HTTPS in production. Web Crypto is also available on
localhostduring local development. - Cart prices passed to the SDK must be integer minor units. For USD,
1299means$12.99.
The package is ESM-only and has no framework dependency.
Installation
With npm:
npm install captain-shipping-protection-sdkWith pnpm:
pnpm add captain-shipping-protection-sdkWith Yarn:
yarn add captain-shipping-protection-sdkQuick start
The package exports a pre-created shippingProtection singleton:
import {
shippingProtection,
type SdkCartData,
} from "captain-shipping-protection-sdk";
await shippingProtection.init({
shop: "example.myshopify.com",
country: "US",
locale: "en",
currency: "USD",
});
const cartData: SdkCartData = {
token: "gid://shopify/Cart/your-cart-id",
currency: "USD",
total_price: 12900,
item_count: 2,
items: [
{
id: 456789,
key: "gid://shopify/CartLine/your-line-id",
quantity: 2,
variant_id: 456789,
product_id: 123456,
final_line_price: 12900,
title: "Example Variant",
product_title: "Example Product",
sku: "EXAMPLE-SKU",
},
],
};
const info = await shippingProtection.getInfo({cartData});
if (!info.isExcluded) {
console.log("Protection product:", info.productId);
console.log("Protection variant:", info.variantsId);
console.log("Protection price:", info.price);
}Call init() once for the active shop, country, locale, and currency. Call
getInfo() whenever the cart changes and immediately before checkout.
Recommended cart flow
The recommended integration keeps protection out of the Shopify cart while the shopper edits it:
- Initialize the SDK.
- Convert the current Shopify cart to
SdkCartData. - Call
getInfo()and render your widget whenisExcludedisfalse. - Keep the shopper's checked choice in your own application state.
- If
includedProtectionistrue, keep the widget checked and remove the existing protection line from Shopify. - When checkout starts, call
getInfo()again with the latest cart. - If the shopper is checked and the latest info is eligible, add one unit of
variantsId. - Redirect immediately to the checkout URL returned by the updated cart.
Adding protection only at checkout prevents repeated cart mutations while the shopper changes products, quantities, or discounts. It also ensures the selected variant matches the latest eligible cart total.
Step 1: Initialize the SDK
import {shippingProtection} from "captain-shipping-protection-sdk";
await shippingProtection.init({
shop: "example.myshopify.com",
country: "US",
locale: "en",
currency: "USD",
});Initialization loads and caches the merchant's Captain configuration.
Re-run init() when any initialization value changes:
- The active Shopify store.
- The shopper country.
- The storefront locale.
- The active currency.
The exported SDK is a singleton. Calling init() again replaces its active
context and setting. A normal storefront should initialize it for one active
storefront context at a time.
The loaded merchant configuration is available after initialization:
console.log(shippingProtection.setting);Do not call getInfo() before init() resolves. It throws an error when the SDK
has not been initialized.
Step 2: Convert your Shopify cart
Shopify's Storefront API normally returns GraphQL GIDs and decimal money strings. The SDK expects numeric Shopify IDs and integer minor-unit prices.
import type {SdkCartData} from "captain-shipping-protection-sdk";
type StorefrontCart = {
id: string;
totalQuantity: number;
cost: {
subtotalAmount: {
amount: string;
currencyCode: string;
};
};
lines: {
nodes: Array<{
id: string;
quantity: number;
cost: {
totalAmount: {
amount: string;
};
};
merchandise: {
id: string;
sku?: string | null;
title: string;
product: {
id: string;
title: string;
};
};
}>;
};
};
function numericShopifyId(gid: string): number {
const value = Number(gid.split("/").pop());
if (!Number.isFinite(value)) {
throw new Error(`Invalid Shopify GID: ${gid}`);
}
return value;
}
function toMinorUnits(amount: string): number {
return Math.round(Number(amount) * 100);
}
export function toSdkCartData(cart: StorefrontCart): SdkCartData {
return {
token: cart.id,
currency: cart.cost.subtotalAmount.currencyCode,
total_price: toMinorUnits(cart.cost.subtotalAmount.amount),
item_count: cart.totalQuantity,
items: cart.lines.nodes.map((line) => ({
id: numericShopifyId(line.merchandise.id),
key: line.id,
quantity: line.quantity,
variant_id: numericShopifyId(line.merchandise.id),
product_id: numericShopifyId(line.merchandise.product.id),
final_line_price: toMinorUnits(line.cost.totalAmount.amount),
title: line.merchandise.title,
product_title: line.merchandise.product.title,
sku: line.merchandise.sku ?? "",
})),
};
}Use the cart subtotal before shipping and tax. Include discounts in
total_price and final_line_price when they affect what the shopper pays.
Step 3: Resolve protection information
const info = await shippingProtection.getInfo({
cartData: toSdkCartData(shopifyCart),
});The result contains:
interface ShippingProtectionInfo {
productId: string;
variantsId: string;
price: string;
includedProtection: boolean;
isExcluded: boolean;
excludedReason?: string;
}productId: Shopify product ID used by Captain Shipping Protection.variantsId: protection variant selected for the current eligible cart total.price: protection price in the active currency's major unit.includedProtection: whether the input cart already contains Captain's protection product.isExcluded: whether protection must not be offered for this cart.excludedReason: machine-readable reason the cart is not eligible.
Render or update your widget only after the promise resolves.
Step 4: Manage checked state
The SDK intentionally does not own UI state. Store checked in your framework,
state manager, or plain JavaScript.
Recommended rules:
- If the cart contains protection, set
checkedtotrue. - Remove that existing protection line from the cart.
- After removal, keep
checkedastrue. - For the first eligible cart without protection, initialize
checkedfromshippingProtection.setting?.tm_default_display_status === 1. - A shopper toggle changes
checkedonly. It should not mutate Shopify. - Hide the widget when
info.isExcludedistrue.
Framework-independent state example:
import type {
ShippingProtectionInfo,
} from "captain-shipping-protection-sdk";
let checked = false;
let checkedInitialized = false;
let currentInfo: ShippingProtectionInfo | null = null;
let latestRequestId = 0;
async function syncProtection(shopifyCart: StorefrontCart) {
const requestId = ++latestRequestId;
const cartData = toSdkCartData(shopifyCart);
const info = await shippingProtection.getInfo({cartData});
// Ignore a response for a cart that is no longer current.
if (requestId !== latestRequestId) {
return;
}
currentInfo = info;
if (info.includedProtection) {
checked = true;
checkedInitialized = true;
await removeProtectionLinesByProductId(info.productId);
await refreshHostCart();
} else if (!checkedInitialized) {
checked =
shippingProtection.setting?.tm_default_display_status === 1;
checkedInitialized = true;
} else if (info.isExcluded) {
checked = false;
}
renderProtection({
checked,
hidden: info.isExcluded,
info,
setting: shippingProtection.setting,
});
}
function onProtectionToggle(nextChecked: boolean) {
checked = nextChecked;
if (currentInfo) {
renderProtection({
checked,
hidden: currentInfo.isExcluded,
info: currentInfo,
setting: shippingProtection.setting,
});
}
}removeProtectionLinesByProductId, refreshHostCart, and renderProtection
are host functions. Implement them with your storefront framework and Shopify
cart layer.
Step 5: Render your UI
The SDK returns data only, so you can use your existing design system.
Minimal HTML example:
<section id="shipping-protection" hidden>
<label>
<input id="shipping-protection-toggle" type="checkbox" />
<span id="shipping-protection-title">Shipping Protection</span>
<span id="shipping-protection-price"></span>
</label>
<p id="shipping-protection-description"></p>
</section>import type {
ShippingProtectionInfo,
} from "captain-shipping-protection-sdk";
const root = document.querySelector<HTMLElement>("#shipping-protection")!;
const toggle = document.querySelector<HTMLInputElement>(
"#shipping-protection-toggle",
)!;
const price = document.querySelector<HTMLElement>(
"#shipping-protection-price",
)!;
const description = document.querySelector<HTMLElement>(
"#shipping-protection-description",
)!;
toggle.addEventListener("change", () => {
onProtectionToggle(toggle.checked);
});
function renderProtection({
checked,
hidden,
info,
}: {
checked: boolean;
hidden: boolean;
info: ShippingProtectionInfo;
}) {
root.hidden = hidden;
toggle.checked = checked;
price.textContent = new Intl.NumberFormat("en-US", {
style: "currency",
currency: "USD",
}).format(Number(info.price));
description.textContent = checked
? "Your order is protected from loss, damage, and theft."
: "Add protection to your order.";
}Merchant-configured text and styling are available through
shippingProtection.setting. See SdkCartSetting in the package's TypeScript
declarations for all currently supported fields.
If you want Captain's standard React widget, use
captain-shipping-protection-react instead.
Step 6: Prepare checkout
Always resolve information again immediately before checkout. The cart may have changed since the widget was rendered.
function toVariantGid(variantId: string): string {
return variantId.startsWith("gid://shopify/ProductVariant/")
? variantId
: `gid://shopify/ProductVariant/${variantId}`;
}
async function prepareProtectionForCheckout() {
let shopifyCart = await getCurrentShopifyCart();
let cartData = toSdkCartData(shopifyCart);
let info = await shippingProtection.getInfo({cartData});
// Never add a second protection line.
if (info.includedProtection) {
await removeProtectionLinesByProductId(info.productId);
shopifyCart = await getCurrentShopifyCart();
cartData = toSdkCartData(shopifyCart);
info = await shippingProtection.getInfo({cartData});
}
if (!checked || info.isExcluded) {
return null;
}
return {
...info,
variantGid: toVariantGid(info.variantsId),
};
}
async function checkout() {
const protection = await prepareProtectionForCheckout();
const checkoutUrl = protection
? await addProtectionAndGetCheckoutUrl(protection.variantGid)
: await getCheckoutUrl();
window.location.assign(checkoutUrl);
}After adding the protection variant, redirect using the checkout URL returned by that same cart mutation.
Do not add protection, publish the updated cart back to the cart page, and remain there. On a later cart-page load, remove the old protection line, keep the shopper checked, and calculate a fresh variant at checkout.
Shopify Storefront GraphQL
Your Shopify integration needs three operations:
- Read the current cart.
- Remove existing protection lines by cart line ID.
- Add one protection variant and return the resulting checkout URL.
Match existing protection lines by info.productId, not only by variant ID. The
correct variant may change when the eligible cart total changes.
Add protection
mutation AddShippingProtection(
$cartId: ID!
$lines: [CartLineInput!]!
) {
cartLinesAdd(cartId: $cartId, lines: $lines) {
cart {
id
checkoutUrl
}
userErrors {
field
message
}
}
}{
"cartId": "gid://shopify/Cart/your-cart-id",
"lines": [
{
"merchandiseId": "gid://shopify/ProductVariant/123456789",
"quantity": 1
}
]
}Remove existing protection
mutation RemoveShippingProtection(
$cartId: ID!
$lineIds: [ID!]!
) {
cartLinesRemove(cartId: $cartId, lineIds: $lineIds) {
cart {
id
checkoutUrl
}
userErrors {
field
message
}
}
}Always inspect Shopify userErrors before updating local state or redirecting.
Using the SDK with frameworks
Vanilla JavaScript
Initialize the SDK after your application starts, store checked in a module or
state object, and call getInfo() after every cart update. The examples above
can be used directly without a framework.
Vue
Initialize the SDK from onMounted(), keep checked, info, and pending flags
in ref() values, and watch the current cart:
import {onMounted, ref, watch} from "vue";
import {
shippingProtection,
type ShippingProtectionInfo,
} from "captain-shipping-protection-sdk";
const checked = ref(false);
const info = ref<ShippingProtectionInfo | null>(null);
onMounted(async () => {
await shippingProtection.init(initParams);
await refreshProtection();
});
watch(
() => cart.value,
() => void refreshProtection(),
{deep: true},
);
async function refreshProtection() {
info.value = await shippingProtection.getInfo({
cartData: toSdkCartData(cart.value),
});
}Apply the checked-state and existing-line rules from the main integration flow
inside refreshProtection().
Svelte
Call init() from onMount(), store the latest result in component state, and
call getInfo() from your cart update handler:
import {onMount} from "svelte";
import {
shippingProtection,
type ShippingProtectionInfo,
} from "captain-shipping-protection-sdk";
let checked = false;
let info: ShippingProtectionInfo | null = null;
onMount(async () => {
await shippingProtection.init(initParams);
info = await shippingProtection.getInfo({
cartData: toSdkCartData(cart),
});
});Angular, Solid, and other frameworks
Initialize once in the browser, map the framework's cart state to SdkCartData,
and keep checked, info, errors, and pending operations in the framework's
normal state primitives. The SDK API is plain asynchronous JavaScript.
SSR applications
Use the SDK from client-side lifecycle code. Do not run init() or getInfo()
during the server render.
For Next.js App Router, place the integration behind a Client Component:
"use client";
import {shippingProtection} from "captain-shipping-protection-sdk";For Hydrogen, Remix, Nuxt, SvelteKit, or other SSR frameworks, call the SDK after the component mounts or from browser-only cart code. Server loaders may fetch the Shopify cart, but pass the mapped cart data to the client before calling the SDK.
Content Security Policy
The SDK sends configuration and eligibility requests to:
https://insurance.captaintop.comIf your storefront has a Content Security Policy, add this origin to
connect-src:
connect-src 'self' https://insurance.captaintop.com;The SDK does not load an external script, iframe, font, or stylesheet. Do not
add broad CSP allowances such as * or 'unsafe-inline' for this package.
API reference
shippingProtection.init(params)
Loads the merchant configuration and stores the active SDK context.
Parameters:
shop: Shopify domain, for exampleexample.myshopify.com.country: shopper country code used for eligibility, for exampleUS.locale: storefront locale, for exampleen.currency: active ISO currency code, for exampleUSD.
Returns Promise<void>.
shippingProtection.getInfo({cartData})
Calculates the current protection product, variant, price, inclusion state, and eligibility.
Returns Promise<ShippingProtectionInfo>.
The method requires a successful init() first.
shippingProtection.setting
The active SdkCartSetting, or null before initialization. It contains
merchant copy, style values, price configuration, default opt-in state, claims
guide values, and exclusion configuration.
Treat the setting as read-only application data. Re-run init() instead of
mutating it.
Cart data reference
interface SdkCartData {
token: string;
currency: string;
total_price: number;
item_count: number;
items: SdkCartDataItem[];
}
interface SdkCartDataItem {
id: number;
quantity: number;
variant_id: number;
key: string;
product_id: number;
final_line_price?: number;
title?: string;
product_title?: string;
sku?: string;
}Field requirements:
total_price: full cart subtotal in integer minor units.items: all current cart lines, including an existing protection line.product_id: numeric Shopify product ID. Eligibility and existing protection detection depend on it.variant_id: numeric Shopify variant ID. Exclusion rules depend on it.final_line_price: line total in integer minor units. It is needed to remove protection and excluded products from eligible-total calculations.title,product_title, andsku: sent for remote eligibility checks when required by the merchant's exclusion configuration.token,currency,item_count,id,key, andquantity: required by the public cart contract for compatibility with Shopify cart-shaped data. The current quote calculation does not read all of these fields directly.
Common exclusion reasons
empty_cart: the cart contains no lines.invalid_cart: at least one line does not have a valid product ID.only_shipping_protection: no non-protection product remains.excluded_variant: the cart contains a merchant-configured excluded variant.check_display_hidden: Captain's eligibility response hides protection.
Exclusion reason values are extensible. Do not assume the list above is exhaustive.
Error handling
Recommended behavior:
- Catch initialization errors and continue checkout without protection.
- Catch
getInfo()errors and hide or disable your protection UI. - Track the latest cart request and ignore stale asynchronous responses.
- Prevent concurrent removal and checkout operations.
- Keep the shopper checked when removal of an existing line fails, but block adding another protection line.
- Inspect Shopify
userErrorsseparately from SDK request errors.
try {
await shippingProtection.init(initParams);
const info = await shippingProtection.getInfo({cartData});
renderProtection(info);
} catch (error) {
console.error("Shipping Protection is unavailable", error);
hideProtection();
}Integration checklist
Before deploying:
- Install
captain-shipping-protection-sdk. - Confirm the store has an active Captain Shipping Protection configuration.
- Confirm
shopuses theexample.myshopify.comformat. - Initialize with the active country, locale, and currency.
- Convert Shopify GIDs to numeric IDs.
- Pass all cart prices as integer minor units.
- Include existing protection lines in
SdkCartData. - Call
getInfo()after every cart change. - Keep checked state in the host application.
- Remove existing protection by product ID and keep checked enabled.
- Call
getInfo()again immediately before checkout. - Add exactly one unit of the latest eligible variant.
- Redirect using the checkout URL from the updated Shopify cart.
- Add
https://insurance.captaintop.comto CSPconnect-srcwhen CSP is enabled. - Test checked, unchecked, excluded, empty-cart, stale-request, API-failure, and cart-mutation-failure flows.
Troubleshooting
ShippingProtection SDK has not been initialized
Wait for shippingProtection.init() to resolve before calling getInfo().
The widget does not appear
This package does not render a widget. Render your own UI when
info.isExcluded is false, or use captain-shipping-protection-react for
Captain's standard React widget.
The protection price does not update
Call getInfo() with a newly mapped cart after every quantity, product,
discount, or currency change. Verify that all prices use integer minor units.
The cart contains duplicate protection lines
Before checkout, remove lines whose product ID matches info.productId, refresh
the cart, and call getInfo() again. Add only one unit of the returned variant.
Requests are blocked by CSP
Allow https://insurance.captaintop.com in connect-src.
The returned variant cannot be added to Shopify
Convert a numeric variantsId to
gid://shopify/ProductVariant/<variantsId>. Also confirm the protection product
is published to the Storefront sales channel.
Updating
Check the installed version:
npm list captain-shipping-protection-sdkUpdate to the latest version:
npm install captain-shipping-protection-sdk@latest