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

brainerce

v3.1.0

Published

Official SDK for building e-commerce storefronts with Brainerce Platform. Perfect for vibe-coded sites, AI-built stores (Cursor, Lovable, v0), and custom storefronts.

Readme

brainerce

Official SDK for building e-commerce storefronts with Brainerce Platform.

This SDK provides a complete solution for vibe-coded sites, AI-built stores (Cursor, Lovable, v0), and custom storefronts to connect to Brainerce's unified commerce API.

AI Agents / Vibe Coders (Cursor, Lovable, Claude Code, VS Code): Use the MCP server for AI-powered store building: npx @brainerce/mcp-server. It provides docs, code templates, and live store capabilities directly inside your IDE. Note: the MCP server runs inside your IDE. It is not available in chat-only tools like Google AI Studio or ChatGPT.

Three SDK modes: choose the right one

The exported class is BrainerceClient. Which mode you get is decided by which key you pass to its constructor:

| Mode | Config key | Use for | Where to run | | ----------------- | ------------------------ | ---------------------------------------- | ---------------------------------- | | Sales channel | salesChannelId: 'vc_*' | Building the customer-facing store | Browser / client-side | | Storefront | storeId | A public storefront on a published store | Browser / client-side | | Admin | apiKey: 'brainerce_*' | Managing products, team, settings | Server only, never in browser code |

import { BrainerceClient } from 'brainerce';

const client = new BrainerceClient({ salesChannelId: 'vc_abc123' });

If you pass more than one, apiKey wins, then salesChannelId, then storeId. An apiKey puts the client in admin mode no matter what else you passed, so never add one "just to also read a channel"; it changes every route the client calls. Passing none throws BrainerceClient: either salesChannelId, apiKey, or storeId is required.

Ask the client which mode it is in with isSalesChannelMode(), isStorefrontMode() or isAdminMode(). Exactly one returns true. (isVibeCodedMode() is a deprecated alias of isSalesChannelMode().)

Not every method works in every mode. A handful (getPaymentStatus(), confirmSdkPayment(), waitForOrder()) are sales-channel mode only and throw BrainerceError 400 elsewhere. storeId mode is not read-only: it can create carts, run a checkout to a real order, and register/log in customers; what it cannot reach is the admin surface.

Building a storefront? You only need your Sales Channel ID (vc_*) from the Brainerce dashboard under Sales Channels. No API key needed. API keys are a server-side admin secret.

connectionId is the deprecated alias of salesChannelId. It still works and logs a deprecation warning on every construction — it is a permanent backward-compat alias, not scheduled for removal. Use salesChannelId in new code.

Installation

npm install brainerce
# or
pnpm add brainerce
# or
yarn add brainerce

What You Must Build

Every Brainerce storefront must include all mandatory features below. Features auto-hide when the underlying capability is disabled, so build them all anyway; they'll appear the moment the store owner enables them.

| Feature | SDK entry point | Mandatory | | ----------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------ | -------------------------------------- | | Product list with search, filter, pagination | client.getProducts(), client.getSearchSuggestions(query) | ✅ | | Product detail with variant picker, stock, price | client.getProductBySlug() + helpers | ✅ | | Buyer customization fields (engraving, uploads, select) | product.customizationFields, client.uploadCustomizationFile() | ✅ | | Cart (add, update, remove, coupon, totals) | client.addToCart(), getCartTotals(cart) | ✅ | | Inventory reservation countdown | Cart expiry timestamp from client.getCart(cartId) | ✅ | | Full checkout end-to-end with payment | setShippingAddress → selectShippingMethod → getPaymentProviders → pay → handlePaymentSuccess → waitForOrder | ✅ | | Gift card redemption at checkout | client.applyGiftCard(checkoutId, code), client.removeGiftCard(checkoutId, tenderId), client.checkGiftCardBalance(code) | ✅ (auto-hides on hasGiftCards) | | Order confirmation (clear cart + wait for real order) | client.handlePaymentSuccess(), client.waitForOrder() | ✅ | | Register + email verification flow | client.registerCustomer(), client.verifyEmail() | ✅ | | Login + verification branch | client.loginCustomer() | ✅ | | Forgot / reset password | client.forgotPassword(), client.resetPassword() | ✅ | | OAuth sign-in buttons + callback handler | client.getAvailableOAuthProviders() | ✅ | | Account area (profile + order history) | client.getMyProfile(), client.updateMyProfile(), client.getMyOrders() | ✅ | | Loyalty & rewards (points balance + tiers + redeem) | client.getLoyaltyStatus(), client.getAvailableRewards(), client.getRecommendedReward(), client.redeemLoyaltyReward(id), client.reportSocialShare() | ✅ (auto-hides on hasLoyaltyProgram) | | Loyalty paid membership (premium subscription) | client.getMembershipPlans(), client.getMySavedPaymentMethods(), client.subscribeToMembership(params), client.cancelMembership() | ✅ (auto-hides on hasPaidMembership) | | Embeddable loyalty widget (points + rewards on ANY site) | client.getLoyaltyWidgetSession() | ✅ (auto-hides on hasLoyaltyProgram) | | Global header: cart count + search autocomplete | client.smartGetCart(), client.getSearchSuggestions(query) | ✅ | | Discount banners + product badges | client.getDiscountBanners(), client.getProductDiscountBadge(productId) | ✅ | | Product reviews on PDP + JSON-LD aggregateRating | client.listProductReviews(id), client.submitProductReview(id, …) | ✅ | | Customer photos on reviews | client.uploadReviewPhoto(productId, file), then imageKeys on submit | conditional | | Site chrome (header + footer + announcement bar) | client.content.header.get(), client.content.footer.get(), client.content.announcement.list() | ✅ | | FAQ page | client.content.faq.get('main', locale) | conditional | | Static pages catch-all (/pages/[slug]) | client.content.page.getBySlug(slug, locale) | conditional | | Multi-language + RTL (when i18n enabled) | client.setLocale(), client.getStoreDirection(locale) | conditional | | Donation page (only when getStoreInfo().donationsEnabled) | client.createDonation(input), client.getDonation(id) | conditional |

⛔ The donation page is the one row that does NOT auto-hide. Every other conditional feature above renders nothing until the merchant configures it, which is why you build them all anyway. createDonation is rejected while donations are closed, so a donation page built for a store that has not opened them collects a donor's name, email and card details and then fails on submit. Gate that one on getStoreInfo().donationsEnabled and build nothing when it is false.


