# Sam's Club Scraper - Products, Prices, Stock & Reviews (`abotapi/samsclub-scraper`) Actor

Scrape Sam's Club (samsclub.com) products by keyword, catalogue, category or URL. Extract prices, original prices, unit prices, ratings, stock availability, variants, images, specifications and customer reviews. Supports scheduled price change tracking.

- **URL**: https://apify.com/abotapi/samsclub-scraper.md
- **Developed by:** [Abot API](https://apify.com/abotapi) (community)
- **Security:** Limited permissions
- **Stats:** 2 total users, 1 monthly users, 100.0% runs succeeded, 0 bookmarks
- **User rating**: No ratings yet

## Pricing

from $1.40 / 1,000 product results

This Actor is paid per event. You are not charged for the Apify platform usage, but only a fixed price for specific events.
Since this Actor supports Apify Store discounts, the price gets lower the higher subscription plan you have.

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

## 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

## Sam's Club Scraper: Products, Prices, Stock & Reviews

Sam's Club Scraper turns samsclub.com, the US warehouse-club retailer, into a clean product data API. Search by keyword, walk the store's product catalogue, pick categories by name, or paste product, category, key-phrase and sitemap links and scrape them directly. Every product comes back as one row with price, was-price, unit price, discount, rating, review count, stock, images and key points, plus full copy, specifications, options, rating breakdown and customer reviews, then export to JSON, CSV or Excel, or pull the results straight into your app through the API.

### Why This Scraper?

- **Four ways to find products.** Search by keyword, walk the product catalogue (optionally narrowed by words in the product name), pick categories by words in their name, or paste product, category, key-phrase, category-hub and sitemap links and bare product numbers.
- **About 50 products per request, at no extra cost.** Category pages already carry price, was-price, unit price, rating, review count, stock, badges and key points, so a plain category run is the cheapest way to watch a whole department.
- **Full detail on demand.** Product pages add the description, labelled specifications, attributes, UPC, rating breakdown, review topics, the full image gallery and every colour or size option with its own item number, price and stock.
- **Customer voice included.** Review text, stars, title, date, first name, helpful votes and the verified-purchase flag, in the order you choose, optionally with the photos reviewers attached.
- **Built for monitoring.** Incremental mode returns only new, changed and, optionally, delisted products on a schedule, a price-only mode keeps description or image edits out of your bill, and a refused run fails loudly instead of returning an empty dataset.
- **Honest about gaps, stock and clubs.** A listed product page that no longer exists is skipped and never billed. Each product row names the club it was read for, and stock answers are kept out of change detection unless you ask for them, because the store's own stock answer flips between two reads of the same product.
- **Resume a large pull.** An interrupted run can continue from where it left off without paying twice for products already collected.

### Use Cases

- **Price and discount monitoring:** track price drops, was-prices, instant savings and unit prices across a category, a brand page or a product list on a schedule.
- **Competitive intelligence for retailers and brands:** compare assortment, pricing, ratings and option depth against the largest US warehouse club.
- **Bulk-pack and unit-price analysis:** price per ounce, per sheet or per count for grocery and household staples, ready to sort.
- **Review mining:** pull thousands of written reviews with their stars, dates and helpful votes to find what breaks and what people love.
- **Catalogue and content teams:** build a structured feed of descriptions, specifications, UPCs and images for a comparison site or app.

### Data You Get

> Sample shape: values are illustrative placeholders, not from a live record.

| Field | Example |
| --- | --- |
| `usItemId` | `"10000001"` (the store's item number; this is the row's identity) |
| `name` / `brand` / `model` / `upc` | `"Sample Paper Towels, 12 ct."` / `"Sample Brand"` / `"ST-12"` / `"000000000001"` |
| `url` | link to this product, without tracking parameters |
| `price` / `priceString` / `currency` | `25.68` / `"$25.68"` / `"USD"` |
| `wasPrice` / `onSale` / `discountPercent` / `savingsAmount` / `savingsText` | `29.98` / `true` / `14.3` / `4.3` / `"SAVE $4.30"` |
| `promotions` | the promotions shown with the product, e.g. `"Instant Savings: $4 off, Ends Nov 29, Limit 3"` |
| `unitPrice` / `unitPriceText` | `0.0042` / `"4.2 ¢/sheet"` |
| `rating` / `reviewCount` | `4.8` / `3098` |
| `ratingHistogram` / `recommendedPercent` / `reviewTopics` | review count per star / `91` / the topics reviewers mention most (needs a product page read) |
| `stockStatus` / `stockLabel` | `"IN_STOCK"` / `"In stock"` for the club this row was read for |
| `shippingStatus` / `pickupStatus` / `deliveryStatus` | availability per fulfilment method (needs a product page read) |
| `category` / `categoryPath` / `breadcrumbs` | the product's category trail (needs a product page read) |
| `description` / `highlights` | the product copy and its bullet points |
| `specifications` / `attributes` | labelled specifications and key attributes such as Brand, Color or Tire Size (needs a product page read) |
| `variants` | every colour or size option with its own `variantId`, `options`, `price`, `wasPrice`, `inStock` and link (needs a product page read) |
| `imageUrl` / `images` / `videos` | main photo, the gallery and product videos |
| `badges` / `socialProof` / `fulfillmentTexts` / `isSponsored` | the category page's badges, popularity flag, delivery texts and paid-placement flag (category pages only) |
| `clubId` / `clubName` | the club the page was read for (needs a product page read) |
| `reviews` | written reviews (with **Fetch customer reviews**) |
| `position` / `page` / `sourceLabel` | where the product ranked in its walk and what it was found through |
| `changeType` / `changedFields` / `firstSeenAt` / `lastSeenAt` | incremental mode: `"NEW"` / `[]` / timestamps |

Every record also carries `reviewsWithTextCount`, `productId`, `abstractProductId`, `productType`, `categoryPathId`, `priceChannel`, `fulfillmentType`, `orderLimit`, `snapEligible`, `freeReturns`, `returnPolicy`, `warranty`, `directions`, `warnings`, `sellerType`, `detailsFetched`, `reviewsRequested`, `reviewsDelivered`, `sourceUrl` and `scrapedAt`.

### How to Use

1. Pick a **mode**: `search` (keywords), `catalog` (walk the product list), `category` (categories by name) or `url` (paste links).
2. Fill in the fields for that mode, then apply any sort, price, rating or sale filters you want.
3. Set **Max products** to control run size and cost, then click **Start**.
4. Download the dataset as JSON, CSV or Excel, or read it through the API.

**Search by keyword, cheapest first:**

```json
{
  "mode": "search",
  "queries": ["coffee", "paper towels"],
  "sortBy": "priceAsc",
  "maxItems": 100
}
```

**Read a category by name, cheapest first, discounted only:**

```json
{
  "mode": "category",
  "categoryKeywords": ["paper towels"],
  "maxCategories": 2,
  "sortBy": "priceAsc",
  "onSaleOnly": true,
  "maxItems": 100
}
```

If you leave `urls` untouched in url mode, the actor scrapes six sample links, one per link shape. Replace them with your own links.

**Paste links, with full product-page detail:**

```json
{
  "mode": "url",
  "urls": [
    "https://www.samsclub.com/browse/paper-towels/1110179",
    "https://www.samsclub.com/browse/gluten-free/710125?facet=Brand%3AQuaker",
    "18040450426"
  ],
  "fetchDetails": true,
  "maxItems": 40
}
```

**Monitor a category on a schedule, price changes only:**

```json
{
  "mode": "url",
  "urls": ["https://www.samsclub.com/browse/paper-towels/1110179"],
  "incrementalMode": true,
  "changeScope": "priceAndStock",
  "emitExpired": true,
  "expireAfterMissedScans": 2
}
```

#### Run it from your code

Python:

```python
from apify_client import ApifyClient

client = ApifyClient("<YOUR_APIFY_TOKEN>")
run = client.actor("abotapi/samsclub-scraper").call(run_input={"mode": "search", "queries": ["paper towels"], "maxItems": 50})
for product in client.dataset(run["defaultDatasetId"]).iterate_items():
    print(product["name"], product["price"], product["rating"])
```

JavaScript:

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

const client = new ApifyClient({ token: '<YOUR_APIFY_TOKEN>' });
const run = await client.actor('abotapi/samsclub-scraper').call({ mode: 'search', queries: ['paper towels'], maxItems: 50 });
const { items } = await client.dataset(run.defaultDatasetId).listItems();
```

Or connect it to Make, Zapier, n8n, Google Sheets or webhooks from the **Integrations** tab.

#### Link shapes you can paste

In url mode the actor reads these shapes: a search page (`/search?q=<keyword>`, read as that keyword, with its `page` and `sort` kept), a product page (`/ip/<name>/<item number>`) or a bare item number, a category page (`/browse/<name>/<id>`, optionally with a store filter such as `?facet=Brand%3AQuaker`, which is kept), a key-phrase page (`/c/kp/<phrase>`), a category hub (`/cp/<name>/<id>`, which returns the products of the category pages it links to) and the store's public product and category sitemaps. A category link that already carries `page=N` starts at that page, and a `sort` value written into a link is kept. Account, cart and checkout pages are not used and are skipped with a warning.

#### Search mode

Each entry in `queries` is searched on the store (up to 50 different keywords per run, repeats ignored) and its result pages are read one after another to the last page the store offers, about 50 products per page, until **Max products** is reached. The store's own result pages end where its match list ends, so a narrow keyword stops after a page or two. **Max products** is shared evenly between the keywords, and a product found by two keywords is returned once, labelled with the first. Only the products the store lists as matches are returned: the "You might also like" and "Other options to consider" strips on a search or key-phrase page are left out. Sorting (recommended, price low to high, price high to low) and the price, rating, sale and sponsored filters apply to search results too; store-side filters such as brand are not offered, so for those paste a category link that carries them.

The store treats some keywords as a category name and answers them with that category's page ("ipad", "tires"): the actor reads that category (a category hub returns the category pages it links to) and labels the rows with your keyword. A keyword the store cannot match still returns its closest products and a keyword with no result at all returns nothing and is logged; only rows that reach the dataset are billed. The store sometimes refuses a single request; the actor retries it on a fresh connection within the run's time limit.

#### Catalogue mode and category mode

**Catalogue mode** walks the product list the store publishes for crawlers. Words in `slugKeywords` narrow the list by the words in each product's name (a product matches when its name holds every word of at least one entry). Every row comes from the product page, so it always carries full detail and is billed one Product page detail event. Some of the pages the store lists no longer exist; they are skipped, never returned and never billed. A page only counts as gone after three fresh connections each received the store's own not-available page, because a product can be absent from one club's range and present at another's.

**Category mode** looks up categories whose name holds your words in the store's public category list (`categoryKeywords`, at most `maxCategories` of them, broadest names first) and walks their result pages. **Max products** is shared evenly between them.

#### Which club a row was read for

The store ties stock, pickup and delivery answers, and even which products exist, to a club that it assigns from the connection's location. Two rows of one run can therefore come from different clubs, and the product-page `clubId` and `clubName` fields say which. In testing, the online price was identical across clubs. A category walk is read on one connection from start to finish so its pages stay consistent. Turn on **Read every page as one fixed club** to read everything as the store's own default club: answers are then consistent, and the order and membership of a capped or very large category stop varying between scheduled runs, but products that club does not sell (for example some tyres) are reported as not available. Use it for scheduled monitoring of a category.

#### Output tab views

The Output tab offers four views: **Products** (price, discount, rating, stock and badges), **Product detail** (specifications, attributes, options and gallery), **Customer reviews** (one row per written review, unwound from the reviews array) and **Changes since the last run** (incremental status and changed fields; with Compact updates on, also the old and new values of each price change and the per-option changes). Customer reviews are capped per product by **Max reviews per product**, from 1 to 1000 (default 20).

#### Keeping a run cheap

A search or category page already carries price, was-price, unit price, rating, stock, badges and images for about 50 products, at no extra cost. **Read each product's page** and **Fetch customer reviews** each need one extra page per product and are billed with one Product page detail event per product that reaches the dataset. They share that one page, so turning both on costs the same as turning on either. Each review returned inside a row is billed with one Customer review event. Catalogue mode and a pasted product link always read the product page, because there is no result card behind them, so they are billed one Product page detail event whenever a row reaches the dataset, whatever the toggle says. The price, rating and sale filters narrow the rows after they are read, so a narrowed catalogue run reads more product pages per row returned; in category mode a filter that the result card can already decide avoids the extra page read.

#### How Max products and the filters shape a run

When **Max products** is set, every category and link in a run gets its own share of it, split evenly, so one large category cannot use up the whole run and starve the rest. The price, rating, sale and sponsored filters narrow the rows this actor returns rather than being sent to the store. For a store-side filter such as brand or product type, open the category on the store, tick the filter and paste the resulting link: the filter written into the link is applied and the run reads exactly the matches the store shows.

#### Resume and recurring updates

- **Resume** (`resumeFromRunId`) continues one interrupted run: paste its run or dataset ID and the actor skips everything already collected there, so you don't pay twice.
- **Incremental mode** (`incrementalMode`) is for scheduled runs over the same scope. Each product is classified `NEW`, `UPDATED` (with `changedFields`), `UNCHANGED` (suppressed and not billed unless `emitUnchanged` is on), `REAPPEARED` or `EXPIRED` (only after a run that scanned the whole tracked scope, and only with `emitExpired`). `stateKey` names or shares the stored state. With Incremental mode off, output is exactly as before.

**What counts as a change.** Compared: price, was-price, discount, on-sale flag, unit price, name, brand, description, specifications, options with their prices and the category path. Excluded by default but still returned in the row: scrape timestamps, the product's rank and page, the sponsored flag, delivery and pickup texts and popularity badges (they change with the day), the club the page was read for, image and video links, the store's generated product summary, and ratings and reviews (average rating, review count, rating breakdown, topics, summary and the review sample, which move with every new review). Stock and availability (`stockStatus`, `stockLabel`, the per-method statuses, order limits and per-option stock) are excluded too, because the store's answer differs between two reads of an unchanged product; turn on **Count stock changes** (`watchStock`) only if you accept that those flips mark products `UPDATED`. Option lists are compared regardless of the order the store returns them. Switching the exclusions never causes a one-time wave of `UPDATED` rows: `UNCHANGED` is decided by re-reading the stored record under the current settings.

- `changeScope: priceAndStock` compares only price, was-price, discount, on-sale flag and per-option price (and stock, when counted), so a name, description or specification edit is never a billable `UPDATED` row. Switching `changeScope`, or editing `ignoreFieldsForChanges`, keeps your existing saved state: nothing comes back as `NEW`, each stored product is simply re-compared under the new setting.
- `compactUpdates` turns `UPDATED` and `EXPIRED` rows into slim rows (ids, link, name, current price and stock, `priceChanges` old and new, `variantChanges` per option as `ADDED`, `REMOVED` or `CHANGED`). `NEW` rows stay full. A compact row does not carry product-page detail fields or reviews, so no detail or review charge applies to it.
- `expireAfterMissedScans` applies with `emitExpired`. `EXPIRED` means the product dropped out of the scanned results or its page is gone; one club's range can differ from another's, so a value of 2 or more avoids false expiries.
- A link that starts past page 1, a run stopped by **Max products**, **Max result pages**, the time limit or your maximum cost per run, a run that could not read some page, a resumed run, or a run whose scope or filters differ from those the state key was last saved with never produces `EXPIRED` rows.

`EXPIRED` rows count against **Max products** like every other row. They are only produced after a run that scanned the whole tracked scope, so a run that stopped because it reached **Max products** returns none. Products held back that way stay tracked and are returned, and billed, as `EXPIRED` on a later complete run.

### Send results into your apps (MCP connectors)

Optionally pipe the scraped results into the apps you already use, via Model Context Protocol (MCP) connectors. This is an extra delivery step **after** the scrape: the Apify dataset is never changed.

**What gets written to the connector:** a condensed, human-readable **summary** of each product, not the full JSON. Each item becomes one entry with a **title** and its key fields flattened to plain text. The **complete record always stays in the Apify dataset**.

1. Authorize a connector once under **Apify, Settings, Integrations** (Notion, Linear, Airtable, or Apify).
2. Select it in the **"Pipe results into your apps"** input field. (If the picker is empty, you haven't authorized a connector yet.)
3. For **Notion**, also set `notionParentPageUrl` to the page where items should be created.

The connection is mediated by Apify's MCP proxy, so this actor never sees your third-party credentials. Leave the field empty to skip.

### Input Parameters

| Parameter | Type | Default | Description |
| --- | --- | --- | --- |
| `mode` | string | `search` | `search` to search by keyword, `catalog` to walk the product list, `category` to pick categories by name, or `url` to scrape pasted links. |
| `queries` | array | `["coffee"]` | Keywords to search (search mode), up to 50 different ones. |
| `slugKeywords` | array | `["paper towels"]` | Words in the product name (catalogue mode). Empty = the first products of the list. |
| `categoryKeywords` | array | `["paper towels"]` | Words in the category name (category mode). |
| `maxCategories` | integer | `5` | How many matching categories are walked (category mode). |
| `urls` | array | six sample links (one per link shape) | Product, category, key-phrase, hub or sitemap links, or bare item numbers (url mode). |
| `sortBy` | string | `relevance` | `relevance`, `priceAsc` or `priceDesc` for search and category pages. A sort written into a pasted link wins. |
| `minPrice` | number | (none) | Only keep products at or above this price, in US dollars. |
| `maxPrice` | number | (none) | Only keep products at or below this price, in US dollars. |
| `onSaleOnly` | boolean | `false` | Keep only products whose current price is below their was-price. |
| `minRating` | number | (none) | Keep only products with an average rating of at least this value (0 to 5). |
| `skipSponsored` | boolean | `false` | Leave out the paid placements mixed into category pages. |
| `fetchDetails` | boolean | `false` | Category and link walks: read every product's own page for the full description, specifications and options. |
| `fetchReviews` | boolean | `false` | Also collect customer reviews (reads the product page too). |
| `maxReviewsPerProduct` | integer | `20` | Cap on reviews collected per product, from 1 to 1000. |
| `reviewSort` | string | `relevance` | `relevance`, `newest`, `oldest`, `highest`, `lowest` or `helpful`. |
| `includeReviewImages` | boolean | `false` | Add the photo links reviewers attached. |
| `fixedClub` | boolean | `false` | Read every page as the store's default club for consistent stock answers. |
| `maxItems` | integer | `20` | Maximum number of products to return across the whole run, up to 100000. `0` = the 100000 maximum. |
| `maxPages` | integer | `0` | Maximum result pages per keyword, category or link. `0` = no limit set; a safety limit always applies, and the log says when a limit stops a keyword or category that still had results. |
| `resumeFromRunId` | string | (none) | Continue one interrupted run without re-collecting or re-billing its products. |
| `incrementalMode` | boolean | `false` | Return only new and changed products on scheduled runs. |
| `stateKey` | string | (none) | Name or share an incremental-mode state. |
| `changeScope` | string | `all` | `priceAndStock` limits change detection to price (and stock, when counted) and per-option price. |
| `watchStock` | boolean | `false` | Count stock and availability changes as changes. |
| `compactUpdates` | boolean | `false` | Slim `UPDATED` and `EXPIRED` rows with `priceChanges` and `variantChanges`. |
| `expireAfterMissedScans` | integer | `1` | Mark `EXPIRED` only after this many consecutive complete scans missed the product. |
| `ignoreFieldsForChanges` | array | (none) | Extra fields (or dotted paths such as `variants.url`) that never count as a change. |
| `emitUnchanged` | boolean | `false` | Also return (and bill) unchanged products. |
| `emitExpired` | boolean | `false` | Also return (and bill) products no longer found after a full scan, within the room left under **Max products**. |
| `proxy` | object | `{"useApifyProxy": true}` | Connection settings. |
| `mcpConnectors` | array | (none) | Optional: send a summary of each product to apps you authorized under Integrations. |
| `notionParentPageUrl` | string | (none) | Notion connector only: page under which products are created. |
| `maxNotifyListings` | integer | `50` | Cap on products written to each connector per run. |

### Output Example

> Sample shape: values are illustrative placeholders, not from a live record.

```json
{
  "recordType": "product",
  "usItemId": "10000001",
  "productId": "ABC123DEF456",
  "name": "Sample Paper Towels, 12 Rolls",
  "url": "https://www.samsclub.com/ip/sample-paper-towels-12-rolls/10000001",
  "brand": "Sample Brand",
  "upc": "000000000001",
  "productType": "Paper Towels",
  "category": "Paper Towels & Napkins",
  "categoryPath": ["Household Essentials", "Disposable Paper Products", "Paper Towels & Napkins"],
  "price": 25.68,
  "priceString": "$25.68",
  "priceChannel": "ONLINE",
  "currency": "USD",
  "wasPrice": 29.98,
  "onSale": true,
  "discountPercent": 14.3,
  "savingsAmount": 4.3,
  "savingsText": "SAVE $4.30",
  "promotions": ["Instant Savings: $4.30 off, Ends Nov 29"],
  "unitPriceText": "4.2 ¢/sq ft",
  "rating": 4.8,
  "reviewCount": 3098,
  "ratingHistogram": {"1": 39, "2": 26, "3": 70, "4": 241, "5": 2722},
  "recommendedPercent": 91,
  "stockStatus": "IN_STOCK",
  "stockLabel": "In stock",
  "shippingStatus": "IN_STOCK",
  "pickupStatus": "IN_STOCK",
  "highlights": ["Sample bullet point"],
  "description": "A sample description of the product.",
  "specifications": {"Net Weight": "2.57 lbs."},
  "attributes": {"Brand": "Sample Brand"},
  "variants": [],
  "imageUrl": "https://i5.samsclubimages.com/asr/00000000-0000-0000-0000-000000000000.jpeg",
  "clubId": "6372",
  "clubName": "Sample Club",
  "position": 3,
  "page": 1,
  "sourceLabel": "paper-towels",
  "detailsFetched": true,
  "reviewsRequested": true,
  "reviews": [
    {"reviewId": "1", "rating": 5, "title": "Great value", "text": "Sample review text.", "date": "2026-09-25", "dateText": "9/25/2026", "author": "Sam", "verifiedPurchase": true, "helpfulVotes": 2, "notHelpfulVotes": 0, "recommended": null, "badges": ["Verified Purchase"], "images": []}
  ],
  "reviewsDelivered": 1,
  "changeType": "NEW",
  "changedFields": [],
  "firstSeenAt": "2026-10-31T12:00:00Z",
  "lastSeenAt": "2026-10-31T12:00:00Z",
  "scrapedAt": "2026-10-31T12:00:00Z"
}
```

### Plan Requirement

The default connection works: the standard Apify datacenter pool served product, category and review pages in testing, so no residential proxy is needed to get started. This store does sometimes refuse a page (about one request in ten); when it does, the run switches to a fresh connection and another client profile and retries. If refusals keep coming it escalates, in order, to backup connections, residential exits and finally Apify Proxy's enhanced group, and it stays on the connection that works for the rest of the run. Residential connections give the most reliable results on a bad day, and you can select them in the Connection section. Proxy traffic is billed on your own Apify plan, and the enhanced group costs more per request.

If your plan does not include proxy access, the run says so in plain words rather than returning an empty dataset. These are the messages you may see in the run log:

- Warning: "The selected connection is unavailable to this run. Apify free plans do not include residential proxies. Continuing on a backup connection; some pages may still be unavailable." followed by an upgrade pointer.
- Info: "Served over the backup connection (the default connection was unavailable)."
- Failure: "The proxy connection was refused for this account, so no page could be read. Your Apify plan does not include proxy access; a paid plan is required".
- Failure: "No results: residential proxies are unavailable on this run (Apify free plans do not include them), and the backup connection could not load the requested Sam's Club pages."
- Failure: "Could not load any page (the requests failed), so nothing was collected. This is a connection problem, not an empty result. Please try again later."

You can supply your own proxy URLs instead; custom proxies get no automatic backup connection.

### FAQ

#### How much does it cost?

Four events can appear on a run, and the **Pricing** tab shows the current rates:

- **Dataset item:** one event per product row returned in the dataset (incremental `UNCHANGED` rows are suppressed and not billed unless **Emit unchanged products** is on).
- **Product page detail:** one event per product row whose own page is read, whether through **Read each product's page**, **Fetch customer reviews**, catalogue mode or a pasted product link. Turning both toggles on still costs one event per product.
- **Customer review:** one event per review returned inside a product row.
- **Actor start:** one event each time a run starts.

Use **Max products** to cap the cost of any run. Failed, empty or missing pages are never billed, and a run that reaches your maximum cost per run stops and says so.

#### Is it legal to scrape Sam's Club?

This actor collects only publicly available product data from pages the store publishes for visitors, including its public search result pages. You are responsible for how you use it: follow the store's terms and the laws that apply to you, and get legal advice if you plan commercial redistribution. Prices and specifications are generally facts, but product photos and brand names may be subject to third-party rights. Reviews are written by private individuals, so treat them as personal data where privacy law applies.

#### Can I search by keyword?

Yes. Use search mode and list one or more keywords. Catalogue mode (words from the product name) and category mode (words from the category name) remain for walking whole product lists and departments, and you can paste a category or key-phrase page you found on the store, with any filter you ticked.

#### Can I get only new or changed products on a schedule?

Yes. Schedule the actor from the **Schedules** tab and turn on **Incremental mode**. Each run then returns only new and updated products, and unchanged ones are not billed unless you turn on **Emit unchanged**.

#### Why does a row show a different stock answer than the website for me?

Stock, pickup and delivery answers depend on the club the store assigns to the connection, and the store's own answer can flip between two reads seconds apart. Each product-page row names its club (`clubId`, `clubName`), and **Read every page as one fixed club** makes the club the same for the whole run. The online price was identical across clubs in testing.

#### Why were some products skipped?

The store lists some product pages that no longer exist, and some products are sold only at certain clubs. A page is skipped, not returned and not billed, only after three fresh connections each received the store's own not-available page. The log reports how many were skipped.

#### Why did my run fail instead of returning an empty dataset?

If the store refuses every request, the run stops with a clear message so "no matches today" is never confused with "nothing could be read". Run it again in a few minutes. If only some pages could be read, you still get what was found, and the run tells you the result is incomplete.

#### Can I use it with AI agents or MCP?

Yes. Call it from any Apify integration or MCP client, and use the connector field to push results into Notion, Linear or Airtable.

### 🔗 Want more e-commerce data?

Pair this actor with these related scrapers from the same team:

<table>
<tr><td>🔌 <a href="https://apify.com/abotapi/pbtech-co-nz-scraper"><b>PB Tech Scraper</b></a><br>Scrape pbtech.co.nz products with full specifications, price, brand, condition, per-store...</td><td>🛒 <a href="https://apify.com/abotapi/harvey-norman-scraper"><b>Harvey Norman Scraper</b></a><br>Scrape Harvey Norman Australia products: name, brand, price, was-price / discount...</td></tr>
<tr><td>🛒 <a href="https://apify.com/abotapi/netshoes-scraper"><b>Netshoes Brazil</b></a><br>Scrape sportswear and footwear from netshoes.com.br. Search by keyword with the store's...</td><td>🛒 <a href="https://apify.com/abotapi/amazon-au-scraper"><b>Amazon Australia Product &amp; Reviews Scraper</b></a><br>Scrape Amazon Australia (amazon.com.au) products and customer reviews. Search by keyword...</td></tr>
<tr><td>🛒 <a href="https://apify.com/abotapi/muji-scraper"><b>MUJI Scraper</b></a><br>Scrape MUJI products from the United States, Canada and Australia storefronts. Search by...</td><td>🛒 <a href="https://apify.com/abotapi/myer-au-scraper"><b>Myer Scraper</b></a><br>Scrape Myer (myer.com.au) department-store products: name, brand, price, was-price...</td></tr>
</table>

👉 [Browse all abotapi scrapers](https://apify.com/abotapi)

### 💬 Support

- 🐞 **Found a bug or a missing field?** Open a ticket on the [Issues tab](https://apify.com/abotapi/samsclub-scraper/issues/open). We usually reply within hours.
- 🛠️ **Need another site, extra fields or a private build?** Email <contact@abotapi.com>.
- ⭐ **Enjoying it?** A quick review on the actor page helps other users find it.

# Actor input Schema

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

Choose 'search' to search by keyword, 'catalog' to walk the store's product list, 'category' to pick category pages by name, or 'url' to scrape pasted links. Each mode reads only its own fields below.

## `queries` (type: `array`):

Only used when mode = search, ignored in the other modes. One entry per search, for example 'coffee' or 'paper towels'. Products found by several keywords are returned once. A keyword that names a whole category (for example 'ipad') is answered by the store with that category's page, which is read instead. Words the store cannot match still return its closest products. Up to 50 different keywords per run; Max products is shared between them.

## `slugKeywords` (type: `array`):

Only used when mode = catalog, ignored in the other modes. Each entry is a word or phrase, for example 'paper towels' or 'tire'. A product matches when its name holds every word of at least one entry. Leave empty to take the products in the order the store lists them. Some listed product pages no longer exist; those are skipped and never charged.

## `categoryKeywords` (type: `array`):

Only used when mode = category, ignored in the other modes. Each entry is a word or phrase, for example 'tires', 'coffee' or 'paper towels'. A category matches when its name holds every word of at least one entry. Category pages list the products with price, was-price, rating and stock; tick 'Read each product's page' for full detail.

## `maxCategories` (type: `integer`):

Only used when mode = category. How many matching categories are walked, broadest names first (a shorter name is a broader category), then in the order the store lists them. Max products is shared between them.

## `urls` (type: `array`):

Only used when mode = url, ignored in the other modes. Accepted: product links (/ip/...) and bare product numbers, category pages (/browse/..., a filter written into the link such as ?facet=Brand%3AQuaker is kept), key-phrase pages (/c/kp/...), category hubs (/cp/...) and the store's sitemaps. A search page (/search?q=...) is read as that keyword. Account, cart and checkout pages are skipped with a warning. A link carrying page=N starts at that page. Tracking parameters are ignored.

## `sortBy` (type: `string`):

Order of search and category result pages. A sort order written into a pasted link wins over this setting. Catalogue mode and product links have no order to set.

## `minPrice` (type: `number`):

Only return products priced at or above this value, in US dollars. Applied to the returned rows.

## `maxPrice` (type: `number`):

Only return products priced at or below this value, in US dollars. Applied to the returned rows.

## `onSaleOnly` (type: `boolean`):

Only return products that currently show a was-price above the current price.

## `minRating` (type: `number`):

Only return products whose average customer rating is at least this value (0 to 5).

## `skipSponsored` (type: `boolean`):

Category pages mix paid placements in with the organic products. Turn on to leave the sponsored ones out; each category row carries an isSponsored flag either way.

## `fetchDetails` (type: `boolean`):

Search, category and key-phrase pages only: read every product's own page for the full description, specifications, options, rating breakdown and image gallery. Off by default because it costs one extra page per product. Catalogue mode and pasted product links always read the product page (there is no result card behind them), so those rows are always billed one 'Product page detail' event whatever this toggle says.

## `fetchReviews` (type: `boolean`):

Also collect customer reviews for each product: rating, title, text, date, first name, helpful votes, verified-purchase flag. Also reads the product page, so it carries the same per-product surcharge as the toggle above, plus one 'Customer review' event per review returned. The average rating, review count and rating breakdown do not need this.

## `maxReviewsPerProduct` (type: `integer`):

Cap on how many individual reviews are collected for each product when 'Fetch customer reviews' is on. The first ten come with the product page; further ones are read ten at a time. Each review returned is billed, so keep this tight.

## `reviewSort` (type: `string`):

Which reviews come first when a product has more than the cap.

## `includeReviewImages` (type: `boolean`):

Add the links of the photos reviewers attached to each review.

## `fixedClub` (type: `boolean`):

The store ties stock, pickup and delivery answers, and which products exist at all, to a club that it assigns by the connection's location, so two rows of one run can come from different clubs (each product row names its club). Turn this on to read every page as the store's own default club instead: the answers are then consistent, but products that club does not sell (for example some tyres) are reported as not available. Prices were identical across clubs in every check. Off by default.

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

Maximum number of products to return across the whole run, EXPIRED rows from Incremental mode included. This is the run's cap. Use 0 for the maximum of 100000 products per run.

## `maxPages` (type: `integer`):

Maximum number of result pages to take per keyword, category or link. Each page carries about 50 products. 0 = no page limit set; the run then stops at Max products, when there are no more results, or at a safety limit of 4000 pages per keyword or category (far above what Max products can reach). When a limit you set here stops a keyword or category that still had results, the log says so. Catalogue mode and product links have no result pages.

## `proxy` (type: `object`):

Connection used for every request. Leave the default; the run escalates on its own when the store refuses a page. Choose residential for the most reliable results.

## `resumeFromRunId` (type: `string`):

Paste a previous run ID or dataset ID to continue a large pull without returning or charging for products already collected there. Use this after an interrupted run. For recurring monitoring of the same scope, use Incremental mode below instead.

## `incrementalMode` (type: `boolean`):

Turn this on for daily or recurring monitoring. The first run returns every matching product as NEW. Later runs normally return only NEW, UPDATED and REAPPEARED products, which makes it a cheap way to watch price drops. What is compared: price, was-price, discount, on-sale flag, unit price, name, brand, description, specifications, options with their prices and the category path. What is excluded by default (still returned in the row, never a change): scrape timestamps, the product's rank and page, the sponsored flag, delivery and pickup texts, popularity badges, the club the page was read for, image and video links, the store's generated summary, and ratings and reviews (average rating, review count, rating breakdown, topics, summary and the review sample). Stock and availability are also excluded unless you turn on 'Count stock changes' because the store's stock answer differs between two reads of an unchanged product. Option lists are compared regardless of the order the store returns them. 'Changes to watch' narrows this to price (and stock when counted) only. Turn on 'Emit unchanged' or 'Emit expired' only when you also want those rows returned (and billed). State is kept separately for each scope and filter setup; use State key to name or deliberately share a monitoring campaign. Which products a category lists, and in what order, can follow the club the connection resolves to, so for scheduled monitoring of a capped or very large category turn on 'Read every page as one fixed club'. To continue one specific interrupted run instead, use Resume from a previous run above.

## `stateKey` (type: `string`):

Optional. Name this monitoring campaign to keep its state stable, or to deliberately share state across differently configured runs. Leave empty to derive a key automatically from the scope and filter settings, so two different scopes never mix state.

## `emitUnchanged` (type: `boolean`):

Off by default. Turn on to also return products that have not changed since the last run, marked UNCHANGED. This returns, and bills, extra rows you already have, so leave it off unless you want a full snapshot every run.

## `emitExpired` (type: `boolean`):

Off by default. Turn on to also return products that were present in a previous run but are no longer found, marked EXPIRED. Only produced once a run has fully scanned the tracked scope without a page that could not be read, never when Max products capped it or Resume was used. A product page the store reports as not available also counts as missing, and one club's range can differ from another's, so consider 'Expire after missed scans' above 1. EXPIRED rows count against Max products: a run returns at most the room left under it, and the rest are returned on a later complete run. This returns, and bills, extra rows, so leave it off unless you need expiry tracking.

## `changeScope` (type: `string`):

All fields (default): any change to the compared product data marks a product UPDATED. Price only (and stock when counted): just price, was-price, discount, on-sale flag and per-option price, so name, description or specification edits never produce a billable UPDATED row. Changing this never starts a new baseline: the existing saved state is kept (under an automatic or a named State key) and each stored product is simply compared under the new setting.

## `watchStock` (type: `boolean`):

Off by default. The store's stock, pickup and delivery answers differ between two reads of the same unchanged product (the same product, same club, two reads seconds apart, in stock then out of stock), so counting them marks products UPDATED, and bills them, without a real change. Turn on only if you accept that: stock status, availability texts, order limits and per-option stock then count as changes.

## `compactUpdates` (type: `boolean`):

Off by default. When on, UPDATED and EXPIRED rows are slim: ids, link, title, current price and stock, plus priceChanges (old and new value of each field that moved) and variantChanges (per-option ADDED, REMOVED or CHANGED). NEW rows stay full. A compact row does not carry product-page detail fields or reviews, so no detail or review charge applies to it.

## `expireAfterMissedScans` (type: `integer`):

Used with 'Emit expired'. A product is marked EXPIRED only after it was missing from this many CONSECUTIVE complete scans (default 1). EXPIRED means the product dropped out of the scanned results or its page is gone; one club's range can differ from another's, so a value of 2 or more avoids false expiries.

## `ignoreFieldsForChanges` (type: `array`):

Field names that must not count as a change, for example name or highlights. Use a dotted path to ignore a key inside every element of a list, for example variants.url. Ignored fields are still returned in the row. Rank, sponsored flag, delivery text, popularity badges, club, media links and ratings and reviews are already ignored by default. Changing this list never starts a new baseline: the saved state is kept and each stored product is re-compared under the new list, so no product comes back as NEW.

## `mcpConnectors` (type: `array`):

Optionally send results into the apps you already use, via Model Context Protocol (MCP) connectors. Authorize one under Apify, Settings, API & Integrations, then select it here. Notion gets a page per product; other connectors get a best-effort write or digest. Each connector receives a condensed summary per product, not the full record; the complete record always stays in the dataset. Leave empty to skip; this never changes the dataset output. Supported: Notion (https://mcp.notion.com/mcp), Linear (https://mcp.linear.app/sse), Airtable (https://mcp.airtable.com/mcp), Apify (https://mcp.apify.com).

## `notionParentPageUrl` (type: `string`):

URL or id of the Notion page under which product pages are created. Required to enable the Notion export; ignored by other connectors.

## `maxNotifyListings` (type: `integer`):

Cap on products written to each connector per run. Does not affect the dataset.

## Actor input object example

```json
{
  "mode": "search",
  "queries": [
    "coffee"
  ],
  "slugKeywords": [
    "paper towels"
  ],
  "categoryKeywords": [
    "paper towels"
  ],
  "maxCategories": 5,
  "urls": [
    "https://www.samsclub.com/ip/Bounty-Select-A-Size-2-Ply-Paper-Towels-12-Rolls-110-sheets-roll/18040450426",
    "https://www.samsclub.com/browse/paper-towels/1110179",
    "https://www.samsclub.com/browse/gluten-free/710125?facet=Brand%3AQuaker",
    "https://www.samsclub.com/c/kp/tires-for-suvs",
    "https://www.samsclub.com/cp/housewares/1373",
    "https://www.samsclub.com/sitemap_products_1.xml"
  ],
  "sortBy": "relevance",
  "onSaleOnly": false,
  "skipSponsored": false,
  "fetchDetails": false,
  "fetchReviews": false,
  "maxReviewsPerProduct": 20,
  "reviewSort": "relevance",
  "includeReviewImages": false,
  "fixedClub": false,
  "maxItems": 20,
  "maxPages": 0,
  "proxy": {
    "useApifyProxy": true
  },
  "incrementalMode": false,
  "emitUnchanged": false,
  "emitExpired": false,
  "changeScope": "all",
  "watchStock": false,
  "compactUpdates": false,
  "expireAfterMissedScans": 1,
  "maxNotifyListings": 50
}
```

# Actor output Schema

## `overview` (type: `string`):

No description

## `detail` (type: `string`):

No description

## `reviews` (type: `string`):

No description

## `changes` (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": "search",
    "queries": [
        "coffee"
    ],
    "slugKeywords": [
        "paper towels"
    ],
    "categoryKeywords": [
        "paper towels"
    ],
    "urls": [
        "https://www.samsclub.com/ip/Bounty-Select-A-Size-2-Ply-Paper-Towels-12-Rolls-110-sheets-roll/18040450426",
        "https://www.samsclub.com/browse/paper-towels/1110179",
        "https://www.samsclub.com/browse/gluten-free/710125?facet=Brand%3AQuaker",
        "https://www.samsclub.com/c/kp/tires-for-suvs",
        "https://www.samsclub.com/cp/housewares/1373",
        "https://www.samsclub.com/sitemap_products_1.xml"
    ],
    "onSaleOnly": false,
    "skipSponsored": false,
    "fetchDetails": false,
    "fetchReviews": false,
    "includeReviewImages": false,
    "fixedClub": false,
    "proxy": {
        "useApifyProxy": true
    },
    "incrementalMode": false,
    "emitUnchanged": false,
    "emitExpired": false,
    "watchStock": false,
    "compactUpdates": false
};

// Run the Actor and wait for it to finish
const run = await client.actor("abotapi/samsclub-scraper").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": "search",
    "queries": ["coffee"],
    "slugKeywords": ["paper towels"],
    "categoryKeywords": ["paper towels"],
    "urls": [
        "https://www.samsclub.com/ip/Bounty-Select-A-Size-2-Ply-Paper-Towels-12-Rolls-110-sheets-roll/18040450426",
        "https://www.samsclub.com/browse/paper-towels/1110179",
        "https://www.samsclub.com/browse/gluten-free/710125?facet=Brand%3AQuaker",
        "https://www.samsclub.com/c/kp/tires-for-suvs",
        "https://www.samsclub.com/cp/housewares/1373",
        "https://www.samsclub.com/sitemap_products_1.xml",
    ],
    "onSaleOnly": False,
    "skipSponsored": False,
    "fetchDetails": False,
    "fetchReviews": False,
    "includeReviewImages": False,
    "fixedClub": False,
    "proxy": { "useApifyProxy": True },
    "incrementalMode": False,
    "emitUnchanged": False,
    "emitExpired": False,
    "watchStock": False,
    "compactUpdates": False,
}

# Run the Actor and wait for it to finish
run = client.actor("abotapi/samsclub-scraper").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": "search",
  "queries": [
    "coffee"
  ],
  "slugKeywords": [
    "paper towels"
  ],
  "categoryKeywords": [
    "paper towels"
  ],
  "urls": [
    "https://www.samsclub.com/ip/Bounty-Select-A-Size-2-Ply-Paper-Towels-12-Rolls-110-sheets-roll/18040450426",
    "https://www.samsclub.com/browse/paper-towels/1110179",
    "https://www.samsclub.com/browse/gluten-free/710125?facet=Brand%3AQuaker",
    "https://www.samsclub.com/c/kp/tires-for-suvs",
    "https://www.samsclub.com/cp/housewares/1373",
    "https://www.samsclub.com/sitemap_products_1.xml"
  ],
  "onSaleOnly": false,
  "skipSponsored": false,
  "fetchDetails": false,
  "fetchReviews": false,
  "includeReviewImages": false,
  "fixedClub": false,
  "proxy": {
    "useApifyProxy": true
  },
  "incrementalMode": false,
  "emitUnchanged": false,
  "emitExpired": false,
  "watchStock": false,
  "compactUpdates": false
}' |
apify call abotapi/samsclub-scraper --silent --output-dataset

```

## MCP server setup

```json
{
    "mcpServers": {
        "apify": {
            "type": "http",
            "url": "https://mcp.apify.com/?tools=fetch-actor-details,abotapi/samsclub-scraper"
        }
    }
}
```

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/vATuN9My1SpcZBExe/builds/CidyyjbJG8E3QmXPQ/openapi.json
