# Estate Sale Scraper with Full Photo Galleries (EstateSales.net) (`scrapelabmax/estate-sale-scraper`) Actor

Scrape estate, garage, and yard sales from EstateSales.net — with every listing's FULL photo gallery (images\[]), not just one thumbnail. Nationwide, per-state, or per-ZIP. Plain-HTTP Cheerio crawling keeps cost ~10x below browser-based scrapers.

- **URL**: https://apify.com/scrapelabmax/estate-sale-scraper.md
- **Developed by:** [Scrapelab Max](https://apify.com/scrapelabmax) (community)
- **Categories:** E-commerce, Real estate, Lead generation
- **Stats:** 3 total users, 1 monthly users, 100.0% runs succeeded, 2 bookmarks
- **User rating**: 5.00 out of 5 stars

## Pricing

from $5.00 / 1,000 results

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/platform/actors/running/actors-in-store#pay-per-event

## What's an Apify Actor?

Actors are web data automations that power AI and operations. They run on the Apify platform to scrape websites, process data, connect APIs, and automate workflows.
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.
Actors are written with capital "A".

## 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.
The best way to integrate Actors is as follows.

- **AI agents and MCP clients** — the [Apify MCP server](https://docs.apify.com/integrations/mcp.md) at `https://mcp.apify.com` (remote, streamable HTTP, OAuth on first use).
- **Agentic workflows and local Actor development** — [Agent Skills](https://apify.com/.well-known/agent-skills/index.json) with the [Apify CLI](https://docs.apify.com/cli/docs.md): `npm install -g apify-cli`, then `apify login`.
- **JavaScript/TypeScript projects** — the official [JS/TS client](https://docs.apify.com/api/client/js/docs.md): `npm install apify-client`.
- **Python projects** — the official [Python client](https://docs.apify.com/api/client/python/docs.md): `pip install apify-client`.
- **Any other language** — the [REST API](https://docs.apify.com/api/v2.md).

For usage examples, see the [API](#api) section below.

For more details, see Apify documentation as [Markdown index](https://docs.apify.com/llms.txt) and [Markdown full-text](https://docs.apify.com/llms-full.txt).

# README

## Estate Sale Scraper with Full Photo Galleries (EstateSales.net)

Scrape estate, garage, and yard sale listings from
[EstateSales.net](https://www.estatesales.net) — including each listing's **full photo gallery**
(`images[]`), not just one thumbnail.

The scraper runs on plain HTTP requests (no headless browser at any point), which keeps its cost
profile roughly **10× cheaper** than browser-based scrapers (~3,000 pages per compute unit vs.
\~300).

### Why this scraper?

- 📸 **Full photo galleries** — every listing includes its complete gallery as `images[]`
  (default up to 25 photos per listing, configurable), in the site's original order. Most
  alternatives give you a single thumbnail.
- 💰 **Cheap to run** — pure HTTP crawling, no browser. Zero-photo listings never trigger an
  extra page fetch.
- 🇺🇸 **Three coverage modes** — specific ZIP codes, a whole state, or every active sale
  nationwide.
- ✅ **Fail-loud integrity** — a blocked or partially-broken crawl fails the run visibly instead
  of silently returning an incomplete dataset.

### Coverage modes

Choose exactly one per run:

| Mode           | Input                    | What it does                                                                  |
| -------------- | ------------------------ | ----------------------------------------------------------------------------- |
| **ZIP**        | `"zips": ["77098", ...]` | Scrapes the listings around each ZIP code.                                    |
| **State**      | `"state": "TX"`          | Walks every city in the state and scrapes each one.                           |
| **Nationwide** | `"nationwide": true`     | Enumerates every currently-active sale in the US via the site's sale sitemap. |

### Input

| Field                 | Type       | Default                   | Notes                                                                                                                                               |
| --------------------- | ---------- | ------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------- |
| `zips`                | `string[]` | `[]`                      | ZIP codes to scrape. Ignored if `state` or `nationwide` is set.                                                                                     |
| `state`               | `string`   | —                         | Two-letter state code (e.g. `TX`). Overrides `zips`.                                                                                                |
| `nationwide`          | `boolean`  | `false`                   | Scrape every active sale in the US. Overrides `zips` and `state`.                                                                                   |
| `maxItems`            | `number`   | *(required)*              | Hard cap on emitted listings — the crawler stops cleanly the moment it's reached, so cost is always bounded.                                        |
| `includeImages`       | `boolean`  | `true`                    | When `true`, listings with photos get one detail-page fetch to collect the full `images[]` gallery. When `false`, `images` is `[]` (cheapest mode). |
| `maxImagesPerListing` | `number`   | `25`                      | Caps gallery size per listing. Truncation is logged, never silent.                                                                                  |
| `excludeTypes`        | `string[]` | `[]`                      | Sale types to drop, matched case-insensitively against each listing's `type` (e.g. `OnlineOnlyAuctions`). Empty = emit every type.                  |
| `proxyConfiguration`  | `object`   | `{ useApifyProxy: true }` | Apify proxy settings. Datacenter proxies by default.                                                                                                |

Example minimal input:

```json
{ "zips": ["77098"], "maxItems": 25 }
```

#### Filtering out sale types (`excludeTypes`)

To skip online-only auctions, for example:

```json
{ "zips": ["77098"], "maxItems": 25, "excludeTypes": ["OnlineOnlyAuctions"] }
```

Matching is case-insensitive against the site's verbatim `type` value. The console offers a
multi-select dropdown of the observed types: `EstateSales`, `OnlineOnlyAuctions`,
`MovedOffsiteToStore`, `MovedOffsiteToWarehouse`, and `MovingSales` (if the site introduces a
new type, it needs to be added to the actor's option list before it can be excluded). Every
dropped listing is counted in `OUTPUT` as `itemsExcluded`.

In ZIP/state mode the type is known from the search page, so excluded listings never consume a
`maxItems` slot — you still get a full `maxItems` of wanted listings. In **nationwide mode** the
sale type is only known after each detail page is fetched, so excluded listings still count
toward `maxItems` there: a nationwide run with `excludeTypes` set can report `capHit: true`
while emitting fewer than `maxItems` items (`itemsEmitted + itemsExcluded` accounts for the
difference).

### Output

One dataset item per sale listing:

```json
{
    "id": 118342,
    "title": "Estate Sale in Houston, TX - Antiques, Furniture & Collectibles",
    "type": "Estate Sale",
    "company": "Caring Transitions of Houston",
    "address": "1234 Bissonnet St",
    "city": "Houston",
    "state": "TX",
    "zip": "77098",
    "latitude": 29.7189,
    "longitude": -95.42,
    "dates": [
        { "start": "2026-07-24T09:00:00-05:00", "end": "2026-07-24T16:00:00-05:00" },
        { "start": "2026-07-25T09:00:00-05:00", "end": "2026-07-25T14:00:00-05:00" }
    ],
    "photo_count": 42,
    "main_image": "https://media.estatesales.net/img/listings/118342/main.jpg",
    "thumbnail": "https://media.estatesales.net/img/listings/118342/thumb.jpg",
    "is_featured": false,
    "url": "https://www.estatesales.net/TX/Houston/77098/118342",
    "scrapedAt": "2026-07-21T14:32:07.418Z",
    "images": [
        "https://media.estatesales.net/img/listings/118342/main.jpg",
        "https://media.estatesales.net/img/listings/118342/img-02.jpg",
        "https://media.estatesales.net/img/listings/118342/img-03.jpg"
    ]
}
```

Field guarantees:

- `dates[].start`/`end` are always offset-aware ISO-8601 timestamps (never naive/local).
- `images[0]` always equals `main_image` when both exist; gallery order matches the site.
- Field names and types are a stable contract — new fields may be added over time, but existing
  ones are never renamed, removed, or retyped.

### Exporting results (JSON, CSV, Excel, XML)

Every run's dataset can be downloaded in **JSON, JSONL, CSV, Excel (XLSX), XML, HTML, or RSS**
from the run's **Storage → Export** dialog, or via the
[Dataset items API](https://docs.apify.com/api/v2/dataset-items-get) (`?format=csv`,
`?format=xlsx`, ...).

Two ready-made table views are included for clean flat exports:

- **Sales** — one row per sale (id, title, type, company, address, city, state, zip,
  photo count, main image, listing URL). Ideal for spreadsheets.
- **Images (one row per photo)** — the `images[]` gallery unwound so each photo URL is its own
  row alongside the sale's id, title, and location. Ideal for feeding image pipelines or
  downloading galleries in bulk.

Select a view in the Export dialog (or pass `?view=sales` / `?view=images` on the API) to get
that shape in any format. Exporting without a view returns the full raw items, including the
complete `images[]` array and `dates[]` ranges.

### Run integrity

The run is designed to never silently return a partial crawl:

- A search page that parses to zero listings without the site's "no sales" marker fails the run
  (guards against the site changing its markup under the scraper).
- A high ratio of blocked responses (403/429/challenge pages) fails the run instead of emitting
  a quietly incomplete dataset.
- Any request that exhausts its retries fails the run rather than dropping listings.
- Every run writes a stats object (`pagesCrawled`, `itemsEmitted`, `detailFetches`,
  `itemsExcluded`, `blockedResponses`, `parseFailures`, `budgetCap`, `budgetHit`, `capHit`,
  ...) to the run's `OUTPUT` key-value store key and logs it as `Run stats: {...}`.

If `maxItems` was reached before full coverage, the run still succeeds and reports
`capHit: true` in `OUTPUT` — intentional truncation is visible, never silent.

Spending limits are honored the same way: if you set a maximum total charge on the run, the
scraper computes how many results your budget pays for, stops cleanly at that number, and
reports `budgetHit: true` (with the computed `budgetCap`) in `OUTPUT` — the run succeeds with
exactly the results you paid for, instead of being aborted mid-crawl by the platform. A budget
too small to pay for even one result fails the run immediately with an explanatory message.

### Cost

Plain-HTTP crawling does roughly **3,000 pages per compute unit**. Each listing costs at most
two page fetches (its search-results share + one detail page for the gallery), and zero-photo
listings skip the detail fetch entirely. A 25-listing ZIP run typically consumes well under
0.01 CU. Use `maxItems` to put a hard ceiling on any run's cost.

### Etiquette

The scraper uses polite concurrency and rate limits, a browser-like User-Agent, and respects the
site's robots.txt disallowed paths.

# Actor input Schema

## `zips` (type: `array`):

ZIP codes to scrape. Ignored if state or nationwide is set.

## `state` (type: `string`):

Two-letter state code (e.g. TX). Overrides zips.

## `nationwide` (type: `boolean`):

Scrape every state. Overrides zips and state. Enumerates every active sale nationwide via the site's sale-detail sitemap (no search-page walk).

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

Hard cap on emitted listings. The crawler stops enqueueing and finishes cleanly at this count.

## `includeImages` (type: `boolean`):

When true, fetch each listing's detail page for the full images\[] array. When false, images\[] is empty (cheapest mode).

## `maxImagesPerListing` (type: `integer`):

Cap on gallery URLs extracted per listing.

## `excludeTypes` (type: `array`):

Sale types to drop from the output (case-insensitive match against the listing's `type` field). Leave empty to emit every type. Note: in nationwide mode the type is only known after the detail-page fetch, so excluded listings still count toward Max items there and the run may emit fewer than Max items.

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

Apify proxy settings. Datacenter proxies by default.

## Actor input object example

```json
{
  "zips": [
    "77098"
  ],
  "nationwide": false,
  "maxItems": 50,
  "includeImages": true,
  "maxImagesPerListing": 25,
  "excludeTypes": [],
  "proxyConfiguration": {
    "useApifyProxy": true
  }
}
```

# Actor output Schema

## `sales` (type: `string`):

One record per estate/garage/yard sale: id, title, type, company, address, city, state, zip, coordinates, sale dates, and the full photo gallery in images\[] (site order, images\[0] == main\_image).

## `runStats` (type: `string`):

Integrity counters for the run: pagesCrawled, itemsEmitted, detailFetches, blockedResponses, parseFailures, lostRequests, sitemapUrlCount, maxItemsCap, capHit.

# 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 = {
    "zips": [
        "77098"
    ],
    "maxItems": 50
};

// Run the Actor and wait for it to finish
const run = await client.actor("scrapelabmax/estate-sale-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 = {
    "zips": ["77098"],
    "maxItems": 50,
}

# Run the Actor and wait for it to finish
run = client.actor("scrapelabmax/estate-sale-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 '{
  "zips": [
    "77098"
  ],
  "maxItems": 50
}' |
apify call scrapelabmax/estate-sale-scraper --silent --output-dataset

```

## MCP server setup

```json
{
    "mcpServers": {
        "apify": {
            "type": "http",
            "url": "https://mcp.apify.com/?tools=fetch-actor-details,scrapelabmax/estate-sale-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/Vb0zWA0Vs8kIplSee/builds/bZznZV3cfO219ErUM/openapi.json