Critical Rules

Violating any of these causes production incidents or broken orders. Read them before writing SDK code.

SDK usage

  • ALWAYS call SDK client methods. Never reconstruct REST URLs or call fetch directly.
  • NEVER invent SDK method names. If it's not in this README or in get-sdk-docs, it doesn't exist.
  • NEVER hardcode product data, categories, or store copy. Brainerce is the database.
  • NEVER use submitGuestOrder(), createGuestOrder() or createOrder() on a store that takes payment. They POST /orders directly, never touch /payment/intent, and produce an order nobody has paid for. They exist only for cash-on-delivery, manual-invoice and sandbox stores. Everything else goes through the checkout sequence below.
  • ALWAYS use SDK helpers (getCartTotals, formatPrice, getProductPriceInfo, getCartItemImage, getCartItemName, getVariantPrice, getStockStatus, getDescriptionContent) instead of reading raw fields.
  • ALWAYS read merchant settings from client.getStoreCapabilities() rather than hardcoding them. The low-stock threshold, the reservation timeout, whether back-in-stock alerts are offered and which optional features exist are all per sales channel. A hardcoded 5 is wrong on every store whose merchant chose something else. Call it once at app start and share the result; it is per channel, not per product. One exception, because getStoreCapabilities() is vibe-coded mode only: read donationsEnabled from getStoreInfo(), which works in every mode and carries the same fact as features.hasDonations.

State management

  • The SDK manages cart, checkout, and session state. Do NOT duplicate it in your own Redux/context.
  • Product lists, categories, and inventory counts are NOT client state; fetch on demand.
  • Discount rules and coupon validity are evaluated server-side. Never re-implement them client-side.

Authentication

  • ALWAYS handle the requiresVerification flag in registerCustomer and loginCustomer responses. If true, route to the verify-email step BEFORE treating the user as logged in.
  • ALWAYS build the verify-email, forgot-password, and reset-password flows even when the store currently has email verification disabled. They auto-hide when unused.
  • ALWAYS read requireBirthday from getStoreInfo() before rendering the signup form. When it is true the merchant made the birthday mandatory on that sales channel, and registerCustomer returns HTTP 400 unless you send both birthMonth (1-12) and birthDay (1-31). Month and day only, never a year.
  • ALWAYS build OAuth button placeholders and a callback handler even when no OAuth provider is configured.
  • NEVER silently swallow auth errors. Render the specific error (invalid credentials, expired token, rate limited).

Checkout & orders

  • The checkout sequence is strict: setShippingAddress → pick a shipping rate → getPaymentProviders → provider payment → handlePaymentSuccesswaitForOrder. Never skip or reorder.
  • ALWAYS call handlePaymentSuccess(checkoutId) on the confirmation page. It clears the cart so users don't see stale items.
  • ALWAYS call waitForOrder(checkoutId) to poll for the real order before showing an order number. The payment callback may return before the order record exists.
  • NEVER use the checkout total as the cart total; they diverge (tax, shipping, discounts). Display checkout.lineItems on the summary, not cart.items.
  • The reservation timer is a hard guarantee. Display the countdown from the cart and let the SDK handle expiry.
  • NEVER render order.totalAmount as "amount paid". It is what the order was WORTH. When order.tenders is non-empty a gift card settled part of it, and the customer handed over totalAmount minus those tenders. A receipt that shows only the total tells them they paid money they did not.

Gift cards

  • A gift card is a means of payment, not a discount. applyGiftCard does NOT change checkout.total, and tax stays calculated on the full order value. What drops is checkout.providerAmountDue, the amount the payment provider will be charged. Render the card on its own line below the total ("Gift card −₪54.50", then "Amount due ₪150.50"), never inside the discount block and never added to discountAmount. Folding it in understates the taxable base to the shopper and on their receipt.
  • ALWAYS render applied cards from checkout.tenders ({ tenderId, amountApplied }[]), re-read with getCheckout(checkoutId). A storefront that only remembers what applyGiftCard returned loses the card on a page reload while the hold is still live on the server, so the shopper applies it twice or is shown an amount the provider will not charge.
  • ALWAYS remove with removeGiftCard(checkoutId, tenderId), never by code. A checkout can carry several cards, and the code is never echoed back.
  • NEVER try to tell refusals apart. An unknown code, an expired one, a spent one, a disabled one and one in the wrong currency all return the same HTTP 400 with the same message, on purpose: a response that distinguished them is an oracle for walking the code space. Show one message ("we can't use this code") and let the shopper re-type it. checkGiftCardBalance answers identically for unknown, disabled and expired cards.
  • Apply and remove cards before you create the payment intent. Once the checkout is PAYMENT_PENDING / PAYMENT_PROCESSING these calls fail with CHECKOUT_LOCKED, which is what stops a card being applied behind a charge that was already quoted.
  • NEVER subtract the card yourself when charging. createPaymentIntent already nets live gift cards off server-side; charge the intent's own amount.
  • When providerAmountDue is '0.00' the cards cover the whole order. There is nothing for a provider to charge: skip the payment step and call completeCheckout(checkoutId) — it is allowed in exactly this case and produces a real paid order. Then still clear the cart, with handlePaymentSuccess(checkoutId), exactly as you would after a payment. completeCheckout returns { orderId }, so there is no waitForOrder poll to do, but skipping the cart clear leaves the shopper looking at items they have just bought.
  • A card pays only in its own currency. There is no conversion, so a USD card is refused on an ILS checkout like any other unusable code.
  • The card follows the order, under a different name and a different shape. Once the order exists the record is order.tenders, not checkout.tenders, and it is { id, type, amountBase, currencyBase, giftCard } rather than { tenderId, amountApplied }. There is no providerAmountDue on an order — you compute what was charged as totalAmount minus the sum of amountBase. Render it on every receipt, confirmation page and order-history row, exactly as you did at checkout.

