# MLS Data API - RESO Web API Listings, Agents, Offices (`nabeelbaghoor/reso-mls-data-api`) Actor

Query MLS listing data through a RESO certified Web API: properties with price, status, beds, baths, area and days on market, agents and brokerages, open houses and rooms, plus the lookup vocabulary, the MLS attribution text you must display, location autocomplete and boundaries.

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

## Pricing

from $8.00 / 1,000 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 Data API - RESO Web API Listings, Agents, Offices

Query live MLS data through a RESO certified Web API, in the RESO standard field vocabulary: listings with price, status, beds, baths, area, days on market and photos; the agents and brokerages behind them; open houses and room level detail; the lookup vocabulary your feed actually publishes; and the MLS attribution text a public-facing site is required to display.

### What this actor does

- **Listings in the interchange standard, not a private dialect.** `ListingKey`, `StandardStatus`, `BedroomsTotal`, `ListPrice`, `DaysOnMarket` - the field names are the RESO Data Dictionary, which means rows from this actor join directly against any other certified feed, any RESO-aware CRM and any IDX tool. Nothing is renamed to look friendlier.
- **Full OData querying.** Pass a `$filter` straight through - `StandardStatus eq 'Active' and ListPrice ge 500000`, `City eq 'San Diego' and BedroomsTotal ge 3` - plus `$select`, `$orderby`, `$expand` and `$top`. Paging then follows the provider's own continuation link to the end of the result.
- **Incremental sync that does not skip records.** Filter and order on the feed's own modification timestamp rather than the MLS's, run the actor on a schedule, and every changed listing since your last run comes back. Both timestamps land on every row so you can see the difference.
- **Agents and brokerages.** Member and Office resources with names, MLS identifiers, status, and contact fields where the originating MLS permits them. Expand `Media` for agent photos.
- **Open houses and room detail.** Scheduled open houses with dates and start and end times, and room level records with types and dimensions where the MLS publishes them.
- **Find out what your feed supports before you query it.** The Field and Lookup metadata resources list exactly which fields your feed publishes and which values each one accepts. That is the difference between writing a filter that works and guessing at a status spelling that this particular MLS does not use.
- **The MLS disclaimer, as its own row.** Most MLS agreements require the attribution text and logo to be shown alongside any listing displayed to the public. It is retrievable here as a first class record rather than buried in a log line.
- **Location autocomplete and boundaries.** Prefix suggestions across cities, counties, states, ZIP codes, subdivisions, MLS areas, street names and addresses, scoped to the feeds your credential can see. Plus neighbourhood, ZIP code and school district boundaries, with the polygon geometry optional.
- **Closed listing statistics.** Average closed price and closed listing count for one agent or one office, over a window you choose or the rolling last twelve months.
- **Pay per result.** You are charged for rows carrying a real record. Filters that match nothing are free.

### Bring your own key

This actor calls the provider with **your own client ID and client secret**, issued in the provider's dashboard. Paste both into the input, or set the secret as the `DATA_API_KEY` secret. Nothing runs on anyone else's credentials, and a run without them ends cleanly with an explanation rather than failing.

Access to MLS data is licensed by each MLS, so what your credential can see, and which fields come back populated, is set by your agreements rather than by this actor.

### A refused credential is never retried

This is the most important operational detail on this API. The provider states that **repeated authentication attempts with a wrong client ID or secret get the caller blocked for five minutes**, and that persisting can lead to permanent suspension. A normal retry loop around the token endpoint would turn a typo into an outage.

So this actor mints a token once, reuses it for its stated lifetime, refreshes it a minute early, and **stops immediately** on a refused credential rather than trying again. Only network faults are retried.

The same reasoning applies to HTTP 403. A key here can be restricted by allowed origin and allowed IP, and a request that breaks the restriction **blocks that token for a full hour** rather than merely failing. Retrying or re-minting would make that worse, so a 403 ends the run and says what to go and check.

### Two modification timestamps, and only one is right for syncing

Every record carries `ModificationTimestamp`, which is when the originating MLS last changed the record, and `RFModificationTimestamp`, which is when the distributor last changed it in the feed.

They are not the same, and the difference matters: a record can reach the feed **later** than the MLS changed it. A sync that pages on the MLS timestamp steps straight over those records and never notices. The provider's own guidance names the feed timestamp as the replication field, so this actor writes both onto every row and the incremental filter examples use the feed one.

### Expanded photos become a count, not a blob

Ask for `$expand=Media` and a listing can come back with forty nested media objects. Putting forty objects in one spreadsheet cell helps nobody, so the expansion is reduced to a count plus the first media URL and key. The listing stays one readable row and the photo set stays retrievable.

### Example output

