Skip to content
decodecodeveloper docs
Storefront → Templates → Commerce

VTEX gotchas

Cookies, sales channel, expectedOrderFormSections, regionId, IS sort sanitization, README drift.

A grab-bag of VTEX-specific gotchas — things that are subtly wrong by default and bite in production. Treat this as a checklist when integrating.

Cookies

vtex_segment must round-trip to upstream

VTEX uses vtex_segment to convey region (CEP-driven), sales channel, and currency. If you call VTEX without forwarding this cookie:

  • Server-side renders show a default region.
  • Client-side hydration shows the user's region.
  • The two disagree → hydration mismatch, wrong availability, wrong prices.

vtexFetch auto-forwards vtex_segment only when the caller didn't supply a cookie header. If you supply your own cookie in init.headers, you also have to include vtex_segment yourself.

For legacy catalog calls (which don't pass through vtexFetch), wrap a custom region-aware fetch — see Region-aware fetch.

IS session cookies (vtex_is_session, vtex_is_anonymous)

These cookies are managed by middleware (propagateISCookies), not by individual actions. The framework strips them from outbound fetch calls so they don't get duplicated in the cache key. If you write a custom action and forget to use vtexFetchWithCookies, you can end up with stale IS cookies that break personalization.

HttpOnly cookies (auth)

The VTEX auth cookie is HttpOnly — JavaScript cannot read it. useUser infers logged-in state via /api/sessions, not from cookies. If your auth proxy (custom domain, CDN) strips cookies on the path that /api/sessions sits on, the user appears logged out.

Sales channel

Default is "1"

Many VTEX accounts use multiple sales channels (B2C vs B2B, by region, by store). The default fallback is "1". If your data shows wrong availability or wrong prices on PDP, the most common cause is salesChannel being "1" when the user's region needs "2".

Sources of truth in priority order:

  1. URL ?sc=N (override).
  2. VTEXSC cookie.
  3. Configured default in the deco-vtex block.

useCart reads VTEXSC from document.cookie for browser calls. Server-side loaders use RequestContext to read the cookie.

intelligentSearch requires sc

The IS query layer always appends sc=N to upstream URLs. If sc doesn't match the catalog's published sales channel, IS returns empty results.

expectedOrderFormSections

The cart action and useCart must request the same set of orderForm sections. The canonical list:

// vtex/actions/checkout.ts
export const DEFAULT_EXPECTED_SECTIONS = [
  "items",
  "totalizers",
  "clientProfileData",
  "shippingData",
  "paymentData",
  "marketingData",
  // ... see source for the full list
];

useCart mirrors this list in client code. If you add a section server-side (e.g. customData) you must add it to the client list too — otherwise updates to that section won't refetch on the client.

If your minicart shows stale data despite a successful add-to-cart, this is the first thing to check.

Region ID from segment

Intelligent Search needs regionId for region-aware availability. The framework extracts it from the vtex_segment cookie via extractRegionIdFromCookies (vtex/client.ts).

If vtex_segment is missing, IS falls back to the account's default region — which may be different from the user's CEP. The fix: ensure your shipping calculator sets the segment cookie (via editSession({ items: [{ key: "country", value: "BRA" }, { key: "postalCode", value: "01310-100" }] })).

IS sort sanitization

VTEX Intelligent Search rejects unknown sort values with a 400. Common cases that cause 400s:

  • Mixed-case sort (e.g. Price:Asc) — IS expects lowercase.
  • Non-IS sort names from legacy catalog (e.g. OrderByTopSaleDESC) — these don't translate.
  • Empty string — accepted but not always.

The inline-loader layer enforces a whitelist via VALID_IS_SORTS:

import { VALID_IS_SORTS } from "@decocms/apps-vtex/utils/intelligentSearch";
 
if (!VALID_IS_SORTS.includes(sort)) sort = "";

If you build a custom PLP and bypass the inline loader, replicate this check.

Empty orderForm pollution

/api/checkout/pub/orderForm creates an empty orderForm if no orderFormId cookie exists. Calling this on every anonymous visit fills your VTEX account with empty carts.

The SSR minicart inline loader guards against this:

const orderFormId = request.headers.get("cookie")?.match(/checkout\.vtex\.com__orderFormId=([^;]+)/)?.[1];
if (!orderFormId) return EMPTY_MINICART; // don't create

If you write custom cart code, replicate this guard. For client-side useCart, the hook is OK to call eagerly — it'll just create one orderForm per session, not per visit.

Slug cache

VTEX returns the same product under multiple slugs (canonical + variants). The framework's slugCache (vtex/utils/slugCache.ts) maps slug → canonical SKU so PDP loaders skip a round-trip when they've seen the slug before.

If a product's slug changes in admin, the cached entry can serve the old SKU until eviction. Workaround: bust the slug cache on publish (manual operations endpoint) or accept the eventual consistency.

README drift

@decocms/apps-vtex/invoke does not exist.

The @decocms/apps-* README references @decocms/apps-vtex/invoke, but it's not in package.json exports. Don't try to import from this path.

The real invoke surface is:

  1. /deco/invoke/... HTTP routes (handled automatically by the framework).
  2. The generated invoke.gen.ts client in your site's src/server/cms/.
  3. The createUseCart / createUseUser / createUseWishlist factories if you need legacy hook shapes.

The README needs to be updated. We've linked to this gotcha from the Commerce overview too.

Hydration mismatches in commerce UI

Three common patterns and their fixes:

Cart badge count

  • Symptom: Badge shows "0" briefly, then jumps to actual count.
  • Cause: SSR doesn't have orderForm cookie context; client fetches after hydration.
  • Fix: Use the SSR minicart inline loader to compute the initial count server-side.

Logged-in state

  • Symptom: "Sign in" link briefly visible, then swaps to user menu.
  • Cause: SSR can't read HttpOnly auth cookie; client fetches /api/sessions after hydration.
  • Fix: Render a neutral placeholder during SSR, swap on hydration. Don't mark this as a hydration mismatch — it's an intentional client-only render.

Wishlist heart state

  • Symptom: Heart renders empty in SSR, fills in on hydration.
  • Cause: Wishlist is loaded client-side via useWishlist.
  • Fix: If wishlist is critical above the fold, fetch via SSR loader. Otherwise accept the brief flicker.

See also