# CVS Scraper - Products, Prices, Ratings & Availability (`abotapi/cvs-product-scraper`) Actor

Scrape CVS.com pharmacy, beauty, health and personal care products by keyword search. Extract product names, brands, prices, descriptions, ratings, customer reviews and availability. Collect structured product data for price comparison, competitor analysis, market research and stock monitoring.

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

## Pricing

from $1.60 / 1,000 product records

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

## CVS Product Scraper

Scrape CVS.com product listings: keyword search, product details, prices, ratings and availability. CVS Pharmacy is the largest US drugstore chain (health, beauty, personal care, household essentials), and this actor turns its catalog into clean structured rows for price monitoring, assortment analytics and competitor research.

### Why This Scraper?

- **Two ways in.** Point it at keyword searches, or paste storefront links - search pages and product pages both walk with the same grammar.
- **Retail-ready product records.** Every row carries title, brand, price with was-price and per-unit price, rating, review count, sponsored flag and badges; detail enrichment adds the description, availability and image gallery straight from the product page. Individual review texts are not collected; each row carries the rating and the review count.
- **Scheduled-run friendly.** Incremental mode classifies every product NEW / UPDATED / UNCHANGED / EXPIRED across runs, so a daily monitor pushes only what moved, and resume mode continues an interrupted walk exactly where it stopped.
- **Fast and light.** A 10-product run with details finishes in about a minute on a small memory size. The storefront geo-blocks non-US lines, so every connection exits in the US (automatic Apify proxy selection by default), and a denied exit is swapped for a fresh one automatically.
- **Connector-ready.** Push records into Notion, Sheets or any MCP-capable app as they land.
- **Honest runs.** Refused pages rotate and retry; a fully closed window fails loudly with a connection-problem message instead of shipping an empty success.

### Use Cases

- **Who:** pricing teams tracking CVS promos on health-and-beauty assortments - price, was-price and per-unit price land per row, updated per scheduled run.
- **Who:** brands auditing their drugstore shelf - sponsored flags, badges, ratings and review velocity per product.
- **Who:** analysts building retail datasets - walk any keyword search with the same grammar and export straight to CSV/Excel or a warehouse.

### Data You Get

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

| Field | Example |
| --- | --- |
| `title` | L.A. Girl Shimmer Spray, Gold |
| `brand` | L.A. Girl |
| `price` / `wasPrice` | 7.99 / 12.99 |
| `pricePerUnit` | $1.62/oz. |
| `rating` | 4.6 |
| `reviewsCount` | 21 |
| `sponsored` | false |
| `badges` | \["Best Seller"] |
| `availability` | InStock |
| `description` | Full product description text |
| `imageUrls` | Full image gallery |
| `skuId` | 186492 |
| `currency` | USD |
| `availability` | InStock |
| `position` | 3 (grid slot) |
| `scrapedAt` | 2026-10-28T12:00:00Z |
| `changeType` / `changedFields` | NEW / UPDATED / \[price] (incremental mode) |

### How to Use

#### Run it from your code

Python:

```python
from apify_client import ApifyClient

client = ApifyClient("YOUR_APIFY_TOKEN")
run = client.actor("abotapi/cvs-product-scraper").call(run_input={
    "mode": "search",
    "searchTerms": ["shampoo"],
    "maxItems": 50,
})
for item in client.dataset(run["defaultDatasetId"]).iterate_items():
    print(item["title"], item["price"])
```

JavaScript:

```javascript
import { Actor } from 'apify';

const run = await Actor.call('abotapi/cvs-product-scraper', {
    mode: 'urls',
    urls: ['https://www.cvs.com/search?q=shampoo'],
    maxItems: 50,
});
const { default: dataset } = await Actor.openDataset(run.defaultDatasetId);
await dataset.forEach((item) => console.log(item.title, item.price));
```

**Search by keyword:**

```json
{
  "mode": "search",
  "searchTerms": ["shampoo", "vitamin c"],
  "maxItems": 50
}
```

Running URL mode with the `urls` field untouched scrapes one example of each supported shape: one product page first, then the shampoo search page (the default Max items fills with the search results after the product row). Search pages work in both forms the site produces (`search?q=shampoo` and `search?searchTerm=shampoo`). Category, brand and other browse pages are not supported yet; they are skipped with a warning naming the supported shapes, and a run with no usable link fails loudly. If an API call sends only `searchTerms` (your own terms) and leaves `urls` at the shipped examples, the run switches to search mode for your terms and does not scrape the example links.

**Walk a search page:**

```json
{
  "mode": "urls",
  "urls": ["https://www.cvs.com/search?q=shampoo"],
  "maxItems": 100
}
```

**One product, details on:**

```json
{
  "mode": "urls",
  "urls": ["https://www.cvs.com/shop/l-a-girl-shimmer-spray-gold-prodid-186492"],
  "maxItems": 1
}
```

**Scheduled price monitor:**

