# Nextdoor Business & Reviews Scraper (`oswaldocarabano/nextdoor-scraper`) Actor

Scrape public Nextdoor business listings, neighbor recommendations and city demographics. No login, no session cookies, nothing that expires.

- **URL**: https://apify.com/oswaldocarabano/nextdoor-scraper.md
- **Developed by:** [Oswaldo Carabano](https://apify.com/oswaldocarabano) (community)
- **Categories:** Lead generation, Social media
- **Stats:** 2 total users, 1 monthly users, 100.0% runs succeeded, 0 bookmarks
- **User rating**: No ratings yet

## Pricing

Pay per event

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

## Nextdoor Business & Reviews Scraper

Extract public Nextdoor business listings, neighbor recommendations and city
demographics as structured JSON, CSV or Excel.

**No login. No session cookies. No browser.** Nothing you have to refresh, and
nothing that expires between runs.

***

### Why that matters more than it sounds

Most Nextdoor scrapers ask you for your session cookies. That works on the day you
set it up and breaks when the cookies expire — and when it breaks, there is nothing
you can do about it except paste new cookies and hope.

This Actor reads only what Nextdoor's own servers hand to a signed-out visitor. There
is no account, no session and no cookie jar, so there is nothing to go stale. A run
today behaves like a run in six months.

That is a deliberate design constraint, not a missing feature. It also means the
Actor **cannot** reach anything behind Nextdoor's login — see
[What this Actor will not do](#what-this-actor-will-not-do).

***

### What you get

**One row per business** in the main dataset, with its recommendations nested
inside. Two ready-made views: **Businesses** (one row each) and
**Recommendations** (one row per recommendation, business as context).

City data, posts and any error rows go to their own separate datasets — `cities`,
`posts`, `errors` — so the main table never has blank rows in it.

#### 1 · `business` — the primary dataset

34 fields per business: name, categories, phone, email, website, full address with
ZIP, coordinates, opening hours, verification status, gallery, Nextdoor's own
summary of what neighbors say, and more.

**Measured fill rates** — counted over **741 distinct businesses** across 5 cities
and 12 categories. These are counts from real runs, not estimates:

| Field | Filled |
|---|---|
| `phone_number` | **93.9 %** |
| `categories` | **84.2 %** |
| `website_url` | **77.2 %** |
| `email` | **62.6 %** |
| `hours_open_status` | **57.4 %** |
| `recommendations_summary` | **47.4 %** |
| `description` | **46.6 %** |
| `gallery_urls` | **44.5 %** |

An earlier version of this page quoted higher numbers for some fields, measured on a
sample of 30. At n = 741 several came out lower — opening hours most of all, 57 %
rather than 77 %. The table above is the one to trust, and it is the one we will keep
correcting as the sample grows. Anything not listed here is not promised.

Anything not on that list is not promised. `address_postal_code` is derived by this
Actor, because Nextdoor does not expose a ZIP field anywhere in its schema.

#### 2 · `recommendation` — with a classifier you will not find elsewhere

Nextdoor's recommendation feed mixes three different things that all look alike:
people praising a business, people *asking* for one, and people just saying thanks.
Scrape it raw and your "500 reviews" are largely neighbors asking *"anyone know a
good plumber?"* — which is worthless as review data and poisonous as sentiment input.

Every row gets a **`kind`** field:

| `kind` | What it is |
|---|---|
| `recommendation` | Actual praise or criticism of a business |
| `service_request` | Someone *looking for* a provider, not reviewing one |
| `thanks` | Acknowledgement with no substance about the business |
| `unknown` | Below the confidence threshold — flagged, not guessed |

How much this matters, measured across two real runs (891 recommendations): **50 %
came back `service_request`** — neighbors asking for a provider — against **19 %
actual recommendations**. Scrape the feed raw and half your "review data" is noise
pointing the wrong way.

**Accuracy: 95.6 %** on a hand-labelled validation set. Each row also carries
`kind_confidence` and `kind_signals`, so you can raise the bar yourself instead of
trusting a black box.

> **A note on wording.** Nextdoor calls these *recommendations*, not reviews. They
> are free-text posts from neighbors, they have **no star rating**, and Nextdoor
> anonymizes the author (typically a first name and last initial). If you need
> numeric ratings, this is not that dataset. What you get is what people actually
> wrote — which for most local-market research is the more useful half.

Measured over the same 741 businesses: **42.4 % have at least one recommendation**,
averaging **2.5** each across all businesses. Plan for that — more than half of any
local category has none, and those businesses are still returned with
`recommendations_count: 0` so your row count stays predictable.

#### 3 · `city` — demographics and market context

Residents, average income and age, homeowner percentage, safety / friendliness /
affordability scores, all 54 business categories Nextdoor tracks, up to 852
neighborhoods, and counts of classifieds, local groups and events.

#### 4 · `posts` dataset — optional

Business posts are corporate content published by the businesses themselves.
Neighbor posts are **off by default** — see [Privacy](#privacy-and-personal-data).

#### Where each thing lands

| Dataset | Contents |
|---|---|
| **default** | one row per business, `recommendations` nested inside |
| `cities` | one row per city: demographics, scores, neighborhoods |
| `posts` | business posts and, if enabled, neighbor posts |
| `errors` | anything that failed, for traceability — **never charged** |

***

### Input

Paste a city and go. Every field has a sensible default, and the defaults are
deliberately small: a first run finishes in about a minute and costs under $0.50, so
you can see the real data before committing to a full sweep. Raise
`maxBusinessesPerCity` when you want one.

```json
{
  "cities": ["san-clemente--ca"],
  "categories": ["Dentist", "Plumber"],
  "maxBusinessesPerCity": 50,
  "includeRecommendations": true,
  "includeCityData": true
}
```

City slugs follow Nextdoor's own format: `city-name--state`, lowercase, two-letter
state. Leave `categories` empty to sweep all 54 categories a city exposes.

#### How Nextdoor orders a city directory — worth 30 seconds

Nextdoor's category directory is **ordered by distance**. It serves every business in
the city you asked for, and once that list runs out it keeps going into neighbouring
towns. The switch is abrupt, not a gradual blend.

We measured it on `san-clemente--ca`, asking for 200 dentists, and counted how many
of each block of 25 were actually in San Clemente:

| Results | In San Clemente |
|---|---|
| 1–75 | **96–100 %** |
| 76–200 | **0 %** |

San Clemente simply has ~72 dentists. Everything past that point is the surrounding
trade area — San Juan Capistrano, Dana Point, Ladera Ranch, Laguna Niguel.

**What this means for you in practice:**

- Keep `maxBusinessesPerCity` near what a city plausibly holds and you get that city,
  full stop. Our 120-business run came back **92 % San Clemente** with no filtering.
- Set it far higher and the surplus is regional overflow. Useful if you want the
  trade area — surprising if you didn't.
- **`onlyRequestedCity: true`** keeps only the city you named. Filtering happens
  during discovery, so you are never charged for a dropped business, and the Actor
  stops paging once the city is exhausted instead of burning through the region.

Every row carries `address_city` either way, so you can always split it yourself.

When you request several categories, the per-city limit is **split evenly between
them**, and whatever a small category cannot fill is handed to the others. Ask for
plumbers and dentists and you get both, not 200 dentists.

You can also skip discovery entirely and pass business URLs directly in
`directBusinessUrls`.

***

### Pricing

Pay per result. **You are never charged for a row that was not delivered**, and
error rows are never charged at all.

| Event | Price |
|---|---|
| Actor start | **$0.00001** — effectively free |
| `business` | **$0.008** |
| `city` | **$0.008** |
| `recommendation` | **$0.002** |
| `post` | **$0.001** |

Starting the Actor costs $0.00001 — Apify does not allow a literal $0 event — so a
run that finds nothing costs you a hundred-thousandth of a dollar, which is as close
to nothing as the platform permits. Set
`maxBusinessesPerCity`, or a spend limit on the run, and the Actor stops cleanly at
the limit — no half-written rows.

***

### Speed and politeness

Measured on the platform: **~3,300 businesses/hour** end-to-end at the default
concurrency of 5 — that is 120 businesses plus 393 recommendations plus city data in
130 seconds, discovery included. The extraction stage alone runs at ~4,900/hour.

The Actor deliberately caps concurrency at 5 and keeps a delay between requests.
Pushing harder triggers rate limiting that costs *more* time than it saves — we
measured a ~4 minute cooldown after tripping it — so when Nextdoor pushes back, the
whole run pauses for minutes rather than hammering through a penalty window. Slower
than a scraper that ignores this, and it finishes; faster in practice.

Requests go out through US residential IPs. You do not need to configure a proxy.

***

### Data freshness

Results may be served from a shared cache, which makes runs faster and cheaper.
Every row tells you exactly what you got:

- `from_cache` — whether this row came from cache
- `fetched_at` — when the data was actually read from Nextdoor
- `data_age_hours` — how old it is

Set `maxCacheAgeDays: 0` to force a fresh fetch on every row.

***

### What this Actor will not do

Stated plainly so you can judge fit before spending anything:

- **No logged-in content.** No private groups, no member feeds, no direct messages,
  nothing that requires an account.
- **No free-text business search.** Nextdoor requires a login for that, so discovery
  runs through the public category directory instead. You find businesses by city
  and category, not by arbitrary keyword.
- **No star ratings.** Nextdoor recommendations do not have them.
- **No de-anonymized authors.** Author names are passed through exactly as Nextdoor
  publishes them and are never cross-referenced, enriched or resolved to real people.
- **No addresses for businesses that hid theirs.** When a business sets its address
  to private — common for people working out of their homes — every address field
  comes back `null` with a warning, even though the coordinates are technically
  visible in the page source.
- **United States only** in this version.

***

### Privacy and personal data

Business contact details are published by the businesses themselves, for the purpose
of being contacted.

Neighbor content is treated differently. `includeCityPosts` is **off by default**
because neighbor posts carry partial real names alongside a specific neighborhood,
and sometimes describe local safety incidents. `includeAuthorNeighborhood` is a
separate opt-in on top, because a partial name plus a precise neighborhood can
identify a real person in a small community. If you turn these on, you are
responsible for how you use the results — including under CCPA, if that applies to
you.

**Removal requests:** if you are a business owner or resident and want your
information excluded from future runs, email **privacy@actorstack.dev** and it will
be added to an exclusion list.

***

### Not affiliated with Nextdoor

This Actor is an independent tool. It is not affiliated with, endorsed by, sponsored
by or connected to Nextdoor, Inc. in any way. "Nextdoor" is a trademark of its
owner, used here only to describe what this Actor reads.

It collects only data that Nextdoor's servers serve publicly to visitors who are not
signed in. You are responsible for complying with the laws that apply to you when
you use the output.

***

### Output sample

<details>
<summary><code>business</code></summary>

```json
{
  "_type": "business",
  "name": "Example Plumbing",
  "slug": "example-plumbing-san-clemente-ca",
  "categories": ["Plumber", "Water Heater Installation"],
  "phone_number": "+19495550123",
  "email": "hello@exampleplumbing.com",
  "website_url": "https://exampleplumbing.com",
  "address_full": "123 Avenida Del Mar, San Clemente, CA 92672",
  "address_postal_code": "92672",
  "hide_address": false,
  "recommendations_count": 12,
  "neighbors_talking_count": 34,
  "recommendations_summary": "Neighbors mention fast response and fair pricing.",
  "is_verified": true,
  "source_city_slug": "san-clemente--ca",
  "source_category": "Plumber",
  "scraped_at": "2026-08-17T12:00:00.000Z",
  "from_cache": false,
  "data_age_hours": 0,
  "_warnings": [],
  "recommendations": [
    {
      "kind": "recommendation",
      "kind_confidence": 0.92,
      "text": "They fixed our water heater the same day we called. Fair price too.",
      "author_display_name": "Sarah M.",
      "author_city": "San Clemente",
      "business_name": "Example Plumbing"
    }
  ]
}
```

</details>

<details>
<summary>one <code>recommendations</code> entry, as the Recommendations view flattens it</summary>

```json
{
  "business_name": "Example Plumbing",
  "business_slug": "example-plumbing-san-clemente-ca",
  "kind": "recommendation",
  "kind_confidence": 0.92,
  "kind_signals": ["praise", "past_service"],
  "text": "They fixed our water heater the same day we called. Fair price too.",
  "author_display_name": "Sarah M.",
  "author_city": "San Clemente",
  "author_state": "CA",
  "sentiment_hint": "positive",
  "char_count": 66
}
```

</details>

Every field is always present. A field that exists but is empty comes back as
explicit `null` — keys are never dropped, so your parser never has to guess.

The nested recommendations omit `scraped_at`, `_type`, `_warnings` and
`business_id`, because the business row already carries all four and the
Recommendations view lifts the nested keys up to the top level — two keys with the
same name would silently overwrite each other.

# Actor input Schema

## `cities` (type: `array`):

Nextdoor city slugs in the format city-name--state, lowercase, two-letter state. Example: san-clemente--ca

## `categories` (type: `array`):

Leave empty to sweep every category a city exposes — Nextdoor publishes 54 per city. The per-city limit is split evenly between the categories you list.

## `onlyRequestedCity` (type: `boolean`):

Nextdoor's category directory is REGIONAL, not municipal: it lists the surrounding trade area. Measured in San Clemente, CA — only 36 % of 200 results were in San Clemente itself, the rest spread across 8 nearby towns. Leave off to get the whole trade area (Nextdoor's own behaviour); turn on to keep only the city you asked for. Either way, every row carries address\_city so you can filter yourself.

## `directBusinessUrls` (type: `array`):

Scrape these business pages directly, in addition to whatever discovery finds. Use this when you already know which businesses you want.

## `maxBusinessesPerCity` (type: `integer`):

Cap PER CITY, shared evenly between the categories you ask for. The default of 50 keeps a first run fast and cheap; raise it for a full sweep. Nextdoor's directory is ordered by distance, so a cap near the city's real inventory keeps results local.

## `includeRecommendations` (type: `boolean`):

Individual posts neighbors wrote about the business, with the author left exactly as Nextdoor anonymizes them. Each one is classified as a real recommendation, a request for a provider, or noise. Measured over 741 businesses: 42.4 % have at least one.

## `maxRecommendationsPerBusiness` (type: `integer`):

Measured over 741 businesses: 2.5 recommendations each on average, 33 the most seen on one, and 57.6 % have none at all. Raising this rarely changes the result.

## `includeCityData` (type: `boolean`):

One row per city in a separate `cities` dataset: population, average income and age, homeowner percentage, safety and friendliness scores, all 54 categories, neighborhoods, and counts of classifieds, groups and events.

## `includeBusinessActivity` (type: `boolean`):

Corporate content the businesses published themselves. No neighbor personal data. Goes to a separate `posts` dataset.

## `includeCityPosts` (type: `boolean`):

⚠️ Contains personal data: a partial author name and free text, sometimes about local safety incidents. Off by default. If you turn this on, you are responsible for how you use the results — including under CCPA, if that applies to you.

## `includeAuthorNeighborhood` (type: `boolean`):

⚠️ A partial name next to a precise neighborhood can identify a real person in a small community. Only applies if you enabled neighbor posts above.

## `expandNearbyCities` (type: `boolean`):

Discover neighbouring cities through Nextdoor's own cross-links, up to the hard cap below.

## `maxExpandedCities` (type: `integer`):

A hard cap, and it is not optional: Nextdoor's city graph connects the whole of the United States, so without a limit one run could keep going indefinitely.

## `maxConcurrency` (type: `integer`):

Measured: clean up to 5; at 10 Nextdoor returns HTTP 429 with a cooldown of about four minutes, which costs more time than the extra speed saves. Raising this is rarely worth it.

## `maxCacheAgeDays` (type: `integer`):

Results may be served from a shared cache, which makes runs faster. Set 0 to force a fresh fetch of everything. Every row tells you what you got: `from_cache`, `fetched_at` and `data_age_hours`.

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

Optional. The Actor already routes through US residential IPs, so you do not need to configure anything here.

## Actor input object example

```json
{
  "cities": [
    "san-clemente--ca"
  ],
  "categories": [
    "Dentist",
    "Plumber"
  ],
  "onlyRequestedCity": false,
  "directBusinessUrls": [],
  "maxBusinessesPerCity": 50,
  "includeRecommendations": true,
  "maxRecommendationsPerBusiness": 100,
  "includeCityData": true,
  "includeBusinessActivity": false,
  "includeCityPosts": false,
  "includeAuthorNeighborhood": false,
  "expandNearbyCities": false,
  "maxExpandedCities": 3,
  "maxConcurrency": 5,
  "maxCacheAgeDays": 7,
  "proxyConfiguration": {
    "useApifyProxy": false
  }
}
```

# Actor output Schema

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

One row per business, with recommendations nested. Switch to the Recommendations view to get one row per recommendation.

## `recommendations` (type: `string`):

The same data flattened to one row per neighbor recommendation, each classified as a real recommendation, a service request or noise.

## `runSummary` (type: `string`):

What was delivered, what was charged, and how much came from cache. Use it to reconcile your invoice against the rows you received.

# 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 = {
    "cities": [
        "san-clemente--ca"
    ],
    "categories": [
        "Dentist",
        "Plumber"
    ]
};

// Run the Actor and wait for it to finish
const run = await client.actor("oswaldocarabano/nextdoor-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 = {
    "cities": ["san-clemente--ca"],
    "categories": [
        "Dentist",
        "Plumber",
    ],
}

# Run the Actor and wait for it to finish
run = client.actor("oswaldocarabano/nextdoor-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 '{
  "cities": [
    "san-clemente--ca"
  ],
  "categories": [
    "Dentist",
    "Plumber"
  ]
}' |
apify call oswaldocarabano/nextdoor-scraper --silent --output-dataset

```

## MCP server setup

```json
{
    "mcpServers": {
        "apify": {
            "type": "http",
            "url": "https://mcp.apify.com/?tools=fetch-actor-details,oswaldocarabano/nextdoor-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/WofEN0Ihuh8L8VAIM/builds/sYJgglHSBu7jXevNr/openapi.json
