# Prices in the shopper's country

A Shopify store can sell in many countries at once, and each group of countries can have its own **price list**: prices the merchant set for that market, not a currency conversion. The same product can cost a different amount, be on sale, be in stock, or not be sold at all depending on where the shopper is.

One jean at one store, in six countries:

| Shopper in | Price |
|---|---|
| United States | 265 USD |
| Canada | 420 CAD |
| United Kingdom | 245 GBP |
| France | 245 EUR |
| Germany | 285 EUR |
| Australia | 445 AUD |

France and Germany both pay in euros and still pay different prices, and Canada's price is not the US price converted. So prices cannot be worked out from a currency; they have to come from the store's price list for that country. Stylor reads every price list a store has, and prices each product for the shopper when you tell it where they are.

## Send the shopper's country

Add `country` to the request: the two-letter ISO 3166-1 alpha-2 code, in any case. On a Shopify storefront it is set on every page as `Shopify.country`, and that is the value to send: it is the store's own decision about which price list the visitor is in, so do not substitute the browser's language or a guess from the IP address.

```javascript
const country = window.Shopify?.country ?? null;

const res = await fetch('https://stylor.ai/api/v2/agent/chat', {
  method: 'POST',
  headers: {
    'Content-Type': 'application/json',
    Authorization: `Bearer ${publicKey}`,
  },
  body: JSON.stringify({ messages, country }),
});
```

`country` is accepted by:

| Endpoint | What it changes |
|---|---|
| `POST /v2/agent/chat` | Every product the agent finds or shows, the sizes it offers, cart-action prices, and sale information. The agent's price limits are in the shopper's currency, and it can search for products on sale. |
| `POST /v2/agent/match` | Every item's product and alternates; one the market does not sell gives way to the next alternate. |
| `POST /v2/agent/looks/complete` | The complete-the-look garments, priced when they are read. |
| `POST /v1/search` | Every result, and the price filter, price scoring and `filters.onSale`, which then compare against the shopper's prices. |

On the v2 endpoints, a `country` that is not a two-letter code is not an error: it is treated as unknown (see "When the shopper's prices are not available" below). `POST /v1/search` rejects one with `422`.

The Stylor storefront embeds already send it. The chat widget (`widget.js`) and the complete-the-look strip (`complete-the-look.js`) read `Shopify.country` from the page themselves.

## What comes back

Products keep their usual shape. The price fields you already read become the shopper's, and a few fields are added to `product`:

| Field | Meaning |
|---|---|
| `price`, `currency` | The shopper's price and its currency. |
| `variants[].price`, `variants[].compareAtPrice`, `variants[].inStock` | Each size's price, its "was" price, and whether it can be bought, in the shopper's market. |
| `market` | Which of the store's price lists this is, for example `us`, `ca` or `international-xof`. |
| `marketExact` | `true` when these are the shopper's own prices. See below for `false`. |
| `marketFallback` | Only when `marketExact` is `false`: why. |
| `available` | `true` when the shopper can buy it: sold in their market, with at least one size in stock there. |
| `onSale` | `true` when a size the shopper can buy is reduced in their market. |
| `discount` | The largest percent off among those sizes, as a whole number. |
| `referencePrice`, `referenceCurrency` | The store's default-market price, whatever the shopper's market. Use these when you add figures up across shoppers. |

The agent only recommends what the shopper can buy: it never searches up, shows, builds a look from or matches a product that is sold out in every size in their market, or not sold there at all. If the shopper asks about one by name, the agent is told it is sold out or not sold where they are, so it can say so rather than claim the store does not carry it. The one place an out-of-stock product can appear is the complete-the-look strip, which shows garments sold in the shopper's market even when they are out of stock there.

On `POST /v1/search`, products the shopper's market does not sell are always left out, and `filters.inStock: true` also leaves out those sold out in every size. That filter is applied while matching, not after, so a store with many old sold-out products still returns a full page of results the shopper can buy.

Cards (match results and complete-the-look garments) carry the same information in their own shape: `price`, `currency`, and when they apply `wasPrice`, `onSale`, `discount`, `market` and `marketExact`.

## Showing a sale

`onSale` and `discount` follow two rules:

- A "was" price equal to the price is not a sale. Some stores fill it in on every product.
- Only sizes the shopper can buy count. A reduced price on a sold-out size is not a sale anyone can have, so a product sold out in every size is never `onSale`.

To show a sale the way a store does, print the price and then the old price struck through:

```javascript
function money(amount, currency) {
  return new Intl.NumberFormat(undefined, { style: 'currency', currency }).format(amount);
}

function priceHtml(product) {
  const was = product.variants?.[0]?.compareAtPrice;
  const now = money(product.price, product.currency);
  return was > product.price
    ? `${now} <s>${money(was, product.currency)}</s>`
    : now;
}
```

`price` is the first size's price, so the "was" price shown beside it is the first size's too. `discount` is the right number for a badge ("Up to 35% off"), since it covers every size the shopper can buy.

## When the shopper's prices are not available

Stylor never shows a converted or guessed price. When it cannot answer for the shopper's market, the product comes back at the store's **default market** prices, with `marketExact: false` and the reason in `marketFallback`:

| `marketFallback` | Why |
|---|---|
| `no-country` | The request did not send `country`. |
| `not-served` | The store does not sell into that country. |
| `no-markets` | The store has one set of prices only: a catalogue uploaded through the API, or a store connected without Shopify's Storefront API. |
| `unread` | The store's last sync could not read that country's prices. It is retried on the next sync. |
| `no-row` | The product, or that price list, is newer than the last sync. |
| `stale` | The store's default market changed and this product has not been refreshed yet. |

Show these prices with their currency, and do not present them as what the shopper will pay in their own country. A store with a single market and a shopper in that market is always exact.

## Good to know

- **Checkout is Shopify's.** The amount a shopper pays is always the one Shopify shows at checkout. The prices here come from the same price lists and are refreshed every time the store syncs.
- **Converted prices can move slightly between syncs.** Where a merchant lets Shopify convert a price instead of setting it, the amount follows exchange rates, so it can differ from checkout by a fraction of a percent until the next sync. Prices the merchant set by hand do not move.
- **Stock can differ by country.** A store that ships each market from its own warehouse can have a size in stock in one country and sold out in another. `inStock` is always the shopper's market's.
- **Store-wide figures stay in the store's own currency.** The price ranges the agent quotes for a whole category come from catalogue statistics, which use the store's default prices, and are labelled with that currency. When the shopper pays in another currency, the agent treats them as a rough guide and gives exact prices from the products themselves.
- **`referencePrice` is for totals.** If you record prices for reporting, record `referencePrice` in `referenceCurrency`, so figures from shoppers in different countries can be added together.
