Blog/Ecommerce
11 min read

Google Shopping API: The Lowest Price Is a Different Product

One search returned the same headphones from 22.99 to 399.99. The cheapest was a weekly rental. Sorting by price picks the row that is not the product.

Google Shopping API: The Lowest Price Is a Different Product

Copy this line to your agent to pull merchant prices for a product.

set up https://monid.ai/SKILL.md and use apify burbn/google-shopping-scraper for a product

On 2026-09-08 one search for a single pair of headphones returned prices from 22.99 to 399.99. Nothing was wrong with the data. The cheapest row was a weekly rental payment, the second cheapest was refurbished, and one of the more expensive ones was a different product with a similar name. A repricing rule that reads the minimum would have chased all three. This guide runs through Monid, the OpenRouter for agent tools.

What does a Google Shopping response actually contain?

Fifteen fields per offer, and three of them decide whether the price above them is comparable to yours.

The record

apify/burbn/google-shopping-scraper for sony wh-1000xm5, United States, on 2026-09-08:

{
  "type": "product",
  "page": 1,
  "product_id": "catalogid:17755277695162489284,productid:8123523306754565002,gpcid:11726664398501807381,...",
  "product_title": "Sony WH-1000XM5 Noise Canceling Wireless Headphones",
  "price": "$291.99",
  "original_price": "$398",
  "on_sale": true,
  "discount_percent": "26% OFF",
  "store_name": "Best Buy",
  "has_multiple_offers": true,
  "product_rating": 4.6,
  "product_num_reviews": 18,
  "shipping": null
}

The three fields that carry the meaning

store_name, original_price and has_multiple_offers. The price is the number everyone wants and the least self-sufficient thing in the record. Each of the next three sections is about one way it misleads on its own.

The identifier is not an identifier

product_id is a composite of Google's internal catalog keys: a catalogid, a productid, a gpcid, a headlineOfferDocid and more, comma-joined into one string. It identifies a position in Google's catalog rather than a product in the world. It is not a GTIN, it is not an ASIN, it will not join to your own catalog, and it is not stable enough to use as a primary key across weeks. Identifier hops like this are a recurring tax, and one we hit again in the insider trading guide.

Six rows we paid for were empty

We asked for limit: 20 and were returned and billed for 26 rows. Rows 21 through 26 carried price: null, an empty product_title, no store_name and has_multiple_offers: undefined.

Six empty records, charged at the same per-result rate as the twenty real ones. On a per-result endpoint that is a real line item, and the defence is one line: drop rows with a null price before you count what you got.

📖 See also How to Monitor Competitor Prices Automatically

Why is the cheapest result never the same product?

Because a shopping feed is a list of offers, and an offer can differ from yours in condition, in packaging, in currency, in payment structure or in being a completely different item.

The five rows at the bottom of the range

PriceStoreWhat it actually was
22.99Rent-A-CenterA rental instalment, not a purchase price
124.99PayMore Milpitasoriginal_price reads "Usually $248", a used-goods reseller
189.99TargetTitle begins "Refurbished"
249.99Walmart marketplaceTitle ends "(Sold without retail box)"
426.76mcgrocer.comoriginal_price "(£315)", and the title is "Sony Wh-1000xm Wireless Earphones"

Only one of those five is a new, boxed, retail unit of the product searched for. The rental row at 22.99 is the extreme case and the instructive one: it is a weekly payment on a rent-to-own agreement, and Google displays it in the same price field as an outright sale because it is, technically, a price.

The last row is not even the product

mcgrocer.com at 426.76 has a title of "Sony Wh-1000xm Wireless Earphones". Earphones, not headphones, and the model number is truncated. Nineteen of the twenty-six titles contained wh-1000xm5. Seven did not. Google matched them because the catalog thinks they are related; your comparison should not.

Why original_price cannot be parsed as a number

It appeared on seven rows in three different formats: "$398", "Usually $248", and "(£315)".

That third one is a pound figure rendered next to a dollar price, which means the row is a UK listing displayed with a converted headline. Running a regex for digits across that field gives you 315 and a currency error you will never see again. discount_percent was present on only three of those seven, so it is not a reliable fallback either.

Why store_name double counts

Four rows read Walmart - Amirah, Walmart - Daily Spirit, Walmart - AVC Photo and Walmart - Datavision. Those are marketplace sellers under one storefront, not four merchants. Group by store_name and Walmart appears four times with four prices; group by the leading token and you lose the distinction between Walmart's own offer and a third party's.

