npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

@bacano/sdk

v1.2.2

Published

TypeScript SDK for building websites on the Bacano ERP

Readme

@bacano/sdk

TypeScript SDK for building e-commerce websites on the Bacano ERP. Zero runtime dependencies, optional React hooks, and full type safety.

Buyer credentials are owned by each storefront's Clerk application. Read AUTHENTICATION.md before implementing registration, sign-in, customer profiles, authenticated checkout, or order history. The guide ships inside the npm package.

Installation

npm install @bacano/sdk
pnpm add @bacano/sdk

React hooks are available from @bacano/sdk/react and require react >= 18 as a peer dependency. The core client has no runtime dependencies and works without React.

Gestión de Content y formularios para agentes

Un agente autorizado puede consultar y mantener tipos, componentes, grupos, listas de opciones, entradas y formularios con el entrypoint administrativo @bacano/sdk/content-management. Cada capacidad exige un scope explícito. El acceso se genera temporalmente desde Bacano, está limitado a una empresa y nunca debe incluirse en el storefront:

import { createContentManagementClient } from "@bacano/sdk/content-management";

Lee CONTENT_TYPE_MANAGEMENT.md antes de usarlo. La guía contiene el flujo de autorización, contratos de campos, entradas y media, estados, referencias, cambios destructivos y límites de seguridad. Con el scope content_media:upload, un agente puede buscar imágenes existentes y cargar JPEG, PNG o WebP mediante el contrato oficial; Bacano valida los bytes, normaliza a WebP y registra el resultado en la biblioteca de la empresa.

Auditoría del storefront

El SDK incluye la skill oficial bacano-web. Instalarla al iniciar un storefront y ejecutarla antes de publicar o después de un cambio relevante del SDK, hosting, autenticación, pagos o medición:

pnpm add @bacano/sdk
pnpm exec bacano-web install --agent all
pnpm exec bacano-web check

Después invocarla desde el agente elegido:

  • Codex: $bacano-web
  • Claude Code: /bacano-web
  • Antigravity: solicitar una auditoría Bacano Web o elegirla desde /skills

install solo instala una copia canónica de la skill y sus enlaces de descubrimiento. check confirma que está vigente. Ninguno ejecuta la auditoría, modifica el storefront ni despliega; esa revisión comienza únicamente cuando el usuario o agente invoca la skill.

Implementación desde Figma

El SDK también incluye bacano-figma-web, la skill oficial para convertir un diseño Figma en una web informativa, tienda o sitio híbrido que conserve el cascarón, Static First y contratos públicos de Bacano:

pnpm exec bacano-figma-web install --agent all
pnpm exec bacano-figma-web check
pnpm exec bacano-figma-web doctor

Después invocarla como $bacano-figma-web en Codex, /bacano-figma-web en Claude Code o solicitar “usar Bacano Figma Web” en Antigravity. La skill guía la conexión al MCP remoto oficial, MCP local oficial de Figma Desktop, Desktop Bridge comunitario o REST autorizado. Prefiere siempre el MCP oficial y nunca guarda ni imprime tokens.

doctor comprueba instalación, endpoint local, puertos del bridge y solamente la presencia del token REST. El OAuth del MCP remoto debe confirmarse dentro del agente con las herramientas de Figma. La extracción se conserva como snapshot normalizado. Cuando el equipo de Content y desarrollo trabajan en tiempos distintos, la skill genera un blueprint de entrega y permite fixtures locales con el mismo contrato de los componentes. Con autorización temporal y scopes explícitos, puede crear o ajustar tipos, componentes, grupos, listas y entradas mediante el entrypoint administrativo oficial. Las entradas nacen en borrador; publicar exige permiso y aprobación humana. Un fixture activo en producción bloquea la aprobación del sitio.

El snapshot puede validarse con:

pnpm exec bacano-figma-web validate-snapshot .bacano/designs/<proyecto>/manifest.json

Las instrucciones completas viajan dentro de skills/bacano-figma-web/references/; no es necesario copiar documentación ni crear integraciones particulares en cada storefront.

Every storefront must follow STOREFRONT_MARKETING.md when instrumenting analytics, advertising signals, contact actions, ecommerce events, attribution, or preview behavior. It is the canonical implementation contract for storefront agents and ships inside the npm package.

Commercial forms and newsletter subscriptions must follow STOREFRONT_LEADS.md. Storefronts own the form UI; Bacano owns consent evidence, identity resolution, attribution and idempotent lead capture through the SDK.

Website forms defined and published from Bacano use the additive contract in STOREFRONT_FORMS.md. Storefronts can mount the official BacanoForm renderer or consume the same typed definition through useBacanoForm() without GraphQL, HTML stored in the database or a rebuild after each publication.

Every visible region sourced from a Bacano ContentEntry must expose the entry-level marker defined in STOREFRONT_CONTENT_LOCATOR.md. Use getContentEntryLocatorAttributes(entry) on the outer section or card so an authorized internal browser tool can show the entry name and open its Bacano editor. This contract identifies entries only; it does not expose field-level editing or grant backoffice access.

Static and hybrid stores must also follow STOREFRONT_PERFORMANCE.md. It defines the official build bootstrap, live-state refresh, responsive media and request ownership rules. It also documents the shared public-config bootstrap, deferred marketing scripts and route-scoped authentication required for fast public catalog pages.

Storefronts that can be hidden temporarily must mount the runtime gate defined in STOREFRONT_CONSTRUCTION.md. It supports static SiteGround exports without rebuilding, responsive Bacano media and signed temporary preview links.

Product detail pages and technical catalog filters must follow STOREFRONT_PRODUCT_SPECIFICATIONS.md.

Quantity-based web pricing is documented in STOREFRONT_VOLUME_PRICING.md. Bacano owns the authoritative basket calculation; storefronts only render the effective prices and tier progress returned by the cart API. It documents the additive specification contract, static publication and the strict separation from commercial attributes and variants.

Instalación y alcance de la skill

El paquete incluye skills/bacano-web, una skill portátil para que un agente audite el repositorio, build y dominio antes de producción. Primero confirma si el proyecto es una web informativa o una tienda virtual y aplica un checklist distinto; no exige carrito, checkout o pagos a un sitio informativo.

Ubicaciones del proyecto después de instalar la skill:

| Agente | Ruta de descubrimiento | Invocación | | ----------- | --------------------------- | ----------------------------- | | Codex | .codex/skills/bacano-web | $bacano-web | | Claude Code | .claude/skills/bacano-web | /bacano-web | | Antigravity | .agents/skills/bacano-web | Solicitud natural o /skills |

Para instalarla solo para un agente puede indicarse explícitamente:

pnpm exec bacano-web install --agent antigravity

También puede ejecutarse sin --agent; en una terminal interactiva pregunta si se usará Antigravity, Claude Code, Codex o todos. Mantiene una sola copia física en .agents/skills/bacano-web y crea enlaces de descubrimiento para los otros agentes seleccionados.

pnpm exec bacano-web check

El paquete incluye un aviso postinstall como ayuda adicional, aunque algunos gestores pueden ocultar su salida. El aviso nunca modifica el repositorio y puede desactivarse en entornos controlados con BACANO_SDK_HIDE_SKILL_NOTICE=1.

