DoorDash Scraper: Menus, Prices, Reviews & Ratings
Pricing
from $1.00 / 1,000 results
DoorDash Scraper: Menus, Prices, Reviews & Ratings
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.
Pricing
from $1.00 / 1,000 results
Rating
0.0
(0)
Developer
Abot API
Maintained by CommunityActor stats
0
Bookmarked
9
Total users
3
Monthly active users
15 days ago
Last modified
Categories
Share
DoorDash Scraper: Stores, Menus, Reviews & Search
Pull clean, structured data from DoorDash at scale. Search any keyword or paste store links, and get one record per store containing meta (name, rating, price tier, geo), the full menu, contact details (phone, website, opening hours) and review insights (recent reviews, the full 1 to 5 star histogram, theme highlights and the Google rating aggregate). Fast, reliable, and comprehensive.
Why this scraper?
- Five things in one actor: Search, Meta, Menu, Contact info and Reviews.
- 40+ fields per store, including the exact numeric rating count (not just "500+"), the full star histogram and review theme mentions.
- Lean search runs return about 30 fields per store and are very cheap.
- Full menu with item names, prices, photos, dietary tags and item ratings.
- Menu items on a genuine markdown are flagged with isOnSpecial, savingsAmount and discountPercent, derived straight from the store's own listed price.
- Opening hours by day, phone number, website and precise latitude/longitude.
- Toggle exactly what you pay for: include menu, contact info and reviews independently.
- Clean, consistent output with stable field names you can map once and reuse.
- Resume & recurring updates — turn on Incremental mode to get only NEW, UPDATED, and REAPPEARED stores on every scheduled run, or resume one specific interrupted run with
resumeFromRunId.
Data you get
Sample shape, values are illustrative placeholders, not from a live listing.
| Field | Example |
|---|---|
| storeId | "00000001" |
| name | "Sample Pizza Place" |
| url | "https://www.doordash.com/store/00000001/" |
| rating | 4.5 |
| ratingCount | 1234 |
| priceTier | "$$" |
| 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 | false |
| isDashpass | true |
| menuItemCount | 42 |
| reviewCount | 18 |
| menu.categories[] | [{ "name": "Most Ordered", "numItems": 6 }] |
| menu.items[] | [{ "name": "Sample Item", "price": "$7.99", "strikethroughPrice": "$9.99", "isOnSpecial": true, "savingsAmount": 2.00, "discountPercent": 20, "dietaryTags": ["Vegetarian"] }] |
| contact.hours[] | [{ "day": "MONDAY", "slots": ["10 AM - 10 PM"] }] |
| reviews.histogram | { "1": 5, "2": 3, "3": 4, "4": 20, "5": 90 } |
| reviews.items[] | [{ "reviewer": "Jane D", "stars": 5, "text": "Sample review text." }] |
| reviews.themes[] | [{ "label": "Great value", "count": 12, "sentiment": "POSITIVE" }] |
How to use
Search by keyword, lean (no detail fetch):
{"mode": "search","search": ["pizza"],"maxStores": 50,"maxPages": 2,"includeMenu": false,"includeBusiness": false,"includeReviews": false,"proxy": { "useApifyProxy": true, "apifyProxyGroups": ["RESIDENTIAL"], "apifyProxyCountry": "US" }}
Search a specific city with full detail:
{"mode": "search","search": ["sushi", "ramen"],"location": "Brooklyn, NY","maxStores": 20,"includeMenu": true,"includeBusiness": true,"includeReviews": true,"proxy": { "useApifyProxy": true, "apifyProxyGroups": ["RESIDENTIAL"], "apifyProxyCountry": "US" }}
Specific stores by URL or ID:
{"mode": "url","urls": ["https://www.doordash.com/store/00000001/", "00000002"],"includeMenu": true,"includeBusiness": true,"includeReviews": true,"proxy": { "useApifyProxy": true, "apifyProxyGroups": ["RESIDENTIAL"], "apifyProxyCountry": "US" }}
Input parameters
| Parameter | Type | Default | Description |
|---|---|---|---|
| mode | string | "search" | "search" for keyword search, "url" for store links or IDs. |
| search | array | ["pizza"] | Keywords to search. One search runs per term. Search mode only. |
| location | string | (empty) | City or address to localize search, e.g. "Brooklyn, NY". Defaults to a major city in your proxy country. |
| 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. |
| urls | array | (example) | Store links or numeric store IDs. URL mode only. |
| includeMenu | boolean | true | Add the full menu (one store detail fetch per store). |
| includeBusiness | boolean | true | Add phone, website and opening hours. |
| includeReviews | boolean | true | Add reviews, star histogram, themes and Google aggregate. |
| 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 — see "Resume & recurring updates" below. |
| maxPages | integer | (empty) | Optional bound on result pages walked per search query (50 stores per page). Leave empty to walk every result page. Search mode only. |
| resumeFromRunId | string | (empty) | Previous run ID (or dataset ID) from this actor. Stores already collected there are skipped, so the run only returns new ones. For recurring daily/weekly monitoring of the same search, use Incremental mode instead — see below. |
| incrementalMode | boolean | false | Daily/recurring monitoring of this same search. First run returns everything as NEW; later runs return only NEW/UPDATED/REAPPEARED by default. See "Resume & recurring updates" below. |
| stateKey | string | (empty) | Optional name for a monitoring campaign, so its incremental state stays stable or is deliberately shared. Auto-derived from your search/filter settings when left empty. |
| emitUnchanged | boolean | false | Incremental mode only. Also return stores unchanged since the last run, marked UNCHANGED. Adds and bills extra rows you already have. |
| emitExpired | boolean | false | Incremental mode only. Also return stores from a previous run no longer found, marked EXPIRED, once a run has fully scanned the search (not capped, not a resume). Adds and bills extra synthetic rows. |
| proxy | object | Residential US | Proxy configuration. Residential is required. |
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 — 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.
- Authorize a connector once under Apify → Settings → Integrations (Notion, Linear, Airtable, or Apify).
- Select it in the "Pipe results into your apps" input field. (If the picker is empty, you haven't authorized a connector yet.)
- For Notion, also set
notionParentPageUrlto 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.
Output example
Sample shape, values are illustrative placeholders, not from a live listing.
{"storeId": "00000001","name": "Sample Pizza Place","url": "https://www.doordash.com/store/00000001/","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 | 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 |
Resume & recurring updates
There are two different things here — pick the one that matches what you're doing:
| Need | Use |
|---|---|
| A run stopped and should continue | resumeFromRunId / automatic checkpoint recovery |
| Run the same search every day and receive only changes | incrementalMode |
| Keep separate daily campaigns for similar searches | distinct stateKey values |
| Run a normal full snapshot | leave both off |
Resume (resumeFromRunId) continues one specific interrupted or previous large run: paste a run ID or dataset ID and this run skips stores already collected there, returning only the remaining new ones. An automatic same-run checkpoint also protects against platform migrations/Resurrects without any input needed.
Incremental mode (incrementalMode) is for a schedule (for example, daily): the actor remembers the previous run of the same search by itself, so you never paste a run ID. The first run returns everything as NEW. Later runs return only NEW, UPDATED, and REAPPEARED stores by default — unchanged stores are suppressed (fetched to check, but not returned and not charged for the detail-enrichment surcharge). Turn on emitUnchanged or emitExpired only when you also want those rows returned (and billed for). State is isolated per search/URL and filter/include-toggle combination automatically (maxStores/maxPages are excluded — raising your cap keeps the same baseline instead of re-billing everything); set stateKey to name or deliberately share a monitoring campaign.
Scheduled-run example — same search, run daily:
Day 1 (first run ever for this search):
{ "mode": "search", "search": ["pizza"], "location": "Brooklyn, NY", "incrementalMode": true }
→ every store comes back with "changeType": "NEW".
Day 2 (the schedule fires again, identical input):
{ "mode": "search", "search": ["pizza"], "location": "Brooklyn, NY", "incrementalMode": true }
→ stores whose rating/menu/hours/etc. changed come back as "changeType": "UPDATED" with changedFields listing what changed; brand-new stores come back as "changeType": "NEW"; stores that vanished and came back come back as "changeType": "REAPPEARED"; stores that are still there, unchanged, are not returned at all (suppressed, not charged) unless emitUnchanged is on.
What counts as "changed": DoorDash's own nextOpenTime/nextCloseTime fields tick with the request's wall-clock time (measured: they advance every request even for always-open stores) and are never treated as a real change. Rating, price tier, delivery fee, ETA, menu items/prices and business contact info are real, comparable data and always drive UPDATED. Tiny GPS-distance floating-point noise is rounded away before comparison so it never triggers a false UPDATED either.
maxStores and Incremental mode: the cap counts stores scanned (collected and detail-fetched), not stores actually returned. A quiet monitoring run where nothing changed still scans up to maxStores stores (proxy cost is unchanged), but the dataset-item and detail-enrichment charges drop to near zero since almost everything is suppressed.
Scale: a monitoring campaign's state is capped at roughly 8.5MB (compressed) in Apify's key-value store. Measured against this actor's own record shapes: about 1,500 tracked stores with menu/business/reviews all enabled, or about 65,000 tracked stores with all three off (lean meta-only monitoring). Use a separate stateKey per campaign to stay well under this if you track more than that.
Plan requirement
This actor needs Apify Residential proxy, which is 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). Recent public reviews are capped at about five per store; the full numeric rating count, the star histogram, theme highlights and the Google rating aggregate are always included.