Administering cards (admin apiKey only — see Gift Cards (administration)):

  • issueGiftCard and reissueGiftCard return plaintextCode exactly once. The platform stores only an HMAC of it. No later call, no dashboard screen and no database query can produce it again, so an integrator that logs the response and moves on has destroyed a card that a customer paid for. Persist it or deliver it in the same code path that made the call. The one recovery that exists is retrying the SAME Idempotency-Key within 24 hours, which replays the identical body; miss that window and the value is stranded on a card nobody can spend.
  • reissueGiftCard is not a resend. It mints a new code, moves the whole balance onto it, and revokes the old card — a printed card in a customer's hand stops working the moment the call returns. Use it when a code is lost, never to "email it again".
  • A note is mandatory on issueGiftCard, reissueGiftCard and adjustGiftCardBalance (3-500 characters), and is written to the append-only ledger permanently. It is the row a finance review reads a year later, so write the reason, not "api".
  • There is no delete. Not one route, not in bulk, not ever — the ledger is append-only and a card can carry a statutory retention life. setGiftCardStatus(id, 'DISABLED') is the reversible substitute. bulkSetGiftCardStatus takes ACTIVE and DISABLED only; REVOKED is refused there because revoking in bulk would strand balances with no replacement to move them to.
  • Ask for the least scope you need. gift_cards:issue mints stored value and gift_cards:adjust rewrites a balance; neither is implied by gift_cards:read. This platform grants them self-serve, where Shopify makes you ask their support for the equivalent — which puts the whole weight on asking for less. ⛔ Never mint gift_cards:* for a read-only integration: the wildcard matches the resource, not the action, so one string hands a BI sync the power to mint and to rewrite balances.
  • Liability is per currency. getGiftCardLiability() returns byCurrency, because balances in different currencies do not add up. Read the array, never the top-level figure alone, on a store that sells in more than one.

Donations

  • NEVER route a donation through the cart or the checkout. A donation has no line item, no quantity, no shipping and no order, and it is reported separately from sales. It has its own pair, createDonation / getDonation. The tell that you have modelled it wrong is the amount: a cart cannot let a donor type one, so a "Donation $18" product is the wrong shape — and it files every gift into the merchant's sales figures.
  • NEVER treat createDonation resolving as a completed gift. It returns status: 'PENDING' and a provider intent; the money has not moved. Complete the intent, then poll getDonation(id) for PAID. Nothing receipt-shaped before that.
  • ALWAYS gate the donation page on getStoreInfo().donationsEnabled. Unlike every other conditional feature in this SDK it does NOT auto-hide: createDonation is rejected while donations are closed, so a page built early collects a donor's details and then fails.

Token handling

  • Customer auth tokens (result.token from loginCustomer/registerCustomer) should be passed to client.setCustomerToken(token). The SDK stores session state internally. setCustomerToken is a plain setter, so always follow it with await client.syncCartOnLogin(), or the shopper's guest cart is never attached to their account and identity-keyed features (first-order discounts, per-customer usage caps, abandoned-cart recovery) misbehave.
  • NEVER put the admin API key (brainerce_*) in client code. It is a server-only secret.
  • OAuth callbacks arrive with a one-time auth_code URL param. Call client.exchangeOAuthCode(authCode) to swap it for the JWT, apply via setCustomerToken, then call client.syncCartOnLogin() to claim the guest cart. (The legacy ?token= URL param is not emitted any more unless the platform operator has opted a store into it server-side; never read it.)

i18n

  • NEVER hardcode currency, locale, or language strings; read them from getStoreInfo().
  • NEVER format prices with toFixed(2); use formatPrice() from the SDK.
  • When i18n is enabled, call client.setLocale(locale) at app init and include a language switcher. For RTL locales (he, ar), set <html dir="rtl"> and do NOT add flex-row-reverse on top.
  • Call setLocale() before createCheckout(), because the order captures the active locale (order.locale), which drives the confirmation-email language AND the product names shown in order history. Without it, orders and their emails fall back to the store default language. Search also needs the locale active to match translated names.
  • Built-in order emails are localized for English and Hebrew; for other languages the merchant must add a custom email template per language (otherwise the email falls back to English).

Type safety

  • NEVER use as any or as unknown as. Fix the type, don't hide it.
  • NEVER write your own copies of SDK types (Cart, Product, Order). Import from 'brainerce'.
  • All prices are STRINGS, so always parseFloat() before math or comparisons.
  • CartItem / CheckoutLineItem = NESTED (item.product.name, item.unitPrice). OrderItem = FLAT (item.name, item.price). Not interchangeable.
  • Cart has no .total field; call getCartTotals(cart).

Business Flows

These sequences are non-negotiable. The order of SDK calls matters.

Checkout flow

  1. Collect customer email, billing address, shipping address (line1, line2, city, region, postalCode, country). email is required. Include an optional "Order notes" textarea by default; its value lands on the order for the merchant.
  2. Submit address to get shipping rates:
    const { checkout, rates } = await client.setShippingAddress(checkoutId, {
      email,
      firstName,
      lastName,
      line1,
      city,
      region,
      postalCode,
      country,
      notes: orderNotes, // optional shopper note (max 2000 chars)
    });
    // rates = available shipping rates; checkout = updated checkout object
  3. Let the customer pick a rate, then persist it:
    await client.selectShippingMethod(checkoutId, rateId);
    Label each rate with rate.speedTier ('cheapest' | 'balanced' | 'fastest') and rate.estimatedDays, not rate.name, which for live carrier rates is the carrier's own service code. Manual zone rates carry no speedTier; use their name.
  4. Fetch available payment providers:
    const providers = await client.getPaymentProviders();
    Each provider has a clientSdk.renderType: 'sdk-widget' (Stripe, PayPal, Grow), 'iframe' (Cardcom), 'redirect' (Morning, Takbull, iCredit), 'sandbox'. Branch on clientSdk.renderType, never on provider name — there is no top-level renderType on the provider itself. A provider's clientSdk.displayModes lists every mode it can serve; you may ask for one with createPaymentIntent(checkoutId, { preferredRenderType: 'iframe' | 'redirect' }), and the platform honours it only from that list, so still branch on what comes back.
  5. Confirm payment using the provider's flow (Stripe Elements stripe.confirmCardPayment, PayPal button, redirect, etc.).
  6. On the confirmation page, always call both:
    client.handlePaymentSuccess(checkoutId); // synchronous, clears cart. Do NOT await it.
    const order = await client.waitForOrder(checkoutId); // polls until order exists
  7. Display checkout.lineItems (not cart.items) on the order summary.

If the store has gift cards on, the redemption field goes between step 3 and step 4 — see the flow below.

Gift card redemption flow

Mandatory, auto-hides on hasGiftCards: in salesChannelId mode, getStoreCapabilities().features.hasGiftCards tells you whether the feature is on, but build the field regardless of what it reports. That call is channel-only and getStoreInfo() carries no gift-card flag, so in storeId mode there is no switch to read — build the field anyway; a code on a store without cards is just refused. (Admin mode does have one: getGiftCardLiability().enabled. It gates issuing, not redemption, so it is not a reason to hide the field either.) It sits inside the checkout, after shipping is picked and before the payment intent, because applying a card changes what the provider is asked for.

