# 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](/docs/search-box.md), a store on any platform with a feed gets search without a developer. Stores on Shopify use the [Shopify app](/docs/shopify.md) instead, which syncs by itself.

## Find your feed

| Platform          | Where the feed is                                                                                                                             |
| ----------------- | --------------------------------------------------------------------------------------------------------------------------------------------- |
| WooCommerce       | [Our plugin](/docs/woocommerce.md), 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 6        | A Google Shopping product export, under Sales Channels. Its address ends with the export's access key                                          |
| Wix               | Marketing Integrations, Google Catalog, **Get Catalog Link**                                                                                  |
| Magento           | A feed extension, which writes the file to an address such as `/media/feed/google.xml`                                                         |
| PrestaShop        | A Google feed module                                                                                                                          |
| Your own platform | Any of the formats below. A JSON file of records, as the [API](/docs/records.md) 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](#variants) turned on.

## What it reads

| Format                  | What it is                                                                                                            |
| ----------------------- | --------------------------------------------------------------------------------------------------------------------- |
| XML                     | Google's and Meta's feeds: RSS 2.0 or Atom, with attributes such as `g:price`                                         |
| Tab-separated text      | Google's text feeds, with a header row of attribute names (`image_link` or `image link`)                              |
| Comma-separated text    | Meta's text feeds, the same way; semicolons work too                                                                  |
| JSON                    | An array of records, or `{ "records": [...] }`, each with an `objectID` (or an `id`)                                   |
| JSON Lines              | A 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 attribute                                                                | Record field                | How                                                                                                                         |
| ----------------------------------------------------------------------------- | --------------------------- | --------------------------------------------------------------------------------------------------------------------------- |
| `id`                                                                          | `objectID`                  |                                                                                                                             |
| `title`, `description`                                                        | `title`, `description`      | HTML removed                                                                                                                |
| `link`                                                                        | `url`                       |                                                                                                                             |
| `image_link`, `additional_image_link`                                         | `image`, `images`           |                                                                                                                             |
| `price`                                                                       | `price`, `currency`         | "15.00 USD" is `15` and `USD`; "1.990 ISK" is `1990`, since krónur have no decimals                                         |
| `sale_price`                                                                  | `price`, `compare_at_price` | While the sale is on, by `sale_price_effective_date`, the sale price is `price` and the regular one `compare_at_price`       |
| `availability`                                                                | `available`                 | `in_stock`, `preorder` and `backorder` are `true`; `out_of_stock` is `false`                                                |
| `product_type`                                                                | `product_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`–`4` | The same names |                                                                                                                             |
| `product_detail`                                                              | `specs`                     | Each detail by its name, such as `specs.width_cm`                                                                           |
| `product_highlight`                                                           | `highlights`                |                                                                                                                             |
| Shipping, tax and ads-only attributes                                         | Left out                    |                                                                                                                             |
| Anything else                                                                 | Its own name                | Up to 40 of them, as text                                                                                                   |

JSON and JSON Lines files are records as the [API](/docs/records.md) 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](/docs/settings.md#variants-inside-each-record) 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](/docs/settings.md#group-variants).

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](/docs/records.md) 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](/docs/woocommerce.md) does. Set the feed's `source` to `pages`; each page answers:

```json
{ "records": [{ "objectID": "1042", "title": "Rain jacket", "price": 129 }], "next": "https://shop.example/feed?page=2" }
```

- `records` are records as the [API](/docs/records.md) 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](/docs/api.md#product-feeds), the [server SDK](/docs/server-sdk.md#product-feeds), and the [MCP server](/docs/agents.md), so a platform can connect its stores' feeds for them:

```ts
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)
```