```json
{
  "recordType": "property",
  "requested": "StandardStatus eq 'Active' and City eq 'San Diego'",
  "resource": "Property",
  "found": true,
  "recordKey": "260017539",
  "originatingSystemName": "SDMLS",
  "modificationTimestamp": "2026-09-12T18:04:11Z",
  "feedModificationTimestamp": "2026-09-12T18:39:52Z",
  "ListingId": "260017539",
  "StandardStatus": "Active",
  "MlsStatus": "ACTIVE",
  "PropertyType": "Residential",
  "PropertySubType": "Single Family Residence",
  "ListPrice": 1295000,
  "OriginalListPrice": 1349000,
  "UnparsedAddress": "4821 Mount Ashmun Dr, San Diego, CA 92111",
  "City": "San Diego",
  "StateOrProvince": "CA",
  "PostalCode": "92111",
  "CountyOrParish": "San Diego",
  "BedroomsTotal": 4,
  "BathroomsTotalInteger": 3,
  "LivingArea": 2184,
  "LotSizeArea": 7405,
  "YearBuilt": 1964,
  "DaysOnMarket": 21,
  "ListAgentMlsId": "671898",
  "ListOfficeName": "Pacific Coast Realty",
  "MediaCount": 38,
  "MediaFirstUrl": "https://media.example-mls.com/260017539/1.jpg"
}
```

### FAQ

#### What is the RESO Web API?

The RESO Web API is the Real Estate Standards Organization's standard for MLS data distribution: an OData interface over a standard field vocabulary called the Data Dictionary. A certified feed means `ListingKey`, `StandardStatus` and `BedroomsTotal` mean the same thing there as in any other certified feed, so integrations built against one feed work against the next. This actor queries a RESO certified Web API and returns records in that standard spelling.

#### What MLS data can I get from this actor?

Listings with price, status, property type, address, beds, baths, living and lot area, year built, days on market, coordinates, listing agent and office, public remarks and photo counts. Also agents and brokerages, scheduled open houses, room level detail, the field and lookup vocabulary your feed publishes, the MLS attribution text, location autocomplete, closed listing statistics for an agent or office, and neighbourhood, ZIP code and school district boundaries.

#### How do I set up an incremental MLS sync?

Filter on the distributor's own modification timestamp - for example `RFModificationTimestamp ge 2026-09-01T00:00:00Z` - order by the same field descending, and schedule the actor. Do not use the MLS's `ModificationTimestamp` for this: a record can arrive in the feed after the MLS changed it, and a sync keyed on the MLS timestamp will step over exactly those records. Both timestamps are on every row so you can verify the gap yourself.

#### Why does my filter return an error about an invalid field?

The API refuses fields outside the RESO standard by name, and each feed publishes only a subset of the standard. Run the actor in search mode against the **Field** resource to see exactly which fields your feed publishes, and against the **Lookup** resource to see which values each field accepts. Writing the filter against that list rather than against the general standard is what makes it work first time.

#### Do I need a licence from the MLS?

Yes. MLS data is licensed by each MLS individually, and your credential sees the feeds your agreements cover. This actor supplies no data of its own: it calls the API with the credentials you provide. Most agreements also require you to display the MLS disclaimer and attribution alongside any listing you show publicly, which is why this actor returns that text as its own retrievable record.

#### What is the difference between StandardStatus and MlsStatus?

`StandardStatus` is the listing status in the RESO normalised vocabulary - Active, Pending, Closed and so on. `MlsStatus` is the status as the originating MLS itself spells it, and every MLS spells its pipeline differently. They disagree routinely, so both are returned. Filter on `StandardStatus` for portable logic and read `MlsStatus` when you need to match what an agent sees in their own system.

#### How large a result can I pull?

The provider pages results and this actor follows its continuation links until your row cap is reached, so the practical limit is the row cap and your plan's monthly request allowance. Plans range from 1 million to 24 million requests a month at 10 to 20 requests per second. Narrowing `$select` to the fields you need is the most effective way to keep large queries inside the gateway timeout.

#### Can I get listing photos?

Yes, through `$expand=Media`. The actor returns the media count and the first media URL and key on the listing row rather than embedding every photo object in one cell. Media availability depends on MLS policy and is not published by every feed.

### Keyword map

MLS API, RESO Web API, MLS data API, RESO certified API, IDX API, real estate listings API, MLS listings data, RESO Data Dictionary, OData real estate API, MLS feed integration, property listings API, real estate agent data API, broker data API, open house data API, MLS replication, incremental MLS sync, ListingKey, StandardStatus, RFModificationTimestamp, MLS disclaimer API, real estate autocomplete API, neighborhood boundary API, school district boundary API, closed sales statistics API, real estate data integration

# Actor input Schema

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

