BusinessesForSale.com Scraper - Businesses, Prices & Financials
Pricing
from $1.50 / 1,000 results
BusinessesForSale.com Scraper - Businesses, Prices & Financials
From $1.5/1K. Extract businesses and franchises listed for sale on BusinessesForSale.com. Search by keywords, country, price, revenue, profit, sort, or use search/listing URLs. Returns title, asking price, revenue, cash flow, location, premises size, employees, photos, categories, and description.
Pricing
from $1.50 / 1,000 results
Rating
0.0
(0)
Developer
Abot API
Maintained by CommunityActor stats
1
Bookmarked
8
Total users
5
Monthly active users
a day ago
Last modified
Categories
Share
BusinessesForSale.com Scraper: Business and Franchise Listings for Sale
BusinessesForSale.com Scraper turns BusinessesForSale.com into structured listing data. Build a search from keywords, market and financial filters, or paste any search-result or individual listing URL, and get the full record: asking price, revenue, cash flow, location, premises size, employees, year established, trading hours, the complete photo gallery and the listing description. Search the worldwide catalogue or pin to a single market, walk as many result pages as you ask for, and export to JSON, CSV or Excel, or pull results straight into your app through the API.
Why This Scraper?
- Two ways to start. Build a search from filters (keywords, country, price/revenue/cash-flow ranges, sort order) or paste any search-result URL, both of which walk as many result pages as you ask for, or paste an individual listing URL for a single record.
- Rich detail on demand. Turn on detail enrichment for the full description, every gallery photo, premises size, employee count, year established, trading hours and lease/freehold tenure, on top of the headline financials.
- Server-side filters that actually narrow the catalogue. Keywords, global coverage or one of seven markets, asking price/revenue/cash-flow ranges, disclosed-only toggles, and eight sort orders.
- Global coverage. Search the worldwide catalogue or pin to a single market: United States, United Kingdom, Australia, Canada, Ireland, New Zealand or South Africa.
- Cost control built in. A results-only search run skips the detail-enrichment surcharge, and Max pages / Max listings cap any run before it grows (Max listings is clamped to 1,000,000 per run).
- Built for monitoring. Incremental mode returns only new and changed listings on repeat runs against the same search, and Resume continues one interrupted run without re-scraping what you already collected.
- Optional delivery into your other apps. Pipe results into Notion, Linear, Airtable or Apify through MCP connectors, no code required.
Use Cases
- Business brokers and M&A advisors: build comparable-listing sets by market, price band and cash flow to benchmark a new mandate.
- Buy-side searchers and search funds: monitor a keyword or category for new listings and get notified only when something changes.
- Franchise researchers: pull franchise fee, investment and financial figures across markets to compare opportunities.
- Market analysts: track listing counts and price/revenue bands in a country over time.
- Lead generation teams: build outreach lists of business sellers in a given industry or region from public listing data.
Data You Get
Sample shape: values are illustrative placeholders, not from a live listing.
| Field | Example |
|---|---|
id | "us:sample-coffee-shop" |
url | "https://us.businessesforsale.com/us/sample-coffee-shop.aspx" |
title | "Sample Coffee Shop For Sale" |
listingClass | "business" (also "franchise") |
labels | ["New", "High Value"] |
searchCountry | "us" |
locationText | "Sample City, Sample State, US" |
locationDetail | "Sample City ST 00000" (when Fetch listing details is on) |
country / region / locality | "US" / "Sample State" / "Sample City" |
askingPriceDisplay / askingPriceValue / priceCurrency | "$500,000" / 500000 / "USD" |
businessFunction | "Sell" (also "LeaseOut") |
revenueDisplay / revenueValue | "$300,000" / 300000 |
cashFlowDisplay / cashFlowValue | "$120,000" / 120000 |
tenure | "Lease" (also "Freehold", when Fetch listing details is on) |
sizeSqFt | "1,200" (when Fetch listing details is on) |
tradingHours | "7 a.m. to 6 p.m." (when Fetch listing details is on) |
employees | "5" (when Fetch listing details is on) |
yearsEstablished | "2015" (when Fetch listing details is on) |
description | full listing description text (when Fetch listing details is on) |
categories | [{ "name": "Cafes & Coffee Shops", "url": "https://..." }] |
financials | { "Asking Price": "$500,000", "Revenue": "$300,000", "Cash Flow": "$120,000" } |
details | { "Size in square feet": "1,200", "Trading hours": "...", ... } (when Fetch listing details is on) |
thumbnail | "https://www.businessesforsale.com/uploadsstatic/00000000mediumthumbnail.jpg" |
imageCount / images | 4 / ["https://us.businessesforsale.com/uploads/00000000.jpg", ...] |
scrapedAt | "2026-01-01T00:00:00.000Z" |
The dataset has three views: Listings (overview), Detail & financials and Changes (incremental mode). Each record carries a seller field that holds the structured-data seller object when the page publishes one; it is null otherwise (the broker name shown under "Listed by" on detail pages is not extracted into it). When Incremental mode is on, every record additionally carries changeType (NEW, UPDATED, UNCHANGED, REAPPEARED or EXPIRED), changedFields, firstSeenAt and lastSeenAt.
How to Use
- Pick a mode: search (build a query from the filters) or url (paste search-result or listing links).
- For search mode, set keywords, the country/market, and any price, revenue or cash-flow filters and a sort order. For url mode, paste one or more links.
- Turn "Fetch listing details" on or off, and set Max pages / Max listings to control run size and cost.
- Click Start, then download the dataset as JSON, CSV or Excel, or read it through the API.
Search by keyword:
{"mode": "search","keywords": "restaurant","country": "us","maxPages": 3,"maxListings": 50}
Search with financial filters:
{"mode": "search","country": "uk","minPrice": 100000,"maxPrice": 1000000,"minProfit": 50000,"profitDisclosedOnly": true,"sortBy": "PriceAsc","maxPages": 5}
Scrape several markets with URL mode:
{"mode": "url","urls": ["https://us.businessesforsale.com/us/search?Keywords=cafe","https://australia.businessesforsale.com/australian/search?Keywords=cafe"],"maxPages": 3,"maxListings": 100}
Fast, results-only run (no detail pages):
{"mode": "search","keywords": "car wash","country": "us","fetchDetails": false,"maxPages": 10}
Run it from your code
Python:
from apify_client import ApifyClientclient = ApifyClient("<YOUR_APIFY_TOKEN>")run = client.actor("abotapi/businessesforsale-com-scraper").call(run_input={"mode": "search", "keywords": "restaurant", "country": "us"})for listing in client.dataset(run["defaultDatasetId"]).iterate_items():print(listing["title"], listing["askingPriceDisplay"])
JavaScript:
import { ApifyClient } from 'apify-client';const client = new ApifyClient({ token: '<YOUR_APIFY_TOKEN>' });const run = await client.actor('abotapi/businessesforsale-com-scraper').call({ mode: 'search', keywords: 'restaurant', country: 'us' });const { items } = await client.dataset(run.defaultDatasetId).listItems();
Or connect it to Make, Zapier, n8n, Google Sheets or webhooks from the Integrations tab.
Resume & recurring updates
- Resume (
resumeFromRunId) continues one interrupted run: paste its run or dataset ID and search results already collected there (matched by listingid) are skipped, so you don't pay twice. Pasted individual listing URLs are always re-fetched regardless of Resume. - Incremental mode (
incrementalMode) is for repeated, scheduled runs over the same search. Each listing is classifiedNEW,UPDATED(withchangedFields),UNCHANGED(suppressed and not billed unlessemitUnchangedis on),REAPPEAREDorEXPIRED.REAPPEAREDonly appears for a listing that was previously markedEXPIREDand is now found again;EXPIREDis only produced after a run that scans every page of the tracked search with no cap hit and no resume in play, and only withemitExpiredon (a pasted URL that starts at page 2 or later is a partial scan and never producesEXPIRED).UPDATEDmeans the listing's own data changed (title, labels, location, asking price, revenue, cash flow, categories, images, description, seller); the scrape timestamp is never compared and the image host (CDN domain) is ignored, so only a different image path counts.ignoreFieldsForChangeslists extra fields (dot paths such asseller.nameallowed) to leave out of the comparison andchangedFields; they stay in the output, and changing the list does not cause a wave ofUPDATEDrows.stateKeynames or shares the stored state. With Incremental mode off, output is exactly as before.
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.
Input Parameters
| Parameter | Type | Default | Description |
|---|---|---|---|
mode | string | search | search builds a query from the filters below; url scrapes the URLs you paste. |
keywords | string | "" (prefill: "restaurant") | Free-text search, used in search mode. Leave blank to list everything. |
country | string | global | Market to search: global, us, uk, australia, canada, ireland, newzealand, southafrica. |
minPrice | integer | none | Minimum asking price, in the market's local currency. |
maxPrice | integer | none | Maximum asking price. |
minRevenue | integer | none | Minimum yearly sales revenue (turnover). |
maxRevenue | integer | none | Maximum yearly sales revenue. |
minProfit | integer | none | Minimum yearly cash flow / net profit. |
maxProfit | integer | none | Maximum yearly cash flow / net profit. |
priceDisclosedOnly | boolean | false | Hide listings whose asking price is "on request". |
revenueDisclosedOnly | boolean | false | Only listings that disclose revenue. |
profitDisclosedOnly | boolean | false | Only listings that disclose cash flow / profit. |
sortBy | string | Default | Default, Newest, PriceAsc, PriceDesc, RevenueAsc, RevenueDesc, ProfitAsc, ProfitDesc. |
urls | array | one example of each supported shape (keyword, bare, Page=N, category slug, CategoryIds, Sort=PriceAsc, broker/agent page, legacy /search/businesses-for-sale, market subdomain search, market .aspx listing) | Search-result or individual listing URLs, used in url mode. Behaviour change: in url mode, leaving the field untouched now scrapes these examples (bounded by Max listings) instead of nothing. The franchises landing page (it lists no rows; use franchise category search URLs instead), /contact, .rss and other-host URLs are skipped with a warning in the log. Filter fields above are ignored. |
fetchDetails | boolean | true | Include the full per-listing record: description, all photos, size, employees, trading hours, year established, categories and tenure. Turn off for a faster, cheaper results-only run. |
pageSize | string | "50" | Listings fetched per request: "50" or "100". Always sent to the site; the default now returns 50 per page instead of the site's own ~25, so maxPages callers get about twice as many listings per page (page boundaries differ from earlier versions). |
maxPages | integer | 0 (unlimited) | Limit on how many result pages each search/URL returns. 0 walks every available page, bounded by Max listings. |
maxListings | integer | 20 | Stop after this many listings total. 0 means unlimited, bounded then only by Max pages. |
resumeFromRunId | string | none | Paste a previous run ID or dataset ID to continue a full-catalogue walk across separate runs. |
incrementalMode | boolean | false | Return only new and changed listings on repeat, scheduled runs over the same search. |
stateKey | string | none | Name this monitoring campaign to keep its state stable, or to deliberately share state across differently configured runs. |
emitUnchanged | boolean | false | Also return (and bill) listings that have not changed since the last run, marked UNCHANGED. |
emitExpired | boolean | false | Also return (and bill) listings that were present in a previous run but are no longer found, marked EXPIRED. Only produced after a complete, uncapped, non-resumed scan. |
ignoreFieldsForChanges | array | none | Fields to leave out of change detection in incremental mode (dot paths allowed). They stay in the output. |
proxy | object | Apify Proxy, RESIDENTIAL group, country US | Starting connection (Apify Proxy or your own proxy URLs). See Plan Requirement below. |
mcpConnectors | array | none | Optional MCP connectors to export results into (Notion, Linear, Airtable, Apify). Leave empty to skip. |
notionParentPageUrl | string | none | Notion page under which item pages are created. Required for the Notion connector, ignored by the others. |
maxNotifyListings | integer | 50 | Cap on items written to each connector per run. Does not affect the dataset. |
queryKeywords | array | none | Multiple keyword searches: one search per keyword with the same filters. Overrides Keywords. |
querySort | string | none | Free-text sort (Newest, PriceAsc, ...), used only when Sort by is Default. |
queryStateOrRegion | string | none | State or region name, added as a location word to the keyword search. |
queryCityOrArea | string | none | City or area name, added as a location word to the keyword search. |
queryIndustrySlug | string | none | Industry slug such as restaurants; searches /search/<slug>-for-sale. |
querySearchPath | string | none | Advanced: custom path under the market host, must start with /search. |
queryMinAskingPrice | string | none | Short money string (10K, 1.5M); used only when minPrice is empty. |
queryMaxAskingPrice | string | none | Short money string; used only when maxPrice is empty. |
queryMinGrossIncome | string | none | Short money string; used only when minRevenue is empty. |
queryMaxGrossIncome | string | none | Short money string; used only when maxRevenue is empty. |
queryMinCashFlow | string | none | Short money string; used only when minProfit is empty. |
queryMaxCashFlow | string | none | Short money string; used only when maxProfit is empty. |
startUrls | array | none | Alias of urls; when filled, switches to URL mode. |
maxItems | integer | none | Alias of maxListings; used only when Max listings is empty. 0 is unlimited. |
Extra inputs and fields
Search mode also accepts: Multiple keyword searches (one search per keyword), a free-text Sort, State / region and City / area (added as location words to the keyword search), Industry slug and Custom search path (both under /search), and short money strings such as 10K or 1.5M for the asking price, revenue and cash flow ranges. Start URLs is an alias of URLs (switches to URL mode) and Max items is an alias of Max listings. Rows also carry listingId, location, ebitda, ffne, inventoryValue, totalPhotos, hasVideo, hasVirtualTour, metadata (page title, description, canonical URL and Open Graph tags) and isVaultTeaser (always false). Fields the listing page does not show are null. Contact-email enrichment and payment-processor detection are not offered. These additions do not change what is charged.
Output Example
Sample shape: values are illustrative placeholders, not from a live listing.
{"id": "us:sample-coffee-shop","url": "https://us.businessesforsale.com/us/sample-coffee-shop.aspx","title": "Sample Coffee Shop For Sale","listingClass": "business","labels": ["New", "High Value"],"searchCountry": "us","locationText": "Sample City, Sample State, US","locationDetail": "Sample City ST 00000","country": "US","region": "Sample State","locality": "Sample City","askingPriceDisplay": "$500,000","askingPriceValue": 500000,"priceCurrency": "USD","businessFunction": "Sell","revenueDisplay": "$300,000","revenueValue": 300000,"cashFlowDisplay": "$120,000","cashFlowValue": 120000,"tenure": "Lease","sizeSqFt": "1,200","tradingHours": "7 a.m. to 6 p.m.","employees": "5","yearsEstablished": "2015","description": "Full listing description text appears here when Fetch listing details is on.","categories": [{ "name": "Cafes & Coffee Shops", "url": "https://us.businessesforsale.com/us/search/cafes-coffee-shops" }],"financials": { "Asking Price": "$500,000", "Revenue": "$300,000", "Cash Flow": "$120,000" },"details": { "Location": "Sample City ST 00000", "Size in square feet": "1,200", "Trading hours": "7 a.m. to 6 p.m." },"thumbnail": "https://www.businessesforsale.com/uploadsstatic/00000000mediumthumbnail.jpg","imageCount": 4,"images": ["https://us.businessesforsale.com/uploads/00000000.jpg"],"scrapedAt": "2026-01-01T00:00:00.000Z"}
Plan Requirement
The site's firewall answers plain HTTP clients with a block page (HTTP 403), whichever proxy type they use. Measured: datacenter, automatic and residential connections all got the same 403 from a plain request, so the connection type alone does not unlock the site. The actor therefore opens each session the way a browser would and reuses the cleared session for the page requests; the Connection field (Apify Proxy, RESIDENTIAL group, country US by default) is the starting connection, and the actor retries with a fresh connection when a request is refused. You can change the starting country, or paste your own proxy URLs, in the Proxy field. Apify Proxy needs a plan that includes it; on a plan without proxy access the actor tries a backup connection, and if that also cannot load the pages the run fails with a message explaining the plan requirement. When a search is blocked partway through, the run ends normally with the listings collected so far and a warning in the log ("Page N: blocked or empty, stopping this search. Re-run for a fresh connection."), so check the log and re-run if that happens.
FAQ
How much does it cost?
Each listing returned is billed once as the apify-default-dataset-item event (one per pushed row). The detail-enrichment event is billed once per listing returned with its detail page successfully read, in addition (turn "Fetch listing details" off to skip it for search results; a pasted individual listing URL always includes the detail surcharge). The Pricing tab shows the current rates. Use Max listings and Max pages to cap the cost of any run.
Is it legal to scrape BusinessesForSale.com?
This actor collects only publicly available listing data. You are responsible for how you use it: follow BusinessesForSale.com's terms and the laws that apply to you, and get legal advice if you plan commercial redistribution. Listing facts like price and location are generally not protected, but photos and descriptions may be subject to third-party rights.
Can I get only new or changed listings on a schedule?
Yes. Schedule the actor from the Schedules tab and turn on Incremental mode. Each run then returns NEW and UPDATED listings; UNCHANGED ones are suppressed (and not billed) unless you turn "Emit unchanged" on. EXPIRED and REAPPEARED are rarer: EXPIRED only appears after a run that scans the whole tracked search with no page/listing cap and no Resume in play, and only with "Emit expired" on; REAPPEARED only appears for a listing that was previously marked EXPIRED this way and is later found again.
What's the difference between Resume and Incremental mode?
Resume (resumeFromRunId) continues ONE specific interrupted run from a pasted run or dataset ID, so a large walk that stopped partway can pick up where it left off. Incremental mode is for running the SAME search again and again, typically on a schedule, to get only what changed since the last run. They solve different problems and are not meant to be combined: starting Incremental mode with resumeFromRunId set while that state key already has saved state fails the run with a clear message instead of mixing the two.
Why did my run fail instead of returning an empty dataset?
The run fails with a clear message only when there's no usable connection to start with, when every pasted individual listing URL comes back unreadable, when a pasted resumeFromRunId can't be read, or when Incremental mode is combined with Resume on a search that already has saved state. A search that gets blocked partway through ends normally instead, with zero or partial listings and warnings in the run log, so check the log and re-run for a fresh connection if that happens.
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.
๐ Want more leads data?
Pair this actor with these related scrapers from the same team:
| ๐ BizBuySell Scraper Extract business-for-sale and franchise listings from bizbuysell.com with financials... | ๐ BizQuest Scraper Extract business-for-sale, franchise, and asset listings from BizQuest. Search by... |
| ๐ Flippa Scraper Scrape Flippa listings with full enrichment, including revenue, profit, valuation... | ๐ Crexi Scraper Scrape crexi.com commercial listings: asking price, cap rate, square footage, units, year... |
| ๐ Google Maps Scraper Extract business data from Google Maps at scale. Get names, addresses, phone numbers... | ๐ Houzz Pro Scraper Pull Houzz pro listings across global sites. Search by category and location, keyword, or... |
๐ Browse all abotapi scrapers
๐ฌ Support
- ๐ Found a bug or a missing field? Open a ticket on the Issues tab. 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.