# Chewy Product Catalog: Prices, Autoship and Pet Food SKUs (`sergeyfaraday/chewy-product-catalog`) Actor

Daily prices, Autoship prices, ratings and stock of Chewy dog food and treats, plus every product id from the public sitemaps, also outside these categories. No browser, no proxy.

- **URL**: https://apify.com/sergeyfaraday/chewy-product-catalog.md
- **Developed by:** [Sergey Faraday](https://apify.com/sergeyfaraday) (community)
- **Categories:** E-commerce
- **Stats:** 2 total users, 0 monthly users, 95.0% runs succeeded, 0 bookmarks
- **User rating**: No ratings yet

## Pricing

Pay per usage

This Actor is paid per platform usage. The Actor is free to use, and you only pay for the Apify platform usage, which gets cheaper the higher subscription plan you have.

Learn more: https://docs.apify.com/actors/running/actors-in-store.md#pay-per-usage

## What's an Apify Actor?

An Actor is a serverless cloud program that runs on the Apify platform. It has two run modes.
In Batch mode, an Actor accepts a well-defined JSON input, performs an action which can take anything from a few seconds to a few hours,
and optionally produces a well-defined JSON output, datasets with results, or files in key-value store.
In Standby mode, an Actor provides a web server which can be used as a website, API, or an MCP server.

Apify vocabulary and the platform model are defined once, in the agent quickstart at https://apify.com/agents.md.

## How to integrate an Actor?

If asked about integration, you help developers integrate Actors into their projects.
You adapt to their stack and deliver integrations that are safe, well-documented, and production-ready.

Do not guess an integration path. Every one of them is in the agent quickstart at https://apify.com/agents.md: the Apify MCP server, Agent Skills with the Apify CLI, the JavaScript and Python clients, the REST API, and the account-free path for an agent with no human to sign in. It also carries the rule on stating cost before the first paid run.

For examples already wired to this Actor's own input schema, see the [API](#api) section below.

Each client library has reference documentation the quickstart does not restate: [JavaScript/TypeScript](https://docs.apify.com/api/client/js/docs.md) (`npm install apify-client`) and [Python](https://docs.apify.com/api/client/python/docs.md) (`pip install apify-client`).

# README

## Chewy Product Catalog: Prices, Autoship and Pet Food SKUs

Tracks Chewy dog food and treats: price, Autoship price, list price, rating, review count, stock and the seller's key benefits, every day if you like. It reads the same catalog data the category pages use, with plain HTTP requests, no browser and no proxy.

A second mode lists the product ids in the public sitemaps, including products outside the dog food categories.

### How to use

1. Keep the default mode `prices` and set `maxProducts` to try it on a small sample.
2. Give each run a `snapshotId` (for example the date) and compare snapshots to follow prices. Use one `snapshotId` per mode: both modes write the same row key for a product.
3. To find new products, compare `parentId` between full prices passes. Use mode `sitemap` when you need ids outside these categories.

### Input

| Field | Default | Meaning |
|---|---|---|
| `mode` | `prices` | `prices`: prices and ratings per product family. `sitemap`: all product ids and names, no prices |
| `scope` | `dog_food_and_treats` | `dog_food`, `dog_food_and_treats`; `sitemap` mode also has `all_dog`, `pet_food_all_species`, `all` |
| `maxProducts` | all | stop after this many rows. This is a cut, not a pause, see below |
| `startFrom` | from the start | prices mode: listing position to start from, from `nextCursor` in the `MANIFEST` of a run that was cut |
| `startGroup` | `first` | prices mode: the category `startFrom` belongs to (`dog_food`, `dog_treats`). Categories before it are not read, categories after it start at their beginning |
| `delayMs` | 1000 | pause between catalog pages |
| `includeCanada`, `maxSitemapFiles` | | sitemap mode only |
| `snapshotId` | `<actor>.<runId>` | your id for this snapshot; re-running with the same id does not create duplicates |

```json
{ "mode": "prices", "scope": "dog_food_and_treats", "maxProducts": 72, "snapshotId": "2026-11-02" }
```

### Output

One `product` row per product family:

```json
{
  "recordType": "product",
  "platform": "chewy",
  "sourceRecordId": "3969182",
  "canonicalUrl": "https://www.chewy.com/pedigree-complete-nutrition-grilled/dp/3969182",
  "time": { "observedAt": "2026-11-02T08:00:00.000Z", "collectedAt": "2026-11-02T08:00:00.120Z" },
  "metrics": {
    "price": { "value": 25.97, "unit": "USD", "sourceName": "advertisedPrice", "priceCondition": "advertised" },
    "priceAutoship": { "value": null, "nullReason": "not_provided_by_source" },
    "priceList": { "value": 26.98, "unit": "USD" },
    "rating": { "value": 4.6356, "unit": "stars_1_5", "scope": "family" },
    "reviewCount": { "value": 11248, "unit": "count", "scope": "family" },
    "pricePerKg": {
      "value": 1.43,
      "unit": "USD/kg",
      "history": "derived",
      "basis": { "priceMetric": "price", "priceSource": "advertisedPrice", "priceCondition": "advertised", "sku": "3969182", "currency": "USD", "netWeightG": 18143.69, "packCount": 1, "weightSource": "title_and_ppu" }
    }
  },
  "data": {
    "name": "Pedigree Complete Nutrition Grilled Steak & Vegetable Flavor Dog Kibble Adult Dry Dog Food, 40-lb bag",
    "brand": "Pedigree",
    "parentId": "141433",
    "productType": "food",
    "productTypeSource": "title_head_noun",
    "inStock": true,
    "foodFormat": "dry",
    "netWeightG": 18143.69,
    "weightResolvedBy": "title_and_ppu",
    "sellerStatements": [{ "text": "Enriched with omega-6 fatty acid...", "status": "seller_statement" }]
  },
  "sampling": { "group": "dog_food", "position": 7, "sort": "byNewest" }
}
```

The `MANIFEST` record in the key-value store reports the status of each category (`ok`, `partial`, `blocked`, `failed`), the total Chewy reports, pages read, any truncation, and rows by product type and by how the weight was resolved.

### How to read the data

**One row is one variant of a family.**

- Prices mode returns one variant per product family, the one shown in the newest-first listing. For about a quarter of families this is another size than the default listing shows, for example a 1-lb bag instead of a 22-lb bag.
- The history key is `sourceRecordId`, not `parentId`: `sourceRecordId` is the variant, `parentId` the family. When another variant takes over the row of a family, that is a change of pack, not a change of price. Group by `parentId`, compare price per kg rather than price across families. `hasMoreVariants` says when other sizes exist.
- `rating` and `reviewCount` belong to the whole family (`scope: "family"`, `data.ratingScope`): a variant flagged `isNew` can show thousands of reviews.
- `isNew` is a source flag on this variant, not a launch date: a new size of an old product carries it too, and which date stands behind it is not established. To find a new product, compare `parentId` between full passes: two passes 3.5 hours apart differed by 6 of 14,482 families and lost none. Do not compare sitemap snapshots: the sitemap changes between fetches, and 2,107 of about 19,000 ids differed within 34 minutes.
- Newest-first is a sort, not a sales or popularity rank, and `position` is a place in that sort. Chewy's default order changes between requests, so a pass on it skips products.

**A limit is a cut, not a pause.**

- `maxProducts` ends the run. `nextFrom` in the `MANIFEST` is the position after the last row taken, `visibleSize` the positions taken by this run, and `nextCursor` where to go on; `nextCursor` is null when the category was read to its end.
- To continue, start a run with the same `snapshotId` and `startFrom` set to `nextCursor` (and `startGroup` when the cut fell in `dog_treats`). The rows of both runs together are the rows of one longer run, with the same positions, as long as the listing did not move in between. A repeat with a larger limit and no `startFrom` gets you there too, it just reads the first rows again.
- A category that the limit cut before its first row is `failed`, with `nextCursor` 0. A run started with `startFrom` reports only what it read: the earlier positions are in the run before it.
- Sitemap mode has no cursor: `applied.cutAtFile` names the file of the cut, `matched` what the files offered, `matchedDelivered` what reached the dataset. A repeat with a larger `maxProducts` under the same `snapshotId` completes the snapshot. `maxSitemapFiles` also ends `partial` with reason `maxSitemapFiles`, so a repeat with a larger limit under the same `snapshotId` completes the snapshot. An empty or short page far below the total leaves `nextCursor` at that position, and the error says the listing ended early.

**Restarts.**

- A restart reads the page or file in flight once more, without using the limit, so the MANIFEST says what one clean run says. Out of time it cannot, then `nextFrom` moves by the rows read back and a note says what the counts leave out.

**Time.**

- `observedAt` is taken after the response of the page or file arrived, `collectedAt` when this actor built the row, so "what did we know at time T" uses `collectedAt`. `runStartedAt` is the platform start, the same on every row of a run.
- Prices mode: the catalog JSON has no date of its own, so `sourceUpdatedAt` is null and `sourceUpdatedAtBasis` says why. Sitemap mode: `sourceUpdatedAt` is the entry's `lastmod`, else the `Last-Modified` of the sitemap file, and the basis says which. `availableAt` stays null with a reason in both modes.

**Records and the total.**

- A category is `partial` with reason `listing_shifted` when more than 0.5% of the total Chewy reports was served twice within the category or never read.
- Some products are listed in both categories, such as veterinary treats and bundles of food and treats. They get one row, under `dog_food`, so `records` of `dog_treats` stays below the total by that many: 25 of 9,089 positions on a full pass. `applied.listedInEarlierCategory` counts them and `applied.listedInEarlierCategoryIds` names them, a note says so, and the status stays `ok`.
- The total is not exact. `dog_treats` served 9,090 positions under a total of 9,088, so reading stops at a short page and not at the total. The total also moves: on 2026-11-02 it read 9,087, 9,092 and 9,088 within about 8 hours. `applied.totalMin` and `totalMax` report it within a pass. A product removed above the reading position during a pass can skip one row without a trace.

**Product type.**

- `productType` comes from the words the title ends on, so line names like "Daily Dish" or "Farm to Bowl" do not count, with the catalog category filling gaps. Values: `food`, `treat`, `supplement`, `medication`, `kit`, `accessory`, `non_food`, `other`. `kit` is a toy, gear or supplement sold together with food or treats, out of the food and treat filters; the parts are in `productTypeEvidence`.
- `productTypeSource` says how the type was found and `productTypeDoubtful` why it may be wrong. Exclude rows with `productTypeDoubtful` when you need a clean set: 332 of 14,476 rows of a full pass carry it. A title that names gear or a kit beside the food (with Puzzle Toy, Treats & Pouch, Cake Mold Kit) keeps its type and carries `productTypeDoubtful`: 40 rows of a full pass. A title with a food part and a treat part carries it too: 5 rows.
- `species` is `other` with evidence for food of other animals (`Snake Food`, `Fish Food`). The dog food category holds such rows. `species: null` means neither the title nor the URL names a species. When only the URL names it, `speciesEvidence` starts with `slug:`, as on compounded drugs whose title names no animal.
- Format and life stage come from the product name, at about 95% precision on a manual sample. Check the `*Evidence` fields when it matters. Format, life stage, dog size and claims come from the product name; the URL is read for them only when a row has no name. A claim quotes the name word for word.

**Weight and price per kg.**

- `netWeightG` is set only when one reading of the title survives. `2.12-oz bag, 3 count` can mean three bags or three pieces in a bag, so it stays null with `weightAmbiguous: true`, `weightAmbiguityReason` and `unitWeightG`, until the source's price per unit settles it. `weightResolvedBy` says what the weight rests on: `title`, `title_and_ppu`, `ppu`, `single_serve_container`.
- The source's price per unit is a witness, not the truth: Chewy computes it from size times count, even where that gives a 53.8 kg bag for $18.99. `impliedQuantityG` shows what it implies and never becomes `netWeightG`.
- `pricePerKg` is null with a reason: `ambiguous` (a weight is there, but not one), `no_weight` (no weight in the title), `sold_by_volume`. Its `basis` names the price and the weight behind the value. It uses the advertised price, not the Autoship price. `ambiguous` also covers a title that names gear or a kit, because the price can pay for more than the food; `metrics.pricePerKg.note` says so. 24 rows of a full pass.
- `bundlePartCount` is the number of part numbers behind the listing: 2 or 3 on a bundle of different products (`isBundle: true`), 1 on the same product in multiples. A title that names several products or weights gets no net weight.
- `inStock` is the flag of this variant for the US without a delivery ZIP (`data.stockScope`); which warehouses it covers is unknown.
- Key benefits are the seller's own words, stored as seller statements, not verified claims.

**When the source changes.**

- Rows were read but what defines a row is missing or unreadable: id, name, link, price. On every row at any count, or on 10% or more of the rows from 10 rows on, per page, per sitemap file or for the source: error `SCHEMA_CHANGED`, status `partial` when rows hold data, `failed` when none does.
- In prices mode the pass stops at that page, `nextCursor` stands after the delivered rows and the error names the position to read again. Such a row has `status: fetch_error` with `statusReason`, and a repeat under the same `snapshotId` replaces it.
- A value that is there and not readable (From $12.99, a range, another currency mark, a bare number) is null with `unparseable` and `raw`. A price of 0 is null with `zero_not_confirmed_as_real`. A rating outside 1 to 5 is null with `unparseable`.
- Fields every row sends (parent id, brand, Autoship and strike price, rating and count, price per unit, key benefits, stock and the other flags) gone or of another shape on that share of rows also give `SCHEMA_CHANGED` and `partial`, but the pass goes on. A response without `recordSetTotal` counts too.
- `applied.rowsInspected`, `rowsNotReadable`, `unreadableFields` and `shapeChangedFields` show what was seen.

### FAQ

**Where are the ingredients and guaranteed analysis?** Not in this data. The companion *Unified Scraper Adapter* actor has them in module `chewy_details`, all variants in `chewy_products` and reviews in `chewy_reviews`.

**What happens if Chewy blocks access?** The run stops and reports `blocked`. No bypass is attempted. In sitemap mode the first blocked file stops the reading, `applied.filesNotAttempted` names the rest. A page answered with HTTP 200 instead of a sitemap counts as a block, and 429 reads as `blocked` in both modes.

**Why is a zero rating shown as null?** A count of 0 means nobody rated the product, which is not a rating of 0: `rating` is null with reason `no_ratings` and `reviewCount` stays 0.

**Is this allowed?** Check Chewy's terms for your use case. Collecting content for third parties may need Chewy's written consent.

### Cost

A full prices pass reads about 400 catalog pages in about 20 minutes for about $0.11 of platform usage at 256 MB of memory, which is enough for this mode. A full sitemap pass reads about 270,000 entries in under a minute for about $0.08 of platform usage.

# Actor input Schema

## `mode` (type: `string`):

Prices reads the catalog data of the category pages (dog food and treats). Sitemap lists product ids and names, also outside these categories. To find new products compare parentId between full prices passes: the sitemap changes between fetches, so a difference between two sitemap snapshots is not a new product.

## `scope` (type: `string`):

Which products become rows. Prices mode covers the first two options. Cat products are never mixed into dog scopes.

## `includeCanada` (type: `boolean`):

Canadian store entries get ids prefixed with ca:. Off: US panel only.

## `maxProducts` (type: `integer`):

Stop after this many rows. This is a cut, not a pause: the run ends here and sources\[].nextCursor in the MANIFEST says where. Empty: everything in scope.

## `startFrom` (type: `integer`):

Prices mode: read the category from this listing position instead of from its start. To continue a cut run, pass sources\[].nextCursor from its MANIFEST under the same snapshotId. Empty: from the start.

## `startGroup` (type: `string`):

Prices mode: the category that Start from position belongs to. Categories before it are not read, categories after it start at their beginning.

## `delayMs` (type: `integer`):

Prices mode: pause between two catalog pages.

## `maxSitemapFiles` (type: `integer`):

Read only the first N pdp sitemap files (about 35,000 entries each). Empty = all.

## `sitemapIndexUrl` (type: `string`):

Override if Chewy renames the sitemap index.

## `snapshotId` (type: `string`):

Your id for this snapshot, one per task of one actor. Rows get recordKey = platform:type:id:snapshotId, so a repeat with the same snapshotId gives the same keys, and a new id gives a new observation. A repeat reads the source again: compare provenance.contentChecksum to see whether the values moved. Default: <actor>.<runId>.

## `queryId` (type: `string`):

Your search profile id, copied into provenance.queryId.

## `queryVersion` (type: `string`):

Search profile version, copied into provenance.queryVersion.

## `panelId` (type: `string`):

Fixed panel id for refresh runs, copied into provenance.panelId.

## `panelVersion` (type: `string`):

Panel composition version; a new composition must get a new version.

## `maxItems` (type: `integer`):

Hard cap on dataset rows for this run. Separate from the run money cap (maxTotalChargeUsd), which is set on the run itself.

## `maxRunSeconds` (type: `integer`):

Stop starting new units this many seconds after the run started and finish with a partial manifest. The budget belongs to the run: a restart does not renew it, and the time between workers counts.

## `keepRaw` (type: `boolean`):

Store the raw upstream item in `raw`. Off by default. provenance.rawChecksum is present on rows built from a raw item; it is null on a state row with no answer behind it and on derived rows.

## `failOnPartial` (type: `boolean`):

By default a partial result finishes as SUCCEEDED with status=partial in the MANIFEST record. Failed and blocked always finish as FAILED.

## `proxyConfiguration` (type: `object`):

Proxy for upstream requests.

## Actor input object example

```json
{
  "mode": "prices",
  "scope": "dog_food_and_treats",
  "includeCanada": false,
  "maxProducts": 72,
  "startGroup": "first",
  "delayMs": 1000,
  "keepRaw": false,
  "failOnPartial": false,
  "proxyConfiguration": {
    "useApifyProxy": false
  }
}
```

# Actor output Schema

## `dataset` (type: `string`):

No description

## `manifest` (type: `string`):

No description

# API

You can run this Actor programmatically using our API. Below are code examples in JavaScript, Python, and CLI, as well as the OpenAPI specification and MCP server setup.

## JavaScript example

```javascript
import { ApifyClient } from 'apify-client';

// Initialize the ApifyClient with your Apify API token
// Replace the '<YOUR_API_TOKEN>' with your token
const client = new ApifyClient({
    token: '<YOUR_API_TOKEN>',
});

// Prepare Actor input
const input = {
    "mode": "prices",
    "scope": "dog_food_and_treats",
    "maxProducts": 72,
    "proxyConfiguration": {
        "useApifyProxy": false
    }
};

// Run the Actor and wait for it to finish
const run = await client.actor("sergeyfaraday/chewy-product-catalog").call(input);

// Fetch and print Actor results from the run's dataset (if any)
console.log('Results from dataset');
console.log(`💾 Check your data here: https://console.apify.com/storage/datasets/${run.defaultDatasetId}`);
const { items } = await client.dataset(run.defaultDatasetId).listItems();
items.forEach((item) => {
    console.dir(item);
});

// 📚 Want to learn more 📖? Go to → https://docs.apify.com/api/client/js/docs

```

## Python example

```python
from apify_client import ApifyClient

# Initialize the ApifyClient with your Apify API token
# Replace '<YOUR_API_TOKEN>' with your token.
client = ApifyClient("<YOUR_API_TOKEN>")

# Prepare the Actor input
run_input = {
    "mode": "prices",
    "scope": "dog_food_and_treats",
    "maxProducts": 72,
    "proxyConfiguration": { "useApifyProxy": False },
}

# Run the Actor and wait for it to finish
run = client.actor("sergeyfaraday/chewy-product-catalog").call(run_input=run_input)

# Fetch and print Actor results from the run's dataset (if there are any)
print(f"💾 Check your data here: https://console.apify.com/storage/datasets/{run.default_dataset_id}")
for item in client.dataset(run.default_dataset_id).iterate_items():
    print(item)

# 📚 Want to learn more 📖? Go to → https://docs.apify.com/api/client/python/docs/quick-start

```

## CLI example

```bash
echo '{
  "mode": "prices",
  "scope": "dog_food_and_treats",
  "maxProducts": 72,
  "proxyConfiguration": {
    "useApifyProxy": false
  }
}' |
apify call sergeyfaraday/chewy-product-catalog --silent --output-dataset

```

## MCP server setup

```json
{
    "mcpServers": {
        "apify": {
            "type": "http",
            "url": "https://mcp.apify.com/?tools=fetch-actor-details,sergeyfaraday/chewy-product-catalog"
        }
    }
}
```

The hosted server signs you in with OAuth on first connect, so no API token belongs in this config. Clients without OAuth support can send an `Authorization: Bearer <APIFY_API_TOKEN>` header instead, using a token from API & Integrations in Apify Console (https://console.apify.com/settings/integrations).

## OpenAPI specification

Download the OpenAPI definition: https://api.apify.com/v2/actors/pxi5ify5BvcuANXgN/builds/Ri1YnHkaltMgKklfX/openapi.json