La skill realiza auditorías de solo lectura por defecto. Primero detecta las capacidades realmente activas y separa gates de publicación (P0), calidad profesional (P1) y mejoras (P2), para no bloquear una web por funciones apagadas. Sus informes usan estados claros con emojis: ✅ APROBADO, ⚠️ ADVERTENCIA, ❌ FALLÓ, ➖ NO APLICA y 🧪 NO VERIFICADO.

Además audita explícitamente la arquitectura Static First y la vista previa oficial. El informe incluye una sección “Arquitectura SDK y tráfico runtime” con instancias del cliente/provider, operaciones GraphQL observadas, duplicados, datos estáticos, actualizaciones vivas y evidencia de noindex, sitemap y cero marketing en preview. Cuando no puede capturar un trace real, lo declara como 🧪 NO VERIFICADO en lugar de asumirlo desde el código.

Entry points

| Import | Contents | | --------------------------- | ----------------------------------------------------- | | @bacano/sdk | Core client, catalog, cart, checkout, orders, profile | | @bacano/sdk/react | BacanoProvider and React hooks | | @bacano/sdk/react/preview | Optional live preview controller and toolbar | | @bacano/sdk/graphql | Raw GraphQL documents, for advanced use |


Live Content Preview

Static storefronts can show current published Bacano Content without rebuilding their public pages. Import the isolated preview entrypoint only from the storefront preview route:

import { BacanoPreviewShell } from "@bacano/sdk/react/preview";

The public storefront remains static. Preview V1 never exposes drafts and automatically disables the SDK marketing runtime inside its tree. See STOREFRONT_PREVIEW.md, the canonical contract for provider hierarchy, responsibilities, security, agent checklist, and the validated reference implementation.


Quick Start

1. Create a Website in Bacano

Before using the SDK, an active website must exist in Bacano. Create it from the Websites section of the Bacano platform, or ask your Bacano contact to set one up.

Required fields:

  • Name: display name.
  • Slug: SDK identifier, for example my-store.
  • Branch / chain: required for stock availability and cart creation.
  • Domain: optional custom domain, without https://.
  • Advanced config: optional JSON object consumed by the external web.

The branch matters: checkAvailability() and checkout/cart sync actions need a chain_id. A website without a branch can be resolved, but shopping actions will fail with NO_CHAIN.

2. Initialize the Client

import { createBacanoClient } from "@bacano/sdk";

const client = createBacanoClient({
  apiUrl: "https://api.bacanoerp.com",
  websiteSlug: "my-store",
});

// Must be called once — resolves the website context.
await client.init();

Next.js example:

NEXT_PUBLIC_BACANO_API_URL=https://api.bacanoerp.com
NEXT_PUBLIC_BACANO_WEBSITE_SLUG=my-store
const client = createBacanoClient({
  apiUrl: process.env.NEXT_PUBLIC_BACANO_API_URL!,
  websiteSlug: process.env.NEXT_PUBLIC_BACANO_WEBSITE_SLUG!,
  tokenStorage: "memory",
});

await client.init();

3. Browse Products (No Auth Required)

catalog.getProducts() only returns products that are active, enabled for the website branch, have a valid CLIENT price and have available inventory in one of the inventory sources configured for the website. Websites without custom sources continue using the branch's default warehouse.

// List products
const products = await client.catalog.getProducts({ limit: 10 });

// Search across product names, descriptions, brands, and attributes
// Results are ranked by relevance when no explicit sortBy is set
const results = await client.catalog.getProducts({ search: "cotton" });

// Search suggestions (lightweight autocomplete)
const suggestions = await client.catalog.getSearchSuggestions({
  query: "shi",
  limit: 5,
});
// → [{ type: 'product', text: 'Shirt', id: '...' }, { type: 'brand', text: 'Shimano', id: '...' }]

// Filter by category
const clothing = await client.catalog.getProducts({ categoryName: "CLOTHES" });

// Filter by price range
const affordable = await client.catalog.getProducts({
  minPrice: 10,
  maxPrice: 50,
});

// In-stock only. This is already the website default, kept for UI clarity.
const available = await client.catalog.getProducts({ inStock: true });

// Filter by color
const redItems = await client.catalog.getProducts({
  colors: ["Red", "Burgundy"],
});

// Filter by reusable content/catalog tags
const babyGirl = await client.catalog.getProducts({
  tags: ["bebe-nina"],
});

// Filter by dynamic attributes (category-specific)
const mediumCotton = await client.catalog.getProducts({
  categoryName: "CLOTHES",
  attributes: { Size: ["M"], Material: ["Cotton"] },
});

// Sort products
const newest = await client.catalog.getProducts({ sortBy: "newest" });
const cheapest = await client.catalog.getProducts({ sortBy: "price_asc" });

// Combine multiple filters
const filtered = await client.catalog.getProducts({
  categoryName: "CLOTHES",
  colors: ["Red"],
  minPrice: 20,
  inStock: true,
  sortBy: "price_asc",
  limit: 20,
});

// Single product
const product = await client.catalog.getProduct("product-uuid");

// Static builds: fetch the complete sellable catalog and its first renderable
// catalog page. This is the canonical source for initial HTML, routes,
// metadata, sitemap and product page data.
const bootstrap = await client.catalog.getStaticCatalogBootstrap({
  categoryListKey: "categorias-web",
  attributeListKey: "atributos",
  pageSize: 100,
  initialPageSize: 24,
  maxProducts: 10_000,
  sortBy: "name_asc",
});

const productByRoute = new Map(
  bootstrap.products.map((item) => [item.slug || item.id, item]),
);

// Cards and filters use product.variants. Detail selectors use the official
// helper so sold-out sizes remain visible and older snapshots stay compatible.
const detailVariants = getProductDetailVariants(product!);

// Do not call getProductBySlug() once per route during the same build.
// Reuse productByRoute from the snapshot instead.
// Render bootstrap.initialPage in the initial catalog HTML instead of
// publishing an empty skeleton that waits for a browser request.

// After hydration, refresh only volatile state for visible variants.
const liveState = await client.catalog.getProductLiveState(
  detailVariants.map((variant) => variant.id),
);
// Each item includes maxPurchasableQuantity for product and cart controls.

// Responsive storage candidates are provided by the SDK.
const srcSet = formatMediaSrcSet(product.media[0]?.responsiveSources ?? []);

// Editorial ContentMedia uses heroUrl plus intrinsic-width-aware
// responsiveSources. See STOREFRONT_PERFORMANCE.md for the <picture> contract.

// Automatic recommendations for the product detail. This is an independent,
// lightweight request; Bacano resolves ranking, availability and public price.
const recommendations = await client.catalog.getProductRecommendations({
  productId: product!.id,
  limit: 16,
});

// Manually curated related products configured in Bacano.
// Use these for recommendations, comparisons or complementary products.
// Their public name always comes from the related product itself. Each item is
// scoped to the current website branch and only includes active catalog items.
for (const related of product?.relatedProducts ?? []) {
  console.log(related.productId);
  console.log(related.slug);
  console.log(related.name);
  console.log(related.sku);
  console.log(related.media[0]?.url);
}

