@kitenzo/core
v0.2.6
Published
Framework-agnostic SDK for [Kitenzo Bundle Builder](https://apps.shopify.com/bundlebuilder). Provides an API client, bundle configuration state machine, types, and utilities for building custom bundle experiences on any JavaScript platform.
Downloads
1,088
Readme
@kitenzo/core
Framework-agnostic SDK for Kitenzo Bundle Builder. Provides an API client, bundle configuration state machine, types, and utilities for building custom bundle experiences on any JavaScript platform.
Use this package directly for vanilla JS, Vue, Svelte, or server-side integrations. For React, use @kitenzo/react which wraps this package with hooks and components.
Install
npm install @kitenzo/coreQuick Start
import { KitenzoClient, createBundleBuilder, addBundleToCart } from '@kitenzo/core';
// 1. Create a client
const client = new KitenzoClient({
apiKey: 'kit_live_...',
});
// 2. Find your bundle
const bundles = await client.listBundles();
const bundleId = bundles[0].id;
// 3. Fetch bundle with full product data
const bundle = await client.getBundle(bundleId);
// 4. Build a bundle configuration
const builder = createBundleBuilder(bundle);
builder.subscribe(() => {
const state = builder.getState();
console.log('Selections:', state.selections);
console.log('Section:', state.currentSectionIndex);
console.log('Valid:', state.isComplete);
console.log('Errors:', state.errors);
});
const section = bundle.sections[0];
builder.addItem(section.id, section.products[0].variants[0].id);
// 5. Submit and add to cart
const selections = builder.getState().selections;
const result = await client.submitBundle(bundle, selections);
await addBundleToCart(result, {
addLines: (lines) => cart.linesAdd(lines),
getAttributes: () => cart.attributes ?? [],
setAttributes: (attrs) => cart.cartAttributesUpdate(attrs),
}, { bundle, selections });Full Bundle Embed
Use createBundleEmbed to render the full admin-configured bundle experience — the same UI merchants see on their storefront — inside any container element. No custom UI code needed.
import { createBundleEmbed } from '@kitenzo/core';
const embed = createBundleEmbed(document.getElementById('bundle-root')!, {
bundleId: 42,
apiKey: 'kit_live_...',
shopDomain: 'my-store.myshopify.com',
onAddToCart: ({ lines, bundleContent }) => {
// lines are Storefront API CartLineInput objects
// Add them to your cart via cartLinesAdd
cart.linesAdd(lines);
// For native bundles, set the _bundles cart attribute so the
// cart transform extension can apply the discount at checkout.
if (bundleContent) {
const existingBundles = JSON.parse(
cart.attributes?.find(a => a.key === '_bundles')?.value ?? '{}'
);
existingBundles[bundleContent.configuredBundleId] = bundleContent;
cart.cartAttributesUpdate([
...(cart.attributes ?? []).filter(a => a.key !== '_bundles'),
{ key: '_bundles', value: JSON.stringify(existingBundles) },
]);
}
},
onError: (error) => console.error('Embed error:', error),
});
// Clean up when done:
embed.destroy();The embed automatically loads the storefront script, fetches shop settings (currency, moneyFormat), and intercepts add-to-cart events — normalizing the payload to Storefront API format.
onAddToCart Payload
The onAddToCart callback receives an EmbedCartPayload with:
| Field | Type | Description |
|-------|------|-------------|
| lines | CartLine[] | Cart lines ready for cartLinesAdd (Storefront API format). |
| bundleContent | BundleContentPayload \| undefined | Bundle metadata for native bundles. When present, write it to the _bundles cart attribute so the cart transform can apply discounts. |
BundleContentPayload contains:
| Field | Type | Description |
|-------|------|-------------|
| id | number | Bundle ID. |
| configuredBundleId | number | Unique ID for this bundle configuration. Use as key in the _bundles attribute. |
| title | string | Bundle title. |
| discount | string | Encrypted discount string (consumed by the cart transform). |
| image | string \| null | Bundle image URL. |
| items | Array<{ variantId, count }> | Products in the bundle. |
Each cart line in lines includes a _bundle_data attribute containing configuredBundleId#parentVariantId#uniqueId. This is how the cart transform matches individual line items to their bundle. When using the embed, these attributes are set automatically. If you construct cart lines manually (e.g. re-adding items from a saved cart), every line item that belongs to a native bundle must have the _bundle_data attribute — without it, the cart transform cannot identify the line as part of a bundle.
Important: Both
_bundle_data(on each line item) and_bundles(on the cart) are required for the cart transform to apply discounts. The embed does not write the_bundlescart attribute itself — youronAddToCarthandler must do it.
Bundle Type Differences
All necessary line item attributes (_bundle_data, _bundle_id, _bundle_price, subscription fields, etc.) are included automatically in lines — pass them through to cartLinesAdd as-is.
Native bundles: bundleContent is present. You must write it to the _bundles cart attribute (see example above) — the cart transform reads it to apply the discount at checkout.
Single-product and multiple-products bundles: bundleContent is undefined. No cart attributes needed — the discount is already baked into the variant price. Just call cartLinesAdd(lines).
Note: Only one embed per page is supported.
Setup
1. Enable the Headless API
In the Kitenzo admin panel, go to Settings > Headless to enable the headless API.
2. Create an API Key
On the same page, click Create API key. Keys are prefixed kit_live_ (production) or kit_test_ (development).
API key security:
kit_live_andkit_test_keys are publishable keys, safe to use in client-side code (similar to Stripe publishable keys). They can only read bundle data and submit configurations — they cannot modify bundles or access admin functionality. Do not confuse these with server-side secrets.
3. Configure Allowed Origins (CORS)
If you're calling the API from a browser, you must add your domain to the API key's allowed origins list. Go to Settings > Headless, click your API key, and add each origin (e.g. https://your-store.com). Requests from unlisted origins will be blocked by CORS.
API Client
const client = new KitenzoClient({
apiKey: 'kit_live_...', // Required — your publishable API key
apiVersion: 'v1', // Optional (default: 'v1')
baseUrl: 'https://...', // Optional — override the API base URL
});When no baseUrl is provided, the SDK uses https://live.bb.eight-cdn.com/api/headless/{apiVersion}. Use baseUrl for local development (e.g. /api/headless/v1 behind a Vite proxy) or custom proxy setups — when provided, apiVersion is ignored.
Methods
| Method | Returns | Description |
|--------|---------|-------------|
| listBundles() | Bundle[] | Published bundles (lightweight, no product data). |
| getBundle(id) | BundleDetail | Bundle with sections, products, and variants — everything needed to render a builder. |
| submitBundle(bundle, selections, options?) | SubmitBundleResult | Submit builder selections — handles product mapping automatically. Accepts an optional { countryCode } for market-aware pricing. |
| getSettings() | ShopSettings | Shop settings (currency, moneyFormat, features). Auto-fetched by KitenzoProvider in React. |
Variant IDs accept GID format (gid://shopify/ProductVariant/123) or plain numeric strings ("123").
API Endpoints
All endpoints require a Bearer token (Authorization: Bearer kit_live_...).
| Method | Endpoint | Description |
|--------|----------|-------------|
| GET | /api/headless/v1/bundles | List published bundles |
| GET | /api/headless/v1/bundles/:id | Bundle detail |
| GET | /api/headless/v1/bundles/:id/products | Product and variant data |
| POST | /api/headless/v1/bundles/:id/configure | Validate and create configuration |
| POST | /api/headless/v1/bundles/:id/price | Calculate price |
| GET | /api/headless/v1/settings | Shop settings |
Rate limit: 100 requests/minute per API key (configurable).
Bundle Builder State Machine
createBundleBuilder(bundle) returns a state machine for step-by-step bundle configuration. It follows the subscribe/getState pattern, making it compatible with React's useSyncExternalStore, Svelte stores, or plain callbacks.
import { createBundleBuilder } from '@kitenzo/core';
const builder = createBundleBuilder(bundleDetail);
// Subscribe to state changes
const unsubscribe = builder.subscribe(() => {
const state = builder.getState();
renderUI(state);
});
// Mutations
builder.addItem(sectionId, variantId, quantity?);
builder.removeItem(sectionId, variantId);
builder.updateQuantity(sectionId, variantId, newQuantity);
builder.reset();
// Navigation
builder.nextSection();
builder.prevSection();
builder.goToSection(index);
// Queries
builder.getSectionQuantity(sectionId); // numberSnapshot Shape
builder.getState() returns a BundleBuilderSnapshot:
{
selections: SectionSelections; // Record<sectionId, BundleSelection[]>
currentSectionIndex: number;
currentSection: BundleSection | null;
isSectionValid: boolean;
isValid: boolean;
isComplete: boolean; // all sections valid + all rules pass
allItems: BundleSelection[]; // flat list of all selections
errors: ValidationError[]; // limit rule / required product violations (empty when valid)
}The snapshot is immutable — a new object reference is created on each state change (for efficient React reconciliation or shallow comparison).
Pricing
calculatePrice(bundle, selections, currency?)
Calculate bundle pricing from variant prices and discount rules. Handles flat discounts (percentage, fixed, price) and tiered discounts with all operation types.
The currency parameter is a label included in the response — pass the shop's currency from getSettings() so the returned PriceResponse.currency is accurate. If omitted, the field is empty.
import { calculatePrice } from '@kitenzo/core';
const settings = await client.getSettings();
const pricing = calculatePrice(bundle, builder.getState().selections, settings.currency);
console.log(pricing.originalPrice); // "45.00"
console.log(pricing.discountedPrice); // "40.50"
console.log(pricing.discountType); // "percentage"
console.log(pricing.discountValue); // "10.00"
console.log(pricing.currency); // "CZK"React users: Use the
useBundlePricehook instead — it returns pre-formatted prices (e.g."$45.00") using the shop's moneyFormat automatically.
formatMoney(amount, moneyFormat)
Format an amount using a Shopify money format string. Supports all standard Shopify placeholders.
import { formatMoney } from '@kitenzo/core';
formatMoney(45, '${{amount}}'); // "$45.00"
formatMoney(1234.5, '€{{amount_with_comma_separator}}'); // "€1.234,50"
formatMoney('99.99', '{{amount}} kr'); // "99.99 kr"Cart Utilities
addBundleToCart(result, cart, context?)
Add a configured bundle to a Shopify cart in one call. Builds cart lines, merges the _bundles cart attribute, and calls both mutations — so the cart transform can always apply the discount. Pass { bundle, selections } as the third argument for native bundles, which is required to produce the correct _bundle_data line attributes and _bundles cart attribute.
import { addBundleToCart } from '@kitenzo/core';
const result = await client.submitBundle(bundle, selections);
await addBundleToCart(result, {
addLines: (lines) => cart.linesAdd(lines),
getAttributes: () => cart.attributes ?? [],
setAttributes: (attrs) => cart.cartAttributesUpdate(attrs),
}, { bundle, selections });buildCartPayload(result, existingAttributes?, context?)
Lower-level alternative when you need direct control. Returns { lines, attributes } — you must call both cartLinesAdd and cartAttributesUpdate yourself. Pass { bundle, selections } as the third argument for native bundles.
import { buildCartPayload } from '@kitenzo/core';
const result = await client.submitBundle(bundle, selections);
const { lines, attributes } = buildCartPayload(result, cart.attributes, { bundle, selections });
cart.linesAdd(lines);
cart.cartAttributesUpdate(attributes);buildCartLines(result)
Build cart lines only (without attribute merging). Useful when you manage cart attributes separately.
import { buildCartLines } from '@kitenzo/core';
const result = await client.submitBundle(bundle, selections);
const lines = buildCartLines(result);
cart.linesAdd(lines);Section Utilities
computeSectionQuantity(selections, sectionId)
Returns the total quantity selected in a section.
isSectionMet(selections, section)
Returns whether a section's minimum/maximum quantity constraints are satisfied.
TypeScript Unions
The package exports typed union types for fields that accept a fixed set of values, providing autocomplete and compile-time safety:
| Type | Values |
|------|--------|
| BundleType | 'single-product' | 'multiple-products' | 'native' |
| DiscountType | 'percentage' | 'fixed' | 'price' |
| DiscountMode | 'flat' | 'tiered' |
| ComparisonOperator | 'gt' | 'gte' | 'lt' | 'lte' | 'eq' |
| TierConditionType | 'total_products' | 'bulk_buy' | 'total_price' |
| LimitRuleType | 'bundle-price' | 'total-number-of-products' | ... |
Variant Availability
The BundleVariant.available field is a boolean indicating whether the variant can be selected. When available is false, the variant should be shown as disabled. The API does not currently provide a specific reason for unavailability — it may be out of stock, restricted by inventory policy, or excluded by bundle rules. Display unavailable variants as disabled to let customers see the full selection, and use the inventoryQuantity field (when present) to distinguish out-of-stock variants from other restrictions.
Troubleshooting
CORS errors -- Your origin is not in the API key's allowed origins list. Go to Settings > Headless, click your API key, and add your domain.
401/403 -- Verify the API key is correct, active, and headless is enabled.
Cart lines added but no discount -- Make sure cart attributes from buildCartPayload() are applied via cartAttributesUpdate(). If using the embed (createBundleEmbed), check that your onAddToCart handler writes bundleContent to the _bundles cart attribute — see the embed section above.
"Unable to add kit to cart" in console (embed mode) -- This happened in older versions when the embed tried to call Shopify Liquid endpoints (/cart.js, /cart/update.js) that don't exist on headless storefronts. Update to the latest version — the embed now skips these calls when onAddToCart handles the event.
