← Shopify App BuilderCONTENT HISTORY

Update to Shopify App Builder

Snapshot Sep 30, 2026 · 23:13 UTC · version 1.4.1

Collection source: not recorded for this historical snapshot.

WHAT CHANGED · RULE-BASED ANALYSIS

First saved snapshot

No earlier snapshot is available to establish a change.

Compare saved observations

Download comparison JSON
Full 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