Product Feed Scraper: Google Merchant & XML Catalogs
Pricing
from $0.74 / 1,000 product scrapeds
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
Maintained by CommunityActor stats
0
Bookmarked
1
Total users
0
Monthly active users
20 days ago
Last modified
Categories
Share

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 field | Meaning |
|---|---|
feedUrl, feedTitle | Feed that was read after redirects, and the store name from the channel |
productId, itemGroupId | g:id, and the g:item_group_id that ties variants of one product together |
title, description | g:title and g:description, description stripped to plain text |
link, imageLink, additionalImageLinks | Product page and images |
price, salePrice, currency | Numbers, not strings, "69.99 USD" becomes 69.99 plus "USD" |
availability, inStock | Google's enum (in_stock, out_of_stock, preorder, backorder) and a boolean |
brand, gtin, mpn, condition | Identity attributes, with g:ean / g:upc / g:isbn accepted as GTIN aliases |
productType, googleProductCategory | The merchant's own taxonomy and Google's |
shippingCost, shippingCurrency, shippingCountry | The first g:shipping group, flattened |
shippingOptions | Every g:shipping group the item declares: country, region, service, cost, currency |
scrapedAt | Run 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
productIdplusprice. - 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
availabilityflip 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
| Field | Type | Default | Notes |
|---|---|---|---|
feedUrls | array | - | Required. Public product feed URLs. .xml and gzipped .xml.gz both work. |
maxProducts | integer | 1000 | Saved products for the whole run, applied after filtering and shared evenly across the feeds, with any unspent share passed on. 0 = no limit. |
maxProductsPerFeed | integer | - | The former name of maxProducts, still accepted. It now bounds the run total, not each feed. |
inStockOnly | boolean | false | Keep only availability: in_stock. Products with no availability are dropped. |
brandFilter | array | [] | Exact, case-insensitive match on g:brand. Empty = all brands. |
minPrice | integer | - | Lower price bound in the feed's own currency. No conversion is done. |
maxPrice | integer | - | 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:
| Platform | Typical 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.atomis 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 conventionalg:prefix declared asxmlns: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 nog:fields at all. - Compression is handled both ways.
Content-Encoding: gzipis unwrapped by the HTTP layer, and a.xml.gzbody is detected by its magic number and decompressed in-process. No extra dependency. - Non-UTF-8 feeds are decoded. The XML declaration's
encodingattribute 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, thecharseton the response is used instead; acharsetthat 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$andR$are read as CAD, AUD, NZD and BRL. A bare$is read as USD — most feeds that write it mean USD, andcurrencyis always worth checking against the merchant's own store before you rely on it. - The shipping group is not the price.
g:shippingcarries its owng: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, sopriceis the product's price. Every group is returned inshippingOptions; the flatshippingCost/shippingCurrency/shippingCountryfields are the first group only. - One feed is charged once, however it is spelled.
example.com/feed.xml,www.example.com/feed.xml, thehttp://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 sameg:idis saved once too. Both are counted inRUN_SUMMARY. - The cap is a total, and it is spent.
maxProductsbounds 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 stockandin_stockinterchangeably; both come out asin_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.failuresand the next feed is read. A single product that cannot be parsed is counted inRUN_SUMMARY.itemFailuresand the rest of the feed continues. The Actor only errors out if every feed fails — and even thenRUN_SUMMARYis 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.