# Store Locator Scraper with Change Detection (`finaldynamics/store-locator-scraper-change-detection`) Actor

Store locator scraper: every store location of a brand with address, coordinates and hours, plus store openings, closures, moves and hours changes on repeat runs.

- **URL**: https://apify.com/finaldynamics/store-locator-scraper-change-detection.md
- **Developed by:** [Final Dynamics](https://apify.com/finaldynamics) (community)
- **Categories:** Lead generation, E-commerce
- **Stats:** 2 total users, 1 monthly users, 100.0% runs succeeded, 0 bookmarks
- **User rating**: No ratings yet

## Pricing

from $4.00 / 1,000 location scrapeds

This Actor is paid per event. You are not charged for the Apify platform usage, but only a fixed price for specific events.

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

## Store Locator Scraper with Change Detection

Turn a brand's "find a store" page into a clean list of store locations data, and then
watch it: run again later and get only the stores that opened, closed, moved or changed
hours.

It reads locators that publish their locations in the page itself: Yext, including the
statically generated `locations.brand.com` sites that chains use, Stockist, Storemapper,
Uberall, common Shopify store-locator apps, and any store page carrying its data as
JSON-LD or microdata. Where a chain publishes a store directory or a sitemap, the actor
walks it and reads each store page.

Checked against 22 well-known chains on 2026-09-17. Working: Chipotle, Wendy's, Family
Dollar, Trader Joe's, Taco Bell, Subway, Best Buy, Petco and Dagne Dover. Not working:
brands that refuse automated visitors (Kohl's, Home Depot, Sephora and Panera answer 403),
brands whose sitemap is broken (Walgreens returns 500, CVS returns 503) and brands that
draw their locations only in the browser (Starbucks, Ace Hardware). A run that finds
nothing says which of these it hit.

Built for retail analysts and site-selection teams, dealer and stockist research,
competitive tracking, and anyone who has been asked for "every location of brand X" or
"tell me when they open a new one".

### What you get

One row per location. Every field is flat, so the dataset drops straight into Google
Sheets, Airtable or a map.

| Field                                                             | Example                                                           |
| ----------------------------------------------------------------- | ----------------------------------------------------------------- |
| `brand`                                                           | `dagnedover.com`                                                  |
| `storeName`                                                       | `Babylist Showroom`                                               |
| `addressFull`, `street`, `city`, `state`, `postalCode`, `country` | `211 S Beverly Dr, Beverly Hills, CA, 90212`                      |
| `latitude`, `longitude`                                           | `34.0644`, `-118.3994`                                            |
| `phone`, `email`, `storeUrl`                                      | When the locator publishes them                                   |
| `hours`                                                           | `{ "mon": "09:00-17:00", ... }` or `null`                         |
| `services`                                                        | Tags or filters the locator attaches                              |
| `locationId`                                                      | The locator's own stable ID                                       |
| `locatorPlatform`                                                 | `stockist`, `yext`, `storemapper`, `uberall`, `shopify`, `custom` |
| `changeType`, `changedFields`                                     | `new`, `closed`, `moved`, `hours_changed`, `unchanged`            |
| `firstSeenAt`, `lastSeenAt`, `scrapedAt`                          | UTC timestamps                                                    |
| `sourceUrl`                                                       | The locator page the row came from                                |

Anything the locator does not publish is `null`.

### How to use it

Paste the locator page URL and run.

```json
{
  "locatorUrls": ["https://www.dagnedover.com/pages/store-locator"]
}
```

Add `"country": "US"` to keep one country, or `"maxLocations": 100` for a capped sample.

**Get changes since last time (one-off):** run once without a cap to build the full list,
then run again with the first run's dataset ID:

```json
{
  "locatorUrls": ["https://www.dagnedover.com/pages/store-locator"],
  "diffAgainstDatasetId": "DATASET_ID_FROM_THE_FIRST_RUN"
}
```

Each row now says whether the store is `new`, `closed`, `moved`, `hours_changed` or
`unchanged`, and which fields changed. The output is a complete list again, so you can use
it as the next baseline. Set `emitUnchanged: false` if you only want the changed rows in
the dataset.

**Watch a brand on a schedule:** create a key-value store in Apify Console, select it as
`monitorStateStoreId`, and schedule the task. The actor keeps its own complete baseline in
that store, so every scheduled run delivers just the changes. Give each monitor its own
store, and let the schedule run one job at a time.

**Several brands at once:** list up to 100 locator URLs in `locatorUrls`. Rows carry
`sourceUrl` so you can tell them apart.

### Quick start

No code needed; about two minutes from input to spreadsheet. For a longer walkthrough with
example output, read the guide
[Build a store location spreadsheet and track changes](https://finaldynamics.com/store-location-guide.html).

1. Open the example "All store locations for one brand (Dagne Dover)" on this actor's
   page, or paste one of the inputs above into the input form.
2. Click **Start** and wait until the run shows **Succeeded**.
3. Open the **Output** tab to see the locations as a table.
4. Click **Export**, choose CSV or Excel, and download the file.
5. In Google Sheets, choose **File > Import > Upload** and select the file.

### Automate it

**A weekly location check:** save the brand's locator URL in a task. Click **Save as a new
task**, then add a schedule for that task under **Schedules** in Apify Console (for
example every Monday at 07:00). Select a key-value store as `monitorStateStoreId` (see
above) and each scheduled run delivers only openings, closures, moves and hours changes.

**Into Google Sheets with n8n:** add the **Apify** node with the **On new Apify Event**
trigger, choose your task and the succeeded event, then add an **Apify** node with **Get
Dataset Items** using the run's `defaultDatasetId`, and finish with a **Google Sheets**
node that appends the rows. Every scheduled run then adds its locations to the same sheet.
Make and Zapier have Apify integrations that work the same way.

### Pricing

Pay per event. There are no platform usage charges on top.

| Event              |  Price | When                                                                |
| ------------------ | -----: | ------------------------------------------------------------------- |
| `actor-start`      |  $0.01 | Once per run                                                        |
| `location-scraped` | $0.004 | Per location fetched this run                                       |
| `change-detected`  |  $0.02 | Per opening, closure, move or hours change found against a baseline |

Examples: a brand with 900 stores costs $3.61 to enumerate. A weekly check of those 900
stores that finds 12 openings, 3 moves, 2 hours changes and 4 closures costs $4.03. The
first run of a brand never charges for changes; everything is simply `new`.

Set a maximum charge on the run if you want a hard ceiling; the actor stops cleanly at the
cap.

### Good to know

- A `closed` row means the store disappeared from a complete result from the brand's own
  locator service. It is a strong signal, not a confirmed closure; check before acting on
  it.
- Closures are only reported when the actor is sure it saw the whole list. If the locator
  errors, blocks the request, or the run was capped, closures are held back rather than
  guessed.
- Yext, Stockist, Storemapper and Uberall locators give complete lists and support
  monitoring. Pages where the actor has to read location data out of the HTML still return
  rows, but cannot be used for closure detection.
- The actor reads public pages and public locator APIs, honours robots rules, and never
  logs in, solves CAPTCHAs or uses proxies. A page that only draws its map with JavaScript
  and exposes no data returns no rows.
- Locations are matched by the locator's own ID, or by address when there is none. Suites
  and units at one street address count as separate locations.
- HTTP redirects are followed for up to five hops. Redirect targets are checked for public
  DNS addresses before their robots.txt or page is fetched. The primary page's final URL
  becomes `sourceUrl` and the fixed scope for directory discovery; later page redirects do
  not expand that scope. A change of registrable domain appears alongside crawl notices.
  Domain comparison is a heuristic using the last two labels, or three for `co.uk`,
  `org.uk`, `gov.uk`, `ac.uk`, `co.nz`, `co.jp`, `com.au` and `co.za`.
- The worker's network permissions cover the locator hosts you pass, their own site
  (`locations.brand.com` also reaches `brand.com` and `www.brand.com`, so the usual
  redirect works) and the fixed provider hosts. A redirect to a different site is refused
  and named in the status message; pass that URL directly if you want it read.
- Transport errors retain nested causes. TLS, SSL and certificate failures are identified
  in the terminal status instead of being reduced to a bare `fetch failed` message.

### FAQ

**How do I get a list of all stores of a brand?**

Paste the brand's store locator page URL into `locatorUrls` and run. You get one row per
location with address, coordinates and, if you want them, opening hours.

**Can it track store closures and new store openings?**

Yes. Select a key-value store as `monitorStateStoreId` and schedule the task; every run
after the first delivers only openings, closures, moves and hours changes.

**Does it work on any website?**

It works on the locator platforms most brands use (Yext, Stockist, Storemapper, Uberall
and common Shopify apps) and on pages that embed location data. If a page returns nothing,
open an issue with the URL.

### Related actors

- [Florida Business Leads by Industry](https://apify.com/finaldynamics/florida-business-leads):
  Florida businesses by NAICS code and county.

Questions or a locator that returns nothing? Open an issue on the actor page with the page
URL.

# Actor input Schema

## `locatorUrls` (type: `array`):

The brand's store locator page, for example https://www.brand.com/stores. The actor recognises the locator platform and reads the same location list the page shows.

## `country` (type: `string`):

Optional ISO 3166-1 alpha-2 code. When set, only locations in that country are kept (applied before the diff).

## `maxLocations` (type: `integer`):

Cap on how many locations to scrape this run. 0 means no cap. Closure reporting is disabled whenever a cap is used because the crawl is incomplete.

## `includeHours` (type: `boolean`):

Add opening hours to each row, one field per weekday (mon to sun). Turn off to leave hours empty.

## `diffAgainstDatasetId` (type: `string`):

The dataset from an earlier complete run, to compare against. Each location is then marked new, closed, moved, hours\_changed or unchanged.

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

Also deliver locations that did not change, so this run's dataset is a complete list you can compare against next time. Turn off only for a changes-only export you will not compare against later.

## `monitorStateStoreId` (type: `string`):

Optional key-value store where the actor keeps the full location list between scheduled runs, so each run reports openings, closures, moves and hours changes without you passing a dataset ID. Use one store per brand schedule and let runs finish one at a time.

## `monitorMaxLocations` (type: `integer`):

Maximum active locations staged in a recurring monitor run. Oversized or incomplete runs emit no rows and no per-row charges.

## Actor input object example

```json
{
  "locatorUrls": [
    "https://www.dagnedover.com/pages/store-locator"
  ],
  "maxLocations": 0,
  "includeHours": true,
  "emitUnchanged": true,
  "monitorMaxLocations": 10000
}
```

# Actor output Schema

## `results` (type: `string`):

Crawled data stored in default dataset

## `files` (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 = {
    "locatorUrls": [
        "https://www.dagnedover.com/pages/store-locator"
    ]
};

// Run the Actor and wait for it to finish
const run = await client.actor("finaldynamics/store-locator-scraper-change-detection").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 = { "locatorUrls": ["https://www.dagnedover.com/pages/store-locator"] }

# Run the Actor and wait for it to finish
run = client.actor("finaldynamics/store-locator-scraper-change-detection").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 '{
  "locatorUrls": [
    "https://www.dagnedover.com/pages/store-locator"
  ]
}' |
apify call finaldynamics/store-locator-scraper-change-detection --silent --output-dataset

```

## MCP server setup

```json
{
    "mcpServers": {
        "apify": {
            "type": "http",
            "url": "https://mcp.apify.com/?tools=fetch-actor-details,finaldynamics/store-locator-scraper-change-detection"
        }
    }
}
```

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/FDvpvHciWaOFZzv4l/builds/mYMRv1vkZAFsfj4YZ/openapi.json
