Product Feed Scraper: Google Merchant & XML Catalogs avatar

Product Feed Scraper: Google Merchant & XML Catalogs

Pricing

from $0.74 / 1,000 product scrapeds

Go to Apify Store
Product Feed Scraper: Google Merchant & XML Catalogs

Product Feed Scraper: Google Merchant & XML Catalogs

Parse public Google Merchant and RSS 2.0 product feeds: GTIN, MPN, brand, price split into amount and currency, sale price, availability, shipping and images. The catalogue retailers publish deliberately.

Pricing

from $0.74 / 1,000 product scrapeds

Rating

0.0

(0)

Developer

Arman Hossain

Arman Hossain

Maintained by Community

Actor stats

0

Bookmarked

1

Total users

0

Monthly active users

20 days ago

Last modified

Share

Product Feed Scraper: Every product in a merchant's Google Shopping feed, price split into amount and currency, availability, brand, GTIN and shipping

Product Feed Scraper reads the product feeds retailers publish for Google Shopping and other ad platforms, and returns one clean record per product, ID, title, description, link, images, price split into an amount and a currency, sale price, availability, brand, GTIN, MPN, condition, category and shipping.

These feeds exist so machines can read them: a merchant generates one deliberately and hands the URL to Google Merchant Center. This Actor reads the same URL. No proxy setup, no browser, no credentials to manage. A 250-product catalogue comes back in under a second.

Agent skill: SKILL.md

https://api.apify.com/v2/key-value-stores/t7YoTxpZEJOWvw4Ug/records/product-feed-scraper.md

What you get

Output fieldMeaning
feedUrl, feedTitleFeed that was read after redirects, and the store name from the channel
productId, itemGroupIdg:id, and the g:item_group_id that ties variants of one product together
title, descriptiong:title and g:description, description stripped to plain text
link, imageLink, additionalImageLinksProduct page and images
price, salePrice, currencyNumbers, not strings, "69.99 USD" becomes 69.99 plus "USD"
availability, inStockGoogle's enum (in_stock, out_of_stock, preorder, backorder) and a boolean
brand, gtin, mpn, conditionIdentity attributes, with g:ean / g:upc / g:isbn accepted as GTIN aliases
productType, googleProductCategoryThe merchant's own taxonomy and Google's
shippingCost, shippingCurrency, shippingCountryThe first g:shipping group, flattened
shippingOptionsEvery g:shipping group the item declares: country, region, service, cost, currency
scrapedAtRun timestamp

A RUN_SUMMARY record in the key-value store holds per-run counts, a per-feed breakdown of listed / saved / filtered / duplicate, the cap and filters that were in force, whether the cap was reached, any feed that failed, any feed skipped as a duplicate of one already read, and any item that could not be parsed.

Common use cases

  • Competitive price monitoring. Run daily and diff on productId plus price.
  • Supplier catalogue ingest. Pull a distributor's whole feed straight into your own catalogue.
  • Comparison shopping. Normalise many merchants' feeds into one schema keyed by GTIN.
  • Stock and assortment tracking. Watch availability flip across a competitor's range.
  • Market research. Price distributions by brand, category or condition across a sector.

Quick start

A whole catalogue:

{
"feedUrls": ["https://houseofjerky.com/wp-content/uploads/woo-feed/google/xml/googlehojfeed.xml"]
}

Several suppliers, capped while you explore. maxProducts is a total for the whole run, shared evenly across the feeds — 500 across two feeds is 500 products, not 1000, and a feed that runs short leaves its share to the others rather than wasting it:

{
"feedUrls": [
"https://example-shop.com/feeds/google-shopping.xml",
"https://another-shop.com/googlebase.xml"
],
"maxProducts": 500
}

Only what is buyable, from one brand, in a price band:

{
"feedUrls": ["https://example-shop.com/feeds/google-shopping.xml"],
"inStockOnly": true,
"brandFilter": ["Sony"],
"minPrice": 100,
"maxPrice": 900
}

Input