How do you pull multi-merchant prices?

Three steps, and discovery is free.

For agents

Grab an API key at app.monid.ai, then paste this to your agent and hand it the key:

set up https://monid.ai/SKILL.md

It learns the whole discover, inspect, run workflow itself. More in the agent quickstart.

For humans

npm install -g @monid-ai/cli
monid keys add -k <your-api-key> -l main

Step 1. Pull the offer set for a product

What it does. Returns every merchant Google is showing for a search term, with prices and conditions.

The endpoints. apify/burbn/google-shopping-scraper, billed per result with a small flat fee.

The call.

monid run -p apify -e /burbn/google-shopping-scraper -w \
  -i '{"searchQuery":"sony wh-1000xm5","country":"us","language":"en","limit":20,"sortBy":"BEST_MATCH"}'

Note BEST_MATCH in capitals. Passing relevance returns HTTP 400 with the legal set named, which is the good kind of failure and the same behaviour described in a wrong enum returns zero.

What comes back. Fifteen fields per offer, including store_name, original_price, on_sale and has_multiple_offers.

What it costs. A fraction of a cent per result plus a small flat fee per run, and empty rows bill like full ones. Current figures at monid.ai/tools.

Step 2. Filter before you compare, not after

What it does. Removes the rows that are not your product.

The call. No endpoint, just the guard that has to exist somewhere:

def comparable(row, model="wh-1000xm5"):
    t = (row.get("product_title") or "").lower()
    if row.get("price") is None:          return False   # the empty rows
    if model.replace("-", "") not in t.replace("-", ""): return False
    for bad in ("refurb", "renewed", "used", "open box", "without retail box"):
        if bad in t:                      return False
    return True

Run it before any aggregation. A minimum computed over unfiltered rows is a number about the long tail of a marketplace, not about your competition.

Step 3. Take a second reading for the products that matter

What it does. Catches the case where one source has a stale or partial view.

The endpoints. apify/damilo/google-shopping-apify reads the same surface independently, also per result.

What comes back. The same class of record with different field names, so the merge is a mapping rather than a union. The reason to bother is the one measured across two Reddit providers in the Reddit scraper comparison: two readings of one ranked surface do not agree, and the disagreement is where the errors live.

Give this to your agent

$Set up https://monid.ai/SKILL.md, and then use Monid to pull Google Shopping offers for my product, drop refurbished and rental listings, and tell me the lowest price for a new boxed unit and which merchant it is.

📖 See also Automate Amazon Product-Detail Lookups From a List of ASINs

How do you decide two rows are the same product?

By condition, packaging, currency and payment structure, in that order, and never by title similarity alone.

Condition first, because it moves the price most

Refurbished units in our result set sat around 190 while the new price sat around 292 to 320. That is a 35 percent gap that has nothing to do with anyone repricing. Google does expose a condition filter on the input side, productCondition, and setting it is cheaper than cleaning afterwards.

Packaging second

"Sold without retail box" is a genuine new unit at a genuine discount, and whether it is comparable depends on what you sell. If you ship boxed retail units, it is a different product. If you sell open-box yourself, it is your closest competitor.

Currency third

The (£315) row is the trap that survives every other check: the title matches, the condition is new, the merchant is real, and the number in price is a converted display figure whose underlying listing is in another currency and probably not shippable to your buyer. country on the request narrows this, and it does not eliminate it.

Payment structure last, and it is the strangest one

A rental instalment and a purchase price are not the same kind of number, and nothing in the record marks the difference. Rent-A-Center at 22.99 is identifiable only by knowing the merchant. There is no field for "this is a weekly payment", so the defence is a merchant blocklist, maintained by hand, of the rent-to-own and financing storefronts in your category.

What that adds up to

Product matching across merchants is the hard part of price intelligence, and the API does not do it for you. The endpoint returns offers, faithfully. Deciding which offers are comparable is your model of your own market, and it belongs in your code where you can change it. This is the same conclusion reached about matching questions across prediction market venues in the Kalshi and Polymarket guide, where a dedicated matching endpoint exists precisely because the join is too hard to leave to string comparison.

Which endpoint should I use for which job?