```json
{
  "mode": "search",
  "searchTerms": ["shampoo"],
  "maxItems": 200,
  "fetchDetails": false,
  "incrementalMode": true
}
```

#### Tips

- The storefront re-ranks results between runs - incremental mode treats a moving `position` as noise and never re-bills it.
- Scheduled incremental runs keep the walk cheap: unchanged rows are suppressed before any product page is re-read and bill nothing.
- Sale prices arrive as price + was-price; per-unit prices ride the storefront's own unit-price text.

### Input Parameters

| Parameter | Type | Default | Description |
| --- | --- | --- | --- |
| `mode` | select | urls | `search` walks keyword searches; `urls` walks pasted storefront links. |
| `searchTerms` | string list | shampoo | Keyword searches to walk (search mode). |
| `urls` | URL list | 2 example links | Search pages and product pages (links mode). |
| `maxItems` | integer | 20 | Maximum records per run. 0 means no limit. |
| `fetchDetails` | checkbox | true | Read each product's detail page (description, availability, gallery). |
| `incrementalMode` | checkbox | false | Classify rows NEW/UPDATED/UNCHANGED/EXPIRED across scheduled runs. |
| `emitUnchanged` | checkbox | false | Also emit rows whose tracked fields did not change. |
| `emitExpired` | checkbox | false | Emit EXPIRED rows for products that left a fully-scanned scope. |
| `proxyConfiguration` | proxy | Automatic (US-pinned) | Automatic Apify proxy selection, pinned to US exits because the storefront geo-blocks non-US lines. A residential pick is honoured as chosen and uses more of your residential proxy traffic; you can also supply your own US proxy URLs. |
| `resumeFromRunId` | string | (hidden) | Internal: continues an interrupted run; collected records are skipped. |
| `stateKey` | string | (hidden) | Internal: names the incremental baseline; empty derives it from the scope. |
| `ignoreFieldsForChanges` | string list | (hidden) | Internal: extra fields excluded from the incremental change fingerprint. |
| `mcpConnectors` | string list | (hidden) | Internal: connector ids that receive records as they push. |
| `notionParentPageUrl` | string | (hidden) | Internal: parent page for the Notion connector. |
| `maxNotifyListings` | integer | 50 (hidden) | Internal: cap on records sent to connectors per run. |

All fields are also accepted through the API exactly as documented, including the hidden internal ones.

### Output Example

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

```json
{
  "recordId": "cvs:186492",
  "productId": "186492",
  "title": "L.A. Girl Shimmer Spray, Gold",
  "brand": "L.A. Girl",
  "price": 7.99,
  "wasPrice": 12.99,
  "currency": "USD",
  "rating": 4.6,
  "reviewsCount": 21,
  "availability": "InStock",
  "url": "https://www.cvs.com/shop/l-a-girl-shimmer-spray-gold-prodid-186492"
}
```

Product rows with the detail enrichment on, from a live search walk.

### Plan Requirement

This actor needs an Apify paid plan. The storefront geo-blocks non-US lines, so every page is read over US exits, chosen by the automatic Apify proxy selection by default. The Apify Free plan does not include Apify Proxy, and residential proxies are not included in it either.

- If the selected connection is unavailable, the run logs: "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." Pages that the backup connection serves are noted with "Served over the backup connection (the default connection was unavailable)."
- If nothing can be read, the run fails with one of these messages: "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." or "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 CVS pages." or "Could not load any results page (the requests failed), so nothing was collected. This is a connection problem, not an empty search."
- You can supply your own US proxy URLs in the Proxy configuration field instead.

### FAQ

#### Is it legal to scrape CVS.com?

The actor collects publicly available product data from the storefront's own pages, at a polite pace, without logging into any account. Review your use case and the site's terms before running at scale.

#### Why does a run fail with a connection problem?

The storefront denies individual proxy exits intermittently. The actor swaps a denied exit for a fresh one (at most five tries per page) and stops early when exit after exit is refused, so a closed window fails within minutes instead of running to the time limit; retry later, the same input usually succeeds on the next run.

#### How much does a run cost?

You pay the actor's per-event pricing (see the Pricing tab) plus your Apify plan's proxy usage. The billed events are the actor start, each product record pushed to the dataset, and the detail enrichment, charged once per product whose detail page was read and delivered. Unchanged rows in incremental mode, card-only rows (details off) and rows whose detail page could not be read bill no enrichment fee.

#### Can I track price changes daily?

Yes - schedule the run with `incrementalMode` on and the same inputs. Only NEW and UPDATED rows push, each with `changedFields` naming what moved.

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

Connect the actor to Notion, Google Sheets and other MCP-capable apps from the Apify Console: records this run pushed are delivered as they land, capped per run. Connector settings ship hidden in the input and need no attention unless you use connectors.

### 🔗 More scrapers you might like

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