FieldTypeDefaultNotes
feedUrlsarray-Required. Public product feed URLs. .xml and gzipped .xml.gz both work.
maxProductsinteger1000Saved products for the whole run, applied after filtering and shared evenly across the feeds, with any unspent share passed on. 0 = no limit.
maxProductsPerFeedinteger-The former name of maxProducts, still accepted. It now bounds the run total, not each feed.
inStockOnlybooleanfalseKeep only availability: in_stock. Products with no availability are dropped.
brandFilterarray[]Exact, case-insensitive match on g:brand. Empty = all brands.
minPriceinteger-Lower price bound in the feed's own currency. No conversion is done.
maxPriceinteger-Upper price bound in the feed's own currency.

Filters combine with AND. Setting either price bound drops products whose price could not be parsed, since there is no way to know whether they qualify.

A cap outside its range is refused before anything is fetched: -5, 0.5 and "abc" end the run with an error rather than quietly meaning "everything". 0 is the only value that means no limit, and it means it on purpose.

Output example

{
"feedUrl": "https://houseofjerky.com/wp-content/uploads/woo-feed/google/xml/googlehojfeed.xml",
"feedTitle": "House of Jerky",
"productId": "97864",
"itemGroupId": "97864",
"title": "Teriyaki Lovers",
"description": null,
"link": "https://houseofjerky.com/shop-for-jerky/teriyaki-lovers/",
"imageLink": "https://houseofjerky.com/wp-content/uploads/2025/10/Teriyaki-Lovers-scaled.jpg",
"additionalImageLinks": [],
"price": 69.99,
"salePrice": 69.99,
"currency": "USD",
"availability": "in_stock",
"inStock": true,
"brand": "Develop-free",
"gtin": null,
"mpn": "teriyakilovers",
"condition": "new",
"productType": "Beef > Exotic > Game > Turkey > Subscriptions",
"googleProductCategory": "423",
"shippingCost": null,
"shippingCurrency": null,
"shippingCountry": null,
"shippingOptions": [],
"scrapedAt": "2026-08-11T15:40:46.492Z"
}

brand is "Develop-free" because that is what this merchant's feed says; gtin and the shipping fields are null because the feed omits them. Nothing here is filled in on the merchant's behalf.

Finding a merchant's feed URL

There is no universal path, but the cart platform narrows it down:

PlatformTypical feed path
WooCommerce (CTX Feed / Product Feed PRO)/wp-content/uploads/woo-feed/google/xml/<name>.xml
Magento and custom carts/googlebase.xml, /google_shopping.xml, /feeds/google-shopping.xml
Feed services (DataFeedWatch, Channable, Feedonomics)A hosted URL on the vendor's domain

Merchants who want you to have the feed, suppliers, dropship partners, affiliate programmes, will simply send you the URL. Do not try to guess your way into one that is not published: a feed behind auth returns 403 and is recorded as a failure.

API example

curl -X POST "https://api.apify.com/v2/acts/arman-bd~product-feed-scraper/run-sync-get-dataset-items?token=YOUR_TOKEN" \
-H "Content-Type: application/json" \
-d '{
"feedUrls": ["https://example-shop.com/feeds/google-shopping.xml"],
"inStockOnly": true,
"maxProducts": 1000
}'

JavaScript example

import { ApifyClient } from 'apify-client';
const client = new ApifyClient({ token: 'YOUR_TOKEN' });
const run = await client.actor('arman-bd/product-feed-scraper').call({
feedUrls: ['https://example-shop.com/feeds/google-shopping.xml'],
inStockOnly: true,
});
const { items } = await client.dataset(run.defaultDatasetId).listItems();
for (const p of items) console.log(`${p.productId}\t${p.price} ${p.currency}\t${p.title}`);