// Customer-facing alternatives configured as one central option group.
// The current product is included in members. Use displayLabel for the
// selector and slug for navigation; keep relatedProducts for recommendations.
if (product?.optionGroup) {
  console.log(product.optionGroup.title);
  for (const option of product.optionGroup.members) {
    console.log(option.displayLabel ?? option.name, option.slug);
  }
}

// Categories and brands
const categories = await client.catalog.getCategories();
const brands = await client.catalog.getBrands();

// Product filters / facets (for building filter UIs)
const filters = await client.catalog.getProductFilters();
// → { categories, brands, colors, priceRange: { min, max }, attributes: [] }

// Category-aware facets (includes dynamic attributes for that category)
const clothingFilters = await client.catalog.getProductFilters({
  categoryName: "CLOTHES",
});
// → { ..., attributes: [{ id, name: 'Size', values: [{ value: 'S', count: 5 }, ...] }] }

// Public attribute list configured in Bacano.
// Without filters it returns the full published list for the company.
const attributeList = await client.catalog.getAttributeList({
  key: "filtros-catalogo",
});

// Without category/search filters, but still limited to real available products.
const applicableAttributeList = await client.catalog.getAttributeList({
  key: "filtros-catalogo",
  onlyApplicable: true,
});

// With filters, Bacano resolves only attributes and values that apply to
// active, priced and in-stock products in the current website context.
const contextualAttributeList = await client.catalog.getAttributeList({
  key: "filtros-catalogo",
  onlyApplicable: true,
  filters: {
    categoryName: "CLOTHES",
    tags: ["bebe-nina"],
    minPrice: 10000,
    maxPrice: 120000,
  },
});

// Stock availability
const availability = await client.catalog.checkAvailability([
  "variant-uuid-1",
  "variant-uuid-2",
]);
// → [{ productVariantId, inStock: true, label: "In Stock",
//      maxPurchasableQuantity: 8 }]

maxPurchasableQuantity is Bacano's current public purchase limit after active reservations, capped by the maximum accepted by public cart and checkout actions. When a website uses multiple warehouses, this is the maximum that one source can fulfill; Bacano intentionally does not add stock from different warehouses. It is not a raw warehouse inventory field. Use it to limit quantity selectors and the total units of that variant already present in the local cart. Refresh the variants in one batch when opening the cart or checkout; do not request availability after every + click. This value can change after it is read, so Bacano still revalidates stock when checkout starts and finishes.

At checkout, each order line is reserved in the first configured warehouse, by priority, that can fulfill that complete line. Different lines may use different warehouses, but Bacano does not split a single line between sources. The storefront does not implement this allocation or send warehouse IDs.

4. Local Cart And Checkout Start

The website checkout remains guest-first. Buyer credentials and email verification are owned by the website's Clerk application; passwords never pass through this SDK or Bacano.

While a visitor is only browsing and adding products, keep the cart in the web app local state. Do not create a Bacano order yet.

When the visitor starts checkout and provides an identifier allowed by the company policy, send the local cart to Bacano in one operation:

const checkoutCart = await client.cart.startCheckout({
  fulfillmentFamily: "SHIPPING", // use 'PICKUP' for store pickup
  contact: {
    name: "Jane Doe",
    email: "[email protected]",
    phone: "+573001112233",
    address: {
      city: "Bucaramanga",
      line1: "Cra 1 # 2-3",
      notes: "Porteria azul",
    },
  },
  items: [
    { productVariantId: "variant-uuid-1", quantity: 2 },
    { productVariantId: "variant-uuid-2", quantity: 1 },
  ],
});

console.log(checkoutCart.id); // WEB + PENDING + UNPAID checkout order id
console.log(checkoutCart.cartToken); // opaque cart token stored by the SDK

This creates or recovers a sales_channel=WEB, PENDING + UNPAID order with orders.website_contact_snapshot and the received items. From this point on, the checkout can be recovered in Bacano as an abandoned checkout if the customer does not pay or confirm. Bacano also stores orders.sales_channel = WEB for these started web checkouts.

fulfillmentFamily tells Bacano how the physical products will be fulfilled: SHIPPING creates/synchronizes the normal delivery flow and PICKUP creates/synchronizes the pickup flow. Bacano stores this in orders.fulfillment_family.

After checkout has started, cart edits go through Bacano and are guarded by order_id + cart_token:

// Set variant quantity after startCheckout has created a Bacano cart session.
await client.cart.setItem("variant-uuid", 2);

// Compatibility alias for adding one variant quantity.
await client.cart.addItem("variant-uuid", 2);

// Update by line id. Internally resolves the variant and calls setItem.
await client.cart.updateItem("cart-item-uuid", 5);

// Remove by line id.
await client.cart.removeItem("cart-item-uuid");

// Discard the complete private cart and clear its SDK session.
await client.cart.clear();

// View current cart from the stored guest session.
const currentCart = await client.cart.get();
console.log(currentCart?.items); // CartItem[]
console.log(currentCart?.totals); // { subtotal, tax, discount, total }

Cart rules:

  • cart.getOrCreate() may create a private server cart without contact. This is the supported path when the storefront needs authoritative stock, totals or volume pricing before checkout.
  • Creating this browsing cart does not create delivery logistics. Delivery is synchronized only by cart.startCheckout() after the customer selects the fulfillment family.
  • Email, phone or a complete document pair can start a Bacano sales_channel=WEB, PENDING + UNPAID checkout.
  • sales_channel=WEB, PENDING + UNPAID without order_number means checkout started or abandoned.
  • These started checkouts are tracked by orders.website_cart_status and should be managed from Bacano's abandoned-cart view, not mixed into the normal sales list.
  • cart.clear() marks the private cart as discarded, preserves its audit history and removes only @bacano/sdk:cart:{websiteSlug}. It never deletes storage owned by the storefront.
  • Removing the final line also discards the server cart and clears its private SDK session automatically.
  • The web must collect fulfillmentFamily before starting checkout: SHIPPING for delivery or PICKUP for store pickup.
  • The cart token is opaque. Bacano stores only its SHA-256 hash.
  • Cart edits go through Hasura Actions, not direct public table mutations.
  • Filling checkout does not create a formal client yet; it stores only website_contact_snapshot on the order.
  • A pending unpaid web checkout does not reserve stock by itself, deduct inventory, create payments or post accounting. Creating an external gateway session reserves stock until the attempt finishes or expires.

5. Guest Checkout Confirmation

Before confirming a shipping checkout, ask Bacano for location, shipping, payment and coupon options:

const deliveryOptions = await client.checkout.getDeliveryOptions();
const locations = await client.checkout.getLocationOptions();
const shippingPolicy = await client.checkout.getShippingPolicy();
const shippingProgress = await client.checkout.getFreeShippingProgress({
  items: cartItems.map((item) => ({
    productVariantId: item.productVariantId,
    quantity: item.quantity,
  })),
});
const quotes = await client.checkout.getShippingQuotes({
  address,
  items: cartItems.map((item) => ({
    productVariantId: item.productVariantId,
    quantity: item.quantity,
  })),
});
const paymentOptions = await client.checkout.getPaymentOptions({
  fulfillmentFamily: "SHIPPING",
});
await client.checkout.applyCoupon({ code: "BEBE10" });

