Skip to content
Start free

Platforms

Product feeds

Connect your store's Google Shopping or Meta catalog feed, or a JSON file, and Findlane reads it every hour and keeps your index in step, with no code on your server.

A product feed is the file most shop platforms make for Google Shopping or Meta's catalog ads: every product, with its title, link, photo, price and stock. Give Findlane its address, and Findlane reads it every hour and keeps your index in step. Nothing runs on your server, and nobody writes a sync.

With the search box for any site, a store on any platform with a feed gets search without a developer. Stores on Shopify use the Shopify app instead, which syncs by itself.

Find your feed

PlatformWhere the feed is
WooCommerceOur plugin, which connects the store and reads it for you; or a free feed plugin that publishes an address, such as CTX Feed or Product Feed PRO
Shopware 6A Google Shopping product export, under Sales Channels. Its address ends with the export's access key
WixMarketing Integrations, Google Catalog, Get Catalog Link
MagentoA feed extension, which writes the file to an address such as /media/feed/google.xml
PrestaShopA Google feed module
Your own platformAny of the formats below. A JSON file of records, as the API takes them, needs no mapping

The Google for WooCommerce plugin sends products to Google itself and has no address, so it can't be used.

To try it first, use our sample feed: the demo store's 286 products, with shoes and clothes in sizes, at https://cdn.findlane.dev/demo-store/feed.xml.

Connect it

  1. On the index's Records page, choose Connect a feed.
  2. Paste the feed's address. If it needs a username and password, turn on It needs a sign-in: the password is stored encrypted and never shown again.
  3. Choose Check the feed. Findlane reads its first 2,000 items and shows the products they make, the fields they have, and any items it refused, with why.
  4. Choose how often to read it: every hour, every 6 hours, or once a day at an hour you pick (UTC). Then Connect and import.

The first import starts at once. If the index still has its default settings, it gets settings for the feed's fields: titles, brands, categories and descriptions searched; brands, categories, colors and sizes as filters; price sorts; and variants turned on.

What it reads

FormatWhat it is
XMLGoogle's and Meta's feeds: RSS 2.0 or Atom, with attributes such as g:price
Tab-separated textGoogle's text feeds, with a header row of attribute names (image_link or image link)
Comma-separated textMeta's text feeds, the same way; semicolons work too
JSONAn array of records, or { "records": [...] }, each with an objectID (or an id)
JSON LinesA record a line

Any of them may be gzipped. Findlane tells the format from the file's first bytes, not its name.

A feed can be up to 1 GB and 1,000,000 items. An index has one feed. Zip and bz2 files, FTP and SFTP aren't read.

How items become records

Google's and Meta's attributes become the fields the Shopify app's records have, so the search box, merchandising and recommendations treat every store alike:

Feed attributeRecord fieldHow
idobjectID
title, descriptiontitle, descriptionHTML removed
linkurl
image_link, additional_image_linkimage, images
priceprice, currency"15.00 USD" is 15 and USD; "1.990 ISK" is 1990, since krónur have no decimals
sale_priceprice, compare_at_priceWhile the sale is on, by sale_price_effective_date, the sale price is price and the regular one compare_at_price
availabilityavailablein_stock, preorder and backorder are true; out_of_stock is false
product_typeproduct_type, categories"Women > Dresses > Maxi": product_type is Maxi, categories lists all three, so each can be a category page
brand, gtin, mpn, color, size, material, pattern, gender, age_group, condition, custom_label_0–4The same names
product_detailspecsEach detail by its name, such as specs.width_cm
product_highlighthighlights
Shipping, tax and ads-only attributesLeft out
Anything elseIts own nameUp to 40 of them, as text

JSON and JSON Lines files are records as the API takes them, without mapping.

Variants

Google wants an item for each variant, such as each size and color of a shirt, all sharing an item_group_id. By default the items of a group become one product:

  • Its objectID is the item_group_id, and its title the words the variants' titles share, without their color or size.
  • price is the lowest, price_max the highest, and available is true when any variant is.
  • color, size and the other attributes that vary are lists, for filters.
  • variants lists each variant's id, url, image, price, available, color and size. With the variants setting on, each hit says which variant the search matched, so "blue shirt" links to the blue one.

Your plan counts records, so a store with 2,000 products in ten sizes uses 2,000. To keep a record for each item instead, choose One result per variant; each keeps its item_group_id, which you can group results by.

List a product's variants together in the feed, as plugins do. A variant listed more than 20,000 items after its product's others is refused, with that reason.

Runs

  • An unchanged feed costs nothing. Findlane asks the server whether the file changed since the last run, and compares the file with the one it last imported, so most hourly runs end there.
  • Only what changed is written. Each run replaces the index's products with the feed's, in one step: products that are new or different are written, and products the feed no longer has are deleted. Searches keep using the current products until the step is done.
  • Sales start and end on time. When a sale_price_effective_date begins or ends, the next run reads the feed even if the file hasn't changed.
  • Fetch now reads the feed at once, in full.
  • Writes between runs through the API, such as a stock change, work as usual, and the next run puts the feed's values back.

The Records page shows the last 20 runs: when, what each did, and why any items were refused.

Refused items

An item is refused, and left out, when it has no id or title, its price can't be read, its id is used by another item, or the index's own checks refuse it, such as a title over the size limit. The rest of the feed is imported. Each run lists how many were refused for each reason, with the first IDs.

When a run is held

A run isn't imported when:

  • the feed has no products;
  • it would remove more than half the index's products;
  • or more than a fifth of its items were refused.

The index keeps its products, and the workspace's owners get an email. If the feed is right, choose Import anyway on the Records page. Otherwise fix the feed: the next run reads it again.

When it fails

When the feed can't be fetched or read, the index keeps its products. After three failed runs in a row, the owners get an email, and Findlane tries every 6 hours, then once a day after a day of failures. After a week of failures the feed pauses, with a second email, until an owner chooses Resume. A run that works puts the feed back on its schedule.

Paged sources

A platform with many stores can answer a JSON endpoint a page at a time instead of writing a file, as the WooCommerce plugin does. Set the feed's source to pages; each page answers:

{ "records": [{ "objectID": "1042", "title": "Rain jacket", "price": 129 }], "next": "https://shop.example/feed?page=2" }
  • records are records as the API takes them, up to a few hundred a page; next is the following page's address on the same site, or null on the last.
  • A paged source's password goes in a findlane-feed-key header rather than a Basic sign-in, which WordPress refuses for anyone but its own users.
  • Up to 5,000 pages; all of them are read on each run.

Let Findlane in

Findlane fetches feeds with the user agent FindlaneFeeds/1.0 (+https://findlane.dev/docs/feeds). If your site blocks unknown visitors, allow it. The address can be https or, without a password, http.

From your own code

Everything above is in the API, the server SDK, and the MCP server, so a platform can connect its stores' feeds for them:

const index = client.index("products")
await index.feed.set({ url: "https://shop.example/feed.xml", schedule: "hourly" })
const feed = await index.feed.get()
console.log(feed?.lastRun?.status, feed?.lastRun?.records)