This flow is redemption only. Issuing, re-issuing, adjusting and disabling cards are admin-key operations — see Gift Cards (administration).

  1. Offer the field on the checkout page (optionally with a "check balance" affordance):

    const { balance, currency, usable } = await client.checkGiftCardBalance(code);
    // usable === false for an unknown, disabled OR expired card — all identical, by design.
    // Never render "expired" or "not found"; you do not know which it was.
  2. Apply it. The card is a tender, so the total does not move:

    const { tenderId, amountApplied, providerAmountDue } = await client.applyGiftCard(
      checkoutId,
      code
    );
    // checkout.total is UNCHANGED. providerAmountDue is what the card leaves for the provider.

    Every refusal is one HTTP 400 with one message. Show a single "we can't use this code" and let the shopper re-enter it.

  3. Re-read the checkout and render from it, never from what you remembered:

    const checkout = await client.getCheckout(checkoutId);
    checkout.tenders; // [{ tenderId, amountApplied }] — oldest first, survives a reload
    checkout.providerAmountDue; // '0.00' means nothing is owed

    Summary order: subtotal → discounts → shipping → tax → total → one line per gift card → amount due.

  4. Removing takes the tenderId, never the code — a checkout can carry several cards:

    const { providerAmountDue } = await client.removeGiftCard(checkoutId, tenderId);

    Nothing was ever debited, so the held value goes straight back to the card.

  5. After the order exists, read the cards back from the order, not the checkout:

    const order = await client.getOrderByCheckout(checkoutId);
    order.tenders; // [{ id, type, amountBase, currencyBase, giftCard: { id, codeLast4 } }]
    // What the customer was actually charged. Decimal strings — never parseFloat
    // for money you display; this is illustrative, use your money library.
    const charged = order.tenders.reduce(
      (n, t) => n - Number(t.amountBase),
      Number(order.totalAmount)
    );

    Different name, different shape, same job: tenderId/amountApplied at checkout become id/amountBase on the order, and there is no providerAmountDue here — you compute it. Snapshotted at order creation, so it is a historical record and does not move if the card is later adjusted, disabled or re-issued.

  6. Then branch on what is still owed:

    • providerAmountDue > '0.00' → the normal payment step. createPaymentIntent already nets the cards off; charge the intent's amount and never subtract anything yourself.
    • providerAmountDue === '0.00' → there is no charge to make. Skip the provider entirely and call completeCheckout(checkoutId), then handlePaymentSuccess(checkoutId) to clear the cart. It returns { orderId } directly, so no waitForOrder poll is needed here.

⛔ Do all of this before creating the payment intent. Once the checkout is PAYMENT_PENDING / PAYMENT_PROCESSING, apply and remove both fail with CHECKOUT_LOCKED.

Registration flow

  1. Collect email, password, first name, last name. Read requireBirthday from getStoreInfo(): when it is true, collect a birthday month and day as well, because the register call is rejected without them.
  2. Call registerCustomer:
    const result = await client.registerCustomer({ email, password, firstName, lastName });
    // Channel requires a birthday? Send both fields, never a year:
    // { email, password, firstName, lastName, birthMonth: 4, birthDay: 17 }
  3. Branch on result.requiresVerification:
    • true → store token temporarily, route to verify-email UI (do NOT set token yet)
    • falseclient.setCustomerToken(result.token), then await client.syncCartOnLogin(), route to account
  4. On verify-email: collect 6-digit code → client.verifyEmail(code). Offer resend via client.resendVerificationEmail().
  5. After verifyEmail resolves: client.setCustomerToken(result.token), then await client.syncCartOnLogin(), route to account.

Build the verify-email step even if verification is currently disabled; it auto-hides.

Login flow

  1. Collect email + password.
  2. Call loginCustomer:
    const result = await client.loginCustomer(email, password);
  3. Branch on result.requiresVerification:
    • true → route to verify-email
    • falseclient.setCustomerToken(result.token), then await client.syncCartOnLogin(), route to previous page or account
  4. Always offer OAuth buttons from client.getAvailableOAuthProviders(). Render the region even when empty; it auto-populates when a provider is enabled. Hide it while the response's redirectReady is false: the channel then has no registered address to send the sign-in back to (a preview URL on a TEST channel), and the buttons would 400 on click. That is a dashboard setting (Sales Channels → Domains), not code.
  5. Render specific errors (bad credentials, rate limited, disabled). Never swallow them.

Order confirmation flow

  1. Read checkoutId from URL or session.

  2. client.handlePaymentSuccess(checkoutId) is mandatory. It clears the cart so purchased items don't show on the next visit. It is synchronous and returns a plain object, not a promise, so awaiting it is a no-op that only looks like it did something:

    const { cleared, mode, userType, itemsRemoved } = client.handlePaymentSuccess(checkoutId);
    // mode: 'full'    the whole cart went, the normal case
    // mode: 'partial' a partial checkout, so only the purchased lines went.
    //                 `itemsRemoved` counts them and the rest stay in the cart
    // mode: 'none'    nothing to do, because this checkout was already handled
    //                 in this browser session. React Strict Mode runs effects
    //                 twice and a refresh re-runs the page, so this is the
    //                 normal repeat-call answer: SUCCESS, not a failure.
    //                 `cleared` is false here. Never show an error on it.
    // userType: 'guest' | 'customer'

    Branch on mode only for what you render: a 'partial' result means the shopper still has a cart worth linking to. Never gate waitForOrder on cleared.

  3. const result = await client.waitForOrder(checkoutId) polls until the webhook writes the order. result.status.orderNumber / result.status.orderId are available on success.

  4. Show a spinner during step 3 (webhook may lag). On timeout: show "we're still processing, check your email" with a link to order history, where the order WILL appear.

  5. On success: render the order number, or, if your design wants more than that, fetch full details:

const result = await client.waitForOrder(checkoutId);
if (result.success) {
  const order = await client.getOrderByCheckout(checkoutId);
  // order.items, order.shippingAddress, order.notes (the shopper's own
  // order note, read-only), order.subtotal, order.shippingAmount,
  // order.taxAmount / order.taxBreakdown, order.totalAmount
  //
  // order.tenders — gift cards that SETTLED this order, empty or absent when a
  // card or cash paid the whole of it. `totalAmount` is what the order was
  // worth; the amount actually charged is `totalAmount` minus the sum of
  // `tenders[].amountBase`. Render one line per tender and that charged figure,
  // or the receipt claims money the customer never handed over.
}