getDeliveryOptions can run before contact, cart or address exists. Use it to show options such as SHIPPING and PICKUP. Call getShippingQuotes only when the selected option is SHIPPING and the customer has provided an address.

Use getShippingPolicy() for general storefront messages before a cart exists. Use getFreeShippingProgress({ items }) to show cart progress before collecting contact or address data. Bacano resolves current prices, stock, website and shipping policy; the storefront must format the structured values instead of hardcoding or recalculating them.

For the policy to be enabled, the website must have delivery enabled, a positive free-shipping threshold, and at least one active shipping service enabled for that website whose collection mode charges shipping with the sale (with_sale). Shipping collected directly by the carrier is intentionally not overridden by this promotion. These prerequisites are configured in Bacano; the storefront must not infer or reproduce them.

getShippingPolicy() is cached for five minutes per SDK client. This keeps catalog navigation from repeatedly requesting the same configuration. After changing the policy in Bacano, recreate the client or reload the storefront to validate it immediately. getFreeShippingProgress() always validates the submitted variants and quantities against Bacano's current prices and returns enabled: false with nullable amounts when the policy does not apply.

getShippingQuotes returns the final shipping price for an address. Before a guest checkout exists, pass items. Once an active cart session exists, the SDK intentionally prefers the persisted cart and ignores loose items so coupons and server-side totals remain authoritative.

Each quote exposes baseAmount and the final amount. When Bacano's website-level free-shipping policy applies, amount is zero and freeShippingApplied is true. Use freeShippingRemaining to show progress before the threshold is reached. Storefronts must not hardcode or recalculate the threshold; Bacano revalidates it during checkout.submit.

Use getPaymentOptions({ fulfillmentFamily }) to render payment modes valid for the selected delivery option. For example, PICKUP may return "Pagar ahora" and "Pagar al recoger", while SHIPPING may return "Pagar ahora" and "Pagar contraentrega".

Only payment methods marked as Disponible para web in Bacano and enabled in the website payment settings are returned. Gateway methods such as Wompi expose gatewayCode, requiresPaymentSession and public configuration only; secrets never leave Bacano.

Each payment option also exposes initialPaymentPolicy (none, optional or required), requiresInitialPayment, minInitialAmount and suggestedInitialAmount. When required, submit the selected option with:

initialPayment: {
  amount: selectedAmount,
  paymentMethodId: selectedInitialMethod.paymentMethodId,
}

The API enforces the policy, minimum and order total. Do not infer deposits from labels or delivery mode.

const payNow = paymentOptions.find((option) => option.mode === "pay_now");

const result = await client.checkout.submit({
  fulfillmentFamily: "SHIPPING",
  contact: {
    firstName: "Jane",
    lastName: "Doe",
    email: "[email protected]",
    cellphone: "+573001112233",
    documentType: "13",
    documentNumber: "123456789",
  },
  address,
  shippingQuoteId: quotes[0].quoteId,
  paymentMode: "pay_now",
  paymentMethodId: payNow?.methods[0]?.paymentMethodId,
  buyerAccountOptIn: true,
});

const publicOrderUrl =
  result.orderId && result.publicOrderToken
    ? `/pedido/${result.orderId}?token=${result.publicOrderToken}`
    : null;

if (result.requiresPaymentSession && result.paymentMethodId) {
  const session = await client.checkout.createPaymentSession({
    orderId: result.orderId ?? undefined,
    publicOrderToken: result.publicOrderToken ?? undefined,
    paymentMethodId: result.paymentMethodId,
    returnUrl:
      `${window.location.origin}/pago/respuesta` +
      `?orderId=${encodeURIComponent(result.orderId ?? "")}` +
      `&token=${encodeURIComponent(result.publicOrderToken ?? "")}`,
  });

  if (
    session.success &&
    session.integrationMode === "redirect" &&
    session.redirectUrl
  ) {
    window.location.assign(session.redirectUrl);
  }
}

if (result.success) {
  console.log(`Order ${result.orderNumber} placed!`);
  console.log(publicOrderUrl);
} else {
  console.error("Checkout failed:", result.errors);
}

buyerAccountOptIn must reflect an explicit, unchecked-by-default buyer choice. For guests, Bacano queues a Clerk activation invitation as soon as a valid order is persisted, even when payment remains pending. Invalid attempts that do not create an order send nothing, and checkout never waits for Clerk. buyerAccountInvitationRequested reports whether Bacano durably accepted the request. Authenticated buyers do not receive an invitation; their verified Clerk token owns the order automatically. See AUTHENTICATION.md for the invitation landing-page and secure order-claim contract.

returnUrl is the storefront response page. session.redirectUrl is the provider-hosted checkout and is the only URL the storefront opens. The storefront never renders gateway widgets or constructs Wompi/Addi/ePayco URLs. On the response page, send the provider's public transaction ID back to Bacano when available and wait for the public order:

await client.checkout.reconcilePaymentSession({
  orderId,
  publicOrderToken,
  paymentSessionId,
  gatewayTransactionId,
});

const order = await client.orders.waitForPayment({
  orderId,
  token: publicOrderToken,
  intervalMs: 1_500,
  timeoutMs: 45_000,
});

Load getWebsitePublicConfig() before rendering contact fields. Pass the same required websiteSlug used to construct the client. Bacano selects that website explicitly and then verifies that the request Origin belongs to it; Origin alone never chooses a storefront. customerIdentification exposes the company-level policy configured in Settings > Sales > Checkout: the primary identifier, additional required fields and valid document types. The storefront renders that contract; Bacano enforces it again and uses the configured primary identifier to recognize a returning guest customer. AUTO preserves the legacy email-first, phone-second behavior.

The storefront never approves a payment from query parameters. Bacano verifies the transaction directly with the provider; the signed webhook remains the primary confirmation path.

After invitation acceptance, Bacano fills only missing commercial profile fields from its persisted order snapshots: name, mobile and primary shipping address. Existing profile data is never overwritten, and the authenticated email always comes from Clerk verification. name and phone remain supported for older storefronts; prefer firstName, lastName and cellphone in new checkouts.

Checkout submission is idempotent for the active SDK cart session. If Bacano returns ORDER_CONFIRMATION_RETRYABLE, keep the current cart session and call checkout.submit again with the same data. Do not call cart.startCheckout again: Bacano resumes the same orderId + cartToken, pending attempts do not consume a sales number, and a replay after confirmation returns the existing order result. For gateway checkouts, the SDK retains the private cart session until createPaymentSession succeeds, then retires it. Payment response and retry pages must continue with the returned orderId + publicOrderToken; a second purchase starts a new cart instead of reusing the finalized order.

