# Google Shopping Scraper — Offers, Price History & Deals (`foxlabs/google-shopping-scraper`) Actor

Google Shopping products by keyword or product ID: price, store, rating, and per product the store offers Google shows (direct links, stock, delivery, returns) with each store's daily price history (up to 12 months in the US, DE, FR) and Google's typical price range.

- **URL**: https://apify.com/foxlabs/google-shopping-scraper.md
- **Developed by:** [Berkan Kaplan](https://apify.com/foxlabs) (community)
- **Categories:** E-commerce, Marketing
- **Stats:** 2 total users, 1 monthly users, 100.0% runs succeeded, 0 bookmarks
- **User rating**: No ratings yet

## Pricing

from $2.00 / 1,000 products

This Actor is paid per event. You are not charged for the Apify platform usage, but only a fixed price for specific events.

Learn more: https://docs.apify.com/actors/running/actors-in-store.md#pay-per-event

## What's an Apify Actor?

An Actor is a serverless cloud program that runs on the Apify platform. It has two run modes.
In Batch mode, an Actor accepts a well-defined JSON input, performs an action which can take anything from a few seconds to a few hours,
and optionally produces a well-defined JSON output, datasets with results, or files in key-value store.
In Standby mode, an Actor provides a web server which can be used as a website, API, or an MCP server.

Apify vocabulary and the platform model are defined once, in the agent quickstart at https://apify.com/agents.md.

## How to integrate an Actor?

If asked about integration, you help developers integrate Actors into their projects.
You adapt to their stack and deliver integrations that are safe, well-documented, and production-ready.

Do not guess an integration path. Every one of them is in the agent quickstart at https://apify.com/agents.md: the Apify MCP server, Agent Skills with the Apify CLI, the JavaScript and Python clients, the REST API, and the account-free path for an agent with no human to sign in. It also carries the rule on stating cost before the first paid run.

For examples already wired to this Actor's own input schema, see the [API](#api) section below.

Each client library has reference documentation the quickstart does not restate: [JavaScript/TypeScript](https://docs.apify.com/api/client/js/docs.md) (`npm install apify-client`) and [Python](https://docs.apify.com/api/client/python/docs.md) (`pip install apify-client`).

# README

## Google Shopping Scraper — Offers, Price History & Deals

Get the products Google Shopping shows for any search, in 26 countries (15 of them tested on the Apify platform): **price, store, rating, review count, discount badges** — and, for each product, what Google's own product page shows: **the store offers Google lists (typically 3–5) with store links, price, stock, delivery cost and returns**, **each store's daily price history for up to about 12 months**, and **Google's typical price range**. One row per product, in one run.

Already know the products? Give their **Google product IDs** (from an earlier run) and re-check offers and prices on a schedule.

No Google account, no API key, no browser. Requests go through Apify's Google SERP proxy.

### Quick start (API)

```bash
curl -X POST "https://api.apify.com/v2/acts/foxlabs~google-shopping-scraper/run-sync-get-dataset-items?token=YOUR_APIFY_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{"queries": ["sony wh-1000xm5"], "country": "us", "maxProductsPerQuery": 5}'
```

### What you get

| Group | Fields |
|---|---|
| Product (search result) | `position`, `title`, `productId` (Google's product ID — use it to re-check the product later), `catalogId`, `price`, `priceText`, `currency`, `oldPrice`, `priceNote` (Google's note next to the price: "Usually $225", or the store's own-currency price such as "(€278)" when Google converted it), `unitPriceText`, `merchant`, `moreOffers` (Google shows "& more" stores), `condition` ("Refurbished"), `deliveryText`, `returnsText`, `rating`, `reviewsCount`, `reviewsCountText` ("17K"), `badge` ("25% OFF", "LOW PRICE"), `imageUrl` |
| Store offers | `offersCount`, `offers[]` — per store: `merchant`, `merchantDomain`, `url` (the seller's link, Google's tracking parameters removed), `price`, `oldPrice`, `discountText`, `deliveryText`, `deliveryCost`, `totalPrice` (price + delivery when the delivery label gives the cost), `returnsText`, `stockText`, `inStock`, `condition` (used, refurbished, pre-owned…), `promotionText` ("Save $15.00"), `merchantRating`, `offerTitle`, `label` ("Best price", "Most popular"), `offerId` · `offersText` (one-line summary) |
| Deal signals | `lowestPrice`, `lowestPriceMerchant`, `lowestPriceUrl`, `lowestPriceCondition`, `highestPrice`, `typicalPriceLow`, `typicalPriceHigh`, `typicalPriceText` (Google's "Typically $199 to $300"), `priceVsTypical` (`below` / `within` / `above`), `historyLowestPrice`, `historyLowestPriceDate` and `historyLowestPriceMerchant` (the most recent day a listed store had that price), `historyHighestPrice`, `historyFrom`, `historyTo`, `isAtHistoryLow`, `pctAboveHistoryLow` |
| Price history | `priceHistory[]` — per store: `merchant`, `firstDate`, `lastDate`, `days`, `minPrice`, `maxPrice`, `lastPrice`, and `changes[]` (the first day, each day the price changed, and the last day) or `points[]` (every day) · `priceHistoryStatus`, `priceHistoryNote` (why there is none, in plain words) |
| Product details | `specs` (Google's specification table), `description`, `variants` (colours/sizes Google lists) |
| Context | `googleShoppingUrl`, `detailsStatus`, `detailsNote`, `query`, `searchVariant` (which Google listing found the product), `inputType` (`search` / `productId`), `inputProductId`, `country`, `language`, `scrapedAt`, `error` (the reason on status rows, `null` on product rows) |

**Deal signals compare like with like.** Offers Google labels used, refurbished or pre-owned are left out of `lowestPrice` and of the history summary, so a refurbished $249.99 offer is not compared with the new-product price history. Promotions Google shows in the same place ("Save $15.00") go to `promotionText` and do not leave an offer out. If every offer carries a condition label, all are used and `lowestPriceCondition` names the condition. `pctAboveHistoryLow` compares `lowestPrice` with `historyLowestPrice` (rounded to 0.1%); it is below 0 when today's price is lower than any price in the history, which in platform runs happened only when the cheapest store had no history series.

#### Sample output

A real row from an Apify platform run on 2026-09-30 (run `HWXUreNVwIQOtIbB7`, build 0.1.5: `sony wh-1000xm5`, United States), trimmed: 2 of 3 offers, 1 of 3 store price histories (3 of its 6 `changes` entries) and 4 of 13 specification lines shown.

```json
{
  "position": 1,
  "title": "Sony WH-1000XM5 Noise Canceling Wireless Headphones",
  "productId": "11726664398501807381",
  "price": 298,
  "priceText": "$298.00",
  "currency": "USD",
  "oldPrice": 398,
  "merchant": "Best Buy",
  "moreOffers": true,
  "condition": null,
  "deliveryText": "Free delivery",
  "rating": 4.6,
  "reviewsCount": 17000,
  "reviewsCountText": "17K",
  "badge": "25% OFF",
  "detailsStatus": "ok",
  "offersCount": 3,
  "offersText": "Best Buy $298.00 · Sony $299.99 · Office Depot $398.09",
  "offers": [
    { "merchant": "Best Buy", "merchantDomain": "bestbuy.com", "url": "https://www.bestbuy.com/product/sony-wh-1000xm5-wireless-noise-cancelling-over-the-ear-headphones-black/J7XSRH5CXG/sku/12903258?ref=212&loc=marketplace", "price": 298, "priceText": "$298.00", "oldPrice": 398, "discountText": "25% off", "deliveryText": "Free delivery", "deliveryCost": 0, "totalPrice": 298, "stockText": "In stock online", "inStock": true, "condition": null, "offerTitle": "Sony - WH-1000XM5 Wireless Noise Cancelling Over-the-Ear Headphones - Black", "label": "Best price" },
    { "merchant": "Sony", "merchantDomain": "electronics.sony.com", "url": "https://electronics.sony.com/audio/headphones/headband/p/wh1000xm5-b", "price": 299.99, "priceText": "$299.99", "oldPrice": 400, "discountText": "25% off", "deliveryText": "Free delivery between Fri - Mon", "deliveryCost": 0, "totalPrice": 299.99, "returnsText": "Free 30-day returns", "stockText": "In stock online", "inStock": true, "condition": null, "merchantRating": 4.8 }
  ],
  "lowestPrice": 298,
  "lowestPriceMerchant": "Best Buy",
  "lowestPriceCondition": null,
  "highestPrice": 398.09,
  "typicalPriceLow": 199,
  "typicalPriceHigh": 300,
  "typicalPriceText": "Typically $199 to $300",
  "priceVsTypical": "within",
  "priceHistoryStatus": "available",
  "historyFrom": "2025-09-29",
  "historyTo": "2026-09-30",
  "historyLowestPrice": 198,
  "historyLowestPriceDate": "2026-09-02",
  "historyLowestPriceMerchant": "Best Buy",
  "historyHighestPrice": 399.99,
  "isAtHistoryLow": false,
  "pctAboveHistoryLow": 50.5,
  "priceHistory": [
    { "merchant": "Best Buy", "firstDate": "2026-08-12", "lastDate": "2026-09-30", "days": 50, "minPrice": 198, "maxPrice": 398, "lastPrice": 298,
      "changes": [{ "date": "2026-08-12", "price": 248 }, { "date": "2026-08-31", "price": 398 }, { "date": "2026-09-02", "price": 198 }] }
  ],
  "specs": { "Brand": "Sony", "Noise Canceling": "Yes", "With Microphone": "Yes", "Color": "Midnight Blue, Smoky Pink" },
  "variants": ["Black", "Silver", "Smoky Pink", "Blue"],
  "query": "sony wh-1000xm5",
  "searchVariant": "relevance",
  "country": "us"
}
```

### How it works

- **Search results.** A Google Shopping results page shows about 40–50 different products (40–49 on 12 saved pages; 47 in platform run `RKYa2UHA1hOlthVFV`). Google has no second results page on the addresses the SERP proxy serves (`start=40` and the infinite-scroll request both returned 0 products in tests), so for more products the Actor also reads the sort orders and price bands Google prints on the first page — the same search sorted by price or rating, or limited to "Under $35", "$35–$70"… — and removes duplicates. On the Apify platform, `wireless earbuds` (US) gave 199 different products from 8 Google pages (run `fpHQhLcCmOyEEZ5qB`), and 6 broad US searches, run 11 times in 2 runs, returned the 200 products asked for 10 times (`4k tv` returned 123 once and 200 in the other run; runs `3dNEzA23rN88qH8cg`, `ofIVrQSh7rVpXCmOr`). It does not promise "every product for a search".
- **Store offers and price history.** For each product that has a Google product page (catalog products; 88% of 2,777 search rows in platform runs — the others are single-store listings), the Actor opens that page: one extra Google request per product. It lists the stores Google shows in "Buying options" (3–5 on 181 of 185 product pages in platform runs, median 3) and, where Google shows it, each store's daily price history (up to 367 days). Google loads more stores ("More stores") with a script; the Actor does not load them, so offers beyond those are not included.
- **Pages that Google marks as another country's are retried and never delivered.** Google sometimes answers as if the request came from another country (on the Apify platform, 6 of 351 Google requests in 54 runs, 1.7%; in local tests, pages came back Spanish, Brazilian or Indian, with EUR, BRL or INR prices). The Actor checks every page's currency and, when Google states it, the page's region, and asks again up to 3 times; a page that is still from the wrong country is dropped and counted in `SOURCE_REPORT` (`marketCheck`). In the platform runs all 6 were retried and none was delivered. A neighbouring country with the same currency and language (for example Austria for Germany) cannot be told apart this way.
- **Only delivered products are charged.** A search with no result, a product ID that cannot be opened and a product ID that cannot be read each leave one free row with an `error` explaining why. `SOURCE_REPORT` in the key-value store lists, per search, the Google pages used, duplicates, price-filtered products and details that failed.

### Price history by country

Measured in September 2026 on product pages where history was requested: Apify platform runs of builds 0.1.1–0.1.6 (57 runs), plus pages saved during development.

| Country | Product pages with price history (different products) | Platform runs |
|---|---|---|
| United States | 93 of 94 (41), plus 14 of 15 saved pages | 17 runs, including `Ddsy80BUMi1fwxItP`, `AWDXSo5gxBZxBtNJF`, `tvfQhGmVtX5F3HwKu` |
| Germany | 40 of 41 (23), plus 6 of 6 saved pages | `KfGZFyVcacK7wREZT`, `5cz3iiIrzJEcxUQHv`, `6cJ72ffLjGdOtrxMu`, `zQMYsJ9FeCkXomTbw` |
| France | 3 of 3, plus 2 of 2 saved pages | `5YzCZcL4onfYOYZOW` |
| Spain | 5 of 5 (4) | `t5zwzNnExuLg6eGei`, `o9TaSVqzEOatahQrN` |
| Canada · Australia · Netherlands | 3 of 3 each | `dmCCFTkoUd52j2XOc` · `e7IqE3FriF3azFdgc` · `zMz1LusbZQ9pCzdlH` |
| India · Japan | 2 of 2 each | `38vsq7ZPes47YquMZ` · `z4SPhE47eM5kDbb6e` |
| Poland | 5 of 6 (5) | `inBgJy8dcRZZVRsv8`, `dQ2VVjlHaUZVzofW6` |
| Italy · Sweden | 1 of 3 · 1 of 2 | `pHp19Yv7bE3jYxGJR` · `T7aRDWbEk4yoowSbB` |
| United Kingdom | **0 of 4**, plus 0 of 5 saved pages | `X3thTxtQjZUVRC0Pr`, `tIQltRmLF8OX8ne71` |
| Turkey | **0 of 3**, plus 0 of 3 saved pages | `jhd5PPQM25PKDY9bb` |
| Brazil | **0 of 8** | `3KEB2AdkbRjKGrXtj`, `bwJ5utuIH5IIoo7cS` |

The small samples show whether Google offers price history in a country, not an exact rate. In the United Kingdom, Turkey and Brazil, `priceHistoryStatus` is `not-available-in-country` and `priceHistoryNote` says so. Ireland, New Zealand, Austria, Switzerland, Belgium, Portugal, Czechia, Norway, Denmark, Finland and Mexico were not tested; a product without history there says that coverage has not been measured yet.

### Input & filters

| Input | What it does | Default |
|---|---|---|
| `queries` | Product searches, one per line ("sony wh-1000xm5", "espresso machine") | — |
| `productIds` | Re-check known products: the `productId` from an earlier run, or a Google Shopping product link containing `prds=…gpcid:…`. Each returns the product with its offers and price history | — |
| `country` | Which Google Shopping: stores, prices and currency (26 countries; 15 tested) | us |
| `language` | Interface language; empty = the country's main language | — |
| `maxProductsPerQuery` | Products per search, 1–300 (tested up to 200: how many more you get depends on the sort orders and price bands Google prints for the search). The Console form starts at 3 | 20 |
| `includeDetails` | Store offers, price history, typical range, specs (one extra Google request per product) | on |
| `priceHistory` | `changes` (the first day, each day the price changed, and the last day), `daily` (every day), `none` | changes |
| `sortBy` | `relevance`, `price_low_to_high`, `price_high_to_low`, `rating` — Google's own sort (one extra Google request per search). Google's price order does not always follow the listed price | relevance |
| `minPrice` / `maxPrice` | Keep products whose search-result price is in this range (the country's currency) | — |
| `maxSearchPagesPerQuery` | Upper bound on Google result pages for one search (the sort orders and price bands Google prints usually give 5–8) | 8 |

Values the input form checks (country, sort, number ranges) are rejected before the run starts, also when sent through the API. Others (no search and no product ID, a malformed language code, `minPrice` above `maxPrice`) stop the run with the reason in its status message. A product ID that cannot be read becomes a free status row, so the other IDs still run.

### Example inputs (copy & paste)

Price check for one product, United States:

```json
{ "queries": ["sony wh-1000xm5"], "country": "us", "maxProductsPerQuery": 10 }
```

Coffee machines in Germany, in Google's "Price: low to high" order (it does not always follow the listed price):

```json
{ "queries": ["kaffeevollautomat"], "country": "de", "sortBy": "price_low_to_high", "maxProductsPerQuery": 20 }
```

Category scan without store offers (faster: the first request gives about 40–50 products, each further page about 30 new ones):

```json
{ "queries": ["wireless earbuds"], "includeDetails": false, "maxProductsPerQuery": 150 }
```

Daily re-check of known products (use a schedule):

```json
{ "productIds": ["11726664398501807381"], "country": "us", "priceHistory": "none" }
```

### Use cases

- **Competitor price monitoring:** which stores sell a product, at what price and delivery cost, and how each store's price moved over the last year.
- **Deal verification:** is today's price really low? Compare `lowestPrice` with `historyLowestPrice` and Google's typical range.
- **Category research:** products, prices, ratings and review counts for a category, sorted or price-banded.
- **Affiliate and deal sites:** current offers with store links and discount badges.
- **Scheduled monitoring:** re-check a list of `productId`s daily or weekly.

### Performance

Apify platform runs of builds 0.1.1–0.1.6 (1024 MB unless noted):

| Input | Products (with store offers) | Google requests | Run time | Run |
|---|---|---|---|---|
| `sony wh-1000xm5`, US, 3 products (the form's starting input) | 3 (3) | 4–5 | 16–90 s (7 runs) | fastest `yFsNWp0wddVSZF0yb`, slowest `axv6WTyFBdWr3S4q0` |
| the same at 256 MB | 3 (3) | 4 | 69 s | `N8ds2skEdXmUdWKv3` |
| `sony wh-1000xm5`, US, 10 products | 10 (8) | 9 · 10 | 81 s · 41 s | `Ddsy80BUMi1fwxItP` · `GuRe6euV8GJSLU9sw` |
| the same at 256 MB | 10 (9) | 13 | 199 s | `OK1okLY6oO4hBRHHr` |
| `kaffeevollautomat`, Germany, 20 products | 20 (19) | 21 | 188 s | `KfGZFyVcacK7wREZT` |
| `robot vacuum`, US, 30 products | 30 (29) | 35 | 397 s | `tvfQhGmVtX5F3HwKu` |
| 5 product IDs, US | 5 (5) | 6 | 22 s | `AWDXSo5gxBZxBtNJF` |
| `wireless earbuds`, US, 200 products, no details | 199 (0) | 8 | 176 s | `fpHQhLcCmOyEEZ5qB` |
| 6 searches × 200 products, US, no details, 256 MB | 1,200 (0) | 38 | 895 s | `ofIVrQSh7rVpXCmOr` |

Run time depends mostly on Google's response time through the proxy: the same 3-product input took 16–90 s. Across 54 platform runs, the median request took 2.4–36 s depending on the run (8 s in a typical run), and the slowest 10% took up to 77 s. A request that gets no data for 90 s, or a 429 or 5xx answer, is sent again after 2 s; if that fails too, the page is requested once more the same way (at most 4 sends, about 6 minutes for one page). 5 of 351 requests were re-sent. Product pages are read 6 at a time. Memory peaked at 64–178 MiB; the three runs at 256 MB (`OK1okLY6oO4hBRHHr`, `ofIVrQSh7rVpXCmOr`, `N8ds2skEdXmUdWKv3`) finished normally.

### Data quality

Measured on the Apify platform in 57 runs of builds 0.1.1–0.1.6:

- **Store offers** (builds 0.1.5–0.1.6, 13 runs: 226 offers on 66 product pages in the US, Germany, the UK, Poland and Spain): store, price, stock and offer title on 100%; store link on 98.7% (3 offers on one page came without a link; since 0.1.6 such a page is read once more); delivery on 98%; returns on 70%.
- **Price history matches the offers:** in the same runs, 204 of 204 store histories belong to a listed store and end at that store's current offer price (all builds: 529 of 537; the 8 others were Polish offers without a price before 0.1.4).
- **Search results** (2,777 rows in 47 runs, 15 countries): title, price and store on 100%; rating and review count on 84%; Google product ID on 88% (the rest are single-store listings); old price 31%; badge 36%. No product appeared twice in a run (51 of 51 runs with products, including 1,200 of 1,200 in `ofIVrQSh7rVpXCmOr`).
- **Price history can be missing on a single answer:** of the 143 product pages that are read again when the block is missing, 14 came without it on the first read and 12 had it on the second. In the 12 countries where we saw Google show price history (US, Canada, Australia, Germany, France, Spain, the Netherlands, India, Japan, Poland, Italy, Sweden), such a page is read once more; `SOURCE_REPORT` counts these (`marketCheck.historyRetries`, `historyRecovered`).
- **Numbers** are read in the country's format ("235,00 €", "₺13.999,30", "$1,299.00"); abbreviated review counts are expanded ("17K" → 17000, "2,2 B" in Turkish → 2200, "2,6 tn" in Swedish → 2600) and the original text is kept next to them. A value Google does not show is `null`, never 0.
- **Images:** search rows often have no `imageUrl` (39% have one) because Google inlines the first thumbnails; rows with store offers take the image from the product page (185 of 185).
- **Rating and review count** come from the search result, so rows opened by product ID have none.

### Pricing

Pay per event:

- `product` — every product row delivered;
- `product-details` — added when the row carries at least one store offer (the product page was read).

Apify may also charge its Actor start event; the Pricing tab lists every event and its price. Status rows (no results, a product ID that cannot be opened or read, a wrong-country page) are free. With `includeDetails: false` each row is charged only `product`.

### Integrations

JavaScript:

```js
import { ApifyClient } from 'apify-client';
const client = new ApifyClient({ token: 'YOUR_APIFY_TOKEN' });
const run = await client.actor('foxlabs/google-shopping-scraper').call({ queries: ['sony wh-1000xm5'], maxProductsPerQuery: 5 });
const { items } = await client.dataset(run.defaultDatasetId).listItems();
```

Python (the same calls; not run as code yet — the same input ran on the platform as run `VtqINt0jDlShJ9pLp`):

```python
from apify_client import ApifyClient
client = ApifyClient("YOUR_APIFY_TOKEN")
run = client.actor("foxlabs/google-shopping-scraper").call(run_input={"queries": ["sony wh-1000xm5"], "maxProductsPerQuery": 5})
items = client.dataset(run["defaultDatasetId"]).list_items().items
```

Make, Zapier and n8n: use Apify's "Run Actor" and "Get dataset items" steps (not tested with this Actor yet).

### FAQ

**Why only 3–5 stores per product?** Those are the stores Google itself lists on the product page. Google loads more ("More stores") with a script; the Actor does not load them.

**Can I search by EAN / GTIN / UPC?** Not reliably. In our tests a barcode typed into Google Shopping returned mostly unrelated products (for a verified Nintendo Switch OLED UPC, the first result was a pair of socks and a Switch OLED in another colour was 3rd). Search by product name, or use the `productId` of a product you found.

**How do I monitor prices every day?** Run a search once, keep the `productId`s you care about, and schedule a run with `productIds`. Each row has the current offers, and where Google shows it (not in the UK, Turkey or Brazil), `priceHistory` has Google's own history, so you do not need to wait months to see a trend.

**Why is `priceHistory` empty?** Google does not show price history for every product, and not in every country (none in the UK, Turkey and Brazil in our tests). `priceHistoryNote` says which case it is.

**What is `productId`?** Google's product group ID (the `gpcid` in Google Shopping links). In our tests the same ID reopened the product hours later; how long an ID stays valid over weeks has not been measured yet. Single-store listings have none.

**Is `lowestPrice` the cheapest offer?** It is the cheapest offer without a used, refurbished or pre-owned label, among the stores Google lists (a promotion does not leave an offer out). The full list, with conditions and promotions, is in `offers`.

**Are store links direct?** Mostly. `offers[].url` is the seller's link from Google's page (98% of offers have one; a few sellers are price-comparison sites). Google's `srsltid`, `utm_*` and `gclid` are removed; other parameters, including some sellers' campaign tags, are kept because stores may need them.

**Why fewer products than I asked for?** The Actor reads page 1 plus the sort orders and price bands Google prints on it — at most `maxSearchPagesPerQuery` pages (default 8) — and stops early when 2 pages in a row bring nothing new. Narrow searches end early; broad ones can end a little short too (`wireless earbuds`: 199 of 200). `SOURCE_REPORT` shows the pages used.

**Why do I get products that do not match my search?** For a search Google cannot match, Google Shopping shows other products instead of an empty page: a nonsense test search returned jeans, shoes and a ring (run `n8NgBFYiIBc8u0l0i`). The Actor delivers what Google shows, so check that the titles fit your search.

### Troubleshooting

- **"Google Shopping shows no products for …"**: try a broader or differently worded search, or check the country.
- **"Google answered as another country"**: Google kept answering from another country after 3 tries; nothing was delivered for that search. Run it again.
- **"Apify Google SERP proxy is not available to this run"**: please open an issue on the Issues tab with the run ID.
- **"… is not a Google product ID"**: use the `productId` value (digits) from an earlier run or a Google Shopping link that contains `prds=…gpcid:…`. Old `/shopping/product/…` links no longer work on Google.

### Notes, limits & legal

- Data is what Google Shopping publicly shows at run time; prices change often.
- The price is the one Google shows on the search result; for a few listings it is a rental or subscription price (seen once in our runs: a rent-to-own listing at $22.99 for headphones).
- Google may change its pages without notice; fields can then come back empty until the Actor is updated. Please open an issue with the run ID.
- Sponsored and organic results are not told apart: the pages the Actor reads carry no "Sponsored" label (0 on 25 saved result pages).
- Google, Google Shopping and the store names are trademarks of their owners; this Actor is not affiliated with them.

### Support

Open an issue on the Issues tab with the run ID and the input, or write to info@foxlabs.com.tr.

### Changelog

Dates are UTC.

#### 0.1.7 — 2026-09-30

- Promotions Google puts where it shows an offer's condition ("Save $15.00", "5% off Summer sale") now go to the new `offers[].promotionText` and no longer count as a condition. Before, such offers were left out of the deal signals: in 4 of 185 platform rows `lowestPrice` skipped a cheaper new offer.
- A foreign store's price in its own currency ("(₹98,990)" under a dollar price) now goes to `priceNote`. Before, it was read as `oldPrice` (22 US search rows).
- Free delivery written in Polish, Swedish or Japanese gives `deliveryCost` 0; Swedish review counts ("2,6 tn") are expanded.
- An offer Google lists twice (same store, price and link) is kept once.
- `historyLowestPriceDate` and `historyLowestPriceMerchant` name the most recent day a store had the lowest price (before: the first store in page order).

#### 0.1.6 — 2026-09-30

- A product page where an offer came without a store link is read once more, and the more complete answer is kept.
- `isAtHistoryLow` agrees with `pctAboveHistoryLow`: a price that rounds to 0% above the history low counts as at the low.

#### 0.1.5 — 2026-09-30

- Every row has the `error` field (`null` on product rows).

#### 0.1.4 — 2026-09-30

- Polish offer prices ("154,99 zł") are read; before, Polish offers had no price. Czech "Kč" prices follow the same rule (not tested on a Czech page yet).
- Store links: a second source is used when Google's page has no direct link for an offer.

#### 0.1.3 — 2026-09-30

- Store links: tracking parameters placed after "#" are removed too.

#### 0.1.2 — 2026-09-30

- Price history coverage measured in 15 countries; Brazil is marked as a country without price history (like the UK and Turkey).
- A product page without the price-history block is read once more in every country where Google was seen showing history.

#### 0.1 — 2026-09-30

First version. See CHANGELOG.md.

# Changelog

This Actor's version history is a separate document: https://apify.com/foxlabs/google-shopping-scraper/changelog.md

# Actor input Schema

## `queries` (type: `array`):

One search per line, as you would type it in Google Shopping: a product ("sony wh-1000xm5"), a category ("espresso machine") or a brand. Each search returns up to "Max products per search" products.

## `productIds` (type: `array`):

Re-check known products, one per line: the productId value from an earlier run (digits), or a Google Shopping product link that contains "prds=…gpcid:…". Each ID returns the product with its store offers and price history. Leave empty when you only search.

## `country` (type: `string`):

Which Google Shopping to search: the country's stores, prices and currency. In tests (September 2026) Google showed price history in the US, Canada, Australia, Germany, France, Spain, the Netherlands, India and Japan (and on some products in Italy, Poland and Sweden), and none in the UK, Turkey and Brazil. Other countries are not measured yet; each product says why it has no history.

## `language` (type: `string`):

Two-letter language code for Google's interface (en, de, fr…). Leave empty for the country's main language. Prices and stores come from the country above either way.

## `maxProductsPerQuery` (type: `integer`):

Google Shopping shows about 40–50 different products per page. For more, the Actor also reads Google's own sort orders and price bands for the same search and removes duplicates, until this number, until no new products appear, or until those pages run out (usually after 5–8 pages). The form starts at 3 for a quick first run.

## `includeDetails` (type: `boolean`):

Open each product's Google page to get the stores Google lists for it (typically 3–5) with store links, price, stock, delivery, returns and store rating, each store's daily price history, and Google's typical price range. One extra Google request per product, billed as a separate event. Off = search results only (faster, cheaper).

## `priceHistory` (type: `string`):

How each store's price history is written. Price changes: the first day, each day the price changed and the last day (compact). Every day: one point per day, up to 367 per store. None: leave it out.

## `sortBy` (type: `string`):

Google Shopping's own sort order. Sorting other than Relevance costs one extra Google request per search. It applies to Google's first page of sorted results; more products beyond it come from other sort orders and price bands. Google's price order does not always follow the listed price.

## `minPrice` (type: `integer`):

Keep only products whose price on the search result is at least this much, in the country's currency. Leave empty for no minimum.

## `maxPrice` (type: `integer`):

Keep only products whose price on the search result is at most this much, in the country's currency. Leave empty for no maximum.

## `maxSearchPagesPerQuery` (type: `integer`):

Upper bound on Google result pages read for one search (page 1, sorted pages and price bands). Each page is one Google request.

## Actor input object example

```json
{
  "queries": [
    "sony wh-1000xm5"
  ],
  "country": "us",
  "maxProductsPerQuery": 3,
  "includeDetails": true,
  "priceHistory": "changes",
  "sortBy": "relevance",
  "maxSearchPagesPerQuery": 8
}
```

# Actor output Schema

## `dataset` (type: `string`):

No description

# API

You can run this Actor programmatically using our API. Below are code examples in JavaScript, Python, and CLI, as well as the OpenAPI specification and MCP server setup.

## JavaScript example

```javascript
import { ApifyClient } from 'apify-client';

// Initialize the ApifyClient with your Apify API token
// Replace the '<YOUR_API_TOKEN>' with your token
const client = new ApifyClient({
    token: '<YOUR_API_TOKEN>',
});

// Prepare Actor input
const input = {
    "queries": [
        "sony wh-1000xm5"
    ],
    "maxProductsPerQuery": 3
};

// Run the Actor and wait for it to finish
const run = await client.actor("foxlabs/google-shopping-scraper").call(input);

// Fetch and print Actor results from the run's dataset (if any)
console.log('Results from dataset');
console.log(`💾 Check your data here: https://console.apify.com/storage/datasets/${run.defaultDatasetId}`);
const { items } = await client.dataset(run.defaultDatasetId).listItems();
items.forEach((item) => {
    console.dir(item);
});

// 📚 Want to learn more 📖? Go to → https://docs.apify.com/api/client/js/docs

```

## Python example

```python
from apify_client import ApifyClient

# Initialize the ApifyClient with your Apify API token
# Replace '<YOUR_API_TOKEN>' with your token.
client = ApifyClient("<YOUR_API_TOKEN>")

# Prepare the Actor input
run_input = {
    "queries": ["sony wh-1000xm5"],
    "maxProductsPerQuery": 3,
}

# Run the Actor and wait for it to finish
run = client.actor("foxlabs/google-shopping-scraper").call(run_input=run_input)

# Fetch and print Actor results from the run's dataset (if there are any)
print(f"💾 Check your data here: https://console.apify.com/storage/datasets/{run.default_dataset_id}")
for item in client.dataset(run.default_dataset_id).iterate_items():
    print(item)

# 📚 Want to learn more 📖? Go to → https://docs.apify.com/api/client/python/docs/quick-start

```

## CLI example

```bash
echo '{
  "queries": [
    "sony wh-1000xm5"
  ],
  "maxProductsPerQuery": 3
}' |
apify call foxlabs/google-shopping-scraper --silent --output-dataset

```

## MCP server setup

```json
{
    "mcpServers": {
        "apify": {
            "type": "http",
            "url": "https://mcp.apify.com/?tools=fetch-actor-details,foxlabs/google-shopping-scraper"
        }
    }
}
```

The hosted server signs you in with OAuth on first connect, so no API token belongs in this config. Clients without OAuth support can send an `Authorization: Bearer <APIFY_API_TOKEN>` header instead, using a token from API & Integrations in Apify Console (https://console.apify.com/settings/integrations).

## OpenAPI specification

Download the OpenAPI definition: https://api.apify.com/v2/actors/4f3XftsBVhw1umUks/builds/Fx5d3DUx5oLNiKtvB/openapi.json