Limits and behaviour

  • RSS 2.0 only. Google Merchant feeds are RSS 2.0 with the g: namespace, and that is what this Actor parses, along with plain RSS <title>, <link> and <description> as a fallback. Atom-based product feeds are out of scope and rejected with a clear message rather than parsed into empty records; Shopify's /collections/all.atom is the common example. CSV and TSV feeds are also out of scope. This is an XML parser.
  • The g: prefix is matched literally. Every generator in the wild emits the conventional g: prefix declared as xmlns:g="http://base.google.com/ns/1.0". A feed that rebinds the namespace to a different prefix will parse as plain RSS and lose the merchant fields; the run log warns when a feed yields no g: fields at all.
  • Compression is handled both ways. Content-Encoding: gzip is unwrapped by the HTTP layer, and a .xml.gz body is detected by its magic number and decompressed in-process. No extra dependency.
  • Non-UTF-8 feeds are decoded. The XML declaration's encoding attribute is honoured, so windows-1252 and ISO-8859-1 catalogues come through with their accents intact. When a feed declares nothing and its bytes are not valid UTF-8, the charset on the response is used instead; a charset that contradicts a valid UTF-8 body is ignored, because a site-wide default header is wrong more often than the file is.
  • Prices become numbers. "69.99 USD", "USD 69.99", "1.299,00 EUR" and "£1,050" all parse correctly. C$, A$, NZ$ and R$ are read as CAD, AUD, NZD and BRL. A bare $ is read as USD — most feeds that write it mean USD, and currency is always worth checking against the merchant's own store before you rely on it.
  • The shipping group is not the price. g:shipping carries its own g:price, and Google's canonical layout puts the shipping groups before the list price. Those nested groups are removed before the product's own fields are read, so price is the product's price. Every group is returned in shippingOptions; the flat shippingCost / shippingCurrency / shippingCountry fields are the first group only.
  • One feed is charged once, however it is spelled. example.com/feed.xml, www.example.com/feed.xml, the http:// spelling and a redirect to any of them return the same catalogue, and only the first is saved — feeds are identified by their content, not their URL. A feed that repeats the same g:id is saved once too. Both are counted in RUN_SUMMARY.
  • The cap is a total, and it is spent. maxProducts bounds the whole run, not each feed, and the split between feeds is a starting share rather than a quota: a feed that fails, or that carries fewer products than its share, hands the remainder to the feeds that still have products left, so a cap of 10 across one live feed and one dead one delivers 10 and not 5. Nothing extra is fetched to do it.
  • Availability is normalised. Feeds write in stock and in_stock interchangeably; both come out as in_stock.
  • Big catalogues need memory. The feed is parsed in memory, so the default is 2 GB and 30 minutes. A feed over roughly 200 MB may need more.
  • One failure never kills the run. A 404, a private feed or an Atom feed is logged into RUN_SUMMARY.failures and the next feed is read. A single product that cannot be parsed is counted in RUN_SUMMARY.itemFailures and the rest of the feed continues. The Actor only errors out if every feed fails — and even then RUN_SUMMARY is written first, so a failed run still says what it delivered.
  • Public data only. No authentication, no access-control bypass, no personal data.

FAQ

Do I need a Google Merchant Center account? No. You supply no credentials.

Will it work on any store? Only on stores that publish a public feed. Most do, because that is how they get into Google Shopping, but the URL is not always advertised.

Does it convert currencies? No. price is the number the feed states and currency is its ISO code. A feed is single-currency; converting is your job.

Why is gtin null on so many products? Because small merchants leave it blank. g:ean, g:upc and g:isbn are read as aliases, but if none are present the field is null rather than fabricated.

Why did a product get filtered out when I set a price range? Either its price is outside the range, or the feed gave no readable price. RUN_SUMMARY.perFeed reports listed vs saved vs filtered per feed, which distinguishes the two.

Can I get only changed prices? Run on a schedule and diff on productId plus price. Combining maxProducts: 0 with a scheduled run gives you a full daily snapshot — and be deliberate about it, since 0 means every product in every feed.

Can I integrate it with something else? Yes, Apify API, client libraries, webhooks, scheduled runs, dataset exports (JSON/CSV/Excel) or MCP. Output is structured JSON.