<table>
<tr><td>🛒 <a href="https://apify.com/abotapi/drogasil-com-br-scraper"><b>Drogasil Brazil Pharmacy, Health &amp; Beauty Scraper</b></a><br>Scrape Drogasil (drogasil.com.br) medicines, health and beauty products by keyword...</td><td>🧩 <a href="https://apify.com/abotapi/jbhifi-com-au-product-scraper"><b>JB Hi-Fi AU Scraper</b></a><br>Scrape JB Hi-Fi Australia (jbhifi.com.au) products from the public storefront. Extract...</td></tr>
<tr><td>🧩 <a href="https://apify.com/abotapi/falabella-marketplace-scraper"><b>Falabella Scraper</b></a><br>Scrape Falabella, Latin America's leading retail marketplace: products with local prices...</td><td>🛒 <a href="https://apify.com/abotapi/rossmann-de-scraper"><b>ROSSMANN.de Scraper</b></a><br>Scrape ROSSMANN (rossmann.de) drugstore products: current + original strike-through price...</td></tr>
<tr><td>🧩 <a href="https://apify.com/abotapi/walmart-scraper"><b>Walmart Scraper</b></a><br>Scrape Walmart.com by keyword, URL, or item ID. Extract prices, was-prices, sellers...</td><td>🛒 <a href="https://apify.com/abotapi/dm-de-scraper"><b>dm.de</b></a><br>Scrape dm-drogerie markt (dm.de) products: current + strike-through Ausverkauf price with...</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/cvs-product-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`):

Search mode walks keyword search result pages. Links mode walks the storefront links you paste: search pages and product pages.

## `searchTerms` (type: `array`):

Keyword searches to walk (search mode). Each term walks its result pages in order.

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

Links to walk (links mode): keyword search pages and product pages.

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

Read each product's detail page: description, availability, gallery, aggregate rating. Billed per enriched record.

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

Maximum records this run may push. 0 means no limit.

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

Automatic Apify proxy selection is the default: connections are pinned to US exits (the storefront geo-blocks non-US lines) and the actor rotates denied exits. You may pick residential or supply your own US proxy URLs; a residential pick is honoured as chosen and uses more of your residential proxy traffic.

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

Internal: continues an interrupted run.

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

Track changes across scheduled runs.

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

Internal: names the incremental baseline.

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

Incremental mode: also emit rows whose tracked fields did not change.

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

Incremental mode: emit EXPIRED rows for products that left a fully-scanned scope.

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

Internal: extra fields excluded from the incremental change fingerprint.

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

Internal: connector ids that receive the run's records as they push.

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

Internal: parent page for the Notion connector.

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

Internal: cap on records sent to connectors per run.

## Actor input object example

```json
{
  "mode": "urls",
  "searchTerms": [
    "shampoo"
  ],
  "urls": [
    {
      "url": "https://www.cvs.com/shop/l-a-girl-shimmer-spray-gold-prodid-186492"
    },
    {
      "url": "https://www.cvs.com/search?q=shampoo"
    }
  ],
  "fetchDetails": true,
  "maxItems": 20,
  "proxyConfiguration": {
    "useApifyProxy": true
  },
  "incrementalMode": false,
  "emitUnchanged": false,
  "emitExpired": false,
  "maxNotifyListings": 50
}
```

# Actor output Schema

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

All records this run produced.

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

Records enriched from their product page.

## `changes` (type: `string`):

Incremental mode: NEW, UPDATED and EXPIRED rows.

## `summary` (type: `string`):

Counts for this run: searches and links read, records, enrichments.

# 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": "urls",
    "searchTerms": [
        "shampoo"
    ],
    "urls": [
        {
            "url": "https://www.cvs.com/shop/l-a-girl-shimmer-spray-gold-prodid-186492"
        },
        {
            "url": "https://www.cvs.com/search?q=shampoo"
        }
    ],
    "proxyConfiguration": {
        "useApifyProxy": true
    }
};

// Run the Actor and wait for it to finish
const run = await client.actor("abotapi/cvs-product-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": "urls",
    "searchTerms": ["shampoo"],
    "urls": [
        { "url": "https://www.cvs.com/shop/l-a-girl-shimmer-spray-gold-prodid-186492" },
        { "url": "https://www.cvs.com/search?q=shampoo" },
    ],
    "proxyConfiguration": { "useApifyProxy": True },
}

# Run the Actor and wait for it to finish
run = client.actor("abotapi/cvs-product-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": "urls",
  "searchTerms": [
    "shampoo"
  ],
  "urls": [
    {
      "url": "https://www.cvs.com/shop/l-a-girl-shimmer-spray-gold-prodid-186492"
    },
    {
      "url": "https://www.cvs.com/search?q=shampoo"
    }
  ],
  "proxyConfiguration": {
    "useApifyProxy": true
  }
}' |
apify call abotapi/cvs-product-scraper --silent --output-dataset

```

## MCP server setup

```json
{
    "mcpServers": {
        "apify": {
            "type": "http",
            "url": "https://mcp.apify.com/?tools=fetch-actor-details,abotapi/cvs-product-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/0a3kv1PQN59vkxTqh/builds/LMfMc6gX2BAFRzEIJ/openapi.json