The private orderId + cartToken session belongs exclusively to the SDK. A storefront must never read, remove or rewrite the @bacano/sdk:cart:{websiteSlug} key. When cart.startCheckout() discovers that a persisted session is no longer usable, Bacano returns the stable INVALID_CART_TOKEN or CART_NOT_FOUND code. The SDK then removes only that private session and retries the same start operation exactly once without a token. Storefront-owned product state is not touched, checkout.submit() is never repeated, and the failed token request creates no order. Other business errors preserve the private session and are returned with their stable code.

Signing in after starting as a guest does not rotate the cart token. Bacano continues an unowned guest cart and can continue a cart already linked to the same authenticated buyer. Signing out, changing buyers, completing an order, or changing the website chain makes a non-matching private session non-recoverable for that browser identity, so the next start creates one fresh cart through the bounded recovery above.

6. Public Order Tracking

Confirmation pages should not depend on browser-only snapshots. After a successful checkout, submit returns publicOrderToken; use it with the order id to build a durable customer link:

const publicOrder = await client.orders.getPublicOrder({
  orderId: result.orderId!,
  token: result.publicOrderToken!,
});

console.log(publicOrder?.orderNumber);
console.log(publicOrder?.status);
console.log(publicOrder?.paymentStatus);
console.log(publicOrder?.payment.requiresPaymentSession);
console.log(publicOrder?.payment.nextAction);
console.log(publicOrder?.items);
console.log(publicOrder?.nextSteps);

getPublicOrder does not require login. Bacano validates the current website, the order id and the opaque public token. The response is safe for customers: order state, payment state, delivery summary, payment summary, totals and item summary. It does not expose private credentials, internal snapshots or editable cart permissions.

payment.nextAction is the structured UI contract: CREATE_PAYMENT_SESSION, WAIT_FOR_PAYMENT, RETRY_PAYMENT, PAY_ON_DELIVERY, PAY_AT_PICKUP, WAIT_FOR_ORDER_CONFIRMATION or NONE. nextSteps is display copy only. A missing payment.session does not by itself mean that no gateway is required.

For payment gateways, the redirect page should be visual only. The gateway webhook updates Bacano; /pago/respuesta can use orderId + publicOrderToken to redirect or link back to /pedido/[id].

The checkout validates:

  • Cart ownership through order_id + cart_token
  • Order has sales_channel=WEB, PENDING + UNPAID and no order_number
  • Cart is not empty
  • Products are active and enabled for the website branch through chain_products
  • Products are enabled for the website branch
  • Sufficient stock for all items
  • All items have a valid positive CLIENT price

It also recalculates totals from current prices at checkout time to prevent stale-price exploits. Shipping and coupons are recalculated in Bacano as well; the web only sends the selected IDs and address. On success the order keeps sales_channel=WEB, PENDING + UNPAID, receives an order number, and Bacano creates or reuses the formal client by email first, then phone. The selected fulfillment family is synchronized through the existing delivery machinery so the ERP can continue pickup/shipping actions from the sale detail.

Checkout intentionally does not create accounting entries, payments, stock reservations or inventory deductions in this iteration. Those effects happen in a future payment-confirmation flow.

6. Optional Auth Module

The SDK still exposes client.auth for future logged-in customer flows, but it is not required for catalog, cart or guest checkout.

7. Order History

const orders = await client.orders.list({ limit: 10 });

const order = await client.orders.get("order-uuid");
console.log(order?.orderNumber); // "WEB-000001"
console.log(order?.status); // "PENDING", "PAYED", "SENT", "COMPLETED"
console.log(order?.items); // OrderItem[]

8. Customer Profile

const profile = await client.profile.get();
console.log(profile.name, profile.email);

const addresses = await client.profile.getAddresses();
const preferredShippingAddress = addresses.find((address) => address.isPrimary);

await client.profile.update({
  name: "Jane",
  lastname: "Smith",
  cellphone: "+573001234567",
  address: "123 Main St",
  addressComplement: "Apto 401",
  countryCode: "CO",
  departmentId: "68",
  municipalityId: "68001",
});

profile.email is read-only and comes from the buyer's verified Clerk identity. Use cellphone for the mobile/WhatsApp number in E.164 format; phone is an optional secondary telephone number. Never send passwords, verification codes or Clerk secrets through this module.

profile.getAddresses() also requires an authenticated Clerk session. It returns only checkout-ready addresses with canonical department and municipality data. A complete commercial profile address is PRIMARY; saved delivery addresses are SHIPPING. departmentId and municipalityId are the canonical geographic codes accepted by checkout.getShippingQuotes(). An incomplete legacy address is intentionally omitted so the storefront can show the location form instead of guessing by display text. The storefront must not send a clientId.


Configuration Reference

createBacanoClient({
  // Required
  apiUrl: string;        // Bacano public API URL
  websiteSlug: string;   // URL-friendly identifier (e.g., "my-store")

  // Optional
  tokenStorage?: 'localStorage' | 'cookie' | 'memory'; // Guest cart session only
  getAccessToken?: () => string | null | Promise<string | null>;
});

Cart Session Storage

tokenStorage is retained for compatibility but stores only the guest cart identifier/token. Buyer access tokens are never persisted by Bacano SDK; the configured identity provider owns their lifecycle.

Website favicon

The website's General settings can define one canonical favicon from Bacano's media library. It is returned by the existing public-config request, so a storefront does not need another API call or direct Storage integration:

const publicConfig = await getWebsitePublicConfig({ apiUrl, websiteSlug });
const faviconUrl = publicConfig.branding.favicon?.originalUrl;

Use faviconUrl in the framework's metadata or document head. When it is null, keep the storefront's packaged fallback icon. The public contract contains only browser-safe media metadata and a public Storage URL.


React Integration

Storefront marketing and measurement

GA4, Meta, and Microsoft Clarity are configured per website in Bacano and are disabled by default. An optional Google Tag Manager web container is also supported via marketing.destinations.gtm.containerId. It adds bacano_* browser events to the data layer without replacing native destinations or server conversions. See GTM setup, limitations and duplication safeguards. The existing public-config request returns only active public destination IDs; provider secrets never reach the SDK. The runtime remains a no-op outside the website's canonical production domain.

Mount one provider at the storefront root, using the same public config already loaded for buyer accounts:

import { getWebsitePublicConfig } from "@bacano/sdk";
import { BacanoMarketingProvider, useMarketing } from "@bacano/sdk/react";

const publicConfig = await getWebsitePublicConfig({ apiUrl, websiteSlug });

<BacanoMarketingProvider publicConfig={publicConfig}>
  <Storefront />
</BacanoMarketingProvider>;

function AddToCartButton({ variant }) {
  const marketing = useMarketing();

  async function add() {
    await updateCartSuccessfully(variant.id);
    marketing.track({
      eventName: "add_to_cart",
      value: variant.price,
      items: [
        {
          itemId: variant.id, // always product_variant_id
          itemName: variant.productName,
          sku: variant.sku,
          price: variant.price,
          quantity: 1,
        },
      ],
    });
  }

  return <button onClick={add}>Add to cart</button>;
}

The browser contract is versioned and accepts page_view, search, view_item_list, select_item, view_item, add_to_cart, remove_from_cart, view_cart, begin_checkout, add_shipping_info, add_payment_info, and contact. Emit events only after the related operation has succeeded. Use one manual page_view source for SPA navigation because the GA4 adapter disables automatic page views.