getOrderByCheckout works for guests too, because possession of the checkout id is the credential, no customer token needed.

Password reset flow

Forgot password step: collect email → client.forgotPassword(email) → always show a generic success message (prevents account enumeration, regardless of whether the account exists).

Reset password step: read token from URL query param → if missing, show error with link back → collect new password → client.resetPassword(token, newPassword) → on success route to login → on expired/invalid token show the specific error with link back.

OAuth flow

  1. Get the list of available provider names:
    const { providers, redirectReady } = await client.getAvailableOAuthProviders();
    // providers = ['GOOGLE', 'FACEBOOK', 'GITHUB'] (strings, not objects)
    // redirectReady = can a sign-in from THIS origin return here? false → render no buttons.
  2. For each provider, get the authorization URL (a 400 with details.code === 'redirect_not_allowed' means this origin is not registered on the channel; show the shopper a localized "not available", never the message):
    const { authorizationUrl } = await client.getOAuthAuthorizeUrl(provider, {
      redirectUrl: `${window.location.origin}/auth/callback`,
    });
    window.location.href = authorizationUrl; // full-page redirect, NOT a popup
  3. On callback, the URL contains auth_code + oauth_success (or oauth_error) query params. Exchange the single-use code for the JWT:
    const params = new URLSearchParams(location.search);
    const code = params.get('auth_code');
    if (code) {
      const result = await client.exchangeOAuthCode(code);
      client.setCustomerToken(result.token);
      await client.syncCartOnLogin(); // REQUIRED — claims the guest cart
      // then redirect to account
    }
    The legacy ?token= URL param is not emitted any more: the redirect carries oauth_success, auth_code and is_new only, unless the platform operator has set CUSTOMER_OAUTH_LEGACY_TOKEN_REDIRECT server-side (off by default, and slated for removal). Never read ?token= from this URL — exchange auth_code instead.
  4. On failure the browser lands on the same redirectUrl (never on the API host), carrying oauth_error + error_description:
    const oauthError = params.get('oauth_error') as OAuthErrorCode | null;
    if (oauthError) {
      // `oauth_error` is a stable snake_case code — switch on it for localized copy.
      // `error_description` is English developer detail; do not show it to shoppers.
      if (oauthError === 'link_blocked_unverified_password_account') {
        router.push('/verify-email'); // a retry will not help — the address needs verifying
      } else {
        router.push(`/login?error=${oauthError}`);
      }
    }
    The code list is open: the provider's own codes (access_denied, …) pass through, so always handle the default case.

Build the OAuth button region AND the callback handler even when no providers are configured.

Inventory reservation flow

  • Display the countdown from cart.reservation?.expiresAt, refreshing once per second (reservation is optional; only present when a reservation strategy is active).
  • On expiry: call client.getCart(cartId) to refresh, or client.smartGetCart() when you are not tracking a cart id yourself. getCart takes the cart id; there is no no-argument form. Items whose reservation expired are flagged server-side.
  • On the checkout page: if reservation has expired, block payment and show "your cart has expired" with a link back to cart.
  • Do NOT implement your own timer logic; the SDK is the source of truth.

Quick Reference - Helper Functions

The SDK exports these utility functions for common UI tasks:

| Function | Purpose | Example | | ---------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------ | | formatPrice(amount, { currency?, locale? }) | Format prices for display | formatPrice("99.99", { currency: 'USD' })$99.99 | | getPriceDisplay(amount, currency?, locale?) | Alias for formatPrice | Same as above | | getDescriptionContent(product) | Get product description (HTML or text) | getDescriptionContent(product) | | isHtmlDescription(product) | Check if description is HTML | isHtmlDescription(product)true/false | | getStockStatus(inventory, opts?) | Human-readable stock status. ⛔ lowStockThreshold defaults to 0, so it NEVER says "Low Stock" until you pass the merchant's value | getStockStatus(inventory, { lowStockThreshold })"Low Stock" | | getProductPrice(product) | Get effective price (handles sales) | getProductPrice(product)29.99 | | getProductPriceInfo(product) | Get price + sale info + discount % (falls back to priceMin when basePrice=0 on VARIABLE) | { price, isOnSale, discountPercent } | | getVariantPrice(variant, basePrice) | Get variant price with fallback | getVariantPrice(variant, '29.99')34.99 | | getCartTotals(cart, shippingPrice?) | Calculate cart subtotal/discount/total | { subtotal, discount, shipping, total } | | getCartItemName(item) | Get name from nested cart item (product + variant) | getCartItemName(item)"Blue T-Shirt - Large" | | getCartItemImage(item) | Get image URL from cart item | getCartItemImage(item)"https://..." | | getVariantOptions(variant) | Get variant attributes as array | [{ name: "Color", value: "Red" }] | | isCouponApplicableToProduct(coupon, product) | Check if coupon applies | isCouponApplicableToProduct(coupon, product) | | isAllowedPaymentUrl(url, options?) | Validate a payment URL host | isAllowedPaymentUrl(intent.clientSecret)true | | safePaymentRedirect(url, options?) | Validate then window.location.href | safePaymentRedirect(intent.clientSecret) | | resolveRenderType(clientSdk, preferred?) | Predict the renderType an intent will come back with (the platform's own rule), so you can pick successUrl before creating it | resolveRenderType(provider.clientSdk, 'iframe')'iframe' | | buildProductJsonLd(product, opts) | schema.org Product JSON-LD (PDPs only) | See SEO section | | buildArticleJsonLd(post, opts) | schema.org Article JSON-LD for blog posts | See SEO section | | buildOrganizationJsonLd(store, opts) | schema.org Organization for the homepage | See SEO section | | buildBreadcrumbJsonLd(items) | schema.org BreadcrumbList | See SEO section | | buildProductFaqJsonLd(product) | schema.org FAQPage from product.faq (null when empty); render the same pairs as visible text | const faq = buildProductFaqJsonLd(product) | | jsonLdScriptProps(data) | XSS-safe <script type="application/ld+json"> props | <script {...jsonLdScriptProps(data)} /> | | getBlogSitemapEntries(client, opts) | Paginate published posts into sitemap entries | See SEO section | | getProductSitemapEntries(client, opts) | ALL published products into sitemap entries (no 100-item clamp) | See SEO section | | getCategorySitemapEntries(client, opts) | Category tree into sitemap entries | See SEO section | | client.resolveSlugRedirect(type, slug) | Renamed slug → current slug (301 support in not-found paths) | See SEO section |

