The Storefront API’s @inContext directive sets the country and language for a single GraphQL request. @inContext(country: CA, language: FR) returns Canadian dollar prices and French translations, and drops products that are not published to that market. The context applies to the whole query. Carts are the exception: they ignore the country argument and read buyerIdentity instead.
Everything below was written against Storefront API version 2026-07, the latest stable version as of September 2026 and accessible until 16 July 2027.
What @inContext does to a Shopify Storefront API query
Four optional arguments, per the Storefront API directives reference and the contextual queries guide.
country takes a CountryCode and drives pricing. language takes a LanguageCode, and has been supported since 2022-04. buyer takes a BuyerInput in 2024-04 and later, where customerAccessToken is required and companyLocationId is optional, which is how a B2B company location’s prices get applied. visitorConsent arrived in 2025-10 and takes four optional booleans, analytics, preferences, marketing and saleOfData; the consent you pass is encoded into the checkoutUrl the cart gives you.
They combine, and the context reaches every subfield of the query. Pass them as variables so one document serves every market:
query ProductForMarket($handle: String!, $country: CountryCode, $language: LanguageCode)
@inContext(country: $country, language: $language) {
localization {
country {
isoCode
currency {
isoCode
symbol
}
}
language {
isoCode
}
}
product(handle: $handle) {
title
description
priceRange {
minVariantPrice {
amount
currencyCode
}
}
}
}
Two side effects are easy to miss. The API filters the catalogue: a product the merchant has unpublished from that market’s catalogue “will behave just like they were archived or deleted”, so product(handle: ...) returns null and the item is absent from the products connection. And the response reports the context it used, under extensions:
{
"extensions": {
"context": {
"country": "CA",
"language": "EN"
}
}
}
Check that block first when a price looks wrong.
Why does the cart ignore my country context?
Cart context is stored on the cart itself, set once and carried until something changes it. The guide is blunt about it: “In Cart queries and mutations the buyer and country arguments for @inContext are ignored.” The language and visitorConsent arguments still apply, so a cartCreate can come back with translated content and the wrong currency if the directive is all you set.
Put the country in the input:
mutation CreateCartForCountry($lines: [CartLineInput!]!, $countryCode: CountryCode!, $language: LanguageCode)
@inContext(language: $language) {
cartCreate(input: {lines: $lines, buyerIdentity: {countryCode: $countryCode}}) {
cart {
id
checkoutUrl
buyerIdentity {
countryCode
}
cost {
subtotalAmount {
amount
currencyCode
}
totalAmount {
amount
currencyCode
}
}
lines(first: 50) {
nodes {
id
cost {
amountPerQuantity {
amount
currencyCode
}
compareAtAmountPerQuantity {
amount
currencyCode
}
}
}
}
}
userErrors {
field
message
}
}
}
buyerIdentity.countryCode contextualises variant prices, confirms the products are published for that country, and sets the currency of the cart’s estimated cost. The reference states the constraint: the buyer’s country “determines international pricing and should match their shipping address”.
One trap sits in Shopify’s international pricing guide itself, which still queries estimatedCost. That field is deprecated on Cart. Use cost, which is a CartCost with subtotalAmount, totalAmount, checkoutChargeAmount and the two booleans subtotalAmountEstimated and totalAmountEstimated; on a line, cost is a CartLineCost with amountPerQuantity, compareAtAmountPerQuantity, subtotalAmount and totalAmount. Our walk through cart creation and the checkout handoff covers the rest of that surface.
Why do prices come back in the shop’s currency anyway?
Usually because the currency is not switched on. The international pricing guide states it plainly: “You need to manually enable each country’s currency in a Shopify store’s payment settings before you can create a query with different country contexts. Any queries for countries that aren’t enabled will default to the store currency.” No error, no warning. Just the shop’s currency where you expected euros.
Scopes are the other half of that checklist. The same guide asks for unauthenticated_read_product_listings, unauthenticated_read_customers and unauthenticated_write_checkouts.
Past orders do not move either. “Order information is returned in the context that it was created”, so an order placed in USD stays in USD inside a French context. An order history will therefore come back with mixed currencies, which means formatting every amount with the currencyCode sitting next to it instead of the one the active context implies.
How do you build the country and language selector?
With the localization query, which returns the shop’s own configuration instead of a list you maintain by hand:
query LocalizationOptions @inContext(country: GB, language: EN) {
localization {
availableCountries {
isoCode
name
unitSystem
currency {
isoCode
symbol
}
defaultLanguage {
isoCode
endonymName
}
availableLanguages {
isoCode
endonymName
}
}
}
}
availableCountries lists every country with a localised experience enabled, each carrying its currency (isoCode, name, symbol), unitSystem, defaultLanguage and its own availableLanguages. The top-level availableLanguages field is narrower: it covers the active country only. endonymName gives you “Français”, which is the label that belongs in a picker.
Then note the deprecation. market is marked deprecated on both Localization and Country in 2026-07, per the localization query reference. Persist the country and language codes against a visitor’s session, because anything you key on a market id is sitting on a deprecated field.
Switching country without rebuilding the cart
A shopper who changes country needs the existing cart re-priced:
mutation SwitchCartCountry($cartId: ID!, $countryCode: CountryCode!) {
cartBuyerIdentityUpdate(cartId: $cartId, buyerIdentity: {countryCode: $countryCode}) {
cart {
id
checkoutUrl
cost {
totalAmount {
amount
currencyCode
}
}
}
userErrors {
field
message
}
}
}
Prices, currency and checkoutUrl come back updated. Buyer identity also “assures that all products are published for the given country”. The lines you get back are not guaranteed to match the lines you sent, so render the cart the mutation returns rather than the copy you were holding.
Caching is where a multi-market storefront gets expensive. Responses vary by country and language, so any shared cache in front of the Storefront API needs both values in its key, or one market will be served another market’s prices. Hydrogen handles the query half: pass i18n to createHydrogenContext and it injects $country and $language into queries that declare those variables. The skeleton template ships i18n: {language: 'EN', country: 'US'} hard-coded, with a comment pointing at detection from the URL path, cookies or another strategy. That detection is the part you own. Our Hydrogen cache layers write-up covers what sits underneath.
If you are calling the API directly, the wrapper is small:
type Locale = {country: string; language: string};
export async function storefrontQuery<T>(
query: string,
variables: Record<string, unknown>,
locale: Locale,
): Promise<T> {
const response = await fetch(
`https://${process.env.SHOP_DOMAIN}/api/2026-07/graphql.json`,
{
method: 'POST',
headers: {
'Content-Type': 'application/json',
'X-Shopify-Storefront-Access-Token': process.env.STOREFRONT_TOKEN!,
},
body: JSON.stringify({
query,
variables: {...variables, country: locale.country, language: locale.language},
}),
},
);
if (!response.ok) {
throw new Error(`Storefront API ${response.status}`);
}
const {data, errors} = await response.json();
if (errors) {
throw new Error(errors[0].message);
}
return data as T;
}
What I would do
Declare $country and $language on every product, collection and page query from the first commit, even for a single-market store. Retrofitting the directive across a query library once the merchant adds their second market is tedious work with no visible result. Set buyerIdentity.countryCode at cartCreate, update it with cartBuyerIdentityUpdate when the selector changes, and read extensions.context while developing.
Skip all of it if the store sells in one currency to one country and has no plan to change. With no directive you get the primary market’s catalogue and currency, which is what a single-market store needs.
Whoooop builds headless and themed Shopify storefronts for UK merchants, including the currency, translation and catalogue wiring that follows a Markets rollout. If a multi-market build is on your roadmap, our Shopify development work is the place to start.