When Microsoft Clarity is active, this same provider loads it automatically and forwards canonical browser events as Clarity custom events. Do not install a second Clarity snippet or package in the storefront. Setup, privacy, and verification are documented in STOREFRONT_CLARITY.md.

Contact actions can identify their stable storefront location with the optional contactPlacement field. It accepts only product_detail, floating, header, footer, or contact_page; visible labels and arbitrary strings are not part of the public contract. The runtime ignores the field on events other than contact.

marketing.track({
  eventName: "contact",
  contactMethod: "email",
  contactPlacement: "footer",
});

A product contact keeps the same variant identity, value, and item payload used by the ecommerce events:

marketing.track({
  eventName: "contact",
  contactMethod: "whatsapp",
  contactPlacement: "product_detail",
  value: price * quantity,
  items: [
    {
      itemId: variant.id,
      itemName: product.name,
      sku: variant.sku,
      price,
      quantity,
      itemBrand: product.brand?.name,
      itemCategory: product.category,
      itemVariant: "Talla: 12 meses / Color: Rosado",
    },
  ],
});

GA4 receives this field as contact_placement; Meta receives the same contact_placement key while preserving contact_method, content_ids, and contents.

For checkout selectors, an automatic default is not yet a customer choice. Emit add_shipping_info and add_payment_info immediately for an explicit, valid selection, or once after a successful checkout submission when the customer kept an automatic default. Deduplicate both paths with the selected delivery/payment identity. On product lists, itemId and price must come from the same public, price-bearing variant; the advertised variant is the lowest-price variant with stable order and ID tie-breakers.

Transfer the runtime attribution snapshot when checkout starts and again when it is submitted. Bacano freezes the touchpoints on the order; the second snapshot may only fill missing GA4 client_id and session_id values that the Google tag resolved asynchronously:

await client.cart.startCheckout({
  contact,
  items,
  marketingAttribution: marketing.getAttribution() ?? undefined,
});

await client.checkout.submit({
  contact,
  marketingAttribution: marketing.getAttribution() ?? undefined,
  advertisingConsent: publicConfig.marketing.consentPolicy
    ? {
        policyId: publicConfig.marketing.consentPolicy.id,
        policyVersion: publicConfig.marketing.consentPolicy.version,
        statementHash: publicConfig.marketing.consentPolicy.statementHash,
        granted: optionalAdvertisingCheckbox,
      }
    : undefined,
});

order_placed, purchase, and refund are server-authoritative events. A storefront must never emit them from a thank-you page or payment response route. URLs are centrally sanitized to retain only approved UTM and click identifiers. Links and QR codes may additionally use bacano_origin=<public-key> to select a company-defined sales origin. The SDK preserves it with the attribution snapshot; Bacano validates it server-side and otherwise falls back to the normalized UTM/click-ID origin or Web. Google or Meta failures are isolated from catalog, cart, checkout, and payment. This isolation also applies to Bacano's server-side outbox producers: losing a marketing event must never roll back an order, payment, or refund transition. The runtime captures GA4 browser identity and Meta browser context (fbp, fbc, and user agent) internally, so storefronts must not read cookies or construct provider identifiers themselves. Contact information remains governed by the website's explicit advertising-consent policy.

An active, validated integration can enable a temporary diagnostic window from Bacano. GA4 events receive debug_mode; Meta server events receive its encrypted Test Events code. The mode is off by default, expires automatically, is cleared when an integration is paused or reconfigured, and never exposes the Meta code through public config or the SDK.

The canonical contract and acceptance checklist live in docs/websites/MARKETING_MEASUREMENT_STOREFRONTS_CHECKLIST.md in the Bacano ERP repository.

Website legal documents and the optional informational cookie notice are available in publicConfig.legal. Their canonical contract lives in STOREFRONT_LEGAL.md, which ships inside this package. The notice does not gate the marketing runtime; storefronts must not reinterpret it as a consent manager.

Setup

Wrap your app with BacanoProvider:

import { BacanoProvider } from "@bacano/sdk/react";

function App() {
  return (
    <BacanoProvider
      config={{
        apiUrl: process.env.NEXT_PUBLIC_BACANO_API_URL!,
        websiteSlug: "my-store",
      }}
    >
      <Shop />
    </BacanoProvider>
  );
}

Initialization State

import { useBacanoState } from "@bacano/sdk/react";

function Shop() {
  const { loading, error } = useBacanoState();

  if (loading) return <p>Loading website...</p>;
  if (error) return <p>Error: {error.message}</p>;

  return <ProductList />;
}

Available Hooks

Buyer authentication with Clerk

import { ClerkProvider, useAuth } from "@clerk/nextjs";
import { BacanoProvider } from "@bacano/sdk/react";

function StorefrontSdk({ children }: { children: React.ReactNode }) {
  const { getToken } = useAuth();
  return (
    <BacanoProvider
      config={{
        apiUrl: process.env.NEXT_PUBLIC_BACANO_API_URL!,
        websiteSlug: "my-store",
        getAccessToken: () => getToken(),
      }}
    >
      {children}
    </BacanoProvider>
  );
}

Mount StorefrontSdk inside the website-specific ClerkProvider. Use Clerk's components or hooks for sign-in, sign-up, verification and password recovery. The SDK obtains a fresh token for each protected request and never receives the buyer password.

useProducts(opts?)

import { getPrimaryProductImage, useProducts } from "@bacano/sdk/react";

function ProductList() {
  const {
    data: products,
    loading,
    error,
    refetch,
  } = useProducts({
    categoryName: "Clothing",
    limit: 20,
  });

  if (loading) return <p>Loading...</p>;
  if (error) return <p>Error: {error.message}</p>;

  return (
    <ul>
      {products?.map((p) => {
        const image = getPrimaryProductImage(p.media);

        return (
          <li key={p.id}>
            {image && <img src={image.url} alt={image.alt ?? p.name} />}
            <h3>{p.name}</h3>
            <p>{p.brand?.name}</p>
            {p.variants.map((v) => (
              <span key={v.id}>
                {v.sku}: ${v.prices[0]?.price}
              </span>
            ))}
          </li>
        );
      })}
    </ul>
  );
}

useProduct(id)

import { useProduct } from "@bacano/sdk/react";

function ProductDetail({ productId }: { productId: string }) {
  const { data: product, loading } = useProduct(productId);

  if (loading || !product) return <p>Loading...</p>;

  return (
    <div>
      <h1>{product.name}</h1>
      <p>{product.description}</p>
      {product.attributes.map((attr) => (
        <span key={attr.id}>
          {attr.name}: {attr.value}
        </span>
      ))}
    </div>
  );
}

useCategories() and useBrands()

import { useCategories, useBrands } from "@bacano/sdk/react";

function Sidebar() {
  const { data: categories } = useCategories();
  const { data: brands } = useBrands();

  return (
    <nav>
      <h4>Categories</h4>
      {categories?.map((cat) => (
        <div key={cat.name}>
          <strong>{cat.name}</strong>
          <ul>
            {cat.subcategories.map((sub) => (
              <li key={sub.id}>{sub.name}</li>
            ))}
          </ul>
        </div>
      ))}

      <h4>Brands</h4>
      {brands?.map((b) => (
        <span key={b.id}>{b.name}</span>
      ))}
    </nav>
  );
}

