# Subbly Developers > Complete documentation for Large Language Models --- ## Document: /reference/subbly-sdk URL: /reference/subbly-sdk Subbly.js is Subbly's browser-side JavaScript library. It calls the Storefront API for you, holds one cart per page, and keeps the cart ID and the customer access token in browser cookies. `Subbly.init` resolves with an SDK instance; every method in this reference is a method of that instance. The SDK emits no events: it has no emitter and dispatches no DOM events. The events you may have seen in Subbly belong to the [cart widget](/reference/cart-widget), a separate product. ## Load ```js title="Start the SDK" import Subbly from '@subbly/sdk' const subbly = await Subbly.init({ apiKey: 'API_KEY' }) ``` The package also ships a browser bundle, `https://cdn.jsdelivr.net/npm/@subbly/sdk`, that defines the global `Subbly`. It is the class, not an instance, so you still call `Subbly.init(config)`. ## Cookies The SDK writes two cookies, each for 365 days on `path=/`: - `subbly_cart_id` — the cart, written by `Subbly.init` when you pass `loadCart`, and by `subbly.cart.create`. - `subbly_access_token` — the customer access token, written by the authentication methods and emptied by `subbly.auth.logout`. Both are plain cookies. The SDK sets no `Secure`, `HttpOnly` or `SameSite` attribute, so any script on the host can read them. ## Amounts and dates Amounts are whole numbers in the minor unit of the currency: `1250` in a `USD` cart means $12.50. Dates and timestamps are strings. ## Currency The product, bundle and survey methods take an optional `headers` object as their last argument. Set `x-currency` on it to price that one response in another currency. When you leave it out, the SDK sends the currency you set with [`subbly.setCurrency`](/reference/subbly-sdk/setup#set-currency); when you set neither, it sends no currency header. The subscription methods also take `headers`, but the SDK adds no currency to them. `setCurrency` does not change the cart. The cart has its own `currencyCode`; change it with `subbly.cart.update({ currencyCode })`. When `Subbly.init` loads a cart, it calls `setCurrency` with the cart currency for you. ```js title="Price one product in another currency" subbly.setCurrency('USD') const product = await subbly.products.load(123) const inEuros = await subbly.products.load(123, {}, { 'x-currency': 'EUR' }) ``` ## Expand parameters Most methods that return a cart or a subscription take an optional `params` object with an `expand` list. Each entry names a relation the API should fill in. A relation you do not name comes back `null`, so ask for what you need to render. The cart methods accept `coupon`, `gift_card`, `items.product`, `items.product.parent`, `items.product.parent.pricings`, `items.bundle.items.product`, `items.bundle.items.product.parent`, `items.metadata`, `shipping_method`, `shipping_address`, `payment_method`, `billing_address`, `metadata` and `shipping_carrier`. The funnel accepts that return a cart take the same list. The subscription methods accept `product`, `product.parent`, `bundle.items.product`, `bundle.items.product.parent`, `metadata`, `addons`, `discounts` and `cancellations`. The funnel fetches accept `parent`, which loads the parent product of each offered variant or plan. ```js title="Load a cart with its items and coupon" await subbly.cart.load(cartId, { expand: ['items.product', 'items.product.parent', 'coupon'] }) ``` ## Errors A failed request rejects with a plain object, not an `Error`. So `error instanceof Error` is `false` and `error.message` may be missing. Read `error.code` instead. ```js title="Handle an error" try { await subbly.cart.addItem({ productId: 3301 }) } catch (error) { if (error.code === 'out_of_stock') { // tell the customer } } ``` The codes are: | `code` | What happened | | :-- | :-- | | `not_found` | The record does not exist. | | `bad_request` | The request or the payload is invalid. | | `out_of_stock` | A product or a survey answer ran out. | | `payment_requires_action` | The card needs 3-D Secure. The object also carries `paymentIntentId`. | | `user_email_is_not_unique` | The email already has an account. | | `unauthenticated` | There is no valid access token. | When the response has no body, the rejection is a real `Error` carrying the HTTP status text instead. Handle both shapes. Two rejections are plain `Error` objects instead, with no `code`: - `Error('Missing required apiKey config')` — `Subbly.init` with no argument, or with an empty or missing `apiKey`. `init` is `async`, so it rejects. - `Error('Cart ID is required')` — `subbly.cart.load` with no ID given and none held. To install, configure and update the library, see the [Subbly.js SDK article](/developer-resources/subbly-js-sdk). --- ## Document: /reference/cart-widget URL: /reference/cart-widget SubblyCart.js is Subbly's cart and checkout widget. One script tag adds a cart, a product configurator and a full checkout to any website, in your shop's colours and languages. It talks to the Storefront API through [Subbly.js](/reference/subbly-sdk), and it keeps the cart ID and the customer's access token in cookies, so a cart survives a page load. ## Load ```html title="Embed the widget" ``` The widget reads `window.subblyConfig` the moment it runs, so write the config object first. Every method and property in this reference belongs to `window.subblyCart`, set once the `subbly-cart-initialized` event has fired on `window`. ## Globals | Global | What it is | | --- | --- | | `window.subblyConfig` | The config object you write, read once while the widget loads. | | `window.SubblyCart` | The widget **class**. Call `SubblyCart.initialize(config)` on it when you set `init: false`. | | `window.subblyCart` | The initialized widget **instance** — the object every method and property in this reference belongs to. `globalName` renames it. | | `window.subblyCheckout` | Optional. The item to configure in standalone checkout mode: an object with a `payload` key holding a [`ConfigureItemPayload`](/reference/cart-widget/methods#configure-item), or `null`. | ## Cookies and storage | Key | Store | What it holds | | --- | --- | --- | | `subbly_cart_id` | cookie, path `/`, 365 days | ID of the active cart. | | `subbly_access_token` | cookie, path `/`, 365 days | Access token of the signed-in customer. | | `referral_id` | cookie, 30 days | Referral ID from the `r` query parameter, kept for the next visit. | | `subbly_checkout_payload` | `localStorage` | The item to configure in standalone checkout mode, tied to one shop and cart. | ## Errors The widget throws a plain `Error` with a readable message — `The cart is not initialized`, for example. Calls that reach the API reject with the Subbly.js error object, which carries a `code`: `not_found`, `bad_request`, `out_of_stock`, `payment_requires_action`, `user_email_is_not_unique` or `unauthenticated`. ```js title="Handle a failure" try { await subblyCart.applyCoupon('WELCOME10') } catch (err) { console.warn(err.code ?? err.message) } ``` To install the widget, wait for it, and set every key of `window.subblyConfig`, see the [Cart widget article](/developer-resources/cart-widget). --- ## Document: Overview Learn how Subbly's APIs and SDKs fit together and where to start. URL: /get-started/overview # Overview Subbly gives you two HTTP APIs and a set of JavaScript tools for building subscription commerce experiences. ## APIs - **Storefront API**. Public endpoints for carts, checkout, customers, and subscriptions. Use it from your storefront, app, or any customer-facing surface. See the [Storefront API reference](/api/storefront). - **Private API**. Admin endpoints for products, orders, invoices, coupons, and more. Call it from your backend with a private key. See the [Private API reference](/api/private). ## SDKs and widgets - **Subbly.js SDK**. A browser client for the Storefront API. [Install and manage](/developer-resources/subbly-js-sdk) · [Reference](/reference/subbly-sdk). - **SubblyCart.js widget**. A drop-in cart and checkout for any website. [Install and manage](/developer-resources/cart-widget) · [Reference](/reference/cart-widget). - **Subbly's MCP server**. Lets an AI agent manage your shop through the Private API. [Connect](/developer-resources/subbly-mcp/connect-a-client) · [Tools](/developer-resources/subbly-mcp/tools). ## Next steps 1. Get your API key and read how to send it in the [Storefront API](/api/storefront#authentication) or [Private API](/api/private#authentication) reference. 2. Install the SDK or the widget from the [Developer resources](/developer-resources/overview) tab. --- ## Document: Subbly's MCP server Connect an AI agent to your shop through the Subbly's MCP server at mcp.subbly.dev. URL: /developer-resources/subbly-mcp # Subbly's MCP server The MCP server is currently only available to [Subbly X](https://www.subbly.co/x) merchants. It may open up to more plans later, but there is no ETA. To ask about access, contact [Subbly support](https://support.subbly.co). The Subbly's MCP server is a [Model Context Protocol](https://modelcontextprotocol.io) server. It exposes the Private API as tools, so an AI agent such as Claude Code, Cursor or VS Code can list your products, ship orders, create coupons and more, in plain language. Each tool wraps one Private API call and shares its fields and its responses. The server runs at `https://mcp.subbly.dev/mcp`. It speaks the streamable HTTP transport and holds no state between requests. Your Private API key travels with every request in the `X-API-KEY` header; the server never stores it, and each key gets its own rate limit. ## In this section - [Connect a client](/developer-resources/subbly-mcp/connect-a-client) — add the server to Claude Code, Cursor or VS Code, then try it in the MCP Inspector. - [Tools](/developer-resources/subbly-mcp/tools) — the full list of tools, grouped by the part of the shop they work on. ## Reference The tools call the [Private API](/api/private). Its reference explains every field a tool takes and returns. --- ## Document: Subbly.js SDK Install, configure and update the JavaScript SDK that calls the Storefront API from the browser. URL: /developer-resources/subbly-js-sdk # Subbly.js SDK Subbly.js is Subbly's browser-side JavaScript library, published on npm as `@subbly/sdk`. It calls the Storefront API for you, holds one cart per page, and keeps the cart ID and the customer's access token in browser cookies. The library is browser-first. It reads and writes `document.cookie` directly, so run it in the browser, not on a server. ## In this section - [Install](/developer-resources/subbly-js-sdk/install) — add the package from npm or load it from a CDN, then start it with `Subbly.init`. - [Configure](/developer-resources/subbly-js-sdk/configure) — pass your API key, a cart to load and a customer to authenticate. - [Keep it up to date](/developer-resources/subbly-js-sdk/keep-it-up-to-date) — pin a version and upgrade on your own schedule. - [Build a checkout](/developer-resources/subbly-js-sdk/build-a-checkout) — the short path from an empty cart to a paid order. - [Handle errors](/developer-resources/subbly-js-sdk/handle-errors) — read the `code` a failed request rejects with. - [Run a funnel](/developer-resources/subbly-js-sdk/run-a-funnel) — show upsell offers before, during and after checkout. ## Reference The [Subbly.js SDK reference](/reference/subbly-sdk) lists every method, with parameters, return values and examples. --- ## Document: Developer resources Install and manage Subbly's SDKs and widgets, and find the references. URL: /developer-resources/overview # Developer resources Subbly gives you two HTTP APIs, a browser SDK and a drop-in cart widget. The articles in this tab show you how to install and manage the SDK and the widget. The references list every method, event and setting. ## SDKs and widgets - **Subbly.js SDK** — `@subbly/sdk`. A browser client for the Storefront API. It holds one cart per page and keeps the cart ID and the customer's access token in cookies. Install it from npm or load it from a CDN. [Install and manage](/developer-resources/subbly-js-sdk) · [Run a funnel](/developer-resources/subbly-js-sdk/run-a-funnel) · [Reference](/reference/subbly-sdk) - **SubblyCart.js widget** — `@subbly/cart`. A drop-in cart, product configurator and checkout for any website. One script tag loads it from `https://assets.subbly.co/cart/cart-widget.js`. [Install and manage](/developer-resources/cart-widget) · [Reference](/reference/cart-widget) - **Subbly's MCP server** — a Model Context Protocol server at `https://mcp.subbly.dev/mcp` that exposes the Private API as tools, so an AI agent such as Claude Code, Cursor or VS Code can manage your shop. [Connect](/developer-resources/subbly-mcp/connect-a-client) · [Tools](/developer-resources/subbly-mcp/tools) ## APIs - **Storefront API**. Public endpoints for carts, checkout, customers and subscriptions, called with a Storefront API key. [Reference](/api/storefront) · [Authentication](/api/storefront#authentication) - **Private API**. Admin endpoints for products, orders, invoices, coupons and more, called from your backend with a private key. [Reference](/api/private) · [Authentication](/api/private#authentication) ## Help Ask a question or get help from [Subbly support](https://support.subbly.co). --- ## Document: SubblyCart.js widget Install, configure, wire and style the SubblyCart.js widget on any website. URL: /developer-resources/cart-widget # SubblyCart.js widget SubblyCart.js adds a cart, a product configurator and a full checkout to any website, in your shop's colours and languages. One script tag is enough. The widget talks to the Storefront API through [Subbly.js](/reference/subbly-sdk), and it keeps the cart ID and the customer's access token in cookies, so a cart survives a page load. ## In this section - [Install](/developer-resources/cart-widget/install) — embed the script and wait for the widget to be ready. - [Configure](/developer-resources/cart-widget/configure) — the keys of `window.subblyConfig`, and how to boot the widget by hand. - [Keep it up to date](/developer-resources/cart-widget/keep-it-up-to-date) — how the hosted widget updates, and how to self-host it. - [Wire it into your page](/developer-resources/cart-widget/wire-it-into-your-page) — buy links, your own cart button and counter, the container, and the content slots in the checkout. - [Drive it from your code](/developer-resources/cart-widget/drive-it-from-your-code) — call the methods and follow the events. - [Style it](/developer-resources/cart-widget/style-it) — override the cart settings at runtime or from your own CSS. ## Reference The [SubblyCart.js reference](/reference/cart-widget) lists every method, event, configuration key and CSS variable. --- ## Document: /api/storefront URL: /api/storefront The Storefront API is the API behind your storefront: the shop and its settings, products and bundles, carts and checkout, and the account of the signed-in customer. It is safe to call from the browser with a Storefront API key; [Subbly.js](/reference/subbly-sdk) wraps it for you. ## Base URL ```text title="Base URL" https://api.subbly.co/storefront/v1 ``` Every path in this reference is relative to it. Requests and responses are JSON; send `Content-Type: application/json` with a body. ## Authentication Send your Storefront API key in the `X-API-KEY` header on every request. Endpoints about one customer — their addresses, subscriptions, orders, wallet — also need the customer's access token, in the `Authorization` header as a bearer token. The [Auth](/api/storefront/auth) endpoints issue the token; it expires after `expires_in` seconds. ```bash title="Both headers" curl https://api.subbly.co/storefront/v1/customer \ -H 'X-API-KEY: ' \ -H 'Authorization: Bearer ' ``` ### Get your API key Storefront API keys live in the Subbly admin under [Settings › API keys](https://www.subbly.co/admin/settings/api-keys). In the **Storefront API keys** card, click **Generate API key**, then copy the key with the icon next to it. You can keep several keys; the first one is the key the Subbly website builder uses. A Storefront key is safe to ship in browser code: it can only read public shop data and act for the customer whose access token comes with it. ## Pagination List endpoints take `page` and `per_page` query parameters and return the items in `data` next to a `pagination` object: ```json title="A page" { "pagination": { "current_page": 1, "last_page": 4, "from": 1, "to": 25, "total": 92 }, "data": [] } ``` ## Errors The API answers with the usual HTTP status codes. Most `4xx` responses carry a JSON body with a `message` and a `code` you can branch on; validation errors (`422`) add an `errors` object keyed by field. | Status | Meaning | | --- | --- | | `400` | The request is malformed, or names a record that is not the customer's. | | `401` | The API key or the access token is missing or wrong. | | `402` | The payment needs another step; the body carries the payment intent to confirm. | | `404` | Nothing at this path, or it belongs to another shop. | | `410` | The endpoint no longer applies to this record; the description names the replacement. | | `422` | The body or the query failed validation. | | `429` | Too many requests; wait and retry. | There is no `403`. When a customer endpoint is handed a record that belongs to someone else — another customer's address, pick-up info, payment method or subscription — it answers `400` with `"code": "bad_request"`. A few guard rules answer the same way, such as deleting the last payment method in the wallet. The description of each endpoint lists the errors it can return. --- ## Document: /api/private URL: /api/private The Private API gives your server full access to the shop: customers, subscriptions, orders, invoices, products and more. Call it from a backend only; a private key in browser code can read and change every customer's data. ## Base URL ```text title="Base URL" https://api.subbly.co/private/v1 ``` Every path in this reference is relative to it. Requests and responses are JSON; send `Content-Type: application/json` with a body. ## Authentication Send your Private API key in the `X-API-KEY` header on every request. ```bash title="Authenticated request" curl https://api.subbly.co/private/v1/customers \ -H 'X-API-KEY: ' ``` ### Get your API key Private API keys live in the Subbly admin under [Settings › API keys](https://www.subbly.co/admin/settings/api-keys). In the **Private API keys** card, click **Generate API key**, then copy the key with the icon next to it. You can keep several keys, so give each integration its own; if one leaks, delete it from the same card and generate a new one. A Private key can read and change everything in your shop, so keep it on your server and out of files you commit. ## Pagination List endpoints take `page` and `per_page` query parameters and return the items in `data` next to a `pagination` object: ```json title="A page" { "pagination": { "current_page": 1, "last_page": 4, "from": 1, "to": 25, "total": 92 }, "data": [] } ``` ## Errors The API answers with the usual HTTP status codes. `4xx` responses carry a JSON body with a `message`; validation errors (`422`) add a `code` and an `errors` object keyed by field. | Status | Meaning | | --- | --- | | `400` | The request is malformed. | | `401` | The API key is missing or wrong. | | `403` | The key is valid but may not do this. | | `404` | Nothing at this path, or it belongs to another shop. | | `422` | The body or the query failed validation. | | `429` | Too many requests; wait and retry. | The description of each endpoint lists the errors it can return. --- ## Document: /api/orders URL: /api/orders The Orders API gives your server the orders of your shop, each with its delivery address, customer, subscription and line items, and lets you mark them as shipped with a carrier and a tracking number. It is made for shipping and fulfillment integrations. Call it from a backend only; the key gives access to every order and customer of your shop. ## Base URL ```text title="Base URL" https://www.subbly.co/api/v1 ``` Every path in this reference is relative to it. Requests and responses are JSON; send `Content-Type: application/json` with a body. ## Authentication Send the API key of your shop as a Bearer token in the `Authorization` header on every request. ```bash title="Authenticated request" curl https://www.subbly.co/api/v1/orders \ -H 'Authorization: Bearer ' ``` Without a valid key the API answers `401` with `{"message": "Unauthorized request"}`. The key is the shop API key of the Subbly admin, the same key the Subbly Zapier app uses. It is not a Private API key; those work with the [Private API](/api/private) only. ## Pagination `GET /orders` takes `page` and `per_page` (1 to 100, 15 by default) and returns the orders in `data` next to the page counters: ```json title="A page" { "data": [], "current_page": 1, "total": 32, "first": 1, "last": 3, "per_page": 15 } ``` `first` and `last` are page numbers. ## Errors The API answers with the usual HTTP status codes. Error responses carry a JSON body with a `message`; validation errors (`422`) add an `errors` object keyed by field. | Status | Meaning | | --- | --- | | `401` | The API key is missing or wrong. | | `404` | No order with this ID in your shop. | | `422` | The body or the query failed validation. | ```json title="Validation error" { "message": "Validation Failed", "errors": { "per_page": ["The per page must be between 1 and 100."], "status": ["The selected status is invalid."] } } ``` --- ## Document: /reference/subbly-sdk/wallet URL: /reference/subbly-sdk/wallet import PaymentMethodFields from "../../../src/reference/subbly-sdk/types/PaymentMethod.mdx"; Gets the payment methods the signed-in customer saved for one currency. A payment method belongs to a gateway, and a gateway belongs to a currency, so a customer who pays in two currencies has two sets. Needs an authenticated customer. Currency to list, such as `USD`. Take it from `shop.currencies[].abbreviation`. The saved payment methods. ```js title="List the payment methods" const paymentMethods = await subbly.wallet.list('USD') await subbly.cart.update({ paymentMethodId: paymentMethods[0].id }) ``` Gets the token a gateway widget needs before it can collect card details. Run it first, hand the token to the gateway's own SDK, then save what the gateway gives back with `subbly.wallet.store`. The gateway to set up. ID of the gateway, from `shop.currencies[].gateways[].id`. The token for the gateway widget. What the token is depends on the gateway. Stripe gives a client secret for `stripe.confirmCardSetup`. Braintree gives a client token that starts the Braintree SDK. PayPal gives an order ID for `createOrder` in `paypal.Buttons`. ```js title="Start a Stripe card setup" const [gateway] = subbly.shop.currencies[0].gateways const { token } = await subbly.wallet.setup({ gatewayId: gateway.id }) const { setupIntent } = await stripe.confirmCardSetup(token, { payment_method: { card: cardElement } }) ``` Saves a payment source for the customer from the token the gateway widget returned. The saved method can then be set on the cart or on a subscription. Needs an authenticated customer. The source to save. The gateway the token came from. The token the gateway widget returned. Stripe gives a setup intent ID. Braintree gives a payment method nonce. PayPal gives an order ID. Authorize.net gives the encrypted payment data, Base64-encoded. The payment method saved. ```js title="Save a Stripe card" const paymentMethod = await subbly.wallet.store({ gatewayId: gateway.id, token: setupIntent.id }) await subbly.cart.update({ paymentMethodId: paymentMethod.id }) ``` Gets a Subbly-hosted page where Stripe runs 3-D Secure on a new card. Send the customer to `subblyUrl`; Stripe returns them to the `redirectUrl` you gave. Needs an authenticated customer. The gateway to verify with, and where to come back to. ID of the Stripe gateway. Page of yours that Stripe sends the customer back to, such as `https://your-shop.com/checkout/CART_ID`. Where to send the customer, and how long the link lasts. ID of the setup intent. The page you asked Stripe to return to. When the hosted page stops working. The Subbly-hosted page to send the customer to. ```js title="Verify a card with 3-D Secure" const { subblyUrl } = await subbly.wallet.setupIntent({ gatewayId: gateway.id, redirectUrl: `https://your-shop.com/checkout/${subbly.cart.id}` }) window.location.assign(subblyUrl) ``` --- ## Document: /reference/subbly-sdk/surveys URL: /reference/subbly-sdk/surveys import SurveyFields from "../../../src/reference/subbly-sdk/types/Survey.mdx"; Loads a survey with its questions, answers and styling. A survey belongs to a plan: read `plan.survey.id` on the plan the customer chose, load the survey, then send the answers as `options` on the cart item. ID of the survey. Extra data to include. Relations to include. `questions.answers.product.parent` loads the product behind each offer and plan answer, with its parent product. Extra request headers. Set `x-currency` to price the answers in another currency. Without it the SDK sends the currency from `subbly.setCurrency`. The survey. ```js title="Load a survey" const survey = await subbly.surveys.load(123, { expand: ['questions.answers.product.parent'] }) ``` ```js title="Answer it and add the item to the cart" const survey = await subbly.surveys.load(plan.survey.id) const [question] = survey.questions await subbly.cart.addItem({ productId: plan.id, quantity: 1, options: [{ questionId: question.id, answers: [{ id: question.answers[0].id }] }] }) ``` --- ## Document: /reference/subbly-sdk/subscriptions URL: /reference/subbly-sdk/subscriptions import PaginationFields from "../../../src/reference/subbly-sdk/types/Pagination.mdx"; import SubscriptionFields from "../../../src/reference/subbly-sdk/types/Subscription.mdx"; import SubscriptionItemFields from "../../../src/reference/subbly-sdk/types/SubscriptionItem.mdx"; Every method here needs a signed-in customer, and reads or writes only that customer's subscriptions. A subscription has a plan, a quantity, and a list of **items**: the subscription line itself plus any add-ons. Change the whole subscription with `update`, `updateBundle` and `updatePreferences`; change one line with the `Item` methods. Most methods take a `params.expand` list, and every method takes a final `headers` object; see [Expand parameters](/reference/subbly-sdk#expand-parameters) and [Currency](/reference/subbly-sdk#currency). Gets a page of the customer's subscriptions. Which subscriptions to return. The argument is required by position, so pass an empty object for the defaults. Page to return, counted from 1. How many subscriptions per page. Keep only these statuses: `active`, `trial`, `pre_order`, `gift_waiting_to_start`, `cancelled`, `switched` or `expired`. Relations to include. Extra request headers. One page of subscriptions. The subscriptions on this page. Where this page sits in the full list. ```js title="List the active subscriptions" const { data, pagination } = await subbly.subscriptions.list({ statuses: ['active', 'trial'], expand: ['product.parent'] }) ``` Loads one subscription. ID of the subscription. Extra data to include. Relations to include. Extra request headers. The subscription. ```js title="Load a subscription" const subscription = await subbly.subscriptions.load(123, { expand: ['product.parent', 'discounts'] }) ``` Changes the quantity or the metadata of a subscription. Quantity changes need `shop.settings.subscriptionQuantitySelector`. ID of the subscription. The change to make. New quantity. Send it even when only the metadata changes. Metafield values to write. Each entry is `{ id, values }`, where `id` is a metafield ID. A value is `{ id }` for a preset field and `{ value }` for a free-text one. Extra data to include in the response. Relations to include. Extra request headers. The subscription, with the change applied. ```js title="Change the quantity" await subbly.subscriptions.update(123, { quantity: 2 }) ``` ```js title="Write a metafield value" await subbly.subscriptions.update(123, { quantity: subscription.quantity, metadata: [{ id: 45, values: [{ id: 67 }] }] }) ``` Rewrites the survey answers of a subscription. Send every answer you want to keep: the call replaces the whole set. This method takes no `params` argument. ID of the subscription. The new answers. One entry per question, as `{ questionId, answers }`. An answer is `{ content }` for a text or email question, `{ id }` for a select, multiple, offer or plan question, and `{ id, quantity }` for a quantity question. Pass `null` to clear the answers. `true` also applies the answers to orders Subbly already generated. Extra request headers. The subscription, with the new answers. ```js title="Answer the survey again" await subbly.subscriptions.updatePreferences(123, { preferences: [ { questionId: 11, answers: [{ id: 22 }] }, { questionId: 12, answers: [{ content: 'No nuts, please' }] } ], updateOrders: true }) ``` Rewrites the bundle contents and the bundle preferences of a subscription. Send the whole list of items: the call replaces what was there. This method takes no `params` argument. ID of the subscription. The new bundle. The plan the bundle belongs to. May be `null`. New quantity of the bundle. What goes in the bundle. The items picked, as `{ productId, quantity }`. `productId` is a bundle item's `productId`. The preferences picked, as `{ attributeId, values }`, where `values` holds attribute value IDs. Extra request headers. The subscription, with the new bundle. ```js title="Swap the bundle contents" await subbly.subscriptions.updateBundle(123, { productId: subscription.productId, quantity: 1, bundle: { items: [ { productId: 456, quantity: 2 }, { productId: 789, quantity: 1 } ], preferences: [{ attributeId: 5, values: [9] }] } }) ``` Loads one item of a subscription: the subscription line itself, or an add-on. ID of the subscription. ID of the item, from `subscription.items[].id`. Extra data to include. Relations to include. Extra request headers. The item. ```js title="Load one item" const item = await subbly.subscriptions.loadItem(123, 456, { expand: ['product.parent'] }) ``` Changes the quantity or the metadata of one subscription item. ID of the subscription. ID of the item. The change to make. New quantity of the item. Metafield values to write, as `{ id, values }`. Extra data to include in the response. Relations to include. Extra request headers. The item, with the change applied. ```js title="Change the quantity of an add-on" await subbly.subscriptions.updateItem(123, 456, { quantity: 3 }) ``` Rewrites the bundle contents of one subscription item. Unlike `updateBundle`, this call carries no quantity, and `bundle` is required. ID of the subscription. ID of the item. The new bundle for the item. The variant or plan the bundle belongs to. May be `null`. What goes in the bundle. The items picked, as `{ productId, quantity }`. The preferences picked, as `{ attributeId, values }`. Extra data to include in the response. Relations to include. Extra request headers. The item, with the new bundle. ```js title="Swap the contents of one bundle item" await subbly.subscriptions.updateItemBundle(123, 456, { productId: item.productId, bundle: { items: [{ productId: 789, quantity: 2 }], preferences: [] } }) ``` Rewrites the survey answers of one subscription item. This method takes no `params` argument. ID of the subscription. ID of the item. The new answers. One entry per question, as `{ questionId, answers }`. Pass `null` to clear the answers. `true` also applies the answers to orders Subbly already generated. Extra request headers. The item, with the new answers. ```js title="Answer the survey for one item" await subbly.subscriptions.updateItemPreferences(123, 456, { preferences: [{ questionId: 11, answers: [{ id: 22 }] }] }) ``` --- ## Document: /reference/subbly-sdk/stock URL: /reference/subbly-sdk/stock Asks Subbly to email the customer when a product or a survey answer is back in stock. Watch for `stockCount: 0` on a variant, a plan or an answer, and offer the alert there. The shop must turn alerts on: `shop.settings.outOfStockEnabled` and `shop.limits.outOfStock` must both be `true`. What to watch, and where to send the alert. Address the alert goes to. The variant or plan that ran out. Send this or `answerId`, not both. The survey answer that ran out. Send this or `productId`, not both. How many units the customer wants. The alert record. ID of the record. The shop the record belongs to. The product being watched. Address the alert goes to. How many units the customer wants. ID of the watched product or survey answer. What is being watched. When the item came back in stock. Empty until it does. When the record was created. When the record last changed. ```js title="Watch a variant" await subbly.stock.subscribe({ email: 'me@example.com', productId: 456, quantity: 1 }) ``` ```js title="Watch a survey answer" await subbly.stock.subscribe({ email: 'me@example.com', answerId: 789 }) ``` --- ## Document: /reference/subbly-sdk/shop URL: /reference/subbly-sdk/shop import SubblyShopFields from "../../../src/reference/subbly-sdk/types/SubblyShop.mdx"; Loads the settings, branding, currencies, gateways and apps of the shop. `Subbly.init` calls this for you, so `subbly.shop` already holds the fields below by the time you get the SDK instance. Call it again only to pick up a change. `subbly.shop` is a model object: it carries the shop fields and the `load` method together. `load` copies the response onto that same object and returns it, so the value you get back and `subbly.shop` are one object. The shop object, with its fields refreshed. ```js title="Load the shop" const shop = await subbly.shop.load() console.log(shop.name, shop.country.currencyCode) ``` ```js title="Read a setting before you draw a form" if (subbly.shop.settings.collectCompanyName) { showCompanyNameField() } const currencies = subbly.shop.currencies.map((c) => c.abbreviation) ``` --- ## Document: /reference/subbly-sdk/setup URL: /reference/subbly-sdk/setup Starts Subbly.js with your Storefront API key and resolves with the SDK instance. Every other method in this reference is a method of that instance. `init` always loads the shop first, so `subbly.shop` is filled in by the time the promise resolves. It then checks the stored access token, and, when you pass `loadCart`, loads or creates a cart and copies the cart currency onto the instance. A failed shop load rejects the promise. Call `init` once per page. The SDK builds one shared HTTP client and keeps it, so a second call with a different `apiKey`, `apiUrl` or `lang` quietly reuses the first one. How to start the SDK. Leave it out, or pass an empty `apiKey`, and `init` rejects with `Error('Missing required apiKey config')`. Your Storefront API key. The SDK sends it as the `x-api-key` header on every request, and you get it from [Shop settings](https://subbly.co/admin/settings/api-keys) in Subbly Admin. Base URL of the Storefront API. Defaults to `https://api.subbly.co/storefront/v1/`. Change it only for a proxy or a test environment. An empty string falls back to the default. Language code, sent as the `accept-language` header. Empty by default, so the server picks. Use a code from `subbly.shop.languages[].code`, and change it later with `subbly.setLanguage`. Cart to load at start-up. The SDK reads it only when `loadCart` is true, and falls back to the `subbly_cart_id` cookie when you leave it out. Load or create a cart during start-up. `false` by default. When it is true, the SDK loads the cart named by `cartId` or the cookie, creates a new cart when there is none or the load fails, writes the `subbly_cart_id` cookie, and calls `subbly.setCurrency` with the cart currency. When it is false, `subbly.cart` has no `id` and the cart methods that need one fail. Customer access token to verify at start-up. It beats the `subbly_access_token` cookie, and the SDK writes it to that cookie when the check passes. A failed check clears the cookie; it does not reject. The SDK instance. Version of the SDK package. The `apiKey`, `apiUrl` and `lang` the SDK started with. The cart: every field of the cart object, plus the cart methods. The [Cart](/reference/subbly-sdk/cart) page writes the fields out. The shop: every field of the shop object, plus `shop.load`. The [Shop](/reference/subbly-sdk/shop) page writes the fields out. The rest of the namespaces, one per group in this reference. They are all built during `init` and are always present. ```js title="Start the SDK" import Subbly from '@subbly/sdk' const subbly = await Subbly.init({ apiKey: 'API_KEY', }) ``` ```js title="Start with a cart and a customer" const subbly = await Subbly.init({ apiKey: 'API_KEY', cartId: 'aaea067c-6364-4157-82e9-71b6edfd84a0', loadCart: true, accessToken: 'eyJh...eyJ9', }) ``` Sets the currency for the prices in later product, bundle and survey responses. The SDK sends it as the `x-currency` header. It only fans the code out to `subbly.products.setCurrency`, `subbly.bundles.setCurrency` and `subbly.surveys.setCurrency`, which each store it for their own requests; an `x-currency` header you pass on a single call still wins. This does not change the cart currency. Use `subbly.cart.update({ currencyCode })` for that. When `init` loads a cart, it calls `setCurrency` with the cart currency itself. A currency abbreviation the shop supports, such as `USD`. Read the list from `subbly.shop.currencies[].abbreviation`. Nothing. ```js title="Set the currency" subbly.setCurrency('EUR') // Later product prices come back in euros. const products = await subbly.products.list() ``` Sets the `accept-language` header the SDK sends with every later request. A language code the shop supports. Read the list from `subbly.shop.languages[].code`. Nothing. ```js title="Set the language" subbly.setLanguage('fr') ``` A read-only property with the version of the `@subbly/sdk` package the page loaded. Useful in a bug report. The version of the `@subbly/sdk` package on the page. ```js title="Read the version" console.log(subbly.VERSION) ``` The three client values `init` received, exactly as you passed them. It leaves out `cartId`, `loadCart` and `accessToken`, and it does not follow a later `setLanguage` call. `subbly.config` is not the configuration in use. The SDK applies its defaults deeper in, when it builds the HTTP client, so a value you left out stays `undefined` here. The values passed to `init`. The Storefront API key you passed. The base URL you passed, or `undefined`. The requests still go to `https://api.subbly.co/storefront/v1/` in that case. The language code you passed, or `undefined`. The server then picks the language. ```js title="Read the configuration" const subbly = await Subbly.init({ apiKey: 'API_KEY' }) subbly.config.apiKey // 'API_KEY' subbly.config.apiUrl // undefined subbly.config.lang // undefined ``` --- ## Document: /reference/subbly-sdk/products URL: /reference/subbly-sdk/products import PaginationFields from "../../../src/reference/subbly-sdk/types/Pagination.mdx"; import ProductOneTimeFields from "../../../src/reference/subbly-sdk/types/ProductOneTime.mdx"; import ProductSubscriptionFields from "../../../src/reference/subbly-sdk/types/ProductSubscription.mdx"; import ProductVariantFields from "../../../src/reference/subbly-sdk/types/ProductVariant.mdx"; import ProductPlanFields from "../../../src/reference/subbly-sdk/types/ProductPlan.mdx"; A shop sells two kinds of product. A one-time product holds **variants**; a subscription product holds **plans**. A parent product is what you list and show on a page; a variant or a plan is what a customer buys, and its `id` is the `productId` you send to `subbly.cart.addItem`. Every method here takes an optional `headers` argument to price one response in another currency; see [Currency](/reference/subbly-sdk#currency). Gets a page of the shop's parent products. Which products to return. Page to return, counted from 1. How many products per page. Keep only products with these tags. Keep only one-time products, or only subscription products. Keep only products with these slugs. `true` keeps only digital products, `false` only physical ones. `true` keeps only gift cards, `false` leaves them out. Relations to include: `variants`, `variants.parent`, `plans`, `plans.parent` and `metadata`. Without them a product carries no buyable children. Extra request headers, such as `x-currency`. One page of products. The products on this page. Each one is a one-time product or a subscription product; narrow on `type`. Where this page sits in the full list. ```js title="List the products" const { data, pagination } = await subbly.products.list({ page: 1, perPage: 10, expand: ['variants.parent'] }) ``` ```js title="List the subscription products only" const { data } = await subbly.products.list({ type: 'subscription', expand: ['plans'] }) ``` Loads one parent product, with its variants or its plans. ID of the parent product. Extra data to include. Relations to include: `variants`, `variants.parent`, `plans`, `plans.parent` and `metadata`. Extra request headers, such as `x-currency`. The product. Narrow on `type` to read `variants` or `plans`. ```js title="Load a product" const product = await subbly.products.load(123, { expand: ['variants.parent', 'plans.parent'] }) const buyable = product.type === 'subscription' ? product.plans : product.variants ``` Loads one product variant, the buyable child of a one-time product. Pass a **variant** ID, not the ID of its parent. The endpoint behind this method also serves plans, so the declared result is a variant or a plan; use `subbly.products.loadPlan` when you hold a plan ID. ID of the variant. Extra data to include. Relations to include. `parent` loads the product the variant belongs to. Extra request headers, such as `x-currency`. The variant, or the plan when the ID belongs to one. ```js title="Load a variant" const variant = await subbly.products.loadVariant(456, { expand: ['parent'] }) await subbly.cart.addItem({ productId: variant.id, quantity: 1 }) ``` Loads one subscription plan, the buyable child of a subscription product. Older material calls a plan a pricing. `loadPlan` and `loadVariant` call the same endpoint; only the type of the result differs. Use `loadPlan` when the ID belongs to a plan. ID of the plan. Extra data to include. Relations to include. `parent` loads the product the plan belongs to. Extra request headers, such as `x-currency`. The plan. ```js title="Load a plan" const plan = await subbly.products.loadPlan(789, { expand: ['parent'] }) console.log(plan.frequencyCount, plan.frequencyUnit) ``` --- ## Document: /reference/subbly-sdk/pickup-info URL: /reference/subbly-sdk/pickup-info import CustomerPickupInfoFields from "../../../src/reference/subbly-sdk/types/CustomerPickupInfo.mdx"; Gets the pick-up contact records the signed-in customer saved. A record says who collects the parcel, and which point they collect it from. Needs an authenticated customer. Which records to return. The argument is required by position, so pass an empty object for all of them. Keep only the records that work with this pick-up shipping method. In practice this filters by country. The saved records. ```js title="List every record" const pickupInfos = await subbly.pickupInfo.list({}) ``` ```js title="List the records for one pick-up method" const [method] = await subbly.cart.getLocalPickups() const pickupInfos = await subbly.pickupInfo.list({ shippingMethodId: method.id }) ``` Creates a pick-up contact record for the signed-in customer. Put its `id` on the cart as `shippingAddressId` once the customer chose a pick-up point. Needs an authenticated customer. The record to create. Given name of the person who collects the parcel. Family name of that person. Phone number of that person. Set this only for a point in an external network. `mondial_relay` is the one value the field takes. ID of the point in that external network. The record created. ```js title="Add a record" const pickupInfo = await subbly.pickupInfo.store({ firstName: 'Ada', lastName: 'Lovelace', phone: '+15551234567' }) await subbly.cart.update({ shippingAddressId: pickupInfo.id }) ``` ```js title="Add a record for a Mondial Relay point" await subbly.pickupInfo.store({ firstName: 'Ada', lastName: 'Lovelace', phone: '+33123456789', pickupPointType: 'mondial_relay', pickupPointId: '005670' }) ``` Deletes a pick-up contact record. Needs an authenticated customer. The request fails while a subscription still uses the record. ID of the record to delete. Nothing. The response body is empty. ```js title="Delete a record" await subbly.pickupInfo.delete(123) ``` --- ## Document: /reference/subbly-sdk/payment-intents URL: /reference/subbly-sdk/payment-intents import PaymentIntentResponseFields from "../../../src/reference/subbly-sdk/types/PaymentIntentResponse.mdx"; Gets a payment intent, including the client secret Stripe needs to run 3-D Secure. Call it when `subbly.checkout.purchase` rejects with `code: 'payment_requires_action'`; the rejection carries the ID to pass here. Needs an authenticated customer. ID of the payment intent. Read it from `err.paymentIntentId` on the purchase error. The payment intent. ```js title="Get the client secret" const { token } = await subbly.paymentIntents.getPaymentIntent(err.paymentIntentId) const { paymentIntent } = await stripe.confirmCardPayment(token) ``` Tells Subbly that Stripe finished verifying the payment intent. Call `subbly.checkout.purchase` again afterwards to finish the order. Needs an authenticated customer. ID of the Subbly payment intent, from `err.paymentIntentId`. ID Stripe gave back: the `paymentIntent.id` from `stripe.confirmCardPayment`. The payment intent, with its new status. ```js title="Finish a 3-D Secure purchase" try { await subbly.checkout.purchase(subbly.cart.id) } catch (err) { if (err.code !== 'payment_requires_action') throw err const { token } = await subbly.paymentIntents.getPaymentIntent(err.paymentIntentId) const { paymentIntent } = await stripe.confirmCardPayment(token) await subbly.paymentIntents.confirm(err.paymentIntentId, paymentIntent.id) await subbly.checkout.purchase(subbly.cart.id) } ``` --- ## Document: /reference/subbly-sdk/metafields URL: /reference/subbly-sdk/metafields import MetafieldFields from "../../../src/reference/subbly-sdk/types/Metafield.mdx"; import PaginationFields from "../../../src/reference/subbly-sdk/types/Pagination.mdx"; Gets a page of the metafield definitions of the shop. Metafields are the custom fields a shop adds to a customer, a subscription or a product. Read them to build your own form, then write the answers back as `metadata` on the cart or on a subscription. The namespace is singular: `subbly.metafield`, not `subbly.metafields`. Which definitions to return. The argument is required by position, so pass an empty object for the defaults. Keep only these metafield IDs. Keep only these slugs. Page to return, counted from 1. How many definitions per page. `true` keeps only preset fields, which hold a fixed list of choices. `false` keeps only free-text fields. Relations to include in the response. One page of definitions. The definitions on this page. Where this page sits in the full list. ```js title="List the metafields" const { data, pagination } = await subbly.metafield.list({ page: 1, perPage: 20 }) ``` ```js title="Load one definition by slug" const { data } = await subbly.metafield.list({ slugs: ['coffee-strength'] }) const [strength] = data ``` --- ## Document: /reference/subbly-sdk/lead URL: /reference/subbly-sdk/lead Saves an email address as a lead. Use it to catch an address early, such as on a newsletter box or the first step of a survey, before the customer reaches checkout. The lead to save. Email address of the lead. Given name of the lead. Family name of the lead. Your own data to keep with the lead, such as the page it came from. The lead record Subbly saved. ID of the lead. The email address saved. Given name of the lead. Family name of the lead. IP address Subbly saw on the request. Browser Subbly saw on the request. ```js title="Capture a lead" const lead = await subbly.lead.subscribe({ email: 'me@example.com', firstName: 'Ada', metadata: { source: 'footer-form' } }) ``` --- ## Document: /reference/subbly-sdk/funnels URL: /reference/subbly-sdk/funnels import FunnelStepFields from "../../../src/reference/subbly-sdk/types/FunnelStep.mdx"; import SubblyCartFields from "../../../src/reference/subbly-sdk/types/SubblyCart.mdx"; import CheckoutPurchaseResponseFields from "../../../src/reference/subbly-sdk/types/CheckoutPurchaseResponse.mdx"; A funnel has three phases: **pre-purchase**, **mid-purchase** and **post-purchase**. In each phase you fetch the next step, then accept it or skip it, and fetch again until `step` is `null`. For the full flow, with one example per phase, see [Run a funnel](/developer-resources/subbly-js-sdk/run-a-funnel). Gets the next pre-purchase offer for the cart. `step` comes back `null` once the customer has seen every offer. ID of the cart, usually `subbly.cart.id`. Extra data to include. Relations to include. `parent` loads the parent product of each offered variant or plan. The next offer, or an empty one. The offer to show. `null` means there is nothing left to offer. ```js title="Show the next offer" const { step } = await subbly.funnels.prePurchaseFetch(subbly.cart.id, { expand: ['parent'] }) if (step) render(step) ``` Accepts a pre-purchase offer and adds its products to the cart. The cart it returns is a plain object, not the `subbly.cart` model, so `subbly.cart` still holds the old contents. Reload it with `subbly.cart.load()`. ID of the cart. ID of the step the customer accepted, from `step.id`. The products the customer took. One entry per product taken. The variant or plan the customer picked, from `step.offer.products[].id`. Survey answers, when the offered plan has a survey. Each entry is `{ questionId, answers }`. An answer is `{ content }` for a text or email question, `{ id }` for a select, multiple, offer or plan question, and `{ id, quantity }` for a quantity question. Extra data to include on the cart that comes back. Cart relations to include, such as `items.product` and `items.product.parent`. The cart after the offer went in. ```js title="Accept an offer" await subbly.funnels.prePurchaseAccept(subbly.cart.id, step.id, { offers: [{ productId: step.offer.products[0].id }] }) await subbly.cart.load() ``` Skips a pre-purchase offer. Subbly records the refusal, so the next fetch moves on to the following step. ID of the cart. ID of the step the customer skipped. Nothing. The response body is empty. ```js title="Skip an offer" await subbly.funnels.prePurchaseReject(subbly.cart.id, step.id) const { step: next } = await subbly.funnels.prePurchaseFetch(subbly.cart.id) ``` Clears which pre-purchase offers the customer accepted or skipped, so the sequence starts from the first step again. Only the pre-purchase phase has this. ID of the cart. Nothing you need to read. Treat the response as empty. ```js title="Start the offers again" await subbly.funnels.prePurchaseRefresh(subbly.cart.id) ``` Gets the next mid-purchase offer for the cart. Show these while the customer works through checkout. ID of the cart. Extra data to include. Relations to include. `parent` loads the parent product of each offered variant or plan. The next offer, or an empty one. The offer to show. `null` means there is nothing left to offer. ```js title="Show the next offer" const { step } = await subbly.funnels.midPurchaseFetch(subbly.cart.id) ``` Accepts a mid-purchase offer and adds its products to the cart. As with the pre-purchase accept, reload `subbly.cart` afterwards. ID of the cart. ID of the step the customer accepted. The products the customer took. One entry per product taken. The variant or plan the customer picked. Survey answers, when the offered plan has a survey. Each entry is `{ questionId, answers }`. Extra data to include on the cart that comes back. Cart relations to include. The cart after the offer went in. ```js title="Accept an offer" await subbly.funnels.midPurchaseAccept(subbly.cart.id, step.id, { offers: [{ productId: step.offer.products[0].id }] }) await subbly.cart.load() ``` Skips a mid-purchase offer so the next fetch moves on. ID of the cart. ID of the step the customer skipped. Nothing. The response body is empty. ```js title="Skip an offer" await subbly.funnels.midPurchaseReject(subbly.cart.id, step.id) ``` Gets the next post-purchase offer for the cart. These run only after `subbly.checkout.purchase` succeeds, on the thank-you page. ID of the cart that was paid. Extra data to include. Relations to include. `parent` loads the parent product of each offered variant or plan. The next offer, or an empty one. The offer to show. `null` means there is nothing left to offer. ```js title="Show the next offer after checkout" await subbly.checkout.purchase(subbly.cart.id) const { step } = await subbly.funnels.postPurchaseFetch(subbly.cart.id) ``` Accepts a post-purchase offer and charges it as a new purchase on the card the customer already used. Unlike the other two accepts, this one returns the invoice, subscription and orders it created. Needs an authenticated customer. ID of the cart that was paid. ID of the step the customer accepted. The products taken, and anything the purchase still needs. One entry per product taken. The variant or plan the customer picked. Survey answers, when the offered plan has a survey. Address to ship to. Send it when the checkout did not set one. Delivery option to use. Send it when the checkout did not set one. The payment intent you confirmed, when you retry after 3-D Secure. Extra data to include in the response. Relations to include, such as `subscription.product` and `order.metadata`. What the extra purchase created. ```js title="Accept a post-purchase offer" const { invoice, orders } = await subbly.funnels.postPurchaseAccept( subbly.cart.id, step.id, { offers: [{ productId: step.offer.products[0].id }] } ) ``` Skips a post-purchase offer so the next fetch moves on. ID of the cart that was paid. ID of the step the customer skipped. Nothing. The response body is empty. ```js title="Skip an offer" await subbly.funnels.postPurchaseReject(subbly.cart.id, step.id) ``` --- ## Document: /reference/subbly-sdk/customers URL: /reference/subbly-sdk/customers import CustomerFields from "../../../src/reference/subbly-sdk/types/SubblyCustomer.mdx"; Every method here needs a signed-in customer. Sign one in with the [authentication methods](/reference/subbly-sdk/auth) first. Gets the profile of the signed-in customer. `subbly.auth.checkAuthenticated` calls it behind the scenes, so a failure here means the token no longer works. The customer profile. ```js title="Get the customer profile" const customer = await subbly.customers.me() ``` Changes the profile of the signed-in customer. Send only the fields you change. The fields to change. Every one is optional. Given name. Family name. Whether the customer agreed to marketing email. Whether the customer accepted the terms. Sending `true` stamps `tosConsentAt` with the current time. Your own ID for this customer. The updated profile. ```js title="Update the customer" const customer = await subbly.customers.update({ firstName: 'Ada', marketingConsent: true, }) ``` Gets the referral link of the signed-in customer, to show on a "refer a friend" page. Referrals must be switched on for the shop; check `subbly.shop.referralSettings.enabled` first. The referral link. The URL the customer shares. ```js title="Get the referral link" const { referralLink } = await subbly.customers.referral() ``` --- ## Document: /reference/subbly-sdk/countries URL: /reference/subbly-sdk/countries import CountryFields from "../../../src/reference/subbly-sdk/types/Country.mdx"; Gets every country Subbly supports, with its regions. Use it to fill a country select, and use the `id` of the chosen country as `countryId` in addresses and in `subbly.cart.getShippingMethods`. This is the full list. The countries your shop actually ships to are in `subbly.shop.shippingCountries`, which has the same shape. The countries. ```js title="List the countries" const countries = await subbly.countries.list() ``` ```js title="Offer only the countries the shop ships to" const options = subbly.shop.shippingCountries.map(({ id, name }) => ({ value: id, label: name, })) ``` --- ## Document: /reference/subbly-sdk/checkout URL: /reference/subbly-sdk/checkout import PurchaseFields from "../../../src/reference/subbly-sdk/types/CheckoutPurchaseResponse.mdx"; Charges the cart and creates the subscription, the invoice and the orders. It needs a signed-in customer, and the cart must be ready: a customer attached, a delivery method set and a payment method chosen. The SDK does not touch `subbly.cart` afterwards. The cart is complete on the server, so drop your local copy and create a new one for the next order. A card that needs 3-D Secure rejects with `code: 'payment_requires_action'` and a `paymentIntentId`. Take the customer through the verification with `subbly.paymentIntents.getPaymentIntent` and `subbly.paymentIntents.confirm`, then call `purchase` again with the same `paymentIntentId`. The call can also reject with `out_of_stock` or `bad_request`. UUID of the cart to charge, usually `subbly.cart.id`. How to pay. Leave it out to charge the payment method already on the cart. Payment method to charge, from `subbly.wallet.list`. Falls back to `cart.paymentMethodId`. The payment intent the customer just verified. Send it when you retry after 3-D Secure. Relations to include in the response. `subscription.product`, `subscription.product.parent`, `subscription.product.parent.bundle`, `subscription.metadata`, `order.metadata`, `order.items.metadata`, `invoice.metadata` and `invoice.items.metadata`. What the purchase created. ```js title="Purchase the cart" const { invoice, subscription, orders } = await subbly.checkout.purchase( subbly.cart.id, ) ``` ```js title="Handle 3-D Secure" try { await subbly.checkout.purchase(subbly.cart.id) } catch (error) { if (error.code !== 'payment_requires_action') throw error const intent = await subbly.paymentIntents.getPaymentIntent( error.paymentIntentId, ) // Verify `intent.token` with the payment gateway, then: await subbly.paymentIntents.confirm(intent.id, externalPaymentId) await subbly.checkout.purchase(subbly.cart.id, { paymentIntentId: intent.id, }) } ``` --- ## Document: /reference/subbly-sdk/cart URL: /reference/subbly-sdk/cart import CartFields from "../../../src/reference/subbly-sdk/types/SubblyCart.mdx"; import AddressPayloadFields from "../../../src/reference/subbly-sdk/types/AddressStorePayload.mdx"; import SurveyAnswerFields from "../../../src/reference/subbly-sdk/types/SurveyAnswers.mdx"; import MetadataFields from "../../../src/reference/subbly-sdk/types/MetadataObject.mdx"; import DeliveryFields from "../../../src/reference/subbly-sdk/types/ShippingMethodDelivery.mdx"; import PickupFields from "../../../src/reference/subbly-sdk/types/ShippingMethodPickup.mdx"; import StartDateFields from "../../../src/reference/subbly-sdk/types/StartDates.mdx"; `subbly.cart` is both the data and the methods. It carries every cart field straight on the object — `subbly.cart.id`, `subbly.cart.items`, `subbly.cart.total` — and the methods below change it. Each of those methods copies the response onto the same object and resolves with it, so the value you `await` and `subbly.cart` are one and the same. There is only ever one cart per SDK instance, and fields the response leaves out keep their old values. Amounts are whole numbers in the minor unit of the cart currency; see [Amounts and dates](/reference/subbly-sdk#amounts-and-dates). Most methods take an optional `params.expand` list; the values it accepts are under [Expand parameters](/reference/subbly-sdk#expand-parameters). Creates a new cart and stores its ID in the `subbly_cart_id` cookie for 365 days. It replaces whatever cart the SDK already held, so call it once, and use `subbly.cart.load` to come back to a cart you already have. The cookie is a plain one on `path=/`, with no `Secure`, `HttpOnly` or `SameSite` attribute. What the new cart starts with. Pass `null` or leave it out for an empty cart. The TypeScript type is the wider update payload, but the create endpoint reads only the four fields below; set anything else with `subbly.cart.update` afterwards. Currency of the cart, such as `EUR`. Defaults to the shop currency. Coupon code to apply straight away. Gift card code to apply straight away. ID of the referral that led to this cart. Relations to include. The relations to fill in. The values are listed at the top of this page. The cart, which is `subbly.cart` itself. ```js title="Create a cart" const cart = await subbly.cart.create() ``` ```js title="Create a cart in a currency, with a coupon" const cart = await subbly.cart.create( { currencyCode: 'EUR', couponCode: 'WELCOME10' }, { expand: ['coupon', 'items.product'] }, ) ``` Loads a cart by ID into `subbly.cart`. Leave the ID out to refresh the cart the SDK already holds. Unlike `create`, this does not write the cookie. `Subbly.init` writes it itself after a successful load. UUID of the cart. Leave it out to reload the cart already held. With no ID and no cart held, the call throws `Error('Cart ID is required')` at once, without reaching the network. Relations to include. The relations to fill in. The values are listed at the top of this page. The cart, which is `subbly.cart` itself. Its fields are written out under [Create a cart](#create). ```js title="Load a cart by ID" const cart = await subbly.cart.load('aaea067c-6364-4157-82e9-71b6edfd84a0') ``` ```js title="Refresh the cart in hand" await subbly.cart.load(undefined, { expand: ['items.product'] }) ``` Changes the cart itself rather than its items: who is buying, where it ships, how it ships, which discounts apply, and when a subscription starts. Send only the fields you change. Most of it works for a guest. Three fields point at records only a signed-in customer has: `paymentMethodId`, `shippingAddressId` and `billingAddressId`. The fields to change. Every one is optional. Guest details on the cart. Use it before the customer signs in. Email address. Given name. Family name. Whether the guest agreed to marketing email. Whether the guest accepted the terms. Your own ID for this guest. A delivery address written straight onto the cart, for a guest with no saved addresses. A billing address written straight onto the cart. Same fields as `shippingAddress`. Who collects the order at a pick-up point. Given name of the person collecting. Family name of the person collecting. Phone number. `mondial_relay` when the point comes from Mondial Relay. ID of the point at the provider. Currency of the cart. This is the only way to change it. Coupon code to apply. Send `null` to take the coupon off. Gift card code to apply. Send `null` to take the gift card off. ID of the referral that led to this cart. Gift settings for the cart. When the gift starts, from `subbly.cart.getGiftingDates()`. How many shipments the gift covers. Message for the recipient. Email of the recipient. When the subscription starts, from `subbly.cart.getStartDates()`. ID of the delivery option, from `subbly.cart.getShippingMethods` or `subbly.cart.getLocalPickups`. ID of the carrier, from the `carrier` of a shipping option. Carrier service code, from `carrier.serviceCodes`. ID of a saved address or pick-up info of the signed-in customer. ID of a saved address of the signed-in customer. ID of a saved payment method, from `subbly.wallet.list`. ID of the onboarding template to apply. Metafield values to save on the cart. Relations to include. The relations to fill in. The values are listed at the top of this page. The updated cart, which is `subbly.cart` itself. Its fields are written out under [Create a cart](#create). ```js title="Set the guest details" await subbly.cart.update({ customer: { email: 'ada@example.com', firstName: 'Ada', lastName: 'Lovelace', tosConsent: true, }, }) ``` ```js title="Set the delivery address and the method" await subbly.cart.update({ shippingAddress: { firstName: 'Ada', lastName: 'Lovelace', phone: '+442071234567', addressOne: '12 Marylebone Road', addressTwo: null, city: 'London', zip: 'NW1 5JD', countryId: 826, }, shippingMethodId: 91, }) ``` ```js title="Remove the coupon" await subbly.cart.update({ couponCode: null }) ``` Adds a product, a plan or a bundle to the cart. Adding the same one-time item again raises its quantity instead of making a second line. The payload has a shape per kind of item. All three share `productId`, `quantity` and `metadata`; the rest depends on what you are adding. The item to add. What to add: a variant ID for a one-time product, a plan ID for a subscription. How many. Defaults to 1. Metafield values to save on the line. `true` adds the item alongside a subscription already in the cart. How long the add-on rides along: `1` for one charge, `0` for every charge. Who a gift card goes to. Email of the recipient. Name of the recipient. Message for the recipient. The customer's answers to the plan's survey, one entry per question. What goes inside the bundle. The picked items, each `{ productId, quantity }` from `subbly.bundles.loadItems`. The answers to the bundle preferences, each `{ attributeId, values }`. Send `[]` when there are none. Survey answers, when the bundle plan also has a survey. Same shape as for a subscription. Relations to include. The relations to fill in. The values are listed at the top of this page. The updated cart, which is `subbly.cart` itself. Its fields are written out under [Create a cart](#create). The call rejects with `code: 'out_of_stock'` when the product ran out. ```js title="Add a one-time product" await subbly.cart.addItem({ productId: 3301, quantity: 2 }) ``` ```js title="Add a subscription with survey answers" await subbly.cart.addItem({ productId: 8801, options: [ { questionId: 11, answers: [{ id: 41 }] }, { questionId: 12, answers: [{ content: 'No nuts, please' }] }, ], }) ``` ```js title="Add a bundle" await subbly.cart.addItem({ productId: 8801, quantity: 1, bundle: { items: [ { productId: 3301, quantity: 2 }, { productId: 3307, quantity: 1 }, ], preferences: [{ attributeId: 9, values: [31] }], }, }) ``` Changes one line in the cart: the quantity, the survey answers, the gift details or the bundle contents. To change which product a line is for, remove it and add the new one. UUID of the line, from `cart.items[].id`. What to change. Every field is optional, and the shape follows the kind of item the line holds. How many. `true` keeps the item alongside a subscription in the cart. `1` for one charge, `0` for every charge. Who a gift card goes to, with `customerEmail`, `customerName` and `message`. The survey answers, one entry per question. Send the full set; it replaces what was there. The new bundle contents, with `items` and `preferences` exactly as in `subbly.cart.addItem`. Survey answers, when the bundle plan also has a survey. Relations to include. The relations to fill in. The values are listed at the top of this page. The updated cart, which is `subbly.cart` itself. Its fields are written out under [Create a cart](#create). ```js title="Change the quantity" const [line] = subbly.cart.items await subbly.cart.updateItem(line.id, { quantity: 3 }) ``` Removes one line from the cart. UUID of the line, from `cart.items[].id`. Relations to include. The relations to fill in. The values are listed at the top of this page. The updated cart, which is `subbly.cart` itself. Its fields are written out under [Create a cart](#create). ```js title="Remove an item" const [line] = subbly.cart.items await subbly.cart.removeItem(line.id) ``` Gets the dates on which the subscription in the cart can start. Send the one the customer picks to `subbly.cart.update` as `startsAt`. It uses the cart the SDK holds, so load or create a cart first. The dates allowed. The API fills one of the two fields and sets the other to `null`: a fixed list of dates, or an open range. Handle both. ```js title="Get the start dates" const { availableDates, range } = await subbly.cart.getStartDates() const startsAt = availableDates?.length ? availableDates[0] : range?.[0] await subbly.cart.update({ startsAt }) ``` Gets the dates on which a gift subscription in the cart can start. Send the one the customer picks to `subbly.cart.update` as `giftInfo.startsAt`. The dates allowed, in the same shape `subbly.cart.getStartDates` returns. ```js title="Get the gifting dates" const { availableDates, range } = await subbly.cart.getGiftingDates() const startsAt = availableDates?.length ? availableDates[0] : range?.[0] await subbly.cart.update({ giftInfo: { startsAt, numberOfOrders: 3, message: 'Happy birthday', recipientEmail: 'grace@example.com', }, }) ``` Gets the delivery options available for the cart at a destination. Send the ID of the one the customer picks to `subbly.cart.update` as `shippingMethodId`. Only `countryId` and `zip` are needed. Pass the rest when a carrier quotes on the full address. Where the order is going. ID of the country, from `subbly.countries.list()`. The field is `countryId`, not `country`. Postal or ZIP code of the destination. ID of the region, from the country's `regions[].id`. Given name of the recipient. Family name of the recipient. Phone number of the recipient. First address line. Second address line. City. Relations to include: `method` and `carrier`. The delivery options. Read `type` first: it is `shipping_option` for a carrier delivery and `local_delivery` for the shop's own, and it decides which of the fields below are set. ```js title="Get the shipping methods" const methods = await subbly.cart.getShippingMethods({ countryId: 826, zip: 'NW1 5JD', expand: ['carrier'], }) await subbly.cart.update({ shippingMethodId: methods[0].id }) ``` Gets the pick-up points available for the cart. Send the ID of the one the customer picks to `subbly.cart.update` as `shippingMethodId`, along with `pickupInfo` for who collects the order. The pick-up points. The array is empty when the shop offers none. ```js title="Get the pick-up points" const points = await subbly.cart.getLocalPickups() await subbly.cart.update({ shippingMethodId: points[0].id, pickupInfo: { firstName: 'Ada', lastName: 'Lovelace', phone: '+442071234567', }, }) ``` Assigns the cart to the signed-in customer. It sets `attached` to `true` and `customerId` to their ID, and clears the guest details in `customer`. Call it after the customer signs in and before you charge the cart. It needs a signed-in customer. Relations to include. The relations to fill in. The values are listed at the top of this page. The attached cart, which is `subbly.cart` itself. Its fields are written out under [Create a cart](#create). ```js title="Attach the customer after a log-in" await subbly.auth.login({ email, password }) await subbly.cart.attachCustomer({ expand: ['items.product'] }) ``` --- ## Document: /reference/subbly-sdk/bundles URL: /reference/subbly-sdk/bundles import BundleFields from "../../../src/reference/subbly-sdk/types/Bundle.mdx"; import BundleItemFields from "../../../src/reference/subbly-sdk/types/BundleItem.mdx"; import BundleGroupFields from "../../../src/reference/subbly-sdk/types/BundleGroup.mdx"; import BundleQuoteFields from "../../../src/reference/subbly-sdk/types/BundleQuote.mdx"; import PaginationFields from "../../../src/reference/subbly-sdk/types/Pagination.mdx"; Every method here takes an optional `headers` argument to price one call in another currency, and every amount is a whole number in the minor unit; see [Currency](/reference/subbly-sdk#currency) and [Amounts and dates](/reference/subbly-sdk#amounts-and-dates). A typical flow: `load` the bundle for its plans and rules, `loadItems` for what the customer can pick, `quote` to show the price, then `subbly.cart.addItem` to buy it. Gets a paginated list of the shop's bundles. Paging and filters. Pass `{}` for the defaults — the argument is required by position even though every field inside it is optional. Page to fetch, counting from 1. How many bundles per page. Keep only bundles carrying these tags. Keep only bundles with these slugs. Keep only these bundle IDs. `true` keeps only digital bundles, `false` only physical ones. `true` keeps only bundles the customer configures, `false` only fixed ones. Relations to include: `plans.variant`, `plans.variant.parent`, `plans.plan`, `plans.plan.parent` and `metadata`. Extra request headers. Currency for the prices in the response, such as `EUR`. One page of bundles. The bundles on this page. Where this page sits in the list. ```js title="List the bundles" const { data, pagination } = await subbly.bundles.list({}) ``` ```js title="List the configurable bundles with their plans" const { data } = await subbly.bundles.list({ perPage: 24, configurable: true, expand: ['plans.plan', 'plans.variant'], }) ``` Gets one bundle with its plans, prices, discounts, rulesets, filters and preferences — everything the picker needs except the items themselves. ID of the bundle. Relations to include. `plans.variant`, `plans.variant.parent`, `plans.plan`, `plans.plan.parent` and `metadata`. Extra request headers. Currency for the prices in the response. The bundle. ```js title="Load a bundle" const bundle = await subbly.bundles.load(512, { expand: ['plans.plan', 'plans.plan.parent'], }) ``` Gets a paginated, filterable list of the products a customer can pick for the bundle. Send the picks to `quote` and to `subbly.cart.addItem` as `{ productId, quantity }`. ID of the bundle. Paging, filters and relations. Page to fetch, counting from 1. How many items per page. Attribute filters, built from `bundle.filters`. ID of the attribute, from `bundle.filters[].attributeId`. IDs of the chosen values, from `bundle.filters[].values[].id`. Keep only these variant IDs. Free-text search over the item names. `product` and `product.parent`. Extra request headers. Currency for the prices in the response. One page of bundle items. The items on this page. Where this page sits in the list. ```js title="Load the bundle items" const { data } = await subbly.bundles.loadItems(512, { perPage: 50, expand: ['product'], }) ``` ```js title="Filter the items by an attribute" const { data } = await subbly.bundles.loadItems(512, { filters: [{ attributeId: 9, values: [31, 32] }], }) ``` Gets the item groups of a bundle. A group gathers the variants of one product and sets its own minimum and maximum quantity, so a customer can be asked for, say, two coffees and one tea. ID of the bundle. Paging and relations. Page to fetch, counting from 1. How many groups per page. `product`, `items.product` and `items.product.parent`. Extra request headers. Currency for the prices in the response. One page of groups. The groups on this page. Where this page sits in the list. ```js title="Load the bundle groups" const { data } = await subbly.bundles.loadGroups(512, { expand: ['items.product'], }) ``` Prices a bundle for the exact items and preferences a customer picked, before it goes in the cart. Use it to show a running total in the picker. ID of the bundle. What the customer picked. `items` and `preferences` are both required; send an empty array when the bundle has no preferences. The plan or variant the customer chose, from `bundle.plans[].plan.id` or `bundle.plans[].variant.id`. How many bundles. The picked items. ID of the picked variant, from `bundleItem.productId`. How many of it. The answers to the bundle preferences. Send `[]` when there are none. ID of the attribute, from `bundle.preferences[].attributeId`. IDs of the chosen values. Relations to include. `items.product` and `items.product.parent`. Extra request headers. Currency for the prices in the response. The priced bundle, with a line for every picked item. ```js title="Quote a bundle" const quote = await subbly.bundles.quote(512, { productId: 8801, quantity: 1, items: [ { productId: 3301, quantity: 2 }, { productId: 3307, quantity: 1 }, ], preferences: [], }) ``` --- ## Document: /reference/subbly-sdk/auth URL: /reference/subbly-sdk/auth import LoginResponseFields from "../../../src/reference/subbly-sdk/types/AuthResourceLoginResponse.mdx"; A successful sign-in writes the access token to the `subbly_access_token` cookie, sets the `Authorization` header on the shared HTTP client, and flips `subbly.auth.isAuthenticated` to `true`. The cookie lasts 365 days and is set on `path=/`. It is a plain cookie: the SDK sets no `Secure`, `HttpOnly` or `SameSite` attribute, so treat it as readable by any script on the host. A `boolean` property, not a method. It is `true` after a successful `checkAuthenticated`, `login`, `register`, `otpLogin` or `social`, and `false` after `logout`. Nothing watches it, so read it after an authentication call rather than expecting it to update on its own. `true` when the SDK holds a token it has checked. ```js title="Branch on the authentication state" if (subbly.auth.isAuthenticated) { const customer = await subbly.customers.me() } ``` Verifies that the SDK can call the methods that need a signed-in customer. `Subbly.init` calls it for you at start-up. The check costs one request: it calls `subbly.customers.me()` behind the scenes. When it passes, the SDK sets the `Authorization` header and, if you passed a token, writes it to the cookie. When it fails, the SDK clears the header and the cookie and logs the error to the console. An access token to check. It beats the token in the cookie. Leave it out to check the token already stored. `true` when the customer is signed in. It resolves with `false` rather than rejecting when there is no token or the token is rejected. ```js title="Check the stored token" const signedIn = await subbly.auth.checkAuthenticated() ``` ```js title="Check a token you hold" const signedIn = await subbly.auth.checkAuthenticated('eyJh...eyJ9') ``` Signs the customer in with an email and a password. The credentials. Email address of the customer. Password of the customer. The new token. The SDK stores it for you. ```js title="Log in" await subbly.auth.login({ email: 'ada@example.com', password: 'correct horse battery staple', }) ``` Creates an account and signs the new customer in. It stores the token exactly as `login` does. The new customer. Email address. It must not already have a Subbly account. Given name. Family name. Password. Leave it out and Subbly generates one and emails it to the customer. The new token, in the same shape `login` returns. ```js title="Register a customer" await subbly.auth.register({ email: 'ada@example.com', firstName: 'Ada', lastName: 'Lovelace', }) ``` Reports whether an email already has a Subbly account. Use it to send the customer to the sign-in form instead of the sign-up form. The email to look up. Email address to check. The answer. `true` when an account already exists for the email. ```js title="Check an email" const { registered } = await subbly.auth.registered({ email: 'ada@example.com', }) ``` Emails a one-time password to the address you give. Pass the code the customer types back to `subbly.auth.otpLogin`. Where to send the code. Email address to send the one-time password to. Nothing. The response body is empty. ```js title="Send a one-time password" await subbly.auth.otp({ email: 'ada@example.com' }) ``` Signs the customer in with the one-time password `subbly.auth.otp` emailed. It stores the token exactly as `login` does. The email and the code. The address the code went to. The one-time password the customer typed, such as `123456`. The new token, in the same shape `login` returns. ```js title="Log in with a one-time password" await subbly.auth.otpLogin({ email: 'ada@example.com', token: '123456', }) ``` Signs the customer in with a Google or Facebook token. The provider must be switched on for the shop; look for a `google_login` or `facebook_login` entry in `subbly.shop.apps`. It stores the token exactly as `login` does. The token from the provider. The ID token the provider's own SDK gave you. Which service issued the token: `google` or `facebook`. The new token, in the same shape `login` returns. ```js title="Log in with Google" await subbly.auth.social({ token: googleIdToken, provider: 'google', }) ``` Clears the authentication state in the browser: it sets `isAuthenticated` to `false`, drops the `Authorization` header, and empties the `subbly_access_token` cookie. It sends no request, so the token itself stays valid until it expires. Do not rely on `logout` to lock anything down on the server. Nothing. This method is not a promise. ```js title="Log out" subbly.auth.logout() ``` Reads the access token out of the `subbly_access_token` cookie. It is synchronous and makes no request. Use it to hand the token to your own backend. The stored token. `null` when there is no cookie, and an empty string after `subbly.auth.logout()`. The TypeScript type says `string | undefined`; the SDK never returns `undefined`. ```js title="Read the access token" const token = subbly.auth.getAccessToken() ``` --- ## Document: /reference/subbly-sdk/addresses URL: /reference/subbly-sdk/addresses import AddressFields from "../../../src/reference/subbly-sdk/types/CustomerAddress.mdx"; import AddressPayloadFields from "../../../src/reference/subbly-sdk/types/AddressStorePayload.mdx"; Every method here needs a signed-in customer. Sign one in with the [authentication methods](/reference/subbly-sdk/auth) first. Gets every saved address of the signed-in customer. The saved addresses. The array is empty when the customer has none. ```js title="List the addresses" const addresses = await subbly.addresses.list() ``` Creates a new saved address for the signed-in customer. Pass the new `id` to `subbly.cart.update` as `shippingAddressId` or `billingAddressId`. The address to save. The saved address. ```js title="Add an address" const address = await subbly.addresses.store({ firstName: 'Ada', lastName: 'Lovelace', phone: '+442071234567', addressOne: '12 Marylebone Road', addressTwo: null, city: 'London', zip: 'NW1 5JD', countryId: 826, }) ``` Deletes a saved address. The call fails when the address is still attached to a subscription. ID of the address to delete. Nothing. The response body is empty. ```js title="Delete an address" await subbly.addresses.delete(4821) ``` --- ## Document: /reference/cart-widget/theming URL: /reference/cart-widget/theming The custom properties the widget computes from the cart settings and writes on its container. A property whose setting is empty is not written, and the widget falls back to the [base token](#base-tokens). To override them from your own CSS, see [Style the widget](/developer-resources/cart-widget/style-it). From `accentColor`, else the shop's brand colour. Default `var(--black)`. The accent at 10% opacity. Default `var(--black-alpha-01)`. The accent at 80% opacity. Default `var(--black-alpha-08)`. Text on the accent. The widget picks dark text on a light accent, white on a dark one. Default `var(--white)`. From `backgroundColor`. The screen behind the widget. Default `var(--black-alpha-06)`. From `widgetBackgroundColor`, and a gradient down to `widgetBackgroundDarker` when that is set. Default `#EAEFF3`. From `fontStyle`, in front of the system stack. Default the system stack. From `fontColorPrimary`. Default `var(--black)`. From `fontColorSecondary`. Default `var(--gray-600)`. The secondary colour at 80% opacity. Default `#464646aa`. The secondary colour at 10% opacity. Default `#4646461a`. From `buttonStyle`, in pixels. Default `8px`. From `cardBackgroundColor`. Default `var(--white)`. From `cardBorderColor`. Default `#D3D9DE`. From `cardStyle`, in pixels. Default `8px`. From `cardDividerColor`. Default `#D6D1C2`. No setting drives it. Default `14px`. Computed from `cardBackgroundColor`: a shade darker on a light card, brighter on a dark one. Default `var(--gray-125)`. From `headerHeight`, in pixels. Default `52px`. From `headerBackgroundColor`, else `widgetBackgroundColor`. Default `var(--widget-bg)`. From `headerFontColor`. Default `var(--font-color-primary)`. From `footerBackground`, else `widgetBackgroundDarker`. Default `var(--widget-bg)`. From `footerFontColor`, else `fontColorPrimary`. Default `var(--font-color-primary)`. From `inputBackground`. Default `var(--white)`. From `inputBorderColor`. Default `var(--gray-300)`. From `inputFontColor`. Default `var(--black)`. From `segmentControlBackground`. Default `var(--accent)`. Text on the segmented control, computed from its background. Default `var(--accent-font)`. Icon on the floating cart button, computed from the accent. Default `#F9FDFF`. Written from `accentColor` and its computed text colour, but no stylesheet reads them. The floating cart button paints its background with `var(--accent)`. Override the accent to change it. ```css title="Override the accent" #subbly-cart-widget { --accent: #701eff !important; --button-border-radius: 24px !important; } ``` The palette the widget's own styles are built on. No setting changes these. They are declared on the container class `.SubblyAppContainer`, so a rule on `#subbly-cart-widget` overrides them without `!important`. `#000` and `#fff`. Black at five alpha steps: `#0001`, `#0002`, `#0004`, `#0006` and `#0008`, which is about 7%, 13%, 27%, 40% and 53% opacity. The grey ramp: `100`, `125`, `150`, `175`, `200`, `250`, `300`, `350`, `375`, `400`, `450`, `500`, `600` and `800`, from `#F7F7F7` down to `#1B1B1B`. `#1624A4`. The error colours: `#FCF0ED`, `#E49D92` and `#C92E2E`. `#D35307`, for warnings. The success colours: `#E8F2ED`, `#82CEA2` and `#16A480`. `var(--green-700)`. `var(--blue-700)`. The ring around a focused control. ```css title="Match the widget to your own palette" #subbly-cart-widget { --gray-100: #f5f3ef; --focus-outline: #701eff; } ``` --- ## Document: /reference/cart-widget/settings URL: /reference/cart-widget/settings The object the widget reads once, the moment its script runs. Write it before the script tag. [`SubblyCart.initialize(config)`](/reference/cart-widget/methods#initialize) takes the same object. Only `apiKey` is required. Your Storefront API key, from the Subbly admin under Settings, Developers. A missing key throws `Missing required apiKey config`. API the widget calls. Default `https://api.subbly.co/storefront/v1/`. An empty string falls back to the default. Page-integration settings: which links the widget catches, which element opens it, which element shows the item count. Every key is on [Settings keys](#settings). Default `{}`. Two-letter language of the widget. Without it, the widget takes the `lang` query parameter of the page, then the `lang` attribute of ``, then the browser's language, and keeps the first two characters. Start from a cart you already know. Default the `subbly_cart_id` cookie. Sign a customer in at start-up. A valid token is written to the `subbly_access_token` cookie. Default that cookie. `false` stops the automatic start-up; you then call [`SubblyCart.initialize`](/reference/cart-widget/methods#initialize) yourself. Only the exact value `false` does this. Default `true`. Name of the global that gets the instance. A value that is not a non-empty string is ignored. Default `subblyCart`. A Subbly.js client you already built. The widget uses it instead of creating its own. Standalone checkout. The widget opens full screen and configures the item from `window.subblyCheckout.payload` at once. Default `false`. Page on your site that owns the checkout. The widget sends the customer there instead of opening its own, and stands down on that path. It accepts `/checkout` and `/checkout/{cartId}`; a bare page gets the current cart ID appended on redirect. Page on your site that owns the cart summary. Same behaviour as `checkoutUrl`. Paths on which the widget stands down. Default `checkoutUrl` (without its cart ID) and `cartSummaryUrl`; an empty array keeps that default. Only the pathname is matched, case-insensitively. `*` matches inside one segment, `**` across segments. An entry without a wildcard also matches on a segment boundary, so `/checkout` covers `/checkout/success` but not `/checkout-foo`. ```html title="A full config object" ``` Page-integration settings. They tie the widget to the markup on your own page: which links it catches, which element opens it, which element shows the item count. Set them in `window.subblyConfig` before the script runs, or pass them to `SubblyCart.initialize()`. These are not the theme settings. Colours, typography and checkout behaviour live in the shop's cart settings, which you override with [`setSettings`](/reference/cart-widget/methods#set-settings). Catch clicks on Subbly buy links anywhere on the page, cancel the navigation, and open the widget instead. See [Sell from your own links](/developer-resources/cart-widget/wire-it-into-your-page#sell-from-your-own-links). Default `false`. Before the widget finishes loading, a stub handler cancels these clicks. It does not remember them, so a click that early is dropped. Show the widget's own floating cart button. Default `false`. The button is suppressed when the page already holds an `a.subbly-cart` element. URL of an image for the floating cart button, in place of the built-in icon. It works only with `cartButton: true`. Default `null`; anything that is not a string is ignored. CSS selector or element that opens and closes the widget. The default click action is cancelled. Clicks are caught on `document`, so elements added later still work. Default `''`. CSS selector or element whose content the widget replaces with the number of active items. It updates on `CART_READY` and `CART_UPDATED`, and after a `popstate` event, where it waits up to 5 seconds for the element and then 300 ms more before writing. Default `''`. The widget also listens for a `pushstate` event, but nothing fires one: its own patch of `history.pushState` dispatches `locationchange` instead. On a framework that routes with `pushState`, update the element yourself from `CART_UPDATED`. Show the currency and language picker in the widget header. Default `false`. Hide the cart summary and use the checkout as the first view. Default `false`. The `?disableSummaryView=1` query parameter does the same. Hide the products inside a bundle, in the summary and at the checkout. Default `false`. The `?hideBundleItems=1` query parameter does the same. ```html title="Configure the widget" ``` Query parameters the widget reads from the page URL when it starts. They make a plain link enough to drive the widget — a campaign link that applies a coupon, a referral link, a translated landing page. Four of them are used once and then remembered as used: `gift`, `coupon`, `r` and `custom_setup_return_url`. A bank confirmation that sends the customer back to the same URL does not apply them twice. Language of the widget, when `languageCode` is not in the config. The widget takes the first two characters. Coupon code to hold and apply once the cart fits it, as [`setPendingCoupon`](/reference/cart-widget/methods#set-pending-coupon) does. Used once. Any value opens the gift view. Used once. ID of the referring customer. Used once. Prefills the checkout email. `customerEmail` is a deprecated fallback. Prefills the first name. Prefills the last name. Prefills the marketing opt-in. Prefills the terms checkbox. Your own ID for the customer. It is stored on the customer profile. Any value hides the summary and uses the checkout as the first view. Any value hides the products inside a bundle. URL of your own receipt page, in place of the widget's. URL the customer returns to after a Stripe 3-D Secure check. Used once. `test` runs the checkout in test mode. Set by Stripe on the return redirect. `payment_method_id` and `redirect_status` come with it. ```html title="A campaign link" 10% off your first box ``` --- ## Document: /reference/cart-widget/properties URL: /reference/cart-widget/properties import SubblyCartFields from "../../../src/reference/cart-widget/types/SubblyCart.mdx"; import CartModelMethods from "../../../src/reference/cart-widget/types/CartModel.mdx"; import SubblyShopFields from "../../../src/reference/cart-widget/types/SubblyShop.mdx"; The current cart: every cart field, plus the cart methods of the SDK. It is one live object, not a snapshot — the SDK writes the new data into the same object after every call, so a reference you store keeps showing current data. Clone it when you need a snapshot. Treat it as read-only and change the cart through the widget methods. The SDK methods on it skip the widget's own store, so the panel does not refresh and no event fires. The cart is ready as soon as the widget is initialized. The cart. ```js title="Read the cart" const { items, total, currencyCode } = subblyCart.cart // Amounts live on the summary items, joined on itemId const amount = subblyCart.cart.summaryItems.find( (summaryItem) => summaryItem.itemId === items[0].id ).total ``` Public information about the shop: branding, currencies, languages, settings, plan limits and shipping countries. The widget loads it once while it starts. Like the cart, it is one live object; `loadShop()` refreshes it in place. The shop. ```js title="List the currencies the shop sells in" const codes = subblyCart.shop.currencies.map( (currency) => currency.abbreviation ) ``` The currency and the language the widget runs on. Reading it builds a fresh plain object every time, so what you get is a snapshot. Currency of the cart, as an ISO 4217 code. It falls back to the shop's default currency while the cart has none. Two-letter code of the language the widget shows, such as `en`. ```js title="Read the state" const { currencyCode, languageCode } = subblyCart.state ``` The [Subbly.js](/reference/subbly-sdk) client the widget uses, for Storefront API calls of your own. Sharing one client keeps the widget and your code on the same cart, customer, language and currency. Calls you make on the client do not go through the widget's store, so the panel does not refresh and no widget event fires. Use the widget methods to change the cart. Version of the SDK. The client configuration: `apiKey`, `apiUrl` and `lang`. Re-prices the products, bundles and surveys the client loads next. It does not change the cart; use `subblyCart.setCurrency` for that. Sets the language the API answers in. `list`, `store`, `delete`. `isAuthenticated`, `checkAuthenticated`, `login`, `registered`, `register`, `otp`, `otpLogin`, `social`, `logout`, `getAccessToken`. `list`, `load`, `loadItems`, `loadGroups`, `quote`, `setCurrency`. The same object as `subblyCart.cart`. `purchase`. `list`. `update`, `me`, `referral`. All three need a signed-in customer. `prePurchaseFetch`, `prePurchaseRefresh`, `prePurchaseAccept`, `prePurchaseReject`, `midPurchaseFetch`, `midPurchaseAccept`, `midPurchaseReject`, `postPurchaseFetch`, `postPurchaseAccept`, `postPurchaseReject`. `subscribe`. `list`. `getPaymentIntent`, `confirm`. `list`, `store`, `delete`. `list`, `load`, `loadVariant`, `loadPlan`, `setCurrency`. `load`. The same object as `subblyCart.shop`. `subscribe`. `list`, `load`, `update`, `updatePreferences`, `updateBundle`, `loadItem`, `updateItem`, `updateItemBundle`, `updateItemPreferences`. `load`, `setCurrency`. `store`, `list`, `setup`, `setupIntent`. ```js title="Call the Storefront API" const { data } = await subblyCart.sdk.products.list({ perPage: 100 }) ``` The event emitter of the widget. `events.type` holds the event names, and every name equals its own value, so `events.type.CART_OPEN` is `'CART_OPEN'`. The emitter comes from `tiny-typed-emitter`, which follows the Node `EventEmitter` API. The event names: `CART_READY`, `CART_UPDATED`, `CART_OPEN`, `CART_CLOSE`, `CART_RESET`, `EMAIL_COLLECTED`, `PURCHASE_COMPLETED`, `SIGN_IN`, `SIGN_UP` and `SIGN_OUT`. Adds a handler. `addListener` is an alias. Adds a handler that runs for one emission. Removes a handler. `removeListener` is an alias. Removes every handler, of one event or of all of them. Adds a handler at the front of the list. `prependOnceListener` does the same for one emission. Lists the events that have handlers. Counts the handlers of an event. Lists the handlers of an event. `rawListeners` keeps the `once` wrappers. Sets how many handlers one event takes before Node warns. `getMaxListeners` reads it back. Internal. The widget emits its own events; do not call this. ```js title="Subscribe and unsubscribe" const { events } = subblyCart const onUpdate = (err, cart) => console.log(cart.items.length) events.on(events.type.CART_UPDATED, onUpdate) events.off(events.type.CART_UPDATED, onUpdate) ``` --- ## Document: /reference/cart-widget/methods URL: /reference/cart-widget/methods import SubblyCartFields from "../../../src/reference/cart-widget/types/SubblyCart.mdx"; import SubblyShopFields from "../../../src/reference/cart-widget/types/SubblyShop.mdx"; import CartUpdatePayloadFields from "../../../src/reference/cart-widget/types/CartUpdatePayload.mdx"; import ConfigureItemPayloadFields from "../../../src/reference/cart-widget/types/ConfigureItemPayload.mdx"; import CartItemAddPayloadFields from "../../../src/reference/cart-widget/types/CartItemAddPayload.mdx"; import CartUpdateItemPayloadFields from "../../../src/reference/cart-widget/types/CartResourceUpdateItemPayload.mdx"; import ShopCartSettingsFields from "../../../src/reference/cart-widget/types/ShopCartSettings.mdx"; Opens the widget on the cart summary, or on the checkout. The call does nothing when the widget is already open. It emits `CART_OPEN`. Which view to open. Default `"summary"`. - `"summary"` shows the cart summary and closes the checkout panel. - `"checkout"` opens the checkout. When `checkoutUrl` is configured, the widget sends the page to that URL with the cart ID appended and stops, so nothing opens. The embedded checkout view needs both the `checkoutWidget` and the `embedCheckout` limits on the shop; without them the widget sends the page to the hosted Subbly checkout instead. - Any other string opens the widget without changing the view. - `true` opens the widget with the summary shown, `false` with it hidden. Both leave the active customization view mounted. Every string value unmounts the customization view, unless `"checkout"` hands over to another page. ```js title="Open the widget" subblyCart.open() // Open the checkout subblyCart.open('checkout') // Open without the summary subblyCart.open(false) ``` Closes the widget. It hides the summary, unmounts any customization view, and clears the widget error. The call does nothing when the widget is already closed. It emits `CART_CLOSE`. ```js title="Close the widget" subblyCart.close() ``` Opens the widget when it is closed, closes it when it is open. It always opens on the summary. It emits `CART_OPEN` or `CART_CLOSE`. ```js title="Toggle the widget" subblyCart.toggle() ``` Returns the shop the widget already holds. It refreshes `subblyCart.shop` in place, so every reference you hold sees the same object. It calls the API only when no shop is loaded yet, which cannot happen after `initialize`, so in practice it never re-reads the shop. The shop. ```js title="Reload the shop" const shop = await subblyCart.loadShop() ``` Applies cart-level changes — currency, coupon, addresses, shipping, gift details — and refreshes the widget, open or closed. Calls run in order through an internal queue, so concurrent updates do not race each other. A cart that was already purchased is reset first, which emits `CART_RESET`. The method throws `The cart is not initialized` when the widget holds no cart. It emits `CART_UPDATED`. The cart fields to change. Every field is optional. The updated cart. ```js title="Set the shipping address" const cart = await subblyCart.updateCart({ shippingAddress: { firstName: 'Ada', lastName: 'Lovelace', addressOne: '12 Bishop Street', addressTwo: null, city: 'Bristol', zip: 'BS2 8EE', countryId: 826 } }) ``` Adds a product, a bundle, or a survey, and asks the customer for what is missing. Use it when you hold a parent product ID, a bundle ID, or a survey ID, and `addItem` when you already hold a variant or a plan. The widget creates a cart when there is none, and opens itself unless the shop setting `afterItemAdded` is `close`. It then mounts the view the item needs: pricing, variant, survey, survey flow, bundle, bundle plan, voucher, subscription replacement, out of stock, or error. It emits `CART_UPDATED` once the item lands in the cart. What to add. Pass one of `productId`, `bundleId` or `surveyId`. The promise stays pending while the customization view is open, and resolves once the customer closes it. It never resolves with `finalized: false`, so the useful signal is when it resolves, not what it carries. `true`. The item is in the cart. ```js title="Add a subscription product" const { finalized } = await subblyCart.configureItem({ productId: 555, quantity: 1 }) ``` ```js title="Start a bundle or a survey" await subblyCart.configureItem({ bundleId: 42 }) await subblyCart.configureItem({ surveyId: 7 }) ``` Adds an item that needs no further input straight to the cart. The `productId` must be a variant ID or a plan ID. A cart that was already purchased is reset first, which emits `CART_RESET`. On failure the widget shows its error view **and** rejects, so your `catch` runs as well. It emits `CART_UPDATED`. The item to add. The updated cart. ```js title="Add a variant" const cart = await subblyCart.addItem({ productId: 8123, quantity: 2 }) ``` Changes a line that is already in the cart. The payload has no required field, so TypeScript cannot tell the three shapes apart; pick the one that matches the line. There is no `productId` and no `metadata` — both are add-time only. It emits `CART_UPDATED`. ID of the line, as a UUID. Read it from `subblyCart.cart.items[].id`. The fields to change. The updated cart. ```js title="Change the quantity" const [item] = subblyCart.cart.items const cart = await subblyCart.updateItem(item.id, { quantity: 3 }) ``` Removes a line from the cart. It emits `CART_UPDATED`. ID of the line, as a UUID. Read it from `subblyCart.cart.items[].id`. The updated cart. ```js title="Remove an item" const [item] = subblyCart.cart.items const cart = await subblyCart.removeItem(item.id) ``` Applies a coupon code to the cart. It is shorthand for `updateCart({ couponCode })`, and it emits `CART_UPDATED`. The call rejects when the coupon is invalid or does not fit what the cart holds, so a coupon tied to a product fails on an empty cart. Use `setPendingCoupon` to hold such a code until the product is in the cart. The code the customer typed. The updated cart. ```js title="Apply a coupon" const cart = await subblyCart.applyCoupon('WELCOME10') ``` Holds a coupon code and applies it once the cart holds the right products. Nothing happens at once: the widget applies the code the next time the summary or the checkout renders, so a product-restricted coupon is checked against a cart that already holds the product. The code is dropped after one attempt, and skipped when it matches the coupon already on the cart. Failures are silent. This is what a `?coupon=CODE` buy link uses. The code to hold. ```js title="Queue a coupon, then add the product" subblyCart.setPendingCoupon('WELCOME10') await subblyCart.configureItem({ productId: 555 }) ``` Removes the coupon from the cart. It calls `updateCart({ couponCode: null })` and emits `CART_UPDATED`. The updated cart. ```js title="Remove the coupon" const cart = await subblyCart.removeCoupon() ``` Applies a gift card code to the cart. It is shorthand for `updateCart({ giftCardCode })`, and it emits `CART_UPDATED`. The gift card code, as a UUID. The updated cart. ```js title="Apply a gift card" const cart = await subblyCart.applyGiftCard( '0b5c0f1a-8f3c-4f2a-9f0e-6a2f6a1f2b3c' ) ``` Removes the gift card from the cart. It calls `updateCart({ giftCardCode: null })` and emits `CART_UPDATED`. The updated cart. ```js title="Remove the gift card" const cart = await subblyCart.removeGiftCard() ``` Switches the language of the widget and asks the API for translated responses. The shop must publish the language; otherwise the widget falls back to the shop's own languages, then to `en`. It updates `subblyCart.state.languageCode` and emits no event. An empty value throws `Cart Widget:setLanguage Language code is required`. Without this call the widget takes the language from `languageCode` in the config. If that is missing it tries the `lang` query parameter, then the `lang` attribute of ``, then the browser. Two-letter language code, such as `fr`. It must be one of `subblyCart.shop.languages[].code`. ```js title="Switch to French" subblyCart.setLanguage('fr') ``` Switches the currency of the cart and re-prices every line. It also re-prices the products, bundles and surveys the SDK loads next, and updates `subblyCart.state.currencyCode`. An empty value throws `Cart Widget:setCurrency currency code is required`. It emits `CART_UPDATED`. ISO 4217 code, such as `GBP`. It must be one of `subblyCart.shop.currencies[].abbreviation`. The updated cart. ```js title="Switch to pounds" const cart = await subblyCart.setCurrency('GBP') ``` Discards the cart and creates an empty one with the same currency. The new cart ID replaces the old one in the `subbly_cart_id` cookie. Any open customization view is unmounted. It emits `CART_UPDATED`, then `CART_RESET`. The new cart. ```js title="Start a new cart" const cart = await subblyCart.resetCart() ``` Overrides the shop's cart settings at runtime: colours, typography, layout and behaviour. Keys you omit keep their value. The widget recomputes the CSS custom properties on its container, and loads the Google font named by `fontStyle` by adding a `` to the head. Nothing is stored, so a page reload brings the shop's own settings back. The settings to change. The method runs asynchronously and returns nothing worth waiting for. ```js title="Restyle the widget" subblyCart.setSettings({ accentColor: '#701eff', fontStyle: 'Inter', buttonStyle: 24, afterItemAdded: 'checkout' }) ``` Mounts the gift view, now or after the current customization. It does not open the widget on its own. Pass `null`. Any other value logs `Payload is not supported yet in the configureGift method.` and is ignored. `true` mounts the gift view now. `false` queues it, so it appears once the current customization view closes. ```js title="Ask for gift details after the item" const { finalized } = await subblyCart.configureItem({ productId: 555 }) subblyCart.configureGift(null, finalized) ``` Prefills the checkout with an email and consent flags for a guest. The call does nothing when a customer is already signed in. The same data can come from the `email`, `marketingConsent` and `tosConsent` query parameters. What to prefill. Email address for the checkout field. `true` ticks the marketing opt-in. `true` ticks the terms checkbox. ```js title="Prefill the checkout" subblyCart.setCustomerData({ email: 'ada@example.com', marketingConsent: true }) ``` Signs a customer in with a Subbly access token. The widget checks the token against the API first. A token you pass is written to the `subbly_access_token` cookie; a token that fails the check is cleared. On success the widget fetches the customer and emits `SIGN_IN`. A customer access token. Without it the widget checks the token in the `subbly_access_token` cookie. The promise rejects with `Cart Widget:authenticate Authentication failed. The provided token is invalid or expired.` when the token does not check out. ```js title="Sign a customer in" await subblyCart.authenticate(myAccessToken) ``` Signs the current customer out of the widget. It clears the widget's sign-in state and the `subbly_access_token` cookie, and emits `SIGN_OUT`. The cart stays as it is. ```js title="Sign the customer out" await subblyCart.signOut() ``` Re-reads the cart and the sign-in state from the API. Use it on a page that rebuilds the cart itself, such as your own checkout. The widget re-checks the shared access token and signs itself out when the token is gone, without touching the cart; a failed check is ignored and the cart still reloads. It emits `CART_UPDATED`. Point the widget at this cart, as a UUID, and write it to the `subbly_cart_id` cookie for 365 days. Without it the widget reloads the cart it already holds. ```js title="Adopt a cart made outside the widget" await subblyCart.reload('14920f9c-f261-4879-a3a9-e1b75993eeed') ``` Merges new URL configuration into the running widget, and checks the current URL against it at once. Each field merges over the current value, so a field you leave out keeps what the config set at init. `disableUrls` keeps the list the widget already holds: it is not rebuilt from the new `checkoutUrl` or `cartSummaryUrl`. Only an empty list falls back to those URLs. The URL configuration to merge. Page on your site that owns the checkout. The widget sends the customer here instead of opening its own checkout. It accepts `/checkout` and `/checkout/{cartId}`; a plain path gets the current cart ID appended. Page on your site that owns the cart summary. It behaves like `checkoutUrl`. Paths on which the widget stands down. `*` matches inside one path segment and `**` across segments. An entry without a wildcard also matches on a segment boundary, so `/checkout` covers `/checkout/success` but not `/checkout-foo`. Only the pathname is matched, case-insensitively; the query and the hash are ignored. ```js title="Hand checkout to your own page" subblyCart.configure({ checkoutUrl: '/checkout', disableUrls: ['/checkout', '/cart'] }) ``` Makes the widget stand down: no panel, no cart button, no checkout. The override sits on top of the URL rules, so the widget stays disabled while either this override is set or the URL matches a `disableUrls` pattern. Older deployments may not have the method yet, so call it with optional chaining. ```js title="Stand the widget down" window.subblyCart?.disable?.() ``` Clears the disable override. The widget comes back only when the current URL no longer matches a disable pattern. A widget that first mounted disabled runs its one-time start-up now. ```js title="Bring the widget back" window.subblyCart?.enable?.() ``` Boots the widget by hand. You need it only with `init: false`, and you call it on the class at `window.SubblyCart`, not on an instance. It creates the container element, starts the SDK, mounts the app, applies the settings, and emits `CART_READY`. It does not set a global: the name in `globalName` is only assigned on the automatic path, so keep the instance the promise gives you. The same object you would put in `window.subblyConfig`. See [Configuration keys](/reference/cart-widget/settings#config) for every key. The widget instance: the object every method and property on these pages belongs to. ```js title="Initialize manually" window.addEventListener('subbly-cart-loaded', async () => { const cart = await window.SubblyCart.initialize({ apiKey: 'YOUR_STOREFRONT_API_KEY' }) cart.open() }) ``` --- ## Document: /reference/cart-widget/events URL: /reference/cart-widget/events import SubblyCartFields from "../../../src/reference/cart-widget/types/SubblyCart.mdx"; import SubblyCustomerFields from "../../../src/reference/cart-widget/types/SubblyCustomer.mdx"; import PurchaseFields from "../../../src/reference/cart-widget/types/CheckoutPurchaseResponse.mdx"; Every handler takes two arguments: an error slot first, then the payload. The widget passes `null` as the error today, so read the second argument. Payloads are deep-cloned before they are emitted, so a handler cannot change the widget state through them. `CART_READY` is the exception: it carries the live cart object. Subscribes to a widget event. `once` adds a handler for one emission; `addListener` is an alias of `on`. Name of the event. Use `subblyCart.events.type.CART_UPDATED` and the other names below. What to run. The payload shape follows the event. The emitter, so calls chain. ```js title="Listen to an event" const { events } = subblyCart events.on(events.type.CART_UPDATED, (err, cart) => { console.log(cart.items.length) }) ``` Removes a handler you added. Pass the same function you passed to `on`. `removeListener` is an alias; `removeAllListeners(type?)` clears them all. Name of the event. The handler to remove. The emitter, so calls chain. ```js title="Stop listening" const { events } = subblyCart const onOpen = () => console.log('open') events.on(events.type.CART_OPEN, onOpen) events.off(events.type.CART_OPEN, onOpen) ``` The widget finished initializing and the cart is loaded. It fires once, inside `initialize()`, before that promise resolves with the instance. So no page code can subscribe in time: neither a listener added after the `subbly-cart-initialized` window event, nor one added after `SubblyCart.initialize()` resolves with `init: false`. To act on the loaded cart, wait for `subbly-cart-initialized` and read `subblyCart.cart`. This is the one payload the widget does not clone: it is the live cart object. Always `null`. The loaded cart. ```js title="Read the cart once the widget is ready" window.addEventListener('subbly-cart-initialized', () => { console.log(window.subblyCart.cart.id) }) ``` The cart changed. It fires on every change: an item added, updated or removed, a coupon, a gift card, a currency, an address, a reload, a reset. Always `null`. The updated cart, as a deep clone. ```js title="Track the item count" const { events } = subblyCart events.on(events.type.CART_UPDATED, (err, cart) => { document.querySelector('.cart-count').textContent = cart.items.length }) ``` The widget panel opened. It carries no payload. Always `null`. ```js title="Listen for the panel opening" const { events } = subblyCart events.on(events.type.CART_OPEN, () => { document.body.classList.add('cart-open') }) ``` The widget panel closed. It carries no payload. Always `null`. ```js title="Listen for the panel closing" const { events } = subblyCart events.on(events.type.CART_CLOSE, () => { document.body.classList.remove('cart-open') }) ``` A new cart replaced the old one, after `resetCart()` or after an attempt to add to a cart that was already purchased. `CART_UPDATED` comes first: the widget stores the new cart before it emits this event. Always `null`. The new cart, as a deep clone. ```js title="Notice a new cart" const { events } = subblyCart events.on(events.type.CART_RESET, (err, cart) => { console.log('new cart', cart.id) }) ``` The customer's email was captured. It fires when a guest's email is checked at the checkout, and when the checkout form opens for a customer who is already signed in. Always `null`. The email. The email address the customer gave. ```js title="Send the email to your own tooling" const { events } = subblyCart events.on(events.type.EMAIL_COLLECTED, (err, data) => { identify(data.email) }) ``` A purchase succeeded. Which members of `purchase` are filled depends on the cart: a future-dated subscription returns the subscription alone, and a cart of one-time products returns an invoice and orders with no subscription. Always `null`. The cart that was bought, and what the purchase created. The cart at the moment of the purchase. What the purchase created. ```js title="Track a purchase" const { events } = subblyCart events.on(events.type.PURCHASE_COMPLETED, (err, data) => { const { cart, purchase } = data track('purchase', { value: purchase.invoice?.total ?? 0, currency: cart.currencyCode }) }) ``` A customer signed in, from the password form, the one-time code form, or `subblyCart.authenticate()`. Always `null`. The customer. The customer who signed in. ```js title="Greet a returning customer" const { events } = subblyCart events.on(events.type.SIGN_IN, (err, data) => { console.log('welcome back', data.customer.firstName) }) ``` A customer account was created at the checkout. Always `null`. The customer. The customer who signed up. ```js title="Track a sign-up" const { events } = subblyCart events.on(events.type.SIGN_UP, (err, data) => { track('sign_up', { customerId: data.customer.id }) }) ``` `subblyCart.signOut()` ran. It carries no payload, and the cart stays as it is. No control in the widget emits this event, and neither does `subblyCart.reload()`, which also clears the sign-in state. Always `null`. ```js title="Clear your own session" const { events } = subblyCart events.on(events.type.SIGN_OUT, () => { clearLocalSession() }) ``` --- ## Document: Tools Every tool the Subbly's MCP server exposes, with a link to the Private API endpoint behind it. URL: /developer-resources/subbly-mcp/tools # Tools The server exposes 106 tools. Each one wraps one Private API endpoint and takes the same fields, so the endpoint's reference page is where you read what a field means and what comes back. The tables below map every tool to its endpoint. Tools that archive or delete something cannot be undone, and the tool descriptions tell the agent to confirm with you first. Publish and unpublish are the safe way to hide something and bring it back. ## Products | Tool | Private API endpoint | | --- | --- | | `list_products` | [Get list of products](/api/private/product#get-list-of-products) | | `get_product` | [Get product by ID](/api/private/product#get-product-by-id) | | `create_one_time_product` | [Create one-time product](/api/private/product#create-one-time-product) | | `update_one_time_product` | [Update one-time product](/api/private/product#update-one-time-product) | | `publish_one_time_product` | [Publish one-time product](/api/private/product#publish-one-time-product) | | `unpublish_one_time_product` | [Unpublish one-time product](/api/private/product#unpublish-one-time-product) | | `archive_one_time_product` | [Archive one-time product](/api/private/product#archive-one-time-product) | | `set_one_time_product_metadata` | [Sync one-time product metadata](/api/private/product#sync-one-time-product-metadata) | | `create_subscription_product` | [Create subscription product](/api/private/product#create-subscription-product) | | `update_subscription_product` | [Update subscription product](/api/private/product#update-subscription-product) | | `publish_subscription_product` | [Publish subscription product](/api/private/product#publish-subscription-product) | | `unpublish_subscription_product` | [Unpublish subscription product](/api/private/product#unpublish-subscription-product) | | `archive_subscription_product` | [Archive subscription product](/api/private/product#archive-subscription-product) | | `set_subscription_product_metadata` | [Sync subscription product metadata](/api/private/product#sync-subscription-product-metadata) | ## Variants | Tool | Private API endpoint | | --- | --- | | `get_variant` | [Get variant by ID](/api/private/product#get-variant-by-id) | | `create_variant` | [Create variant](/api/private/product#create-variant) | | `update_variant` | [Update variant](/api/private/product#update-variant) | | `archive_variant` | [Archive variant](/api/private/product#archive-variant) | | `batch_variants` | [Batch create/update/archive variants](/api/private/product#batch-create-update-archive-variants) | ## Plans | Tool | Private API endpoint | | --- | --- | | `get_plan` | [Get plan by ID](/api/private/product#get-plan-by-id) | | `create_plan` | [Create plan](/api/private/product#create-plan) | | `update_plan` | [Update plan](/api/private/product#update-plan) | | `archive_plan` | [Archive plan](/api/private/product#archive-plan) | ## Bundles | Tool | Private API endpoint | | --- | --- | | `list_bundles` | [Get list of bundles](/api/private/bundle#get-list-of-bundles) | | `get_bundle` | [Get bundle by ID](/api/private/bundle#get-bundle-by-id) | | `create_bundle` | [Create a new bundle](/api/private/bundle#create-a-new-bundle) | | `update_bundle` | [Update a bundle](/api/private/bundle#update-a-bundle) | | `publish_bundle` | [Publish a bundle](/api/private/bundle#publish-a-bundle) | | `unpublish_bundle` | [Unpublish a bundle](/api/private/bundle#unpublish-a-bundle) | | `archive_bundle` | [Archive a bundle](/api/private/bundle#archive-a-bundle) | | `set_bundle_metadata` | [Sync bundle metadata](/api/private/bundle#sync-bundle-metadata) | | `list_bundle_groups` | [Get list of bundle groups](/api/private/bundle#get-list-of-bundle-groups) | ## Bundle items | Tool | Private API endpoint | | --- | --- | | `list_bundle_items` | [Get list of bundle items](/api/private/bundle#get-list-of-bundle-items) | | `get_bundle_item` | [Get bundle item by ID](/api/private/bundle#get-bundle-item-by-id) | | `create_bundle_item` | [Create a bundle item](/api/private/bundle#create-a-bundle-item) | | `update_bundle_item` | [Update a bundle item](/api/private/bundle#update-a-bundle-item) | | `delete_bundle_item` | [Delete a bundle item](/api/private/bundle#delete-a-bundle-item) | | `batch_bundle_items` | [Batch create bundle items](/api/private/bundle#batch-create-bundle-items) | ## Bundle plans | Tool | Private API endpoint | | --- | --- | | `get_bundle_plan` | [Get bundle plan by ID](/api/private/bundle#get-bundle-plan-by-id) | | `create_bundle_plan` | [Create a bundle plan](/api/private/bundle#create-a-bundle-plan) | | `update_bundle_plan` | [Update a bundle plan](/api/private/bundle#update-a-bundle-plan) | | `archive_bundle_plan` | [Archive a bundle plan](/api/private/bundle#archive-a-bundle-plan) | ## Collections | Tool | Private API endpoint | | --- | --- | | `list_collections` | [Get list of product collections](/api/private/product-collection#get-list-of-product-collections) | | `get_collection` | [Get product collection by ID](/api/private/product-collection#get-product-collection-by-id) | | `create_collection` | [Create a product collection](/api/private/product-collection#create-a-product-collection) | | `update_collection` | [Update a product collection](/api/private/product-collection#update-a-product-collection) | | `publish_collection` | [Publish a product collection](/api/private/product-collection#publish-a-product-collection) | | `unpublish_collection` | [Unpublish a product collection](/api/private/product-collection#unpublish-a-product-collection) | | `archive_collection` | [Archive a product collection](/api/private/product-collection#archive-a-product-collection) | | `list_collection_items` | [Get list of collection groups with items](/api/private/product-collection#get-list-of-collection-groups-with-items) | | `batch_collection_items` | [Sync collection items](/api/private/product-collection#sync-collection-items) | ## Subscriptions | Tool | Private API endpoint | | --- | --- | | `list_subscriptions` | [Get list of subscriptions](/api/private/subscription#get-list-of-subscriptions) | | `get_subscription` | [Get subscription by ID.](/api/private/subscription#get-subscription-by-id) | | `cancel_subscription` | [Cancel a subscription](/api/private/subscription#cancel-a-subscription) | | `undo_subscription_cancellation` | [Undo cancellation](/api/private/subscription#undo-cancellation) | ## Subscription items | Tool | Private API endpoint | | --- | --- | | `create_subscription_item` | [Create a subscription item.](/api/private/subscription#create-a-subscription-item) | | `get_subscription_item` | [Show a subscription item.](/api/private/subscription#show-a-subscription-item) | | `update_subscription_item` | [Update a subscription item.](/api/private/subscription#update-a-subscription-item) | | `delete_subscription_item` | [Remove a subscription item.](/api/private/subscription#remove-a-subscription-item) | | `update_subscription_item_preferences` | [Update a subscription item's preferences.](/api/private/subscription#update-a-subscription-items-preferences) | | `update_subscription_item_bundle` | [Update a subscription item's bundle.](/api/private/subscription#update-a-subscription-items-bundle) | ## Invoices | Tool | Private API endpoint | | --- | --- | | `list_invoices` | [Get list of invoices.](/api/private/invoice#get-list-of-invoices) | | `get_invoice` | [Get invoice by ID.](/api/private/invoice#get-invoice-by-id) | | `schedule_invoice_payment` | [Schedule the next payment for invoice](/api/private/invoice#schedule-the-next-payment-for-invoice) | | `void_invoice` | [Void an invoice.](/api/private/invoice#void-an-invoice) | | `forgive_invoice` | [Forgive an invoice.](/api/private/invoice#forgive-an-invoice) | | `mark_invoice_as_paid` | [Mark an invoice as paid.](/api/private/invoice#mark-an-invoice-as-paid) | ## Orders | Tool | Private API endpoint | | --- | --- | | `list_orders` | [Get list of orders.](/api/private/order#get-list-of-orders) | | `get_order` | [Get order by ID.](/api/private/order#get-order-by-id) | | `update_order` | [Update an order.](/api/private/order#update-an-order) | | `ship_order` | [Ship an order](/api/private/order#ship-an-order) | | `cancel_order` | [Cancel an order](/api/private/order#cancel-an-order) | | `mark_order_as_pending` | [Mark an order as pending](/api/private/order#mark-an-order-as-pending) | | `archive_order` | [Archive an order](/api/private/order#archive-an-order) | | `mark_order_as_future_shipment` | [Mark an order as a future shipment](/api/private/order#mark-an-order-as-a-future-shipment) | | `batch_order_shipping_items` | [Sync order shipping items.](/api/private/order#sync-order-shipping-items) | ## Customers and payments | Tool | Private API endpoint | | --- | --- | | `list_customers` | [Get list of customers](/api/private/customer#get-list-of-customers) | | `get_customer` | [Get customer by ID.](/api/private/customer#get-customer-by-id) | | `get_payment_method` | [Get payment method by ID.](/api/private/payment-method#get-payment-method-by-id) | | `list_transactions` | [Get list of transactions.](/api/private/transaction#get-list-of-transactions) | | `get_transaction` | [Get transaction by ID.](/api/private/transaction#get-transaction-by-id) | | `list_cancellation_reasons` | [Get shop cancellation reasons](/api/private/cancellation#get-shop-cancellation-reasons) | ## Coupons | Tool | Private API endpoint | | --- | --- | | `list_coupons` | [Get list of coupons](/api/private/coupon#get-list-of-coupons) | | `get_coupon` | [Get coupon by ID](/api/private/coupon#get-coupon-by-id) | | `create_coupon` | [Create a coupon](/api/private/coupon#create-a-coupon) | | `update_coupon` | [Update a coupon](/api/private/coupon#update-a-coupon) | | `archive_coupon` | [Archive a coupon](/api/private/coupon#archive-a-coupon) | ## Inventory | Tool | Private API endpoint | | --- | --- | | `list_inventory_items` | [Get list of inventory items](/api/private/inventory#get-list-of-inventory-items) | | `get_inventory_item` | [Get inventory item by ID](/api/private/inventory#get-inventory-item-by-id) | | `create_inventory_item` | [Create a new inventory item](/api/private/inventory#create-a-new-inventory-item) | | `update_inventory_item` | [Update an inventory item](/api/private/inventory#update-an-inventory-item) | | `archive_inventory_item` | [Archive an inventory item](/api/private/inventory#archive-an-inventory-item) | | `list_inventory_groups` | [Get list of inventory groups](/api/private/inventory#get-list-of-inventory-groups) | | `get_inventory_group` | [Get inventory group by ID](/api/private/inventory#get-inventory-group-by-id) | | `create_inventory_group` | [Create a new inventory group](/api/private/inventory#create-a-new-inventory-group) | | `update_inventory_group` | [Update an inventory group](/api/private/inventory#update-an-inventory-group) | ## Metafields | Tool | Private API endpoint | | --- | --- | | `list_metafields` | [Get shop's metafields](/api/private/metafield#get-shops-metafields) | | `create_metafield` | [Create a new metadata](/api/private/metafield#create-a-new-metadata) | | `update_metafield` | [Update the metadata](/api/private/metafield#update-the-metadata) | ## Shop settings | Tool | Private API endpoint | | --- | --- | | `list_tags` | [Get list of tags](/api/private/tag#get-list-of-tags) | | `list_surveys` | [Get list of surveys](/api/private/survey#get-list-of-surveys) | | `get_survey` | [Get survey by ID](/api/private/survey#get-survey-by-id) | | `list_funnels` | [Get list of funnels](/api/private/funnel#get-list-of-funnels) | | `get_funnel` | [Get funnel by ID](/api/private/funnel#get-funnel-by-id) | | `list_options` | [Get list of options](/api/private/option#get-list-of-options) | | `list_countries` | [Get list of countries](/api/private/country#get-list-of-countries) | ## Next steps - [Connect a client](/developer-resources/subbly-mcp/connect-a-client) — add the server to Claude Code, Cursor or VS Code. - [Private API reference](/api/private) — every endpoint, with fields, responses and examples. --- ## Document: Connect a client Get a Private API key, then add the Subbly's MCP server to Claude Code, Cursor or VS Code. URL: /developer-resources/subbly-mcp/connect-a-client # Connect a client The server runs at one URL and speaks the streamable HTTP transport, so any MCP client that supports HTTP servers with a custom header can use it. ```text title="MCP server URL" https://mcp.subbly.dev/mcp ``` Every request must carry your Private API key in the `X-API-KEY` header. The server does not store the key; it creates a fresh Private API client for each tool call with the key that came in. ## Get your API key Private API keys live in the Subbly admin under [Settings › API keys](https://www.subbly.co/admin/settings/api-keys). In the **Private API keys** card, click **Generate API key** to create one for the agent, then copy it with the icon next to it. You can keep several keys, so give the agent its own; if it leaks, delete that key from the same card and generate a new one. A key can read and change everything in your shop, so treat it as a password and keep it out of files you commit. Each client below can read the key from an environment variable. Export it once in your shell: ```bash title="Export the key" export SUBBLY_PRIVATE_API_KEY='PRIVATE-API-KEY' ``` ## Claude Code One command adds the server. Use `--scope user` to make it available in every project, or leave the default to keep it to the current one. ```bash title="Add the server" claude mcp add --transport http subbly https://mcp.subbly.dev/mcp \ --header "X-API-KEY: ${SUBBLY_PRIVATE_API_KEY}" ``` To share the setup with a team without sharing the key, commit a `.mcp.json` in the project root instead. Claude Code expands `${VAR}` from the environment when it reads the file. ```json title=".mcp.json" { "mcpServers": { "subbly": { "type": "http", "url": "https://mcp.subbly.dev/mcp", "headers": { "X-API-KEY": "${SUBBLY_PRIVATE_API_KEY}" } } } } ``` Run `claude mcp list` to check that the server shows as connected, then ask Claude something like "list my published subscription products". ## Cursor Add to Cursor opens Cursor with the server filled in; replace `PRIVATE-API-KEY` with your key before you save. Or add it by hand to `.cursor/mcp.json` in the project, or to `~/.cursor/mcp.json` for every project. Cursor reads environment variables with `${env:VAR}`. ```json title=".cursor/mcp.json" { "mcpServers": { "subbly": { "url": "https://mcp.subbly.dev/mcp", "headers": { "X-API-KEY": "${env:SUBBLY_PRIVATE_API_KEY}" } } } } ``` The server appears under Settings › MCP with its tools listed. Toggle it on if it is off. ## VS Code Install in VS Code opens VS Code with the server filled in; replace `PRIVATE-API-KEY` with your key before you save. Or add it by hand to `.vscode/mcp.json`, or run **MCP: Open User Configuration** to add it for every workspace. An `inputs` entry asks for the key once and stores it in the secret store, so it never lands in the file. ```json title=".vscode/mcp.json" { "inputs": [ { "type": "promptString", "id": "subbly-api-key", "description": "Subbly Private API key", "password": true } ], "servers": { "subbly": { "type": "http", "url": "https://mcp.subbly.dev/mcp", "headers": { "X-API-KEY": "${input:subbly-api-key}" } } } } ``` ## Any other client Most clients take the same JSON shape. Give it the URL and the header: ```json title="Generic config" { "mcpServers": { "subbly": { "url": "https://mcp.subbly.dev/mcp", "headers": { "X-API-KEY": "PRIVATE-API-KEY" } } } } ``` ## Try it in the MCP Inspector The Inspector is a browser UI for calling tools by hand. It is the quickest way to see what a tool returns before you hand it to an agent. ```bash title="Open the Inspector" npx @modelcontextprotocol/inspector ``` In the Inspector pick **Streamable HTTP**, enter the server URL, add an `X-API-KEY` header under Authentication and click **Connect**. The **Tools** tab lists every tool with its input schema. ## What the client sees - **Tools only.** The server offers tools, no prompts or resources. - **Structured output.** Every tool returns its result twice: as JSON text in `content` and as `structuredContent`, typed by the tool's output schema. - **Stateless.** Each request is its own session. Nothing is remembered between calls, so a client can reconnect at any time. - **Errors.** A tool that fails returns an MCP error whose message starts with `Tool execution failed:` followed by the Private API's message. A missing key returns `API key is required`. ## Clients that need OAuth Claude.ai, Claude Desktop and ChatGPT add remote servers as connectors, and a connector cannot send a custom header. Those clients need an OAuth flow, which the server does not offer today. Use one of the clients above. ## Next steps - [Tools](/developer-resources/subbly-mcp/tools) — everything the agent can now do. - [Private API reference](/api/private) — the meaning of every field a tool takes. --- ## Document: Run a funnel Show the offers of a funnel before, during and after checkout with the Subbly.js SDK. URL: /developer-resources/subbly-js-sdk/run-a-funnel # Run a funnel A funnel is a sequence of offers you set up in Subbly. Each offer is a **step**: a headline, a description, and one or more variants or plans, often at a discount. The Subbly.js SDK gives you the next step for a cart; you draw it, and the customer takes it or skips it. ## The three phases A funnel has three phases, and each phase has its own set of methods: - **Pre-purchase** offers come before checkout, while the customer still edits the cart. - **Mid-purchase** offers come during checkout. - **Post-purchase** offers come after the customer pays. They run only after `subbly.checkout.purchase` succeeds, on the thank-you page. ## The loop Every phase works the same way: 1. Fetch the next step for the cart with the phase's `Fetch` method. The response is `{ step }`. 2. If `step` is `null`, the customer has seen every offer. Stop. 3. Show the offer: `step.title`, `step.description`, and the variants and plans in `step.offer.products`. `step.offer.type` says how many the customer may take: `single`, `multiple` or `one_click`. 4. Call the phase's `Accept` method with `step.id` and the `productId` of each product taken, or the phase's `Reject` method with `step.id` to skip. 5. Fetch again. Accepting a pre-purchase or a mid-purchase offer adds the products to the cart and returns a plain cart, not the `subbly.cart` model. `subbly.cart` still holds the old contents, so call `subbly.cart.load()` afterwards. Accepting a post-purchase offer charges a new purchase on the card the customer already used. It returns the invoice, the subscription and the orders it created, not a cart. The pre-purchase phase also has `prePurchaseRefresh`, which clears what the customer accepted or skipped so the sequence starts again from the first step. ## Pre-purchase Run this loop on the cart page, or wherever the customer reviews the cart before checkout. ```js title="Offer before checkout" async function runPrePurchase() { const cartId = subbly.cart.id while (true) { const { step } = await subbly.funnels.prePurchaseFetch(cartId, { expand: ['parent'] }) if (!step) break const productId = await showOffer(step) // resolves with an id, or null if (productId) { await subbly.funnels.prePurchaseAccept(cartId, step.id, { offers: [{ productId }] }) await subbly.cart.load() } else { await subbly.funnels.prePurchaseReject(cartId, step.id) } } } ``` `showOffer` is your own function. It draws the step and resolves with the `id` of the product the customer picked from `step.offer.products`, or `null` when they skip. With `expand: ['parent']`, each product also carries its parent product, so you can show its name and images. ## Mid-purchase Run the same loop during checkout, with the `midPurchase` methods. The payload and the return value are the same as in the pre-purchase phase, so reload `subbly.cart` after each accept here too. ```js title="Offer during checkout" async function runMidPurchase() { const cartId = subbly.cart.id while (true) { const { step } = await subbly.funnels.midPurchaseFetch(cartId) if (!step) break const productId = await showOffer(step) if (productId) { await subbly.funnels.midPurchaseAccept(cartId, step.id, { offers: [{ productId }] }) await subbly.cart.load() } else { await subbly.funnels.midPurchaseReject(cartId, step.id) } } } ``` ## Post-purchase Run this loop after `subbly.checkout.purchase` resolves. Pass the ID of the cart that was paid. The accept needs a signed-in customer, and each accepted offer becomes its own purchase, so the result carries its own `invoice`, `subscription` and `orders`. When the checkout did not set a shipping address or a shipping method, send `shippingAddressId` and `shippingMethodId` in the payload. When the charge asks for 3-D Secure, the call rejects with `payment_requires_action`; finish the verification with the [payment intent methods](/reference/subbly-sdk/payment-intents), then call the accept again with `paymentIntentId`. ```js title="Offer after checkout" async function runPostPurchase(cartId) { while (true) { const { step } = await subbly.funnels.postPurchaseFetch(cartId) if (!step) break const productId = await showOffer(step) if (productId) { const { invoice, subscription, orders } = await subbly.funnels.postPurchaseAccept(cartId, step.id, { offers: [{ productId }] }) showReceipt(invoice, subscription, orders) } else { await subbly.funnels.postPurchaseReject(cartId, step.id) } } } const cartId = subbly.cart.id await subbly.checkout.purchase(cartId) await runPostPurchase(cartId) ``` ## Next steps - [Funnels reference](/reference/subbly-sdk/funnels) — every method, with the fields of a step and of the accept payloads. - [Cart reference](/reference/subbly-sdk/cart) — `subbly.cart.load` and the other cart methods. - [Checkout reference](/reference/subbly-sdk/checkout) — `subbly.checkout.purchase`, which the post-purchase phase follows. --- ## Document: Keep Subbly.js up to date Pin a version of the Subbly.js SDK in production and upgrade on your own schedule. URL: /developer-resources/subbly-js-sdk/keep-it-up-to-date # Keep Subbly.js up to date Pin a version in production, so a new release cannot change your storefront before you test it. - **npm.** `npm install` writes the exact version to your lock file. To upgrade, run `npm install @subbly/sdk@latest`, then test and commit the new lock file. - **CDN.** The bare URL always serves the latest release. Put the version in the URL instead, and change it when you upgrade: ```html title="Pin the CDN version" ``` To see which version a page runs, read [`subbly.VERSION`](/reference/subbly-sdk/setup#version). --- ## Document: Install Subbly.js Add the Subbly.js SDK from npm or load it from a CDN, then start it with Subbly.init. URL: /developer-resources/subbly-js-sdk/install # Install Subbly.js Subbly.js ships as an npm package and as a browser bundle. Pick one, then start the SDK with `Subbly.init`. ## Install from npm ```bash title="Install the package" npm install @subbly/sdk ``` Import it with ES modules. This is the way to use Subbly.js with a modern JavaScript framework, such as React or Vue. The package ships its own TypeScript types. ```js title="Start the SDK" import Subbly from '@subbly/sdk' const subbly = await Subbly.init({ apiKey: 'API_KEY', }) ``` `Subbly.init` is the only supported entry point. It loads the shop before it resolves, so `subbly.shop` is ready when you get the instance. Call it once per page: the SDK keeps one shared HTTP client, and a second call with a different key quietly reuses the first one. ## Load from a CDN The package also ships a browser bundle. Load it on your HTML page and it defines the global `Subbly`. `window.Subbly` is the class, not an instance, so you still call `Subbly.init`. Load it once per page. ```html title="Load the script" ``` ## Next steps - [Configure](/developer-resources/subbly-js-sdk/configure) — every option `Subbly.init` takes. - [Keep it up to date](/developer-resources/subbly-js-sdk/keep-it-up-to-date) — pin a version before you go to production. --- ## Document: Handle errors Read the error code a failed Subbly.js request rejects with. URL: /developer-resources/subbly-js-sdk/handle-errors # Handle errors A failed request rejects with a plain object carrying a `code`, not an `Error`. ```js title="Read the error code" try { await subbly.cart.addItem({ productId: 3301 }) } catch (error) { if (error.code === 'out_of_stock') { // tell the customer } } ``` The full list of codes is on the [Subbly.js SDK reference](/reference/subbly-sdk#errors). --- ## Document: Configure Subbly.js Pass your Storefront API key, a cart to load and a customer to authenticate to Subbly.init. URL: /developer-resources/subbly-js-sdk/configure # Configure Subbly.js `Subbly.init` takes your Storefront API key, and optionally a cart to load and a customer to authenticate. For every field, see [`Subbly.init(config)`](/reference/subbly-sdk/setup#init). ```js title="Start with a cart and a customer" const subbly = await Subbly.init({ apiKey: 'API_KEY', cartId: 'aaea067c-6364-4157-82e9-71b6edfd84a0', loadCart: true, accessToken: 'eyJh...eyJ9', }) ``` With `loadCart: true`, the SDK loads the cart named by `cartId` or by the `subbly_cart_id` cookie, creates a new one when there is none, and copies the cart currency onto the instance. Your Storefront API key is in the Subbly admin, under Settings, Developers. ## Next steps - [Build a checkout](/developer-resources/subbly-js-sdk/build-a-checkout) — from an empty cart to a paid order. - [Setup reference](/reference/subbly-sdk/setup) — `Subbly.init`, `VERSION` and the instance properties. --- ## Document: Build a checkout The short path from an empty cart to a paid order with the Subbly.js SDK. URL: /developer-resources/subbly-js-sdk/build-a-checkout # Build a checkout A short path from an empty cart to a paid order: ```js title="From cart to purchase" await subbly.cart.create() await subbly.cart.addItem({ productId: 3301, quantity: 1 }) await subbly.cart.update({ customer: { email: 'ada@example.com', tosConsent: true }, shippingAddress: { /* … */ }, }) const methods = await subbly.cart.getShippingMethods({ countryId: 826, zip: 'NW1 5JD', }) await subbly.cart.update({ shippingMethodId: methods[0].id }) await subbly.auth.login({ email, password }) await subbly.cart.attachCustomer() const { invoice } = await subbly.checkout.purchase(subbly.cart.id) ``` Each step has its own page: [cart](/reference/subbly-sdk/cart#create), [items](/reference/subbly-sdk/cart#add-item), [shipping](/reference/subbly-sdk/cart#get-shipping-methods), [authentication](/reference/subbly-sdk/auth#login) and [checkout](/reference/subbly-sdk/checkout#purchase). ## Next steps - [Handle errors](/developer-resources/subbly-js-sdk/handle-errors) — what a failed call rejects with. - [Run a funnel](/developer-resources/subbly-js-sdk/run-a-funnel) — add upsell offers before, during and after checkout. - [Storefront API](/api/storefront) — the HTTP API the SDK calls. --- ## Document: Wire the widget into your page Buy links, your own cart button and counter, the widget container, and the content slots in the checkout. URL: /developer-resources/cart-widget/wire-it-into-your-page # Wire the widget into your page The widget reads four kinds of markup on your page: buy links, a cart button and a counter, its own container, and content slots in the checkout. ## Sell from your own links Turn on `interceptProductLinks` and the widget watches every click on the page. When the link points at a Subbly product, bundle or survey, the widget cancels the navigation and calls [`configureItem`](/reference/cart-widget/methods#configure-item) with a quantity of `1`. So a buy button is an ordinary link: ```html title="Buy buttons" Subscribe Build your box Take the quiz Send as a gift ``` The widget reads the nearest `` above the clicked element, ignores the host name, and matches case-insensitively, so a link to the Subbly-hosted checkout works as well as a path on your own site. `add` and `buy` are both accepted. | Link | What it does | | :-- | :-- | | `/checkout/buy/{productId}` | Adds a product. The widget asks for the plan or the variant when the customer still has a choice to make. | | `/checkout/buy/bundle/{bundleId}` | Opens the bundle builder. | | `/checkout/buy/survey/{surveyId}` | Starts the survey flow. | | `?gift=1` | Any value opens the gift view after the item is added. | | `?coupon=CODE` | Holds the coupon and applies it once the cart fits it. | When a link cannot carry a Subbly URL, put the product ID (digits only) in a `data-subbly-product` attribute instead. The widget reads it from the nearest `` above the clicked element; a Subbly URL in the `href` wins when the link carries both. ```html title="A button with no Subbly URL" Subscribe ``` Before the widget has loaded, a stub handler cancels clicks on these links but does not remember them. A click that early is dropped. ## Show your own cart button and item count The widget can draw a floating cart button (`cartButton: true`), but most sites have a cart icon of their own. Point the widget at it with `cartToggleEl`, and at the badge that shows the number of items with `cartCounterEl`. Both take a CSS selector or an element: ```html title="Your own cart button" Cart (0) ``` A click on a toggle match opens or closes the widget, and the default click action is cancelled. The widget replaces the content of every counter match with the number of active items on every cart change. Clicks are caught on `document`, so a header your framework renders later still works. On a framework that routes with `pushState`, update the counter yourself from [`CART_UPDATED`](/reference/cart-widget/events#cart-updated). One more selector needs no setting: when the page holds an `a.subbly-cart`, the widget hides its own floating cart button. Both keys are on [Settings keys](/reference/cart-widget/settings#settings). ## Place the widget The widget mounts in a `div#subbly-cart-widget`. By default it creates the element, appends it to ``, and gives it `position: fixed`, `z-index: 2147483000` and the class `SubblyAppContainer`. The app renders inside, under `.SubblyApp`. To choose where the element sits, put an empty one in your markup and the widget reuses it in place. Any other element with that ID is left alone, and the widget creates its own with a random suffix, such as `subbly-cart-widget-a1b2c`. ```html title="Place the container yourself"
``` The class names inside are hashed at build time and change between releases. Style the widget through its [CSS variables](/reference/cart-widget/theming#css-variables), not its classes. ## Add content to the checkout The checkout and the receipt render empty elements with stable IDs at fixed points. Each collapses while it is empty, so filling one leaves no gap when you do not. Fill them from Checkout JS, which you write in the Subbly admin under Shop settings, Checkout JS. It reaches the page as `subblyCart.shop.checkoutJs`. The widget injects it only in standalone checkout mode (`checkoutOnly: true`): it re-creates inline ` ``` Your Storefront API key is in the Subbly admin, under Settings, Developers. The script tag carries no `data-` attributes; everything goes through the config object. `cart-widget.js` is a small loader that imports the real bundle and its stylesheet, so do not cache it. The files it pulls from `assets/` carry a hash in the name and can be cached for a long time. ## Wait for the widget The script loads asynchronously. The widget fires two events on `window`. Neither carries data. | Event | Meaning | | --- | --- | | `subbly-cart-loaded` | The code is downloaded and `window.SubblyCart`, the class, is set. There is no instance yet. | | `subbly-cart-initialized` | The widget is up and `window.subblyCart`, the instance, is set. **Wait for this one.** | There is no ready callback and no command queue. Guard against the event having already fired: ```js title="Run code once the widget is ready" const onReady = (fn) => { if (window.subblyCart) { fn(window.subblyCart) return } window.addEventListener('subbly-cart-initialized', () => fn(window.subblyCart) ) } onReady((cart) => { cart.open() }) ``` The other pages in this section use this `onReady` helper. The widget also emits its own [`CART_READY`](/reference/cart-widget/events#cart-ready) event, but it fires inside `initialize()`, before the instance is handed out, so no handler of yours can see it. Read the cart from `subblyCart.cart` once `subbly-cart-initialized` has fired instead. ## Next steps - [Configure](/developer-resources/cart-widget/configure) — the keys of `window.subblyConfig`. - [Wire it into your page](/developer-resources/cart-widget/wire-it-into-your-page) — buy links, your own cart button and the container. --- ## Document: Drive the widget from your code Call the SubblyCart.js methods and follow what the customer does through its events. URL: /developer-resources/cart-widget/drive-it-from-your-code # Drive the widget from your code Once the widget is ready, every method is on `window.subblyCart`. Use the `onReady` helper from [Wait for the widget](/developer-resources/cart-widget/install#wait-for-the-widget). ## Call a method ```js title="Open the checkout from your own button" onReady(async (cart) => { await cart.configureItem({ productId: 555 }) cart.open('checkout') }) ``` Every method is on [Methods](/reference/cart-widget/methods). ## Follow what the customer does The widget emits an event for every step the customer takes: the cart changes, the panel opens, an email is captured, a purchase completes. ```js title="Track purchases" onReady(({ events }) => { events.on(events.type.PURCHASE_COMPLETED, (err, data) => { track('purchase', { value: data.purchase.invoice?.total ?? 0, currency: data.cart.currencyCode }) }) }) ``` Every event and its payload is on [Events](/reference/cart-widget/events#on). --- ## Document: Configure the widget The keys of window.subblyConfig, and how to boot the SubblyCart.js widget by hand. URL: /developer-resources/cart-widget/configure # Configure the widget `window.subblyConfig` takes a dozen keys. Five of them cover most first integrations: - `apiKey`: your Storefront API key. The only required key. - `languageCode`: a two-letter language. Without it, the widget takes the `lang` query parameter, then the `lang` attribute of ``, then the browser's language. - `settings`: the page hooks — which links the widget catches, which element opens it, which element shows the item count. See [Wire it into your page](/developer-resources/cart-widget/wire-it-into-your-page). - `init`: set it to `false` to boot the widget yourself. - `checkoutUrl`: a page on your site that owns the checkout. The widget sends the customer there and stands down on that path. ```html title="A typical config" ``` ## Boot the widget by hand With `init: false`, the loader sets no global. Call [`SubblyCart.initialize`](/reference/cart-widget/methods#initialize) yourself and keep the instance it returns: ```js title="Boot the widget by hand" window.subblyConfig = { init: false } window.addEventListener('subbly-cart-loaded', async () => { const cart = await window.SubblyCart.initialize({ apiKey: 'YOUR_STOREFRONT_API_KEY' }) cart.events.on(cart.events.type.CART_UPDATED, () => { console.log('cart changed') }) }) ``` Every key, with its type and default, is on [Configuration keys](/reference/cart-widget/settings#config). [`SubblyCart.initialize(config)`](/reference/cart-widget/methods#initialize) takes the same object, and you can change the URL keys later with [`configure`](/reference/cart-widget/methods#configure).