# DoorDash Scraper - Menus, Prices, Reviews & Ratings (`abotapi/doordash-scraper`) Actor

Extract structured doordash.com data at scale. Search by keyword or paste store URLs to get store details, ratings, price tier, location, full menus, contact info, opening hours, review insights, star-rating breakdowns, and Google rating data.

- **URL**: https://apify.com/abotapi/doordash-scraper.md
- **Developed by:** [Abot API](https://apify.com/abotapi) (community)
- **Categories:** E-commerce, Developer tools, Automation
- **Features:** Limited permissions
- **Stats:** 38 total users, 7 monthly users, 97.1% runs succeeded, 1 bookmarks
- **User rating**: No ratings yet

## Pricing

from $1.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.
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

## DoorDash Scraper: Stores, Menus, Reviews & Search

DoorDash Scraper turns DoorDash into structured restaurant and store data. Search any keyword or paste store links or IDs, and get one record per store with rating, price tier, cuisines and location, plus the full menu, contact details and review insights when you switch them on. Export to JSON, CSV or Excel, or pull results straight into your app through the API.

### Why This Scraper?

- **Two ways to find stores.** Run keyword searches localized to a city, or paste store links or bare numeric IDs directly. No need to build URLs by hand.
- **One rich record per store.** Meta (name, rating, price tier, cuisines, geo) is always included; menu, contact info and reviews are added on top when you turn them on.
- **The exact numeric rating count.** Not a rounded bucket like "500+": the real count, plus the full 1 to 5 star histogram, review theme highlights and the Google rating aggregate. The written review text itself is capped at roughly the 5 most recent public reviews per store; see the reviews note under Input Parameters.
- **Full menu with markdown detection.** Item names, prices, photos and dietary tags, with `isOnSpecial`, `savingsAmount` and `discountPercent` derived from the store's own listed price and strikethrough price.
- **Pay only for what you turn on.** Menu, contact info and reviews all come from one store-detail fetch, billed as a single surcharge per returned store no matter how many of the three you enable; a lean search with all three off skips that fetch and surcharge entirely.
- **Opening hours by day**, phone number, website and precise latitude and longitude for every enriched store.
- **Resume and recurring updates.** Continue one interrupted run with `resumeFromRunId`, or turn on Incremental mode for scheduled runs that return only new and changed stores.

### Use Cases

- **Food delivery market research:** compare pricing tiers, delivery fees and ETAs for a cuisine across a city or a whole metro area.
- **Local business directories:** republish store contact details, opening hours and menus for a region.
- **Competitive intelligence for restaurants:** track a competitor's menu prices, specials and rating trends over time with Incremental mode.
- **Data science and demand modeling:** build historical datasets of ratings, reviews and menu changes for machine learning.
- **Lead generation and outreach:** find stores that are missing a website or offering deals right now.

### Data You Get

Besides the default Stores overview, the Apify Console's dataset **Overview** tab also offers a **Reviews** view (one row per review), a **Menu items** view (one row per menu item), a **Contact & hours** view (one row per store's contact block) and, in Incremental mode, a **Changes** view (what's new, updated, back or gone since the last run). Each is just a different slice of the same underlying rows: nothing is scraped twice, and switching views never changes what is in the dataset or what you're billed.

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

| Field | Example |
| --- | --- |
| `storeId` | `"1673363"` |
| `name` | `"Sample Pizza Place"` |
| `url` | `"https://www.doordash.com/store/1673363/"` |
| `rating` | `4.5` |
| `ratingCount` | `1234` |
| `priceTier` / `priceRange` | `"$$"` / `2` |
| `cuisines` | `["Pizza", "Italian"]` |
| `city` / `state` | `"Sampleton"` / `"CA"` |
| `latitude` / `longitude` | `25.0000` / `55.0000` |
| `phone` | `"+10000000000"` |
| `website` | `"https://example.com"` |
| `etaMinutes` | `25` |
| `deliveryFee` | `"$0 delivery fee"` |
| `isSponsored` / `isDashpass` / `pickupAvailable` | `false` / `true` / `true` (last is search mode only; `null` in URL mode) |
| `businessName` | `"Sample Restaurant Group"` |
| `distance` | `"1.1 mi"` |
| `menuItemCount` | `42` |
| `menu.categories[]` | `[{ "name": "Most Ordered", "numItems": 6 }]` |
| `menu.items[]` | `[{ "name": "Sample Item", "price": "$7.99", "isOnSpecial": true, "savingsAmount": 2.0, "discountPercent": 20, "dietaryTags": ["Vegetarian"] }]` |
| `contact.hours[]` | `[{ "day": "MONDAY", "slots": ["10 AM - 10 PM"] }]` |
| `reviewCount` | `18` |
| `reviews.histogram` | `{ "1": 5, "2": 3, "3": 4, "4": 20, "5": 90 }` |
| `reviews.items[]` | `[{ "reviewer": "Jane D", "stars": 5, "text": "Sample review text.", "markedUpReviewText": "Sample review <<taggedItemId=123>> text.", "taggedItemIds": ["123"] }]` |
| `reviews.themes[]` | `[{ "label": "Great value", "count": 12, "sentiment": "POSITIVE" }]` |
| `scrapedAt` | `"2026-10-06T12:00:00.000Z"` (this row's own read time; excluded from change detection) |

### How to Use

1. Pick a **mode**: `search` (keyword) or `url` (paste store links or numeric IDs).
2. For `search` mode, add search terms and, optionally, a location, a store type and the well-rated, deals-only, minimum rating or fast delivery filters.
3. Toggle **Include menu**, **Include contact and hours** and **Include reviews** for the detail you need, and set **Max stores** to control run size and cost.
4. Click **Start**, then download the dataset as JSON, CSV or Excel, or read it through the API.

**Search by keyword, lean (no detail fetch):**

```json
{
  "mode": "search",
  "search": ["pizza"],
  "maxStores": 50,
  "includeMenu": false,
  "includeBusiness": false,
  "includeReviews": false
}
```

**Search a specific city with full detail:**

```json
{
  "mode": "search",
  "search": ["sushi", "ramen"],
  "location": "Brooklyn, NY",
  "maxStores": 20,
  "includeMenu": true,
  "includeBusiness": true,
  "includeReviews": true
}
```

**Specific stores by URL or ID:**

```json
{
  "mode": "url",
  "urls": ["https://www.doordash.com/store/673363/", "1673363"],
  "includeMenu": true,
  "includeBusiness": true,
  "includeReviews": true
}
```

**URL mode: supported link shapes**

If you switch to URL mode and leave **Store URLs or IDs** untouched, the run scrapes the example links listed in that field (one for each supported shape below), so a first run always returns real stores; replace them with your own links. Search mode ignores the field.

Every shape below was verified live. A store id is read from the start of the URL's path only, so the locale prefix and query string never change what gets scraped. Links copied from page source with `&amp;` or `&#x27;` escapes (for example `/store/domino&#x27;s-easley-33931599`) are decoded first.

| Shape | Example | Resolves to |
| --- | --- | --- |
| Canonical, with name slug | `https://www.doordash.com/store/juniors-restaurant-new-york-704090/` | store `704090` |
| Canonical, bare id | `https://www.doordash.com/store/673363/` | store `673363` |
| Bare numeric id (no URL) | `673363` | store `673363` |
| Canonical + query string (`pickup`, `cursor`, tracking params, ...) | `https://www.doordash.com/store/673363/?pickup=true&cursor=abc123` | store `673363` |
| A specific menu (breakfast, lunch, dinner, ...) | `https://www.doordash.com/store/2691971/1531826/` | store `2691971`, served with menu `1531826` |
| Convenience/grocery store page | `https://www.doordash.com/convenience/store/370903/` | store `370903` |
| The store's reviews page | `https://www.doordash.com/reviews/store/2691971/` | store `2691971` |
| Any locale prefix (`en-CA`, `fr-CA`, `en-AU`, `en-GB`, `es-US`, `en`, `es`, `fr`, `it`, ...) | `https://www.doordash.com/en-CA/store/673363/` | store `673363` |

**Not a single store, skipped with a warning in the log, never silently dropped:**

| Shape | Example | Why it's skipped |
| --- | --- | --- |
| Business/chain page | `https://www.doordash.com/business/kfc-4477/`, `https://www.doordash.com/b/new-york-ny/burger-king-10171/` | Lists every location of a chain; the trailing number is a *business* id, a different id space from a store id (verified live: feeding it in as a store id returns a real but unrelated store). Open one of the individual store links from that page instead. |
| Category / cuisine / dish / city page | `https://www.doordash.com/cuisine/pizza-near-me/`, `https://www.doordash.com/dish/brookie-near-me/`, `https://www.doordash.com/food-delivery/new-york-ny-restaurants/` | A listing of many stores, not one. Use Search mode with a keyword and/or location. |
| Search-results page | `https://www.doordash.com/search/store/pizza/` | Same reason: `store` here is a path segment of the search feature, not an id (so `/search/store/7-eleven/` is never mistaken for store `7`). Use Search mode. |
| `drd.sh` short link | `https://drd.sh/tF5uns/` | DoorDash's generic marketing short-link domain (referral links, promo/legal terms, app-store "restrictions apply" links); real examples resolve to a help-center or legal page, not reliably a store page. Paste the resolved `doordash.com` store URL instead. |
| A link to another website | `https://www.example.com/store/123/` | Not a DoorDash page. |

**Well-rated restaurants with active deals in a city:**

```json
{
  "mode": "search",
  "search": ["burgers"],
  "location": "Austin, TX",
  "storeType": "restaurant",
  "ratedOnly": true,
  "dealsOnly": true,
  "maxStores": 30
}
```

#### Run it from your code

Python:

```python
from apify_client import ApifyClient

client = ApifyClient("<YOUR_APIFY_TOKEN>")
run = client.actor("abotapi/doordash-scraper").call(run_input={"mode": "search", "search": ["pizza"], "maxStores": 20})
for store in client.dataset(run["defaultDatasetId"]).iterate_items():
    print(store["name"], store["rating"], store["priceTier"])
```

JavaScript:

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

const client = new ApifyClient({ token: '<YOUR_APIFY_TOKEN>' });
const run = await client.actor('abotapi/doordash-scraper').call({ mode: 'search', search: ['pizza'], maxStores: 20 });
const { items } = await client.dataset(run.defaultDatasetId).listItems();
```

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

#### Detail fetch and cost by mode

Search mode only fetches, and only charges for, store detail (menu, contact and reviews) when at least one of the three include toggles is on; leave all three off for a lean, meta-only search that skips the detail fetch entirely. URL mode always fetches the full store detail for every store you list, regardless of those toggles, since the toggles only decide which of menu, contact and reviews end up in the output record, not whether the detail page is loaded.

#### Resume and recurring updates

- **Resume** (`resumeFromRunId`) continues one specific interrupted or previous run: paste its run or dataset ID and this run skips stores already collected there, so you don't pay twice. An automatic same-run checkpoint also protects against platform migrations without any input needed. An invalid or unresolvable id stops the run before anything is scraped or charged.
- **Incremental mode** (`incrementalMode`) is for a schedule, for example daily, over the same search or URLs. The actor remembers the previous run of the same search itself, so you never paste a run id. The first run returns everything as `NEW`. Later runs return `NEW`, `UPDATED` (with `changedFields`) and, if you turn on `emitUnchanged`, `UNCHANGED` stores that did not change (suppressed and not billed by default). `EXPIRED` rows for stores no longer found are only produced when `emitExpired` is on and the run scanned the full tracked search (not capped by Max stores or Max pages, and not a Resume run). `REAPPEARED` is only produced for a store a previous run already marked `EXPIRED` this way; if a monitoring campaign has never turned `emitExpired` on, no store can ever come back as `REAPPEARED`, because none was ever marked gone in the first place.
- **What counts as changed:** these fields are excluded from change detection by default, built into the actor, not something you need to configure: they still appear in the row, they just never drive `UPDATED` or appear in `changedFields` by themselves:

  - `nextOpenTime`, `nextCloseTime`: tick with each request's clock even for an always-open store, never a real change.
  - `scrapedAt`: this row's own read time; always differs between runs by construction.
  - `asapAvailable`: flips with the store's open/closed clock, the same wall-clock dependence as `nextOpenTime`/`nextCloseTime` above.
  - `searchEtaMinutes`: the search card's live ETA to the searched address; measured drifting run to run with no store change (load-driven, not store-driven). `etaMinutes` (the store-detail ETA) is NOT excluded and always drives `UPDATED`.
  - `menuId`, `menuName`, `menuHours`: which menu the store is currently serving (e.g. Breakfast vs. Dinner); each menu's own schedule stays tracked via `menus`, and switching menus already changes `menu`.
  - `reviews.items.reviewerDescription`: the reviewer's own profile line, not a fact about this store.
  - `storeName`: a pure duplicate of `name` (kept only as a stable identity column for the `reviews`/`menu` output views); `name` itself stays tracked.

  Small GPS-distance floating point noise is rounded away too. Rating, price tier, delivery fee, `etaMinutes`, menu items, reviews and business contact info were all measured byte-identical across two live fetches of the same store minutes apart and are real data that always drives `UPDATED`. If a field in your own monitoring setup still turns out noisy, add it (or a dotted path into a nested field, e.g. `reviews.items`) to `ignoreFieldsForChanges` and it is excluded from then on, on top of the list above; it still appears in the row, just never flags a change by itself.
- A search page that comes back with zero stores on its very first page is treated the same as a connection problem for `EXPIRED` purposes, not as "this search is empty now": `EXPIRED` detection is skipped for that run (the previous baseline is kept untouched) rather than risk marking real, still-open stores as gone.
- `stateKey` names or shares one monitoring campaign's stored state; `maxStores` and `maxPages` never affect it, so raising your cap keeps the same baseline instead of re-billing everything. `maxStores` counts stores scanned (collected and detail fetched), not stores actually returned, so a quiet monitoring run where nothing changed still scans up to the cap.

### 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, and the Apify dataset is never changed.

**What gets written to the connector:** a condensed, human-readable **summary** of each record, 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` for keyword search, `url` for store links or IDs. |
| `search` | array | none (prefill: `["pizza"]`) | Keywords to search on DoorDash. One search runs per term. Search mode only. |
| `location` | string | none | City or address to localize search, for example "Brooklyn, NY". If empty, a default city for your proxy country is used: New York (Manhattan, `New York, NY 10001`) for US, Toronto for CA, Sydney for AU, Auckland for NZ. DoorDash lists no stores in other countries, so any other proxy country searches New York and logs a warning. A location DoorDash cannot place stops the run with a message saying so; another place is never guessed. Search mode only. |
| `storeType` | string | `any` | Limit search to a category: `any`, `restaurant`, `grocery`, `retail`. `restaurant` excludes convenience/grocery stores. Search mode only. |
| `ratedOnly` | boolean | `false` | Only return stores that have a customer rating. Search mode only. |
| `dealsOnly` | boolean | `false` | Only return stores currently offering deals. Search mode only. |
| `minRating` | string | `any` | Only return stores rated at least this many stars, using DoorDash's own rating filter: `any`, `4`, `4.5`, `4.6`, `4.7`, `4.8` or `4.9` (the values DoorDash's own Ratings filter offers). When set, this overrides the `ratedOnly` filter above. Search mode only. |
| `fastDeliveryOnly` | boolean | `false` | Only return stores with a delivery estimate of 30 minutes or less for your location, using DoorDash's "Under 30 min" filter. Search mode only. |
| `urls` | array | one example of each supported shape (the same list is the prefill) | Store links or bare numeric store IDs, see "URL mode: supported link shapes" above for every supported shape and the ones that are skipped with a warning instead. URL mode only. |
| `includeMenu` | boolean | `true` | Add the full menu. Requires a store-detail fetch (always made in URL mode; made in search mode only if this or another include toggle is on). |
| `includeBusiness` | boolean | `true` | Add phone, website and opening hours. |
| `includeReviews` | boolean | `true` | Add reviews, star histogram, themes and the Google rating aggregate. The written review text is DoorDash's own public preview, up to ~5 most recent reviews per store, logged out; see "Why does `reviews.items` only have a few entries?" in the FAQ. |
| `maxReviews` | integer | `20` | Maximum written reviews kept in `reviews.items` per store. DoorDash serves at most about 5 to a logged-out visitor, so the default keeps everything it serves; lower it to keep fewer, or `0` for none (the star histogram, review counts, themes and Google rating are still included). Adds no charge. |
| `maxStores` | integer | `20` | Max stores across all searches/URLs. `0` means no limit. Counts stores scanned (collected and detail fetched), before any Incremental mode suppression. |
| `maxPages` | integer | none, unlimited when omitted (prefill: `0`, also unlimited) | Optional bound on result pages walked per search query. Leave empty or `0` to walk every result page. Search mode only. |
| `resumeFromRunId` | string | none | Previous run ID or dataset ID from this actor. Stores already collected there are skipped, so this run only returns new ones. |
| `incrementalMode` | boolean | `false` | Turn on for scheduled monitoring of the same search or URLs; see "Resume and recurring updates" above. |
| `stateKey` | string | none | Name a monitoring campaign, so its state stays stable, or deliberately share it across differently configured runs. Auto derived when left empty. |
| `emitUnchanged` | boolean | `false` | Incremental mode only. Also return (and bill) stores unchanged since the last run, marked `UNCHANGED`. |
| `emitExpired` | boolean | `false` | Incremental mode only. Also return (and bill) stores from a previous run no longer found, marked `EXPIRED`, once a run has fully scanned the tracked search. |
| `ignoreFieldsForChanges` | array | `[]` | Incremental mode only. Extra field names to exclude from `UPDATED` change detection, on top of the built-in defaults: `nextOpenTime`, `nextCloseTime`, `scrapedAt` (wall-clock state, never a real store change), `asapAvailable` (ticks with the open/closed clock), `searchEtaMinutes` (load-driven search-card ETA noise; `etaMinutes` is NOT excluded), `menuId`/`menuName`/`menuHours` (which menu is currently being served), `reviews.items.reviewerDescription` (the reviewer's own profile line) and `storeName` (a duplicate of `name`). See "What counts as changed" above for the full reasoning. Accepts a top-level field (e.g. `rating`) or a dotted path into a nested field (e.g. `reviews.items`). Use this if a field in your own setup turns out to be noisy. |
| `proxy` | object | Residential, US | Apify Residential proxy is required. |
| `mcpConnectors` | array | none | Optional: send a summary of each store to apps you authorized under Integrations. |
| `notionParentPageUrl` | string | none | Notion connector only: page under which store pages are created. |
| `maxNotifyListings` | integer | `50` | Cap on items written to each connector per run. Does not affect the dataset. |

### Output Example

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

```json
{
  "storeId": "1673363",
  "name": "Sample Pizza Place",
  "url": "https://www.doordash.com/store/1673363/",
  "rating": 4.5,
  "ratingCount": 1234,
  "priceTier": "$$",
  "cuisines": ["Pizza", "Italian"],
  "city": "Sampleton",
  "state": "CA",
  "latitude": 25.0000,
  "longitude": 55.0000,
  "etaMinutes": 25,
  "deliveryFee": "$0 delivery fee",
  "isSponsored": false,
  "isDashpass": true,
  "phone": "+10000000000",
  "website": "https://example.com",
  "contact": {
    "phone": "+10000000000",
    "website": "https://example.com",
    "hours": [{ "day": "MONDAY", "slots": ["10 AM - 10 PM"] }],
    "address": { "street": "1 Sample St", "city": "Sampleton", "state": "CA" }
  },
  "menu": {
    "categories": [{ "name": "Most Ordered", "numItems": 6 }],
    "items": [{ "name": "Sample Item", "price": "$7.99", "strikethroughPrice": "$9.99", "isOnSpecial": true, "savingsAmount": 2.00, "discountPercent": 20, "dietaryTags": ["Vegetarian"] }],
    "popularItems": [{ "name": "Sample Item", "price": "$9.99", "strikethroughPrice": "", "isOnSpecial": false, "savingsAmount": null, "discountPercent": null }]
  },
  "menuItemCount": 42,
  "reviews": {
    "avgRating": 4.5,
    "numRatings": 1234,
    "numReviews": 18,
    "histogram": { "1": 5, "2": 3, "3": 4, "4": 20, "5": 90 },
    "items": [{ "reviewer": "Jane D", "stars": 5, "text": "Sample review text.", "isVerified": true }],
    "themes": [{ "label": "Great value", "count": 12, "sentiment": "POSITIVE" }],
    "google": { "avgRating": 4.4, "numRatings": 500 }
  },
  "reviewCount": 18
}
```

**Incremental mode only.** When `incrementalMode` is on, every returned record also carries:

| Field | Description |
| --- | --- |
| `changeType` | `NEW`, `UPDATED`, `UNCHANGED`, `REAPPEARED` or `EXPIRED` |
| `changedFields` | Top-level fields that changed since last seen; non-empty only for `UPDATED` |
| `firstSeenAt` | When this store was first observed by this monitoring campaign |
| `lastSeenAt` | When this store was last observed |

### Plan Requirement

This actor needs Apify Residential proxy, available on the Starter plan and above. Residential is the default and recommended setting; set the proxy country to the region you want search results localized to, for example US. The store-detail surcharge applies once per returned store whenever the detail page is loaded: in search mode only if at least one include toggle is on, in URL mode always. A lean search with all three toggles off costs the least.

### FAQ

#### How much does it cost?

You pay per record returned, plus one detail surcharge per returned store whose detail page was loaded (search mode: only when at least one include toggle is on; URL mode: always). An unchanged store suppressed by Incremental mode is fetched to check but not billed for that surcharge. The **Pricing** tab shows current rates. Use **Max stores** to cap the size of any run.

#### Is it legal to scrape DoorDash?

This actor collects only publicly available store, menu and review data. You are responsible for how you use it: follow DoorDash's terms and the laws that apply to you, and get legal advice if you plan commercial redistribution. Store names, menus and prices are generally facts, but photos and review text may be subject to third-party rights.

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

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

#### What is the difference between Resume and Incremental mode?

**Resume** (`resumeFromRunId`) continues one specific interrupted or previous run: paste its run or dataset id once and this run skips what it already collected. **Incremental mode** is for running the exact same search or URLs again and again on a schedule; the actor remembers the previous run itself, with nothing to paste. Turning both on together for a search that already has saved incremental state is rejected before any scraping happens; pick one. Also note that `REAPPEARED` only ever appears for a store a previous run already marked `EXPIRED`, which itself requires `emitExpired` to have been on for that campaign.

#### Which city does a search use when Location is empty?

The default city for your proxy country: New York for US, Toronto for CA, Sydney for AU and Auckland for NZ. Since the October 2026 update the US default is the precise `New York, NY 10001` (Manhattan). Earlier versions sent the bare `New York, NY`, which DoorDash placed near Binghamton, NY, so empty-Location US searches returned upstate stores; a scheduled Incremental search with an empty Location therefore sees one round of NEW Manhattan stores after the update. Proxy countries DoorDash does not serve (for example GB, IE, DE, AT) used to resolve to same-named North American towns (London, Ontario; Berlin, Wisconsin); they now search New York and log a warning telling you to set Location.

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

The run fails before any scraping happens if your input cannot make sense: an invalid or unresolvable `resumeFromRunId`, or turning on Incremental mode together with Resume for a search that already has saved incremental state. It also fails, with a clear status message instead of a silent zero-result success, whenever every proxy connection the run tried was refused or exhausted and no page was ever read:

- The proxy connection was refused outright for your account (no plan access, or your own proxy URLs are invalid): the message points you to upgrading at https://apify.com/pricing or checking your proxy credentials.
- Residential proxies were unavailable on a Free plan and the backup connection also could not load the requested pages (including a backup connection that came up but never delivered a page): the message tells you to upgrade or supply your own proxy URLs.
- Every request failed for some other connection reason, with no proxy entitlement signal either way: the message says this is a connection problem, not an empty search, and to try again later.

A first results page that could not be fetched at all is reported the same way, so a connection failure never masquerades as "no stores found". A first page that is fetched fine but holds zero stores is a different case: an **unfiltered** zero-match search normally still returns this site's own fallback results, and restrictive filters (for example rating + deals + fast-delivery together) remove those fallbacks, so a genuinely empty result completes as a normal zero-result run with a `ZERO_RESULTS` status message instead of a failure. One exception is checked separately: when you set a `location` and every first page comes back empty, the run repeats your first search once at the default city for your proxy country. If that returns stores, the location is the cause (DoorDash could not place it, or does not deliver there), and the run fails with a message naming your location and how to fix it ("City, ST", a full street address, or an empty Location). In URL mode, a run in which none of your store links or IDs ever loaded a store page also never ends as a silent success: if a page request failed it fails with the messages above, and if the pages answered but none was a store it completes with a `ZERO_RESULTS` status message saying so. An ordinary site-side block or a single failed fetch on one store, by contrast, still completes as a normal run with partial results and a status message explaining what was skipped.

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

#### Why does `reviews.items` only have a few entries?

DoorDash itself only serves a public preview of written reviews, up to about 5 of the most recent, to a logged-out visitor; the full list only loads after the visitor signs in to a DoorDash consumer account on doordash.com, which this actor does not do (no login/cookies are accepted as input). `reviewCount` and the `reviews.histogram` 1-to-5 star breakdown are the real site-wide totals, not capped, so they will usually be larger than `reviews.items.length`: that gap is DoorDash's own preview limit, not a bug or an artificial cutoff on our side. The `maxReviews` input (default `20`) can only keep fewer of the served reviews, never fetch more; reviews never add a charge of their own, they ride on the store's single detail fetch.

### 🔗 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/wolt-restaurants-scraper"><b>Wolt.com</b></a><br>From $0.8/1K. Scrape Wolt restaurants and full menus at the city scale. Extract 60+...</td><td>🛒 <a href="https://apify.com/abotapi/grabfood-restaurants-scraper"><b>GrabFood Restaurant Scraper</b></a><br>Scrape GrabFood restaurants across Southeast Asia, including SG, MY, TH, VN, PH, ID, KH...</td></tr>
<tr><td>✈️ <a href="https://apify.com/abotapi/naver-map-scraper"><b>Naver Map Scraper</b></a><br>From $1/1K. Scrape Naver Map places by keyword or URL: names, categories, ratings...</td><td>🛒 <a href="https://apify.com/abotapi/zomato-scraper"><b>Zomato Scraper</b></a><br>Scrape restaurants and reviews from zomato.com. Get names, cuisines, ratings and votes...</td></tr>
<tr><td>📇 <a href="https://apify.com/abotapi/google-maps-scraper"><b>Google Maps Scraper</b></a><br>Extract business data from Google Maps at scale. Get names, addresses, phone numbers...</td><td>🛒 <a href="https://apify.com/abotapi/naver-brand-store-scraper"><b>Naver Shopping Scraper</b></a><br>Scrape Naver Shopping products by Korean or English keyword, or by URL. Returns 190+...</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/doordash-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`):

How to discover stores. 'search' runs keyword searches; 'url' takes store links or IDs directly.

## `search` (type: `array`):

Keywords to search on DoorDash (e.g. "pizza", "sushi", "McDonald's"). One search runs per term. Only used when mode = search.

## `location` (type: `string`):

City or address to localize search results, e.g. "Brooklyn, NY". If empty, a major city in your proxy country is used (US: New York, CA: Toronto, AU: Sydney, NZ: Auckland; DoorDash lists no stores elsewhere, so other proxy countries use New York and log a warning). If DoorDash cannot place the location, the run stops with a message saying so (it never guesses another place). Only used when mode = search.

## `storeType` (type: `string`):

Limit search to a store category. 'restaurant' excludes convenience/grocery stores. Only used when mode = search.

## `ratedOnly` (type: `boolean`):

Only return stores that have a customer rating (drops unrated / very low rated stores). Only used when mode = search.

## `dealsOnly` (type: `boolean`):

Only return stores currently offering deals/offers. Only used when mode = search.

## `minRating` (type: `string`):

Only return stores rated at least this many stars using DoorDash's rating filter. When set, this overrides the 'Well-rated only' filter. Search mode only.

## `fastDeliveryOnly` (type: `boolean`):

Only return stores with delivery estimates of 30 minutes or less for your location, using DoorDash's 'Under 30 min' filter. Search mode only.

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

DoorDash store links or bare numeric store IDs. Supported link shapes: the canonical page (with or without a name slug, with any query string), a specific menu (/store/<name>-<id>/<menuId>/), /convenience/store/<id>/, the store's reviews page (/reviews/store/<id>/), and any locale-prefixed variant (/en-CA/, /fr-CA/, /es-US/, /en/, ...) of those. Links copied from page source with & or ' escapes are decoded. Multiple supported. A business/chain page (/business/<name>-<id>/ or /b/<city>/<brand>-<id>/), a category, dish, city or search-results page, a drd.sh short link or a non-DoorDash link has no single store id and is skipped with a warning in the log -- open an individual store link instead. If you leave this untouched in URL mode, the example links shown here are scraped. Only used when mode = url.

## `includeMenu` (type: `boolean`):

Fetch the full menu (categories + items with price, image, dietary tags). Requires a store-detail fetch per store.

## `includeBusiness` (type: `boolean`):

Fetch phone number, website and opening hours by day.

## `includeReviews` (type: `boolean`):

Fetch reviews preview (up to ~5 recent public reviews), the full 1-5 star histogram, theme highlights and Google rating aggregate.

## `maxReviews` (type: `integer`):

Maximum written reviews kept in reviews.items for each store when 'Include reviews' is on. DoorDash itself serves at most about 5 recent public reviews per store to a logged-out visitor, so the default of 20 keeps everything it serves; lower it to keep fewer. 0 = no written reviews (the star histogram, review counts, themes and Google rating are still included). Never adds a charge: reviews ride on the store's single detail fetch.

## `maxStores` (type: `integer`):

Maximum number of stores to return across all searches/URLs. 0 = no limit.

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

Optional bound on result pages walked per search query (50 stores per page). Leave empty to walk every result page -- the run already stops at the Max stores cap, so this only matters if you also want to limit page depth. Only used when mode = search.

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

Apify Residential proxy is required for a clean IP. Pick the country your searches should be localized to. On a block the connection automatically rotates to a fresh residential IP, then to a neighbouring country; a plan without Residential proxy access falls back to a backup connection (results may be reduced).

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

Optional: paste a previous run ID (or dataset ID) from this actor. Stores already collected there are skipped, so this run only returns new ones.

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

Turn this on for daily or recurring monitoring. The first run returns all matching stores as NEW. Later runs normally return only NEW, UPDATED, and REAPPEARED stores. Turn on "Emit unchanged" or "Emit expired" below to also see stores that did not change or that disappeared.

## `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 let the actor derive a key automatically from the search/URLs and filters you set (Max stores/Max pages do not affect this key).

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

Off by default. Turn on to also return stores 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 specifically need the full snapshot every run.

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

Off by default. Turn on to also return stores that were present in a previous run but are no longer found, marked EXPIRED. Only produced once a run has fully scanned the tracked search — not when Max stores/Max pages capped the run, and not on a Resume run.

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

Optional. Field names to exclude from UPDATED change detection (in addition to the fields that never count: nextOpenTime/nextCloseTime/asapAvailable/searchEtaMinutes and the currently-served menuId/menuName/menuHours tick with the clock or live load rather than reflecting a real store change, and reviews.items.reviewerDescription describes the reviewer, not the store). Accepts a top-level field name (e.g. "rating") or a dotted path into a nested field (e.g. "reviews.items"). Use this if a field in your setup turns out to be noisy.

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

Optionally send the scraped results into the apps you already use, via Model Context Protocol (MCP) connectors. Authorize a connector once under Apify → Settings → Integrations, then select it here. The connector receives a condensed, human-readable summary per item (title + key fields), not the full JSON — the complete record stays in the dataset. Leave empty to skip. 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 item pages are created. Required to enable the Notion export; ignored by other connectors.

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

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

## Actor input object example

```json
{
  "mode": "search",
  "search": [
    "pizza"
  ],
  "storeType": "any",
  "ratedOnly": false,
  "dealsOnly": false,
  "minRating": "any",
  "fastDeliveryOnly": false,
  "urls": [
    "https://www.doordash.com/store/juniors-restaurant-new-york-704090/",
    "https://www.doordash.com/store/673363/?pickup=true",
    "https://www.doordash.com/store/2691971/1531826/",
    "https://www.doordash.com/convenience/store/370903/",
    "https://www.doordash.com/reviews/store/2691971/",
    "https://www.doordash.com/en-CA/store/673363/",
    "25943279"
  ],
  "includeMenu": true,
  "includeBusiness": true,
  "includeReviews": true,
  "maxReviews": 20,
  "maxStores": 20,
  "maxPages": 0,
  "proxy": {
    "useApifyProxy": true,
    "apifyProxyGroups": [
      "RESIDENTIAL"
    ],
    "apifyProxyCountry": "US"
  },
  "incrementalMode": false,
  "emitUnchanged": false,
  "emitExpired": false,
  "ignoreFieldsForChanges": [],
  "maxNotifyListings": 50
}
```

# Actor output Schema

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

No description

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

No description

## `menu` (type: `string`):

No description

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

No description

## `contact` (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",
    "search": [
        "pizza"
    ],
    "urls": [
        "https://www.doordash.com/store/juniors-restaurant-new-york-704090/",
        "https://www.doordash.com/store/673363/?pickup=true",
        "https://www.doordash.com/store/2691971/1531826/",
        "https://www.doordash.com/convenience/store/370903/",
        "https://www.doordash.com/reviews/store/2691971/",
        "https://www.doordash.com/en-CA/store/673363/",
        "25943279"
    ],
    "maxPages": 0,
    "proxy": {
        "useApifyProxy": true,
        "apifyProxyGroups": [
            "RESIDENTIAL"
        ],
        "apifyProxyCountry": "US"
    },
    "ignoreFieldsForChanges": []
};

// Run the Actor and wait for it to finish
const run = await client.actor("abotapi/doordash-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",
    "search": ["pizza"],
    "urls": [
        "https://www.doordash.com/store/juniors-restaurant-new-york-704090/",
        "https://www.doordash.com/store/673363/?pickup=true",
        "https://www.doordash.com/store/2691971/1531826/",
        "https://www.doordash.com/convenience/store/370903/",
        "https://www.doordash.com/reviews/store/2691971/",
        "https://www.doordash.com/en-CA/store/673363/",
        "25943279",
    ],
    "maxPages": 0,
    "proxy": {
        "useApifyProxy": True,
        "apifyProxyGroups": ["RESIDENTIAL"],
        "apifyProxyCountry": "US",
    },
    "ignoreFieldsForChanges": [],
}

# Run the Actor and wait for it to finish
run = client.actor("abotapi/doordash-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",
  "search": [
    "pizza"
  ],
  "urls": [
    "https://www.doordash.com/store/juniors-restaurant-new-york-704090/",
    "https://www.doordash.com/store/673363/?pickup=true",
    "https://www.doordash.com/store/2691971/1531826/",
    "https://www.doordash.com/convenience/store/370903/",
    "https://www.doordash.com/reviews/store/2691971/",
    "https://www.doordash.com/en-CA/store/673363/",
    "25943279"
  ],
  "maxPages": 0,
  "proxy": {
    "useApifyProxy": true,
    "apifyProxyGroups": [
      "RESIDENTIAL"
    ],
    "apifyProxyCountry": "US"
  },
  "ignoreFieldsForChanges": []
}' |
apify call abotapi/doordash-scraper --silent --output-dataset

```

## MCP server setup

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