useProductFilters(opts?)

import { useProducts, useProductFilters } from "@bacano/sdk/react";
import { useState } from "react";

function FilteredProductList() {
  const [category, setCategory] = useState("CLOTHES");
  const [selectedColors, setSelectedColors] = useState<string[]>([]);
  const [priceRange, setPriceRange] = useState<[number, number]>([0, 1000]);
  const [selectedAttrs, setSelectedAttrs] = useState<Record<string, string[]>>(
    {},
  );

  // Fetch available filter options for the selected category
  const { data: facets } = useProductFilters({ categoryName: category });

  // Fetch products with active filters applied
  const { data: products, loading } = useProducts({
    categoryName: category,
    colors: selectedColors.length > 0 ? selectedColors : undefined,
    minPrice: priceRange[0],
    maxPrice: priceRange[1],
    attributes:
      Object.keys(selectedAttrs).length > 0 ? selectedAttrs : undefined,
    inStock: true,
    sortBy: "price_asc",
  });

  return (
    <div style={{ display: "flex", gap: "2rem" }}>
      {/* Filter sidebar built from facets */}
      <aside>
        <h4>
          Price: ${facets?.priceRange.min} – ${facets?.priceRange.max}
        </h4>

        <h4>Colors</h4>
        {facets?.colors.map((c) => (
          <label key={c.value}>
            <input
              type="checkbox"
              onChange={() => {
                /* toggle color */
              }}
            />
            {c.value} ({c.count})
          </label>
        ))}

        <h4>Dynamic Attributes</h4>
        {facets?.attributes.map((attr) => (
          <div key={attr.id}>
            <strong>{attr.name}</strong>
            {attr.values.map((v) => (
              <label key={v.value}>
                <input
                  type="checkbox"
                  onChange={() => {
                    /* toggle attr */
                  }}
                />
                {v.value} ({v.count})
              </label>
            ))}
          </div>
        ))}
      </aside>

      {/* Product grid */}
      <main>
        {products?.map((p) => (
          <div key={p.id}>{p.name}</div>
        ))}
      </main>
    </div>
  );
}

useSearchSuggestions(opts?)

import { useSearchSuggestions } from "@bacano/sdk/react";
import { useState } from "react";

function SearchBar() {
  const [query, setQuery] = useState("");
  const { data: suggestions, loading } = useSearchSuggestions({
    query,
    limit: 8,
  });

  return (
    <div>
      <input
        type="text"
        placeholder="Search products..."
        value={query}
        onChange={(e) => setQuery(e.target.value)}
      />
      {query && suggestions && suggestions.length > 0 && (
        <ul>
          {suggestions.map((s, i) => (
            <li key={i}>
              <span>{s.type === "brand" ? "🏷️" : "📦"}</span>
              {s.text}
            </li>
          ))}
        </ul>
      )}
    </div>
  );
}

useAvailability(variantIds)

import { useAvailability } from "@bacano/sdk/react";

function StockBadge({ variantIds }: { variantIds: string[] }) {
  const { data: availability } = useAvailability(variantIds);

  return (
    <>
      {availability?.map((a) => (
        <span
          key={a.productVariantId}
          style={{ color: a.inStock ? "green" : "red" }}
        >
          {a.label}
        </span>
      ))}
    </>
  );
}

useCart()

import { useCart } from "@bacano/sdk/react";

function CartPage() {
  const { cart, loading, startCheckout, updateItem, removeItem } = useCart();

  async function handleStartCheckout() {
    await startCheckout({
      contact: { email: "[email protected]", phone: "+573001112233" },
      items: [{ productVariantId: "variant-uuid", quantity: 1 }],
    });
  }

  if (loading) return <p>Loading cart...</p>;
  if (!cart || cart.items.length === 0) return <p>Cart is empty</p>;

  return (
    <div>
      {cart.items.map((item) => (
        <div key={item.id}>
          <span>{item.productVariant.product.name}</span>
          <span>SKU: {item.productVariant.sku}</span>
          <span>Qty: {item.quantity}</span>
          <button onClick={() => updateItem(item.id, item.quantity + 1)}>
            +
          </button>
          <button onClick={() => updateItem(item.id, item.quantity - 1)}>
            -
          </button>
          <button onClick={() => removeItem(item.id)}>Remove</button>
        </div>
      ))}
      <p>Total: ${cart.totals.total}</p>
      <button onClick={handleStartCheckout}>Start checkout</button>
    </div>
  );
}

useCheckout()

import { useCheckout, useCart } from "@bacano/sdk/react";

function CheckoutButton() {
  const { cart } = useCart();
  const { checkout, loading, error } = useCheckout();

  async function handleCheckout() {
    if (!cart) return;
    try {
      const result = await checkout({
        contact: { email: "[email protected]", phone: "+573001112233" },
      });
      alert(`Order ${result.orderNumber} placed!`);
    } catch (err) {
      // error state is also updated automatically
    }
  }

  return (
    <div>
      <button onClick={handleCheckout} disabled={loading || !cart}>
        {loading ? "Processing..." : "Place Order"}
      </button>
      {error && <p style={{ color: "red" }}>{error.message}</p>}
    </div>
  );
}

useOrders(opts?) and useOrder(id)

import { useOrders } from "@bacano/sdk/react";

function OrderHistory() {
  const { data: orders, loading } = useOrders({ limit: 10 });

  if (loading) return <p>Loading...</p>;

  return (
    <ul>
      {orders?.map((order) => (
        <li key={order.id}>
          {order.orderNumber} — {order.status} — ${order.total}
        </li>
      ))}
    </ul>
  );
}

useProfile()

import { useProfile } from "@bacano/sdk/react";

function ProfilePage() {
  const { profile, loading, updateProfile } = useProfile();

  if (loading || !profile) return <p>Loading...</p>;

  return (
    <form
      onSubmit={async (e) => {
        e.preventDefault();
        const formData = new FormData(e.currentTarget);
        await updateProfile({
          name: formData.get("name") as string,
          address: formData.get("address") as string,
        });
      }}
    >
      <input name="name" defaultValue={profile.name ?? ""} />
      <input name="address" defaultValue={profile.address ?? ""} />
      <button type="submit">Save</button>
    </form>
  );
}

Hooks Summary

| Hook | Returns | Auth Required | | ----------------------------- | --------------------------------------------------------------------------------------- | ------------- | | useBacano() | BacanoClient | No | | useBacanoState() | { client, loading, error } | No | | useProducts(opts?) | { data, loading, error, refetch } | No | | useProduct(id) | { data, loading, error, refetch } | No | | useCategories() | { data, loading, error, refetch } | No | | useBrands() | { data, loading, error, refetch } | No | | useProductFilters(opts?) | { data, loading, error, refetch } | No | | useSearchSuggestions(opts?) | { data, loading, error, refetch } | No | | useAvailability(ids) | { data, loading, error, refetch } | No | | useCart() | { cart, loading, error, startCheckout, getOrCreate, addItem, updateItem, removeItem, clear } | No | | useCheckout() | { checkout, loading, error } | No | | useOrders(opts?) | { data, loading, error, refetch } | Yes | | useOrder(id) | { data, loading, error, refetch } | Yes | | useProfile() | { profile, loading, error, updateProfile } | Yes |


