# MLS Listings API - Sold Prices, Comps, Agent Contacts (`nabeelbaghoor/mls-listings-data-api`) Actor

Search MLS listings across the US and Canada with sold prices and status history, pull the expanded record with listing agents, comparables and room detail, group listings by building, and compute market statistics over any filter set.

- **URL**: https://apify.com/nabeelbaghoor/mls-listings-data-api.md
- **Developed by:** [Nabeel Hassan](https://apify.com/nabeelbaghoor) (community)
- **Categories:** Real estate, Business, Developer tools
- **Stats:** 2 total users, 1 monthly users, 100.0% runs succeeded, 0 bookmarks
- **User rating**: No ratings yet

## Pricing

from $12.00 / 1,000 mls listing returneds

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/actors/running/actors-in-store.md#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

## MLS Listings API - Sold Prices, Comps, Agent Contacts

Search MLS listings across the US and Canada, pull the expanded record with the listing agents, the full status history and the comparables, and compute market statistics over any filter set you can express.

### What this actor does

- **Search live and off-market listings.** Over a hundred real filters: city, area, neighbourhood, ZIP, price, sold price, bedrooms, bathrooms, square footage, lot dimensions, year built, days on market, taxes, maintenance fee, basement, heating, garage, pool, water source, sewer, amenities, exterior construction, and the board's status and last-status codes.
- **Sold prices and status history.** The expanded record for one MLS number carries the complete MLS history: every previous listing of that property with its own number, status, list price, sold price and dates. That is what separates one property relisted three times at falling prices from three separate listings.
- **Listing agent contacts.** Where the board publishes them, each agent's name, position, email, phone numbers, website, brokerage and office ID.
- **Comparables and room detail.** The provider's own comparables with distance and prices, plus the room by room breakdown with each room's level, dimensions and features, and the open house schedule.
- **Buildings, not just listings.** Building search groups every current and historical listing at one address under one record with the unit size range and the unit count, which is what a condominium directory or a pre-construction study needs.
- **Market statistics.** Averages, medians, minimums, maximums, sums, standard deviations and counts of list price, sold price, price per square foot, days on market, taxes and maintenance fees, grouped by day, month, quarter or a trailing window, computed over whatever your filters select.
- **Agents, offices and locations.** The reference sides of the MLS, for building pickers and directories.
- **Pay per result.** You are charged for rows that carry a real record. MLS numbers your boards do not carry are free.

### Bring your own key

This actor calls the provider with **your own API key**. Paste it into the API key field, or set it as the `DATA_API_KEY` secret. Nothing runs on anyone else's credentials, and a run without a key ends cleanly with an explanation rather than failing.

Your key reaches the MLS boards your own agreement covers. An MLS number on a board outside it comes back as not found rather than as an error, so a run over a mixed list tells you which ones your key can see.

### Respect the display permissions

Every listing row carries the three permission fields the MLS sets per listing: `displayPublic`, `displayAddressOnInternet` and `displayInternetEntireListing`. They travel on the row rather than being dropped because ignoring them is the one thing that reliably gets an MLS data licence pulled. If you are publishing this data anywhere public, filter on them first.

### Example output

```json
{
  "recordType": "listing",
  "requested": "city in Toronto",
  "found": true,
  "mlsNumber": "W10440893",
  "status": "A",
  "lastStatus": "New",
  "class": "ResidentialProperty",
  "type": "Sale",
  "listPrice": 1249000,
  "originalPrice": 1299000,
  "soldPrice": null,
  "listDate": "2026-08-19T21:25:27.868Z",
  "daysOnMarket": 24,
  "streetNumber": "623",
  "streetName": "Prosser",
  "streetSuffix": "Circ",
  "city": "Milton",
  "neighborhood": "Harrison",
  "state": "Ontario",
  "latitude": 43.493111,
  "longitude": -79.8736743,
  "propertyType": "Townhouse",
  "style": "3-Storey",
  "bedrooms": 3,
  "bedroomsPlus": 1,
  "bedroomsTotal": 4,
  "bathrooms": 2,
  "sqft": "1100-1500",
  "yearBuilt": "6-15",
  "garageSpaces": 1,
  "parkingSpaces": 2,
  "annualTaxes": 4210,
  "brokerageName": "Example Realty Inc.",
  "displayPublic": "Y"
}
```

### FAQ

#### Why are there three bedroom columns?

Because local listings quote "3+1", meaning three bedrooms plus a converted basement room, and the two numbers are recorded separately. Dropping the plus count understates the property and silently folding it in overstates it, so the output carries `bedrooms`, `bedroomsPlus` and the computed `bedroomsTotal` and lets you choose. The filters follow the same split.

#### Why is square footage text rather than a number?

Because many boards publish a band such as "1100-1500" rather than a figure, and coercing that to a number produces NaN. The field is kept exactly as the board published it. The `minSqft` and `maxSqft` filters still work, because the provider handles the band comparison on its side.

#### How do I get sold prices?

Filter on a sold status - the board's last status code such as `Sld`, or the standard status `Closed` - and set a sold date range. Unsold listings carry a sold price of 0 in the provider's data, which would read as "sold for nothing" in a report, so this actor writes null there instead and only reports a sold price when there is one.

#### How do I build a comparables set?

Two ways. Listing details mode on the subject's MLS number returns the provider's own comparables with their distances and prices. Or search directly: same neighbourhood, a sold status, a sold date within the last six months, and a bedroom and square footage band around the subject. The second gives you control over what counts as comparable; the first is faster.

#### Can I tell whether a property has been relisted?

Yes, and this is the main reason to use listing details rather than search. The expanded record's history array lists every previous listing of that same property with its own MLS number, status, list price and sold price. A property that has been withdrawn and relisted twice looks like fresh inventory in a plain search and does not here.

#### How do I run a daily delta instead of the whole market?

Filter on `minRepliersUpdatedOn` with yesterday's date. That is the provider's own ingestion timestamp rather than the board's, which makes it the reliable one for a delta: a board can restate a record without changing its own updated date, and the provider's timestamp still moves.

#### What is an address key?

The provider's identifier for a physical address, which is how it groups every listing at that address over time. It is in the `address.addressKey` field of a building record, and putting it into the listing search's address key filter returns the whole listing history for that one address.

#### How do I discover what values a filter accepts?

Use market statistics mode with the aggregates field set to the field name you are unsure about. The provider counts the distinct values it actually holds for that field within your filters, which is more reliable than guessing at a board's vocabulary - boards spell the same idea differently, and the style and property type fields are the worst for it.

#### Which boards and regions are covered?

The provider aggregates MLS boards across the US and Canada, and which ones your key reaches depends on your own agreement with them. Set a board ID filter when the same MLS number exists on more than one board you can see.

#### What does it cost?

Pricing is pay per result: 0.012 USD per listing, 0.02 USD per expanded listing with agents, history and comparables, 0.01 USD per building, 0.006 USD per market statistic, and 0.005 USD per location, agent or office, with Apify platform usage included. Rows the provider found nothing for are not charged. You also need your own account with the data provider, which bills on its own plans.

#### Does the actor fail if my key is wrong?

No. A missing key, a rejected key, an exhausted plan allowance or an empty input all end the run cleanly with a message saying what to change. Only a real outage or a bug fails the run.

### Keyword map

MLS listings API, real estate API US Canada, MLS data feed, sold prices API, RESO Web API, property comparables API, listing agent contact data, MLS status history, days on market, real estate market statistics API, condominium building directory, pre-construction data, IDX alternative, Toronto MLS data, Ontario real estate data, property listings scraper alternative, brokerage office directory, real estate agent directory API

# Actor input Schema

## `mode` (type: `string`):

Listing search pages the live and historical listing set. Listing details reads one MLS number in the expanded form that adds the agents, the full status history and the comparables. Market statistics asks the same route for aggregates. Building search groups every listing at one address. Locations, agents and offices read the reference sides of the MLS.

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

One city per line. Array filters repeat rather than join, so two cities match either of them.

## `areas` (type: `array`):

One area, also called a region, per line.

## `neighborhoods` (type: `array`):

One neighbourhood per line. More precise than a city and the usual unit for comparables.

## `states` (type: `array`):

One state or province per line.

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

One code per line.

## `mlsNumbers` (type: `array`):

One MLS number per line. In listing details and similar listings modes this is what the run reads; in search mode it is a filter.

## `addressKeys` (type: `array`):

Building details mode. One address key per line, as returned in the addressKey column of a building search.

## `boardIds` (type: `array`):

One MLS board ID per line. Keys often reach several boards, and the same MLS number can exist on more than one.

## `streetName` (type: `string`):

Listing search. Narrows to one street, which with a street number is how to find a specific address.

## `streetNumber` (type: `string`):

Listing search. Used together with a street name.

## `addressKey` (type: `string`):

Listing search. Returns every listing the provider has grouped at one address.

## `latitude` (type: `string`):

Centre of a radius search. Give a longitude and a radius with it.

## `longitude` (type: `string`):

Centre of a radius search. Give a latitude and a radius with it.

## `radiusKm` (type: `integer`):

Radius searches and similar listings mode. In the provider's own units, kilometres.

## `mapPolygon` (type: `string`):

Listing search. A polygon in the provider's map format, for searching an arbitrary drawn area rather than an administrative one.

## `mapOperator` (type: `string`):

How the map polygon combines with the other filters.

## `classes` (type: `array`):

One class per line, for example ResidentialProperty, CondoProperty or CommercialProperty.

## `types` (type: `array`):

Sale returns properties for sale; lease returns rentals.

## `propertyTypes` (type: `array`):

One property type per line, in the board's own spelling. Use the aggregates parameter to discover what a board actually uses.

## `styles` (type: `array`):

One style per line, for example Bungalow, 2-Storey or Apartment.

## `statuses` (type: `array`):

The board's short status codes. A is active and U is unavailable, which is how to search off-market history.

## `standardStatuses` (type: `array`):

The RESO standard status, which is comparable across boards where the short codes are not.

## `lastStatuses` (type: `array`):

The board's last-status code, for example New, Sld, Lsd, Ter or Exp. This is the field that says how a listing ended.

## `minPrice` (type: `integer`):

In the listing's own currency. On a lease listing this is the monthly rent.

## `maxPrice` (type: `integer`):

In the listing's own currency. On a lease listing this is the monthly rent.

## `minSoldPrice` (type: `integer`):

Only meaningful alongside a sold status, since unsold listings carry no sold price.

## `maxSoldPrice` (type: `integer`):

Only meaningful alongside a sold status.

## `minBedrooms` (type: `integer`):

The base bedroom count, not counting a converted basement room.

## `maxBedrooms` (type: `integer`):

The base bedroom count, not counting a converted basement room.

## `minBedroomsPlus` (type: `integer`):

The extra rooms in the local "3+1" notation, meaning a converted basement room.

## `maxBedroomsPlus` (type: `integer`):

The extra rooms in the local "3+1" notation.

## `maxBedroomsTotal` (type: `integer`):

Base and plus bedrooms added together.

## `minBaths` (type: `integer`):

Full bathrooms.

## `maxBaths` (type: `integer`):

Full bathrooms.

## `minBathroomsHalf` (type: `integer`):

Powder rooms, counted separately from full bathrooms.

## `maxBathroomsHalf` (type: `integer`):

Powder rooms, counted separately from full bathrooms.

## `minSqft` (type: `integer`):

Interior area. Many boards publish a band rather than a figure, so this filters on the band's edge.

## `maxSqft` (type: `integer`):

Interior area.

## `minLotSizeSqft` (type: `integer`):

In square feet.

## `maxLotSizeSqft` (type: `integer`):

In square feet.

## `minLotWidth` (type: `integer`):

In the board's own units, usually feet.

## `maxLotWidth` (type: `integer`):

In the board's own units, usually feet.

## `minLotDepth` (type: `integer`):

In the board's own units, usually feet.

## `maxLotDepth` (type: `integer`):

In the board's own units, usually feet.

## `minYearBuilt` (type: `integer`):

A four digit year. Many boards publish a band here too.

## `maxYearBuilt` (type: `integer`):

A four digit year.

## `minDaysOnMarket` (type: `integer`):

A high floor finds stale stock, which is where negotiation room usually is.

## `maxDaysOnMarket` (type: `integer`):

A low ceiling finds fresh listings.

## `minParkingSpaces` (type: `integer`):

Total spaces including the garage.

## `maxParkingSpaces` (type: `integer`):

Total spaces including the garage.

## `minGarageSpaces` (type: `integer`):

Garage spaces only.

## `minKitchens` (type: `integer`):

More than one kitchen usually means a legal second suite.

## `maxKitchens` (type: `integer`):

More than one kitchen usually means a legal second suite.

## `minStories` (type: `integer`):

Storeys in the building.

## `maxStories` (type: `integer`):

Storeys in the building.

## `maxMaintenanceFee` (type: `integer`):

Condominium listings. The monthly common element fee.

## `minTaxes` (type: `integer`):

The annual property tax amount on file.

## `maxTaxes` (type: `integer`):

The annual property tax amount on file.

## `minListDate` (type: `string`):

As YYYY-MM-DD.

## `maxListDate` (type: `string`):

As YYYY-MM-DD.

## `minSoldDate` (type: `string`):

As YYYY-MM-DD. This is the filter for a sold comparables study.

## `maxSoldDate` (type: `string`):

As YYYY-MM-DD.

## `minClosedDate` (type: `string`):

As YYYY-MM-DD. Closing is later than the sale date.

## `maxClosedDate` (type: `string`):

As YYYY-MM-DD.

## `minUpdatedOn` (type: `string`):

As YYYY-MM-DD. The board's own timestamp.

## `maxUpdatedOn` (type: `string`):

As YYYY-MM-DD.

## `minRepliersUpdatedOn` (type: `string`):

As YYYY-MM-DD. The provider's own ingestion timestamp, which is the right one for a reliable daily delta.

## `maxRepliersUpdatedOn` (type: `string`):

As YYYY-MM-DD.

## `minUnavailableDate` (type: `string`):

As YYYY-MM-DD.

## `maxUnavailableDate` (type: `string`):

As YYYY-MM-DD.

## `minOpenHouseDate` (type: `string`):

As YYYY-MM-DD.

## `maxOpenHouseDate` (type: `string`):

As YYYY-MM-DD.

## `basements` (type: `array`):

One basement value per line, for example Finished, Walk-Out or None.

## `heating` (type: `array`):

One heating value per line, for example Forced Air or Radiant.

## `garages` (type: `array`):

One garage value per line, for example Built-In, Attached or Detached.

## `swimmingPools` (type: `array`):

One pool value per line, for example Inground or Above Ground.

## `waterSources` (type: `array`):

One water source per line, for example Municipal or Well.

## `sewers` (type: `array`):

One sewer value per line, for example Sewer or Septic.

## `amenities` (type: `array`):

One amenity per line, for example Gym or Concierge.

## `exteriorConstruction` (type: `array`):

One material per line, for example Brick or Stucco.

## `streetSuffixes` (type: `array`):

One suffix per line, for example St, Ave or Circ.

## `unitNumbers` (type: `array`):

One unit number per line.

## `lockers` (type: `array`):

One locker value per line. Condominium listings.

## `driveways` (type: `array`):

One driveway value per line, for example Private or Mutual.

## `balconies` (type: `array`):

One balcony value per line, for example Open or Enclosed.

## `dens` (type: `array`):

Whether the listing has a den. Boards publish this as Y or N.

## `agents` (type: `array`):

One agent name or ID per line, which is how to pull one agent's whole book.

## `keywords` (type: `string`):

Full text search across the listing. Also the keyword filter in agents and offices modes.

## `searchFields` (type: `string`):

Which fields the keyword search looks at, as a comma separated list. Narrowing it makes a keyword search far more precise.

## `hasImages` (type: `boolean`):

Leave this alone to ignore it. Ticked keeps only listings with photographs.

## `hasAgents` (type: `boolean`):

Leave this alone to ignore it. Ticked keeps only listings the board publishes agent contact details for.

## `filterOperator` (type: `string`):

Whether a listing must match every filter or any of them.

## `sortBy` (type: `string`):

Listing search and similar listings modes.

## `fields` (type: `string`):

A comma separated list of fields, which cuts the response down when you only need a few. Leave empty for the full record.

## `includeLocations` (type: `boolean`):

Listing details mode. Adds the resolved location objects for the listing.

## `listPriceRange` (type: `integer`):

Similar listings mode. How far either side of the subject's list price a similar listing may be.

## `statistics` (type: `array`):

Market statistics mode. The provider's own statistic names: a prefix of avg, med, min, max, sum, sd or cnt, then the measure. Add a grp prefix to group the result by period.

## `aggregates` (type: `string`):

Market statistics mode. A comma separated list of fields to count distinct values for, which is how to discover what a board actually publishes in a field.

## `groupAggregates` (type: `boolean`):

Market statistics mode. Computes each statistic once per aggregate value rather than once overall.

## `buildingNames` (type: `array`):

Building search. One building name per line.

## `streetNames` (type: `array`):

Building search. One street name per line.

## `buildingDealType` (type: `string`):

Building search. Whether to weigh the building by its sale or its lease listings.

## `locationSearches` (type: `array`):

Locations mode. One partial place name per line switches the mode to autocomplete, which is what a location picker needs. Leave empty to list locations by the city, area and neighbourhood filters instead.

## `locationClassifications` (type: `array`):

Locations mode. One classification per line, which is how to ask for only schools, or only neighbourhoods.

## `withBoundaries` (type: `boolean`):

Locations mode. Adds each location's polygon, which is what you need to draw it on a map.

## `agentName` (type: `string`):

Agents mode. A partial agent name to search for.

## `agentId` (type: `string`):

Agents mode. One agent ID to read.

## `brokerage` (type: `string`):

Agents mode. A partial brokerage name.

## `officeId` (type: `string`):

Agents and offices modes. One office ID.

## `hasEmail` (type: `boolean`):

Agents mode. Ticked keeps only agents the board publishes an email address for.

## `maxResults` (type: `integer`):

The most rows this run will save and charge for. This is the spending control on both sides.

## `pageSize` (type: `integer`):

How many rows to ask for at a time, up to the provider's ceiling of 100.

## `requestsPerMinute` (type: `integer`):

How fast to call the provider. Lower this if your plan is metered tightly.

## `baseUrl` (type: `string`):

Only set this if the provider has given you a different host. Leave empty to use the documented one.

## `apiKey` (type: `string`):

Your own API key for the MLS listings provider. This actor is bring your own key: it never uses anyone else's credentials, and the key is stored as a secret.

## Actor input object example

```json
{
  "mode": "listingSearch",
  "filterOperator": "AND",
  "sortBy": "repliersUpdatedOnDesc",
  "maxResults": 1000,
  "pageSize": 100,
  "requestsPerMinute": 300
}
```

# Actor output Schema

## `records` (type: `string`):

One row per record, alongside the search or MLS number that produced it.

# 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 = {};

// Run the Actor and wait for it to finish
const run = await client.actor("nabeelbaghoor/mls-listings-data-api").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 = {}

# Run the Actor and wait for it to finish
run = client.actor("nabeelbaghoor/mls-listings-data-api").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 '{}' |
apify call nabeelbaghoor/mls-listings-data-api --silent --output-dataset

```

## MCP server setup

```json
{
    "mcpServers": {
        "apify": {
            "type": "http",
            "url": "https://mcp.apify.com/?tools=fetch-actor-details,nabeelbaghoor/mls-listings-data-api"
        }
    }
}
```

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/rhjhHGxoCySbLHh6k/builds/Hwr0cLXhorEywQ88U/openapi.json