Search queries one RESO resource with an OData filter and pages through the whole result. Record reads listings, agents or offices by key. Disclaimer returns the MLS attribution text a public site has to display. Autocomplete suggests cities, counties, ZIP codes and streets from a prefix. Agent statistics summarise closed listings for an agent or office. Neighbourhoods, ZIP codes and school districts return boundaries.

## `resource` (type: `string`):

Which RESO resource to query in search and record modes. Property is listings, Member is agents, Office is brokerages, OpenHouse is scheduled open houses, PropertyRooms is room level detail. Field and Lookup are the metadata resources: Field lists the fields your feed publishes and Lookup lists the values each one accepts, which is how you find out what to filter on.

## `filter` (type: `string`):

An OData $filter expression, passed to the provider verbatim. For example StandardStatus eq 'Active' and ListPrice ge 500000, or RFModificationTimestamp ge 2026-09-01T00:00:00Z for an incremental sync. In agent statistics mode this is required and must name either ListAgentMlsId or ListOfficeMlsId.

## `select` (type: `array`):

One RESO field name per line to narrow the payload, for example ListingId or ListPrice. Leave empty for every field the feed publishes. Narrowing this is the single most effective way to avoid a gateway timeout on a large query.

## `expand` (type: `array`):

One related collection per line, for example Media or OpenHouse. Expanded collections are returned as a count plus the first media link rather than as nested JSON, so a listing with forty photos stays one readable row.

## `orderBy` (type: `string`):

An OData $orderby expression, for example RFModificationTimestamp desc. Ordering on the feed timestamp is what makes an incremental sync resumable.

## `includeCount` (type: `boolean`):

Ask the provider how many records match in total. Reported in the log on the first page, so you can tell a filter that matched three records from one that matched three hundred thousand.

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

How many records to ask for per page, up to 200. The provider enforces its own ceiling and refuses an over-large page outright rather than trimming it, so this stays conservative. Paging then follows the provider's own continuation link until the row cap is reached.

## `recordKeys` (type: `array`):

One record key per line for record mode. Which key depends on the resource: ListingKey for Property, MemberKey for Member, OfficeKey for Office.

## `searchPrefixes` (type: `array`):

One prefix per line for autocomplete mode. Matching is by prefix on exact field values across city, county, state, ZIP code, subdivision, MLS area, street name and unparsed address, scoped to the feeds your credential is permitted.

## `metrics` (type: `array`):

Which closed listing metrics to return. Leave empty for both.

## `neighbourhoodNames` (type: `array`):

One neighbourhood name per line for neighbourhood mode.

## `zipcodes` (type: `array`):

One five digit ZIP code per line for ZIP code mode.

## `points` (type: `array`):

One "latitude,longitude" pair per line for school district mode, in that order. The provider's own body takes longitude first and a swapped pair returns a real district somewhere else in the country rather than an error, so the order is fixed for you here.

## `includeGeometry` (type: `boolean`):

Return the actual boundary polygon alongside the attributes. Off by default because a district boundary is thousands of coordinates and belongs in a map layer rather than in a spreadsheet cell.

## `geometrySimplifyRatio` (type: `number`):

How much to downsample a returned boundary, between 0 and 0.5. Higher is coarser and much smaller. Only used when boundary geometry is switched on.

## `geoLimit` (type: `integer`):

How many boundary records to return per request, between 1 and 100.

## `clientId` (type: `string`):

Your own client ID from the provider dashboard. Note that a wrong client ID or secret gets the caller blocked for five minutes, so this actor never retries a refused credential.

## `clientSecret` (type: `string`):

Your own client secret from the provider dashboard. Stored encrypted and never written into the dataset. Can also be supplied as the DATA\_API\_KEY environment secret.

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

Stop after this many rows. Counts every row: listings, agents, offices, suggestions and boundaries alike.

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

How fast to call the data endpoints. Plans allow between 10 and 20 requests per second, so 300 per minute sits inside the slowest of them. Lower it if you see rate limiting.

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

Overrides the host the API is called on. Leave empty unless the provider has given you a different one.

## Actor input object example

```json
{
  "mode": "search",
  "resource": "Property",
  "includeCount": false,
  "pageSize": 200,
  "includeGeometry": false,
  "geometrySimplifyRatio": 0.1,
  "geoLimit": 10,
  "maxResults": 100,
  "requestsPerMinute": 300
}
```

# Actor output Schema

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

One row per record, alongside the filter, key or prefix 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/reso-mls-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/reso-mls-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/reso-mls-data-api --silent --output-dataset

```

## MCP server setup

```json
{
    "mcpServers": {
        "apify": {
            "type": "http",
            "url": "https://mcp.apify.com/?tools=fetch-actor-details,nabeelbaghoor/reso-mls-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/duuRRA9dWmBD0GgqE/builds/DHVKcoqsZGeeLv3eN/openapi.json