Advanced: Bring Your Own GraphQL Client

If you use Apollo, urql, TanStack Query, or any other GraphQL client, you can import the raw typed documents instead:

import { documents, createAuthHeaders } from "@bacano/sdk/graphql";
import type { Product, Cart } from "@bacano/sdk/graphql";

With Apollo Client

import { documents } from "@bacano/sdk/graphql";
import { useQuery, useMutation } from "@apollo/client";

function ProductList({ companyId }: { companyId: string }) {
  const { data, loading } = useQuery(documents.GET_PRODUCTS, {
    variables: { companyId, limit: 20 },
  });

  // data.products is the raw Hasura response (snake_case)
}

function StartCheckoutButton({ variantId }: { variantId: string }) {
  const [startCheckout] = useMutation(documents.START_GUEST_CHECKOUT);

  return (
    <button
      onClick={() =>
        startCheckout({
          variables: {
            websiteId: "website-uuid",
            contact: { email: "[email protected]" },
            items: [{ product_variant_id: variantId, quantity: 1 }],
          },
        })
      }
    >
      Start checkout
    </button>
  );
}

With fetch (SSR / Server Components)

import { documents, createAuthHeaders } from "@bacano/sdk/graphql";

// In a Next.js Server Component or API route:
async function getProducts(apiUrl: string, companyId: string) {
  const res = await fetch(`${apiUrl}/api/v1/website/graphql`, {
    method: "POST",
    headers: {
      "Content-Type": "application/json",
      // No auth headers = public role (anonymous catalog browsing)
    },
    body: JSON.stringify({
      query: documents.GET_PRODUCTS,
      variables: { companyId, limit: 20 },
    }),
  });

  const { data } = await res.json();
  return data.products;
}

// Authenticated request:
async function getOrders(
  apiUrl: string,
  getToken: () => Promise<string | null>,
) {
  const token = await getToken();
  const res = await fetch(`${apiUrl}/api/v1/website/graphql`, {
    method: "POST",
    headers: {
      "Content-Type": "application/json",
      ...createAuthHeaders(token),
    },
    body: JSON.stringify({
      query: documents.GET_ORDERS,
      variables: { limit: 10 },
    }),
  });

  const { data } = await res.json();
  return data.orders;
}

Available Documents

| Document | Type | Description | | ---------------------------- | -------- | ---------------------------------------------------------------------- | | RESOLVE_WEBSITE_BY_SLUG | Query | Resolve website by slug | | GET_PRODUCTS | Query | List products with variants, prices, media, attributes | | GET_PRODUCT_BY_PK | Query | Single product with full details | | GET_CATEGORIES | Query | All categories with subcategories | | GET_BRANDS | Query | Company brands | | CHECK_AVAILABILITY | Query | Stock availability (boolean + label) | | GET_PRODUCT_FILTERS | Query | Universal facets (price range, brands with counts, categories/colors) | | GET_ATTRIBUTE_FACETS | Query | Category-specific attribute facets with value counts | | GET_SEARCH_SUGGESTIONS | Query | Lightweight product/brand name search for autocomplete | | CREATE_CART | Mutation | Compatibility cart creation | | GET_CART | Mutation | Current WEB + PENDING + UNPAID checkout with line items | | START_GUEST_CHECKOUT | Mutation | Create/update WEB + PENDING + UNPAID from contact + local cart items | | SET_CART_ITEM | Mutation | Update item quantity after checkout has started | | REMOVE_CART_ITEM | Mutation | Remove item after checkout has started | | GET_SHIPPING_POLICY | Mutation | Public website free-shipping policy without a cart | | GET_FREE_SHIPPING_PROGRESS | Mutation | Server-calculated progress from current cart items | | GET_SHIPPING_QUOTES | Mutation | Final shipping quotes for an address | | CHECKOUT | Mutation | Confirm checkout, assign client and order number | | GET_ORDERS | Query | Order history | | GET_ORDER_BY_PK | Query | Single order with items | | GET_PROFILE | Query | Customer profile | | UPDATE_PROFILE | Mutation | Update customer profile |


SSR / Next.js Server-Side Usage

For server-side rendering or Server Components, use memory cart storage and provide a server-side Clerk token provider when the request is authenticated:

import { createBacanoClient } from "@bacano/sdk";

// In getServerSideProps or a Server Component:
const client = createBacanoClient({
  apiUrl: process.env.BACANO_API_URL!,
  websiteSlug: "my-store",
  tokenStorage: "memory",
  getAccessToken: async () => tokenFromClerkServerAuth ?? null,
});

await client.init();
const orders = await client.orders.list();

Protected server-side requests must also reach Bacano with the storefront's exact configured Origin. Browser requests add it automatically; server proxies must preserve it.


Error Handling

The SDK throws typed errors you can catch and handle:

import {
  WebsiteError,
  RateLimitError,
  AuthError,
  CheckoutError,
  NotInitializedError,
  GraphQLRequestError,
} from "@bacano/sdk";

try {
  await client.checkout.submit({
    contact: { email: "[email protected]", phone: "+573001112233" },
  });
} catch (err) {
  if (err instanceof CheckoutError) {
    // Checkout validation failed
    console.log("Validation errors:", err.errors);
    // e.g., ["Insufficient stock for SKU-001", "Product XYZ is no longer available"]
    // ORDER_CONFIRMATION_RETRYABLE can be retried with checkout.submit;
    // the SDK intentionally preserves the current cart session.
  } else if (err instanceof AuthError) {
    // Not logged in or session expired
    console.log("Please log in again");
  } else if (err instanceof GraphQLRequestError) {
    // Hasura returned GraphQL errors
    console.log("GraphQL errors:", err.graphqlErrors);
  } else if (err instanceof NotInitializedError) {
    // client.init() was not called
  } else if (err instanceof RateLimitError) {
    console.log("Retry after seconds:", err.retryAfterSeconds);
  } else if (err instanceof WebsiteError) {
    // Other SDK error (network, rate limit, etc.)
    console.log("Error code:", err.code);
    // Codes: NETWORK_ERROR, RATE_LIMITED, INVALID_RESPONSE,
    // WEBSITE_NOT_FOUND, NO_CHAIN, etc.
  }
}

RATE_LIMITED errors expose status = 429 and retryAfterSeconds. Normal SDK operations do not retry mutations automatically. The static catalog snapshot is a read-only build contract and safely retries up to four times, never before the server's Retry-After window and with a three-minute cumulative wait cap.

Common setup errors:

| Code / message | Meaning | Fix | | ------------------- | ---------------------------------------------- | -------------------------------------------------------------- | | WEBSITE_NOT_FOUND | No active website matches slug/domain. | Check slug/domain and active status in Bacano. | | NO_CHAIN | Website has no branch.