@bacano/sdk
v1.2.2
Published
TypeScript SDK for building websites on the Bacano ERP
Maintainers
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/sdkpnpm add @bacano/sdkReact 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 checkDespué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 doctorDespué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.jsonLas 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 antigravityTambié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 checkEl 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-storeconst 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 SDKThis 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 + UNPAIDcheckout. sales_channel=WEB,PENDING + UNPAIDwithoutorder_numbermeans checkout started or abandoned.- These started checkouts are tracked by
orders.website_cart_statusand should be managed from Bacano's abandoned-cart view, not mixed into the normal sales list. cart.clear()marks the private cart asdiscarded, 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
fulfillmentFamilybefore starting checkout:SHIPPINGfor delivery orPICKUPfor 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
clientyet; it stores onlywebsite_contact_snapshoton 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 + UNPAIDand noorder_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
CLIENTprice
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.
