← Shopify App BuilderCONTENT HISTORYWHAT CHANGED · RULE-BASED ANALYSIS
Update to Shopify App Builder
Snapshot Sep 30, 2026 · 23:13 UTC · version 1.4.1
Collection source: not recorded for this historical snapshot.
First saved snapshot
No earlier snapshot is available to establish a change.
Compare saved observations
Download comparison JSONFull technical diff · 0 changed fields
Full snapshot data
{
"description": "Use when building Shopify B2B catalogs, multi-storefront B2B, company location pricing, wholesale checkout, Shopify Markets (multi-region), multi-currency pricing with @inContext, currency formatting, market-specific catalogs, payment terms (NET 30/60), or anything involving B2B Plus / Markets / international commerce features. Triggers: B2B, wholesale, company, locations, customer accounts B2B, Markets, @inContext, country code, currency code, market, catalog, price list, payment terms, NET 30, draft order B2B, vaulted card, 'shopify b2b', 'shopify markets'.",
"included_files": [],
"name": "b2b-markets",
"skill_md_contents": "---\nname: b2b-markets\ndescription: \"Use when building Shopify B2B catalogs, multi-storefront B2B, company location pricing, wholesale checkout, Shopify Markets (multi-region), multi-currency pricing with @inContext, currency formatting, market-specific catalogs, payment terms (NET 30/60), or anything involving B2B Plus / Markets / international commerce features. Triggers: B2B, wholesale, company, locations, customer accounts B2B, Markets, @inContext, country code, currency code, market, catalog, price list, payment terms, NET 30, draft order B2B, vaulted card, 'shopify b2b', 'shopify markets'.\"\n---\n\n# Shopify B2B + Markets\n\n## When to use this skill\n\nCall this skill when:\n- Merchant is setting up wholesale, B2B, or bulk ordering\n- Building company catalogs, price lists, or location-based pricing\n- Implementing payment terms (NET 30, NET 60, etc.) or vaulted card checkout\n- Handling Shopify Markets setup, multi-region, multi-currency, or multi-language storefronts\n- Using @inContext directive for currency/language-specific Storefront API queries\n- Displaying multi-currency prices in Liquid, handling exchange rates, or market-aware inventory\n- Building customer account UI extensions for B2B contacts\n- Webhook listeners for B2B events: company.created, companyLocation.created, companyContact.created, draftOrder.created\n- Checking which features require B2B Plus plan vs Shopify Markets standard vs core Shopify\n\n## B2B on Shopify: the fundamentals\n\nShopify B2B Plus enables merchants to:\n- Create **companies** (e.g., \"Acme Corp\") with multiple **company locations** (warehouses, branches)\n- Assign **catalogs** (product + pricing rules) to each location\n- Set **price lists** with percentage or fixed discounts, currency-specific pricing, date ranges\n- Define **payment terms** (NET 30, NET 60, custom) and enable **vaulted card** recurring payments\n- Create **draft orders** for company contacts, convert to paid orders with payment terms\n- Use **customer account B2B** login (company contact sign-in) with location-switching UI\n\n**B2B Plus-only features:**\n- Company object and CompanyLocation hierarchy\n- PriceList and Catalog system\n- PaymentTerms (NET 30+, custom days)\n- DraftOrder for B2B checkout\n- Customer Account B2B extensions (login UI, location switching)\n\n**Core Shopify (all plans can do):**\n- Multiple currencies (requires Shopify Markets)\n- Shopify Markets regions, domains, subfolders\n- @inContext for Storefront API (language/country override)\n\n**When to recommend B2B Plus:**\n- Merchant has >2 wholesale partners\n- Needs tiered pricing by location or customer segment\n- Wants self-service company portal (contact management, order history, payment vaults)\n- Needs recurring NET-term invoicing (vaulted cards)\n\n---\n\n## Core B2B objects\n\n### Company\n\nA company record represents a wholesale customer (e.g., a distributor, retailer, or reseller).\n\n```graphql\nquery {\n companies(first: 10) {\n edges {\n node {\n id # gid://shopify/Company/12345\n name # \"Acme Corp\"\n externalId # Sync ID to your CRM\n metafields {\n key\n value\n namespace\n }\n locations(first: 10) {\n edges {\n node {\n id\n name # \"Acme - Chicago\"\n }\n }\n }\n }\n }\n }\n}\n```\n\nMutations:\n```graphql\nmutation {\n companyCreate(input: {\n name: \"Acme Corp\"\n externalId: \"crm-12345\"\n }) {\n company {\n id\n name\n }\n userErrors {\n field\n message\n }\n }\n}\n```\n\n### CompanyLocation\n\nRepresents a specific address/warehouse for a company. Each location can have its own catalog + price list assignment.\n\n```graphql\nquery {\n companyLocationConnection(first: 5) {\n edges {\n node {\n id\n name\n address {\n address1\n city\n province\n zip\n country\n }\n contact {\n title\n lastName\n firstName\n email\n phone\n }\n catalogs(first: 5) {\n edges {\n node {\n id\n title\n }\n }\n }\n }\n }\n }\n}\n```\n\nCreate a location:\n```graphql\nmutation {\n companyLocationCreate(input: {\n companyId: \"gid://shopify/Company/12345\"\n name: \"Acme - Chicago\"\n address: {\n address1: \"123 Main St\"\n city: \"Chicago\"\n province: \"IL\"\n zip: \"60601\"\n country: \"US\"\n }\n }) {\n companyLocation {\n id\n name\n }\n }\n}\n```\n\n### CompanyContact\n\nRepresents a person at a company (authorized buyer, approver, etc.). They log in via Customer Account B2B to place orders.\n\n```graphql\nquery {\n companyContactConnection(first: 10) {\n edges {\n node {\n id\n email\n firstName\n lastName\n role # ADMIN, BUYER, APPROVER\n company {\n id\n name\n }\n companyLocations(first: 5) {\n edges {\n node {\n id\n name\n }\n }\n }\n }\n }\n }\n}\n```\n\nAdd a contact:\n```graphql\nmutation {\n companyContactCreate(input: {\n email: \"buyer@acme.com\"\n firstName: \"John\"\n lastName: \"Doe\"\n companyId: \"gid://shopify/Company/12345\"\n role: BUYER\n }) {\n companyContact {\n id\n email\n }\n }\n}\n```\n\n### Catalog\n\nA catalog is a collection of products available to a company location, with associated prices + discounts.\n\n```graphql\nquery {\n catalogs(first: 5) {\n edges {\n node {\n id\n title\n description\n publishedAt\n status # ACTIVE, ARCHIVED, DRAFT\n companyLocationCount\n priceLists {\n edges {\n node {\n id\n name\n }\n }\n }\n }\n }\n }\n}\n```\n\nCreate a catalog:\n```graphql\nmutation {\n catalogCreate(input: {\n title: \"Wholesale - Acme\"\n description: \"Exclusive products and pricing for Acme Corp\"\n }) {\n catalog {\n id\n title\n }\n }\n}\n```\n\nAssign catalog to locations:\n```graphql\nmutation {\n catalogPublish(input: {\n catalogId: \"gid://shopify/Catalog/999\"\n companyLocationIds: [\n \"gid://shopify/CompanyLocation/111\"\n \"gid://shopify/CompanyLocation/222\"\n ]\n }) {\n catalog {\n id\n publishedAt\n }\n }\n}\n```\n\n### PriceList\n\nDefines pricing rules for a catalog: fixed discounts, percentage reductions, or absolute prices. Currency-specific and date-range aware.\n\n```graphql\nquery {\n priceList(id: \"gid://shopify/PriceList/555\") {\n id\n name\n currency # USD, CAD, EUR, etc.\n parent {\n __typename\n }\n prices(first: 10) {\n edges {\n node {\n id\n variantId: variantId\n amount\n isRelative # true = %, false = absolute\n startsAt\n endsAt\n }\n }\n }\n }\n}\n```\n\nCreate a price list:\n```graphql\nmutation {\n priceListCreate(input: {\n name: \"10% Wholesale Discount - USD\"\n currency: USD\n parent: {\n catalogId: \"gid://shopify/Catalog/999\"\n }\n }) {\n priceList {\n id\n name\n }\n }\n}\n```\n\nAdd prices to the list:\n```graphql\nmutation {\n priceListPriceAddOrUpdate(input: {\n priceListId: \"gid://shopify/PriceList/555\"\n prices: [\n {\n variantId: \"gid://shopify/ProductVariant/111\"\n amount: \"-10\"\n isRelative: true # 10% discount\n }\n {\n variantId: \"gid://shopify/ProductVariant/222\"\n amount: \"99.99\"\n isRelative: false # Absolute USD price\n startsAt: \"2026-06-01T00:00:00Z\"\n endsAt: \"2026-06-30T23:59:59Z\"\n }\n ]\n }) {\n prices {\n id\n }\n }\n}\n```\n\n### PaymentTerms\n\nDefines NET payment options (NET 30, NET 60, COD, etc.) available to a company.\n\n```graphql\nquery {\n paymentTerms(first: 5) {\n edges {\n node {\n id\n name # \"NET 30\"\n translatedName\n dueInDays: dueInDays # 30 for NET 30\n standardTemplate\n }\n }\n }\n}\n```\n\nAssign payment terms to a company:\n```graphql\nmutation {\n companyAssignPaymentTerms(input: {\n companyId: \"gid://shopify/Company/12345\"\n paymentTermsIds: [\n \"gid://shopify/PaymentTerms/NET_30\"\n \"gid://shopify/PaymentTerms/NET_60\"\n ]\n }) {\n company {\n id\n paymentTerms {\n edges {\n node {\n id\n name\n }\n }\n }\n }\n }\n}\n```\n\n### BuyerExperience\n\nDefines checkout behavior (payment methods, vaulted cards, etc.) for B2B customers.\n\n```graphql\nquery {\n buyerExperience(companyLocationId: \"gid://shopify/CompanyLocation/111\") {\n id\n paymentMethods # Vaulted cards, NET terms enabled\n checkoutTouchPoint\n }\n}\n```\n\n---\n\n## Catalog + price list patterns\n\n### Multi-location pricing\n\nAssign the same catalog to multiple locations, each with its own price list (same products, different discounts per location):\n\n```graphql\nmutation {\n # Create base catalog\n catalogCreate(input: {\n title: \"Premium Products\"\n }) {\n catalog {\n id\n }\n }\n}\n\n# Then create 2 price lists (one per currency or region)\nmutation {\n priceListCreate(input: {\n name: \"Premium - USD (5% off)\"\n currency: USD\n parent: { catalogId: \"gid://shopify/Catalog/BASE\" }\n }) {\n priceList { id }\n }\n}\n\nmutation {\n priceListCreate(input: {\n name: \"Premium - CAD (3% off)\"\n currency: CAD\n parent: { catalogId: \"gid://shopify/Catalog/BASE\" }\n }) {\n priceList { id }\n }\n}\n\n# Assign catalog, but each location sees its own currency\nmutation {\n catalogPublish(input: {\n catalogId: \"gid://shopify/Catalog/BASE\"\n companyLocationIds: [\n \"gid://shopify/CompanyLocation/US_HQ\"\n \"gid://shopify/CompanyLocation/CA_HQ\"\n ]\n }) {\n catalog { id }\n }\n}\n```\n\n**Key insight:** PriceList is currency-scoped. If a company location operates in CAD, query the CAD price list. The catalog is location-scoped but currency-agnostic; pricing is attached via the price list.\n\n### Percentage + tiered discounts\n\nUse `isRelative: true` for percentage, `isRelative: false` for absolute:\n\n```graphql\nmutation {\n priceListPriceAddOrUpdate(input: {\n priceListId: \"gid://shopify/PriceList/555\"\n prices: [\n # Tier 1: 5% off\n { variantId: \"v111\", amount: \"-5\", isRelative: true }\n # Tier 2: 10% off (active June 1 - 30)\n {\n variantId: \"v111\"\n amount: \"-10\"\n isRelative: true\n startsAt: \"2026-06-01T00:00:00Z\"\n endsAt: \"2026-06-30T23:59:59Z\"\n }\n # Absolute wholesale price\n { variantId: \"v222\", amount: \"49.99\", isRelative: false }\n ]\n }) {\n prices { id }\n }\n}\n```\n\n---\n\n## B2B mutations: draft orders & payment terms\n\n### Create a draft order for a company contact\n\n```graphql\nmutation {\n draftOrderCreate(input: {\n lineItems: [\n {\n variantId: \"gid://shopify/ProductVariant/111\"\n quantity: 100\n customAttributes: [\n {\n key: \"special_instructions\"\n value: \"Ship to warehouse\"\n }\n ]\n }\n ]\n companyContactId: \"gid://shopify/CompanyContact/buyer1\"\n billingAddress: {\n firstName: \"John\"\n lastName: \"Doe\"\n address1: \"123 Main St\"\n city: \"Chicago\"\n province: \"IL\"\n zip: \"60601\"\n country: \"US\"\n }\n shippingAddress: {\n firstName: \"Warehouse\"\n lastName: \"Receiving\"\n address1: \"456 Dock St\"\n city: \"Chicago\"\n province: \"IL\"\n zip: \"60602\"\n country: \"US\"\n }\n customAttributes: [\n {\n key: \"po_number\"\n value: \"PO-2026-001\"\n }\n ]\n }) {\n draftOrder {\n id\n lineItemsCount\n subtotalPrice {\n amount\n currencyCode\n }\n }\n }\n}\n```\n\n### Complete draft order with payment terms\n\n```graphql\nmutation {\n draftOrderComplete(input: {\n id: \"gid://shopify/DraftOrder/12345\"\n paymentPending: false # Set true for NET terms (invoice model)\n paymentTerms: {\n paymentTermsId: \"gid://shopify/PaymentTerms/NET_30\"\n dueAt: \"2026-07-01T23:59:59Z\"\n }\n }) {\n draftOrder {\n id\n completedAt\n order {\n id\n name\n paymentTerms {\n paymentTermsTemplate\n dueAt\n }\n }\n }\n }\n}\n```\n\n### Order with vaulted card (recurring payment)\n\n```graphql\nmutation {\n orderCreate(input: {\n lineItems: [\n {\n variantId: \"gid://shopify/ProductVariant/111\"\n quantity: 50\n }\n ]\n customer: {\n id: \"gid://shopify/Customer/cust123\"\n }\n paymentTerms: {\n paymentTermsId: \"gid://shopify/PaymentTerms/NET_30\"\n }\n billingAddress: {\n firstName: \"John\"\n lastName: \"Doe\"\n address1: \"123 Main\"\n city: \"Chicago\"\n province: \"IL\"\n country: \"US\"\n }\n shippingAddress: {\n firstName: \"Warehouse\"\n lastName: \"Receiving\"\n address1: \"456 Dock\"\n city: \"Chicago\"\n province: \"IL\"\n country: \"US\"\n }\n }) {\n order {\n id\n name\n paymentTerms {\n dueAt\n paymentTermsTemplate\n }\n }\n }\n}\n```\n\n---\n\n## Customer Account B2B\n\nShopify provides **Customer Account B2B** login UI extension that lets company contacts:\n1. Sign in with email + password\n2. Switch between assigned company locations\n3. View order history and payment status\n4. Place orders with location-specific catalogs\n\n### Implementation\n\nInstall the `customer-account-ui-extensions` API and define a location switcher:\n\n```javascript\n// extensions/location-switcher/src/index.tsx\nimport { useCustomer } from \"@shopify/customer-account-ui-extensions-react\";\n\nexport default function LocationSwitcher() {\n const { customer } = useCustomer();\n\n if (!customer?.companyContact) {\n return <p>Not a B2B contact</p>;\n }\n\n return (\n <div>\n <h2>Company: {customer.companyContact.company.name}</h2>\n <select onChange={(e) => switchLocation(e.target.value)}>\n {customer.companyContact.assignedLocations.map((loc) => (\n <option key={loc.id} value={loc.id}>\n {loc.name}\n </option>\n ))}\n </select>\n </div>\n );\n}\n```\n\nThe Customer Account context automatically adjusts catalog visibility + pricing based on selected location.\n\n---\n\n## Shopify Markets overview\n\nShopify Markets enables multi-region, multi-currency, multi-language, and multi-domain storefronts from a single product catalog.\n\n**Markets vs separate stores:**\n- **One storefront, multiple markets:** Single catalog, region-specific domains/currencies/languages\n- **Separate stores:** Isolated inventory, multiple checkouts, more operational overhead\n- **Recommendation:** Use Markets if you want to share inventory and simplify management; use separate stores if you need full isolation per region\n\n**Key capabilities:**\n- Define regions (US, Canada, EU, Asia, etc.)\n- Assign currencies, languages, and tax settings per region\n- Publish to multiple domains or use subfolders (/en, /fr, etc.)\n- Automatic currency conversion or fixed exchange rates\n- Region-specific shipping and payment methods\n\n---\n\n## Markets data model\n\n### Market\n\nRepresents a regional storefront (e.g., \"Europe - EUR - DE/FR/IT\").\n\n```graphql\nquery {\n markets(first: 5) {\n edges {\n node {\n id\n name\n handle\n enabled\n primary\n regions {\n edges {\n node {\n id\n name\n }\n }\n }\n webPresences {\n edges {\n node {\n id\n domain\n subfolderRoot\n }\n }\n }\n }\n }\n }\n}\n```\n\nCreate a market:\n```graphql\nmutation {\n marketCreate(input: {\n name: \"Europe\"\n handle: \"europe\"\n regions: {\n DE: \"Germany\"\n FR: \"France\"\n IT: \"Italy\"\n }\n }) {\n market {\n id\n name\n }\n }\n}\n```\n\n### MarketRegion\n\nA country or region within a market (e.g., Germany, France).\n\n```graphql\nquery {\n marketRegion(id: \"gid://shopify/MarketRegion/DE\") {\n id\n name\n country\n currency\n taxIncluded\n shippingZones {\n id\n name\n }\n }\n}\n```\n\n### MarketWebPresence\n\nMaps a market to a domain or subfolder (e.g., example.eu for Europe, example.com/fr for France).\n\n```graphql\nquery {\n marketWebPresences(first: 10) {\n edges {\n node {\n id\n domain # example.eu\n subfolderRoot # /en, /fr (if subfolderRoot is set, ignores domain)\n market {\n id\n name\n }\n published\n }\n }\n }\n}\n```\n\nCreate web presence:\n```graphql\nmutation {\n marketWebPresenceCreate(input: {\n marketId: \"gid://shopify/Market/EU\"\n domain: \"example.eu\"\n }) {\n marketWebPresence {\n id\n domain\n }\n }\n}\n```\n\n### MarketCurrencySettings\n\nConfigure auto-conversion or fixed exchange rates.\n\n```graphql\nquery {\n marketCurrencySettingsByCountry(countryCode: \"DE\") {\n id\n currency # EUR\n market { id }\n exchangeRateMode # AUTO, FIXED\n exchangeRate # Only set if FIXED\n }\n}\n```\n\nSet fixed exchange rate:\n```graphql\nmutation {\n marketCurrencySettingsUpdate(input: {\n marketId: \"gid://shopify/Market/EU\"\n countryCode: \"DE\"\n exchangeRateMode: FIXED\n exchangeRate: 0.92 # 1 USD = 0.92 EUR\n }) {\n marketCurrencySetting {\n id\n exchangeRate\n }\n }\n}\n```\n\n### MarketLocalization\n\nSets language preferences per market/region.\n\n```graphql\nquery {\n marketLocalizations(first: 10) {\n edges {\n node {\n id\n market { id }\n locale # de, fr, it\n language # German, French, Italian\n default\n }\n }\n }\n}\n```\n\n---\n\n## @inContext directive: Storefront API currency + language override\n\nWhen querying the Storefront API (client-side), use `@inContext` to fetch prices and content in a specific market/currency/language.\n\n**Exact syntax:**\n```graphql\nquery {\n products(first: 10) @inContext(country: DE, language: DE) {\n edges {\n node {\n id\n title\n variants(first: 5) {\n edges {\n node {\n id\n price {\n amount\n currencyCode # EUR (because country=DE)\n }\n }\n }\n }\n }\n }\n }\n}\n```\n\n**Valid country codes:** US, CA, DE, FR, IT, JP, AU, etc. (ISO 3166-1 alpha-2)\n\n**Valid language codes:** EN, DE, FR, IT, JA, etc.\n\n**What @inContext changes:**\n- Product prices (converted to region currency)\n- Shipping rates (region-specific)\n- Tax calculations\n- Content translations (if available)\n\n**When NOT to use @inContext:**\n- Admin API queries (use normal queries; Admin already knows store context)\n- Server-side commerce context (use currency/country from request headers)\n\n**When to use:**\n- Storefront API (client-side, JavaScript/GraphQL)\n- Multiple currencies on same domain (auto-detect from request headers)\n- Multi-region storefronts (subfolder or domain-based)\n\nExample: auto-detect country from geolocation and fetch prices:\n```javascript\n// client-side\nconst country = geoip.country(request.ip); // \"DE\"\nconst language = country === \"DE\" ? \"DE\" : \"EN\";\n\nconst query = `\n query {\n products(first: 10) @inContext(country: ${country}, language: ${language}) {\n edges {\n node {\n variants {\n priceV2 {\n amount\n currencyCode\n }\n }\n }\n }\n }\n }\n`;\n```\n\n---\n\n## Currency handling in Shopify\n\n### MoneyV2 format\n\nAll prices return as `MoneyV2` objects:\n```graphql\n{\n amount: \"99.99\"\n currencyCode: \"EUR\"\n}\n```\n\nThe `amount` is always a string (to preserve decimal precision). Always parse as `BigDecimal` or similar; avoid floating-point math.\n\n### Liquid money filters\n\nIn theme Liquid templates:\n\n```liquid\n{{ product.variants[0].price | money }}\n{# Output: $99.99 (uses store currency) #}\n\n{{ product.variants[0].price | money_with_currency }}\n{# Output: $99.99 USD #}\n\n{{ product.variants[0].price | money_without_currency }}\n{# Output: 99.99 #}\n\n{{ product.variants[0].price | money_without_trailing_zeros }}\n{# Output: 99.99 (or 100 if ends in .00) #}\n```\n\n### Shopify auto-conversion behavior\n\nIf a market is set to **AUTO** exchange rate mode:\n- Shopify fetches real-time rates daily\n- Prices are converted automatically for display\n- Transactions settle in store currency; customer is charged in market currency\n- Conversion loss/gain is absorbed by merchant\n\nIf **FIXED** exchange rate:\n- You set the rate (e.g., 1 USD = 0.92 EUR)\n- Shopify uses only that rate (no real-time updates)\n- Useful for stability over accuracy\n\n### Currency in apps\n\nIf you're building a Shopify app (not a theme):\n1. **Fetch shop currency** from shop object:\n ```graphql\n query {\n shop {\n currency\n currencyCode # USD, EUR, etc.\n }\n }\n ```\n\n2. **For multi-currency stores**, query available currencies:\n ```graphql\n query {\n shop {\n currencies {\n isoCode\n name\n weight\n }\n }\n }\n ```\n\n3. **Charge app fees** in shop currency only:\n ```graphql\n mutation {\n appSubscriptionCreate(input: {\n returnUrl: \"https://example.com/...\"\n name: \"Silver Plan\"\n price: {\n amount: \"29.99\"\n currencyCode: USD # Always shop currency\n }\n trialDays: 7\n }) {\n appSubscription {\n id\n }\n }\n }\n ```\n\n4. **In webhooks**, check `currencyCode` on prices and amounts. Multi-currency stores may send prices in different currencies depending on market context.\n\n---\n\n## i18n in Liquid\n\n### Translation files\n\nStore translations in `/locales/` (theme root):\n```\nlocales/\n en.default.json\n de.json\n fr.json\n```\n\n**en.default.json:**\n```json\n{\n \"header\": {\n \"menu\": \"Menu\",\n \"search\": \"Search\"\n },\n \"product\": {\n \"add_to_cart\": \"Add to cart\",\n \"price\": \"Price: {{ price }}\"\n }\n}\n```\n\n**de.json:**\n```json\n{\n \"header\": {\n \"menu\": \"Menü\",\n \"search\": \"Suche\"\n },\n \"product\": {\n \"add_to_cart\": \"In den Warenkorb\",\n \"price\": \"Preis: {{ price }}\"\n }\n}\n```\n\n### Using translations\n\n```liquid\n{{ 'header.menu' | t }}\n{# Output: \"Menu\" (if English) or \"Menü\" (if German) #}\n\n{{ 'product.price' | t: price: product.variants[0].price | money }}\n{# Output: \"Price: $99.99\" or \"Preis: 99,99€\" #}\n```\n\n### Detecting language\n\nShopify reads `request.locale` from URL or browser headers:\n\n```liquid\n{% if request.locale.iso_code == 'de' %}\n <p>Willkommen</p>\n{% else %}\n <p>Welcome</p>\n{% endif %}\n```\n\n### Fallback language\n\nIf no translation exists in the requested locale, Shopify falls back to the default language (en.default.json). Ensure all keys exist in the default file.\n\n---\n\n## Markets-aware checkout\n\n### Server-side: detect market from request\n\n```javascript\n// Node.js / Next.js\nexport default async function handler(req, res) {\n const domain = req.headers.host; // example.eu or example.com\n const country = req.headers['cloudflare-ipcountry'] || 'US';\n\n // Query market + currency for this domain\n const market = await getMarketByDomain(domain);\n const currency = market?.currency || 'USD';\n\n // Pass to Storefront API with @inContext\n const products = await storefrontQuery({\n country: market?.countryCode,\n language: market?.locale,\n currency\n });\n\n res.json({ products, currency });\n}\n```\n\n### Client-side: @inContext + checkout\n\n```javascript\n// Fetch cart with market context\nconst cartQuery = `\n query Cart($cartId: ID!) @inContext(country: ${countryCode}, language: ${language}) {\n cart(id: $cartId) {\n cost {\n totalAmount {\n amount\n currencyCode\n }\n }\n lines {\n merchandise {\n price {\n amount\n currencyCode\n }\n }\n }\n }\n }\n`;\n```\n\n### Payment methods per market\n\nIn checkout settings, assign payment methods per region:\n- US: Stripe, PayPal, Shopify Payments\n- EU: Stripe, SEPA Direct Debit, iDEAL, Sofort\n- JP: Stripe, Konbini, Bank Transfer\n\nShopify handles filtering automatically based on request origin (Cloudflare geo-IP).\n\n---\n\n## Common gotchas\n\n### Unfulfillable orders due to wrong catalog\n\n**Problem:** Company contact places order for a product that isn't in their assigned catalog.\n\n**Cause:** Catalog assignment is stale; location's price list doesn't include the variant.\n\n**Fix:** Always validate against the location's active catalog before showing checkout:\n```graphql\nquery {\n companyLocationCatalogAssignments(companyLocationId: \"loc123\") {\n catalogs {\n id\n priceLists {\n prices(variantId: \"var456\") {\n id\n amount\n }\n }\n }\n }\n}\n```\n\n### Currency rounding\n\nShopify uses **bankers' rounding** for some internal calculations (round-to-even). If you're doing your own math:\n- Store amounts as strings or BigDecimal, NOT floats\n- 99.995 rounds to 100.00 (round-to-even)\n- Always respect the 2-decimal-place format; don't truncate\n\n### Missing payment_terms webhooks\n\n**Problem:** You set `paymentTerms` on an order, but no webhook fires.\n\n**Cause:** The `orders/create` webhook doesn't always include `payment_terms` in the payload.\n\n**Fix:** Query the order directly to get payment terms:\n```graphql\nquery {\n order(id: \"gid://shopify/Order/123\") {\n id\n paymentTerms {\n paymentTermsId\n paymentTermsTemplate\n dueAt\n }\n }\n}\n```\n\n### Scope requirements for B2B\n\nYour app must request these scopes:\n```toml\n# shopify.app.toml\nscopes = [\n \"write_products\",\n \"read_companies\", # Read B2B company data\n \"write_companies\", # Create/update companies\n \"read_orders\",\n \"write_draft_orders\",\n \"read_customer_payment_methods\"\n]\n```\n\nWithout `read_companies` / `write_companies`, you cannot access B2B objects.\n\n### Markets webfront detection\n\nIf a request comes from your Markets storefront, Shopify adds a header:\n```\nX-Shop-Market-Id: gid://shopify/Market/eu-market\n```\n\nUse this to detect which market the user is browsing and apply locale/currency accordingly.\n\n---\n\n## Decision tree\n\n| Merchant says... | Do this |\n|---|---|\n| \"I need to sell wholesale at different prices by location\" | Set up Company > CompanyLocation > Catalog > PriceList hierarchy. Assign same catalog, different price lists per currency/location. |\n| \"I want NET 30 invoicing for my B2B customers\" | Enable B2B Plus, assign PaymentTerms to Company, use draftOrderComplete with paymentTerms (paymentPending: true). |\n| \"I sell in 5 countries with different currencies\" | Use Shopify Markets. Create one Market per region, assign currencies + domains/subfolders. Use @inContext in Storefront queries. |\n| \"My theme shows wrong prices for different visitors\" | Add @inContext(country: AUTO_DETECT) to Storefront queries, or detect country server-side and pass to Storefront API. |\n| \"I need my B2B contacts to log in and switch locations\" | Install Customer Account B2B extension. It automatically detects companyContact + assigned locations and provides UI for switching. |\n| \"Exchange rates are wrong; I want to lock them\" | Set MarketCurrencySettings to FIXED mode and specify the exchange rate. Shopify won't auto-convert. |\n| \"I'm getting duplicate products in my Markets setup\" | Markets share the same product catalog. If a product has variant overrides per market, use PriceList (not separate SKUs). |\n| \"Vaulted card payments aren't working for my B2B customer\" | Ensure customer has a stored payment method (vault) + company has PaymentTerms assigned + draftOrder is completed with paymentTerms payload. |\n\n---\n\n## Worked recipes\n\n### Recipe 1: Show wholesale-only products to logged-in B2B contacts\n\n**Scenario:** You have a \"Wholesale Only\" collection that should only be visible to company contacts.\n\n```graphql\n# Liquid template\n{% if customer and customer.email %}\n {% assign is_b2b = customer.metafields.custom.is_company_contact.value %}\n\n {% if is_b2b %}\n <!-- Show wholesale collection -->\n {% for product in collections.wholesale_only.products %}\n <div>\n <h3>{{ product.title }}</h3>\n <p>{{ product.variants[0].price | money }}</p>\n </div>\n {% endfor %}\n {% else %}\n <p>This collection is for wholesale partners only. Contact us for access.</p>\n {% endif %}\n{% else %}\n <p>Please sign in to view wholesale pricing.</p>\n{% endif %}\n```\n\n**In the app:** Set the metafield when a company contact is created:\n```graphql\nmutation {\n companyContactCreate(input: {\n email: \"buyer@acme.com\"\n companyId: \"gid://shopify/Company/12345\"\n metafields: [\n {\n namespace: \"custom\"\n key: \"is_company_contact\"\n value: \"true\"\n type: \"boolean\"\n }\n ]\n }) {\n companyContact {\n id\n metafields {\n value\n }\n }\n }\n}\n```\n\n### Recipe 2: Multi-currency price display using @inContext\n\n**Scenario:** Single domain, multiple currencies. Detect visitor country and show prices in their currency.\n\n**Storefront API query (client-side):**\n```javascript\nconst detectCountry = async (request) => {\n const cfCountry = request.headers.get('cf-ipcountry') || 'US';\n return cfCountry;\n};\n\nconst fetchProducts = async (countryCode) => {\n const query = `\n query {\n products(first: 10) @inContext(country: ${countryCode}) {\n edges {\n node {\n id\n title\n variants(first: 3) {\n edges {\n node {\n id\n title\n price {\n amount\n currencyCode\n }\n }\n }\n }\n }\n }\n }\n }\n `;\n\n const response = await fetch('https://example.myshopify.com/api/2026-01/graphql.json', {\n method: 'POST',\n headers: {\n 'Content-Type': 'application/json',\n 'X-Shopify-Storefront-Access-Token': 'your-token'\n },\n body: JSON.stringify({ query })\n });\n\n return response.json();\n};\n\n// On page load\nconst country = await detectCountry(request);\nconst { data } = await fetchProducts(country);\n// data.products[0].variants[0].price will be in the country's currency\n```\n\n**Liquid (fallback):**\n```liquid\n<div class=\"product\">\n <h2>{{ product.title }}</h2>\n <p class=\"price\">\n {{ product.variants[0].price | money }}\n <span class=\"currency\">({{ shop.currency }})</span>\n </p>\n</div>\n```\n\n### Recipe 3: Webhook listener for new company contacts; provision in your app\n\n**Scenario:** When a company contact is created in Shopify, you want to sync them to your CRM or user system.\n\n**Webhook registration (in your app setup):**\n```graphql\nmutation {\n webhookSubscriptionCreate(topic: COMPANY_CONTACT_CREATED, webhookSubscription: {\n callbackUrl: \"https://yourapp.com/webhooks/company-contact-created\"\n format: JSON\n }) {\n webhookSubscription {\n id\n topic\n endpoint {\n __typename\n }\n }\n }\n}\n```\n\n**Webhook handler (Node.js):**\n```javascript\nimport crypto from 'crypto';\n\nexport default async function handler(req, res) {\n const { body } = req;\n const hmac = req.headers['x-shopify-hmac-sha256'];\n const topic = req.headers['x-shopify-topic'];\n\n // Verify webhook authenticity\n const message = body;\n const hash = crypto\n .createHmac('sha256', process.env.SHOPIFY_WEBHOOK_SECRET)\n .update(message, 'utf8')\n .digest('base64');\n\n if (hash !== hmac) {\n return res.status(401).json({ error: 'Unauthorized' });\n }\n\n if (topic === 'company_contacts/create') {\n const companyContact = JSON.parse(body);\n\n // Provision in your CRM\n await provisionInCRM({\n email: companyContact.email,\n firstName: companyContact.first_name,\n lastName: companyContact.last_name,\n companyId: companyContact.company_id,\n shopifyId: companyContact.id\n });\n\n // Send welcome email\n await sendWelcomeEmail(companyContact.email, {\n company: companyContact.company,\n role: companyContact.role\n });\n }\n\n res.json({ ok: true });\n}\n```\n\n**Related webhooks to subscribe:**\n- `company_contacts/create`\n- `company_contacts/update`\n- `companies/create`\n- `company_locations/create`\n- `draft_orders/create` (for order tracking)\n- `orders/create` (to capture payment terms settlement)\n\n---\n\n## Resources and references\n\n- **B2B Plus overview:** https://shopify.dev/docs/apps/b2b-beyond (via Shopify Admin)\n- **Company GraphQL:** https://shopify.dev/docs/api/admin-graphql/2026-01/objects/Company\n- **PriceList API:** https://shopify.dev/docs/api/admin-graphql/2026-01/objects/PriceList\n- **Draft Orders:** https://shopify.dev/docs/api/admin-graphql/2026-01/mutations/draftOrderCreate\n- **Shopify Markets:** https://shopify.dev/docs/apps/markets (multi-region setup)\n- **@inContext directive:** https://shopify.dev/docs/api/storefront/2026-01#directive-inContext\n- **Customer Account B2B:** https://shopify.dev/docs/apps/customer-accounts/b2b\n- **Liquid i18n:** https://shopify.dev/docs/themes/architecture/localization\n- **Webhook topics:** https://shopify.dev/docs/api/admin-rest/2026-01#webhook_topics\n- **Admin API scopes:** https://shopify.dev/docs/api/admin-rest/2026-01#api_access_scopes\n\n---\n\n## Summary for app developers\n\n**B2B Plus + Markets = powerful international B2B:**\n1. Use **Companies + Locations + Catalogs + PriceLists** for location-aware wholesale pricing\n2. Use **PaymentTerms + vaulted cards** for NET invoicing and recurring payments\n3. Use **Shopify Markets** for multi-region, multi-currency, multi-language support\n4. Use **@inContext** in Storefront API queries to fetch prices in the customer's market currency\n5. Use **Customer Account B2B** extensions for company contact login + location switching\n6. Listen to B2B webhooks to keep your CRM, ERP, and fulfillment systems in sync\n7. Always validate catalogs before fulfillment; currency mismatches and rounding errors are the main gotchas\n\nThis skill covers the full B2B + Markets stack as of May 2026. Refer to https://shopify.dev for the latest API versions and feature updates."
}SHA-256 of public snapshot: 7fd5121ba7da4c43afc692212ec51b4347fbc204c773116fdd1618cca74514e5