import {
  formatPrice,
  getDescriptionContent,
  getStockStatus,
  getProductPrice,
  getProductPriceInfo,
  getCartTotals,
  getCartItemName,
  getCartItemImage,
} from 'brainerce';

// Format price for display
const priceText = formatPrice(product.basePrice, { currency: 'USD' }); // "$99.99"

// Get product description (handles HTML vs plain text)
const description = getDescriptionContent(product);

// Get stock status text. Pass the merchant's threshold, or it never says
// "Low Stock": the option defaults to 0, which disables the low-stock state.
const caps = await client.getStoreCapabilities();
const lowStockThreshold = caps.connection.lowStockWarning ? caps.connection.lowStockThreshold : 0; // the merchant switched low-stock messaging off
const stockText = getStockStatus(product.inventory, { lowStockThreshold }); // "In Stock", "Low Stock", "Out of Stock"

// Get effective price (handles sale prices automatically)
const price = getProductPrice(product); // Returns number: 29.99

// Get full price info including sale status
const priceInfo = getProductPriceInfo(product);
// { price: 19.99, originalPrice: 29.99, isOnSale: true, discountPercent: 33 }

// Calculate cart totals
const totals = getCartTotals(cart, shippingRate?.price);
// { subtotal: 59.98, discount: 10, shipping: 5.99, total: 55.97 }

// Access cart item details (handles nested structure)
const itemName = getCartItemName(cartItem); // "Blue T-Shirt - Large"
const itemImage = getCartItemImage(cartItem); // "https://..."

⚠️ DO NOT CREATE YOUR OWN UTILITY FILES! All helper functions above are exported from brainerce. Never create utils/format.ts, lib/helpers.ts, or similar files - use the SDK exports directly.


⚠️ CRITICAL: Payment Integration Required!

Your store will NOT work without payment integration. The store owner has already configured payment providers (Stripe/PayPal) - you just need to implement the payment page.

// On your checkout/payment page, ALWAYS call this first:
const { providers } = await client.getPaymentProviders();

// `hasPayments` is always `true` and `providers` is never empty — a store with
// no gateway installed is served a synthetic `sandbox` provider instead of an
// empty list, so do not gate a "not set up" notice on `hasPayments`. The real
// signal that a shopper cannot pay is `createPaymentIntent()` throwing a 503
// with `details.code === 'no_active_payment_provider'`; handle that at the pay
// step, not here.

// Show payment forms for available providers
const stripeProvider = providers.find((p) => p.provider === 'stripe');
const paypalProvider = providers.find((p) => p.provider === 'paypal');

See the Payment Integration section for complete implementation examples.


Quick Start

For Vibe-Coded Sites (Recommended)

import { BrainerceClient } from 'brainerce';

// ✅ salesChannelId (vc_*) is all you need — no API key for storefronts
const client = new BrainerceClient({
  salesChannelId: 'vc_YOUR_SALES_CHANNEL_ID', // found in Brainerce dashboard → Sales Channels
});

// Fetch products
const { data: products } = await client.getProducts();

Product customization fields (buyer input)

Products can expose customizationFields, the merchant-defined inputs the buyer fills on the product page (engraving text, photo upload, select / multi-select options, date pickers, etc.). Render the form from the array, upload any images via uploadCustomizationFile(), then pass values as metadata on add-to-cart. The server validates and snapshots everything onto the order line. Definitions flagged appliesToAllProducts: true are folded into every product's customizationFields automatically, with no client-side merging required.

if (product.customizationFields?.length) {
  // Render a form control per field using field.type (TEXT, SELECT,
  // MULTI_SELECT, IMAGE, GALLERY, DATE, ...) — see the Core Integration guide §2.8
}

// For IMAGE / GALLERY fields: upload first
const { url: photoUrl } = await client.uploadCustomizationFile(file);

await client.addToCart(cart.id, {
  productId: product.id,
  quantity: 1,
  metadata: {
    engraving_text: 'Happy Birthday!',
    frame_color: 'Gold', // SELECT — must be in enumValues
    upload_photo: photoUrl, // IMAGE — URL from uploadCustomizationFile
    addons: ['Gift wrap'], // MULTI_SELECT — always an array
  },
});

Full rendering guide + per-type validation rules: Core Integration §2.8 and Rules & Reference.

Modifier groups (restaurant / build-your-own products)

Products can expose modifierGroups, the merchant-defined option blocks like "Toppings" (max 8, first 3 free) or "Sauce" (pick exactly one). Render radios for selectionType: 'SINGLE' and checkboxes for 'MULTIPLE', honor defaultModifierIds and isDefault on first render, disable modifiers with available: false, and pass selections on add-to-cart. The server is the source of truth for free-allocation and final pricing.

// 5-line add-to-cart with modifiers
await client.addToCart(cart.id, {
  productId: 'prod_pizza',
  quantity: 1,
  selections: [
    { modifierGroupId: 'mg_bread', modifierIds: ['m_thick'] },
    { modifierGroupId: 'mg_toppings', modifierIds: ['m_olive', 'm_mushroom', 'm_bacon'] },
  ],
});

Money on the wire is always strings (priceDelta: "5.00"). Validation failures arrive as a structured 400 envelope on BrainerceError.details with code: 'MODIFIER_VALIDATION_FAILED'; the per-issue list is nested at details.errors[], so from the SDK it reads err.details.details.errors (err.details is the whole response body). See Rules & Reference "Modifier validation errors" for the full code list.

Full rendering guide: Core Integration §2.9. Restaurant features (scheduled availability, nested combos to depth 3, downsell modifiers): Optional Features "Restaurant / build-your-own products".

Content (FAQ / Footer / Header / Announcements / Pages)

Merchants edit site chrome and static content in the Brainerce dashboard under Sell → Content; storefronts pick up changes within ~5 minutes. Six types: FAQ, FOOTER, HEADER, ANNOUNCEMENT, RICH_TEXT, PAGE. Every type has 'main' as its universal default key.

// Fetch chrome at the root layout (server component if Next.js)
const [header, footer, announcements] = await Promise.all([
  client.content.header.get('main', locale),
  client.content.footer.get('main', locale),
  client.content.announcement.list(locale),
]);