EndpointWhat it doesInputOutputBest forBilling
apify/burbn/google-shopping-scraperOffers for a search termsearchQuery, countryFifteen fields per offerThe default multi-merchant pullPer result plus flat fee
apify/damilo/google-shopping-apifyThe same surface, second readingSearch termOffer rowsCross-checking a price you will act onPer result
apify/axesso_data/amazon-search-scraperOne marketplace, in depthKeyword, domainPrice plus retailPrice, rank, sales volumeAmazon-specific trackingPer result
api.strale.io/x402/product-searchProduct search, per callQueryProduct recordsWide sweeps where volume is unpredictablePer call

Every row was verified with monid inspect on 2026-09-08. The table gives billing shape rather than figures, because shape drives design and current numbers live on monid.ai/tools.

Three of the four bill per result, which is the shape to watch here: a search that returns 26 rows when you asked for 20 costs what 26 rows cost. On a catalog of a thousand products, checked daily, the difference between a limit you set and a count you receive is the whole budget question.

When is shopping data the wrong source?

Four cases.

You need your own listings. If the question is about products you sell, Google's Merchant Center and Content API are the correct route and they are authoritative in a way scraped display data never is. Reading your own catalog through a shopping scraper is a strange thing to do.

You need one marketplace in depth. Google Shopping gives you breadth across merchants and shallow detail on each. For Amazon specifically, the marketplace endpoints return list price, sales rank and review volume that never appear here, which is the argument made in the Amazon reviews comparison.

You need to know a price changed rather than what it is. A single pull is a snapshot. Change detection needs a schedule, a stored history and a rule for what counts as an event, all of which is a different design problem covered in the price monitoring guide.

You are in a category where the catalog is thin. Shopping coverage is excellent in consumer electronics and patchy in industrial, B2B and long-tail categories, where merchants often do not submit a feed at all. Test your own SKUs before building on it; a category with three merchants per product will not support the filtering described above.

And the disclosure: this is Monid's blog and we sell per-call access to these endpoints. The main argument here is to throw away most of the rows you paid for, which is not a pitch for volume. The empty-row observation in particular is a cost to us to publish, and it is reproducible in one call.

Conclusion

There is no such thing as "the price" for a product across merchants, and the minimum of a shopping feed is the least useful number in it. Our search spanned 22.99 to 399.99 for one pair of headphones, and the bottom of that range was a rental agreement, a refurbished unit and a used-goods reseller in that order. Every one of them is a real offer and none of them is a competitor to a new boxed sale.

What matters more than the endpoint you pick is where product matching lives. Condition, packaging, currency and payment structure are four independent ways a row can be non-comparable, and only one of them is exposed as a field. The rest sit in the title, in the merchant name, or nowhere at all, which means the matching rule is a piece of your own business logic and it has to be written down.

Free next step: run one search for a product you sell, sort the rows by price, and read the cheapest five titles. It takes one call and it will show you your own version of the rental row. monid discover -q "google shopping" costs nothing. Start at monid.ai.

FAQ

Is it legal to collect competitor prices this way?

Collecting publicly displayed prices is generally accepted commercial practice and price comparison is a long-established business, but the rules that bind you are contractual and jurisdictional rather than universal. Read the terms of the surface you are reading, respect robots directives, keep request rates modest, and take advice if you are operating at scale or in a regulated category. Separately, using competitor prices to coordinate rather than to compete is an antitrust question and has nothing to do with how the data was collected.

Does Google's official Content API do this?

No, and the confusion is common. The Content API for Shopping is how a merchant manages their own product feed, inventory and promotions inside Merchant Center. It is authoritative for your listings and gives you nothing about anyone else's. There is no official Google product that hands you a competitor's offer set, which is why third-party readings of the public shopping surface exist at all.

How do country and currency work on these calls?

The request takes a country code and a language code, and they change which merchants and which localised prices are returned. Setting them is necessary and not sufficient: our United States search still returned a listing whose original_price read in pounds, because Google surfaces some international merchants with a converted headline. Store the country you requested alongside every row, and treat a currency symbol appearing anywhere in the record as a signal to exclude rather than to convert.

How often does the shopping feed change?

Merchant prices update on the merchant's own schedule, commonly daily, and promotional prices can change several times a day in competitive categories. The ranking of offers changes more often than the prices do, so a daily pull will show you a different set of merchants even when nobody has repriced. That is the argument for storing the whole record and keying change detection on the merchant plus condition rather than on position, which is the same discipline argued in the price monitoring guide.

Last updated September 2026.

google shopping apiprice comparisonecommerce dataproduct matchingrepricing