// FAQ page
const faq = await client.content.faq.get('main', locale);

// Static pages — catch-all route
const page = await client.content.page.getBySlug(params.slug, locale);
if (!page) notFound();

All get / getBySlug return null on 404. Render a hard-coded fallback so the page never crashes when the merchant hasn't seeded yet.

Admin mode (API key) — every call needs an explicit storeId. The reads above (get, list, getBySlug) are storefront APIs; in admin mode they throw and point you at the admin pair below. Admin mode has no ambient store — storeId is only set in storefront mode — and the admin content routes are store-scoped, so omitting it is rejected fail-closed by the store scope guard (403 STORE_SCOPE_REQUIRED). Pass the id of the store your API key is bound to; naming any other store is rejected as cross-tenant.

// Read (drafts included)
const rows = await client.content.listAdmin({ storeId, type: 'FAQ', status: 'DRAFT' });
const row = await client.content.findById('cnt_123', storeId);

// Write
const created = await client.content.faq.create(
  { key: 'shipping', name: 'Shipping FAQ', data: { items: [{ question: '…', answer: '…' }] } },
  storeId
);
await client.content.update('cnt_123', { name: 'Shipping FAQ' }, storeId);
await client.content.publish('cnt_123', storeId);
await client.content.unpublish('cnt_123', storeId);
await client.content.remove('cnt_123', storeId);

Your API key needs the content:read scope for the reads and content:write for the writes.

Security: FAQ.items[i].answer, RICH_TEXT.html, PAGE.html, and Product.description are merchant-authored HTML. The server does NOT pre-sanitize FAQ/RICH_TEXT/PAGE (merchants may embed iframes); Product.description is server-sanitized on write but you still sanitize on render. Product.description may contain <video> and host-locked YouTube/Vimeo <iframe> embeds, so allow those tags (iframe restricted to www.youtube.com / www.youtube-nocookie.com / player.vimeo.com) and add those hosts to your CSP frame-src. ALWAYS sanitize before injecting:

import DOMPurify from 'isomorphic-dompurify';
<div dangerouslySetInnerHTML={{ __html: DOMPurify.sanitize(rawHtml) }} />

The create-brainerce-store scaffold ships ready-made components (<AnnouncementBar>, <SiteHeader>, <SiteFooter>, <FaqSection>, <RichTextBlock>) + a /pages/[slug] catch-all route. Use them rather than rolling your own renderers.

Full guide: Core Integration "Content". Advanced patterns (channel scoping, custom fields, translations, admin writes): Optional Features "Content". Validation + sanitize rules: Rules & Reference "Content".

Blog

Merchants publish blog posts in Content → Blog. Storefronts choose their own URL scheme: render posts at /blog/[slug], /articles/[slug], or whatever fits the brand.

// List published posts (storefront / vibe-coded mode)
const { data: posts, meta } = await client.blog.getPosts({ page: 1, limit: 10 });

// Filter by category or tag
const { data: news } = await client.blog.getPosts({ category: 'news' });
const { data: tips } = await client.blog.getPosts({ tag: 'tutorial' });

// Fetch one by slug — returns null on 404 (storefront / vibe-coded mode)
const post = await client.blog.getPost(params.slug);
if (!post) notFound();

Admin mode (API key) — every call needs an explicit storeId. Admin mode has no ambient store (storeId is only set in storefront mode) and the admin blog routes are store-scoped, so omitting it is rejected fail-closed by the store scope guard (403 STORE_SCOPE_REQUIRED). Pass the id of the store your API key is bound to; naming any other store is rejected as cross-tenant. Admin lookups are by id, not by sluggetPost(slug) is a storefront read and throws in admin mode.

// Read (drafts included)
const { data, meta } = await client.blog.getPosts({ page: 1, limit: 10 }, storeId);
const post = await client.blog.findById('post_123', storeId); // null on 404

// Write
const draft = await client.blog.create({ title: 'Hello World' }, storeId);
await client.blog.update('post_123', { title: 'Renamed' }, storeId);
await client.blog.publish('post_123', storeId);
await client.blog.unpublish('post_123', storeId);
await client.blog.remove('post_123', storeId);

Your API key needs the blog:read scope for the reads and blog:write for the writes.

Security: post.content is merchant-authored HTML. Always sanitize before rendering:

import DOMPurify from 'isomorphic-dompurify';
const safeHtml = DOMPurify.sanitize(post.content);
return <div dangerouslySetInnerHTML={{ __html: safeHtml }} className="prose" />;

Scheduling: A post is visible once status === 'PUBLISHED' and publishedAt <= now(). Set a future publishedAt when publishing to schedule.

SEO Autopilot writes here too: the platform's SEO Autopilot publishes AI-written articles into this same blog automatically. Render whatever getPosts() returns, and see the SEO section below for the required discoverability pieces.

SEO: JSON-LD builders, sitemap helpers, IndexNow key, llms.txt + agents.md

The SDK ships schema.org builders that encode Google's structured-data rules (aggregateRating gated on reviewCount > 0 with explicit bestRating/worstRating, AggregateOffer with offerCount for VARIABLE products, XSS-safe serialization, and the full availability mapping: InStock from the backend's pre-computed inventory.inStock, BackOrder for purchasable-while-out-of-stock products, OutOfStock otherwise). buildProductJsonLd's Offer also always includes itemCondition (hardcoded NewCondition, for a first-party new-goods catalog), priceValidUntil when the product has an active sale-price window (salePriceEndsAt), and shippingDetails when you pass shipping (real flat-rate/free zones from storeInfo.shipping, omitted entirely and never fabricated if you don't pass it). A 'KIT' product is emitted with no offers block at all, which is deliberate: kits are not published to Google or Meta shopping feeds, so the builder does not advertise a price for one. Do not hand-roll an Offer to add it. Prefer these builders over hand-rolled JSON-LD:

import {
  buildProductJsonLd,        // single-product pages ONLY (never listing pages)
  buildArticleJsonLd,        // blog post pages
  buildOrganizationJsonLd,   // homepage
  buildCollectionPageJsonLd, // category landing pages
  buildBreadcrumbJsonLd,
  buildProductFaqJsonLd,     // FAQPage from product.faq (returns null when empty);
                             // ALWAYS render the same Q&A as visible text on the page
  jsonLdScriptProps,         // XSS-safe <script> props
} from 'brainerce';

// Blog article page
<script {...jsonLdScriptProps(buildArticleJsonLd(post, {
  siteUrl: process.env.NEXT_PUBLIC_SITE_URL!,
  path: `/blog/${post.slug}`,
  organizationName: storeInfo.name,
}))} />

// Product page
<script {...jsonLdScriptProps(buildProductJsonLd(product, {
  siteUrl: process.env.NEXT_PUBLIC_SITE_URL!,
  path: `/products/${product.slug}`,
  currency: storeInfo.currency,
  shipping: storeInfo.shipping, // optional — adds shippingDetails when present
}))} />

Product + category + blog entries in sitemap.xml (required). ⚠️ Products must use getProductSitemapEntries, because the public listing API clamps limit to 100, so a naive getProducts({ limit: 1000 }) sitemap silently truncates at 100 products. The helper uses a dedicated lightweight endpoint (slug + updatedAt + localeSlugs, up to 5000 in one call) and falls back to pagination on older backends:

// app/sitemap.ts
import {
  getProductSitemapEntries,
  getCategorySitemapEntries,
  getBlogSitemapEntries,
} from 'brainerce';

const productPages = await getProductSitemapEntries(client, {
  siteUrl: baseUrl,
  locales: supportedLocales, // optional (multi-locale stores)
  defaultLocale,
}).catch(() => []);
const categoryPages = await getCategorySitemapEntries(client, {
  siteUrl: baseUrl,
  locales: supportedLocales,
  defaultLocale,
}).catch(() => []);
const blogPages = await getBlogSitemapEntries(client, {
  siteUrl: baseUrl,
  locales: supportedLocales,
  defaultLocale,
}).catch(() => []);
return [...staticPages, ...productPages, ...categoryPages, ...blogPages];

robots.txt (required): allow the AI search crawlers by name (OAI-SearchBot, ChatGPT-User, Claude-SearchBot, Claude-User, PerplexityBot, Perplexity-User, Bingbot, Applebot, Amazonbot); they power ChatGPT/Claude/Perplexity/Copilot shopping answers and respect robots.txt. Keep /api/, /auth/, /checkout/, /account/ disallowed.

IndexNow key file (required): the platform pings IndexNow when posts publish; search engines verify by fetching GET /indexnow-key.txt. Serve getStoreInfo().seo.indexNowKey as text/plain, 404 while null. The key is not a secret (public by protocol design).

llms.txt + agents.md (required): /llms.txt is a plain-text site summary (store name, categories, key pages, recent article links) for AI answer engines; /agents.md is the agent-facing guide (machine surfaces, key URLs, currency, how buying works). Multi-locale stores: keep these dotted routes (plus indexnow-key.txt) at the app ROOT, because locale middleware matchers skip dotted paths, so a locale-nested copy serves the homepage HTML instead.

Site verification : when getStoreInfo().seo.googleSiteVerification is set, render <meta name="google-site-verification" content={token} /> in the root layout head (Search Console verification + Merchant Center website claim).

Renamed slugs 301 instead of 404 (required): the platform records every product/blog slug rename. In the not-found path of the product and blog pages call client.resolveSlugRedirect('product' | 'blog', slug). On a hit, permanentRedirect() to the returned currentSlug; null means a genuine 404 (never throws, safe to call unconditionally). Rename chains collapse to one hop.


Common Mistakes to Avoid

AI Agents / Vibe-Coders: Read this section carefully! These are common misunderstandings.

1. Guest Checkout - Use startGuestCheckout() for Guests

For guest users, use startGuestCheckout() which creates a checkout from the session cart:

// ✅ CORRECT - Use startGuestCheckout() for guest users
const result = await client.startGuestCheckout();
if (result.tracked) {
  const checkout = await client.getCheckout(result.checkoutId);
  // Continue with payment flow...
}

// ⚠️ CASH-ON-DELIVERY / SANDBOX ONLY - submitGuestOrder() places the order with
// NO payment collected. Never call it on a store with a payment provider.
const order = await client.submitGuestOrder();

Rule of thumb:

  • Guest user + Session cart → startGuestCheckout()
  • Logged-in user + Server cart → createCheckout({ cartId })
  • Store that collects no money at checkout (cash on delivery, manual invoice, sandbox) → submitGuestOrder()

2. ⛔ NEVER Create Local Interfaces - Use SDK Types!

This causes type errors and runtime bugs!

// ❌ WRONG - Don't create your own interfaces!
interface CartItem {
  id: string;
  name: string; // WRONG - it's item.product.name!
  price: number; // WRONG - prices are strings!
}

// ❌ WRONG - Don't use 'as unknown as' casting!
const item = result as unknown as MyLocalType;

// ✅ CORRECT - Import ALL types from SDK
import type {
  Product,
  ProductVariant,
  Cart,
  CartItem,
  Checkout,
  CheckoutLineItem,
  Order,
  OrderItem,
  CustomerProfile,
  CustomerAddress,
  ShippingRate,
  PaymentProvider,
  PaymentIntent,
  PaymentStatus,
  SearchSuggestions,
  ProductSuggestion,
  CategorySuggestion,
  OAuthAuthorizeResponse,
  CustomerOAuthProvider,
} from 'brainerce';

⚠️ SDK Type Facts - Trust These!

| What | Correct | Wrong | | ------------------------ | ----------------------------- | --------------------- | | Prices | string (use parseFloat()) | number | | Cart item name | item.product.name | item.name | | Order item name | item.name | item.product.name | | Cart item image | item.product.images[0] | item.image | | Order item image | item.image | item.product.images | | Address state/province | region | state or province | | OAuth redirect URL | authorizationUrl | url | | OAuth providers response | { providers: [...] } | [...] directly |

If you think a type is "wrong", YOU are wrong. Read the SDK types!

3. formatPrice Expects Options Object

// ❌ WRONG
formatPrice(amount, 'USD');

// ✅ CORRECT
formatPrice(amount, { currency: 'USD' });

4. Cart/Checkout vs Order - Different Item Structures!

IMPORTANT: Cart and Checkout items have NESTED product data. Order items are FLAT.

// CartItem and CheckoutLineItem - NESTED product
cart.items.forEach((item) => {
  console.log(item.product.name); // ✅ Correct for Cart/Checkout
  console.log(item.product.sku);
  console.log(item.product.images);
});

// OrderItem - FLAT structure
order.items.forEach((item) => {
  console.log(item.name); // ✅ Correct for Orders
  console.log(item.sku);
  console.log(item.image); // singular, not images
});

| Type | Access Name | Access Image | | ------------------ | ------------------- | --------------