# Trulia Scraper - Property Listings, Prices, Agents & History (`abotapi/trulia-scraper`) Actor

Scrape Trulia.com property listings by location, filters, or URL. Extract prices, beds, baths, square footage, addresses, GPS coordinates, photos, agents, and brokers, plus detailed price and tax history, property features, and open house information.

- **URL**: https://apify.com/abotapi/trulia-scraper.md
- **Developed by:** [Abot API](https://apify.com/abotapi) (community)
- **Categories:** Real estate, Developer tools, Automation
- **Stats:** 1 total users, 0 monthly users, 100.0% runs succeeded, 0 bookmarks
- **User rating**: No ratings yet

## Pricing

from $1.00 / 1,000 listing scrapeds

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?

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

## Trulia Scraper

Extract property listings from Trulia across the United States: for sale, for rent, and recently sold. Search by location with optional keyword and sort, or paste Trulia URLs directly. Every listing comes back as one clean record with price, beds, baths, square footage, full address, GPS coordinates, photos, and the listing agent and broker. Turn on detail mode to also pull price history, tax history, the full feature list, open houses, agent contact, and affordability data from each property page.

### Why this scraper

- Rich output out of the box: search results already include price, beds, baths, size, full address, neighborhood, GPS, photo gallery, listing agent name and broker, status flags, and tracking ids. No detail fetch needed for the common fields.
- Optional deep detail: one toggle adds price history, tax history, features, open houses, listing agent and broker contact, local protections, estimated rent, and more.
- Three listing channels: for sale, for rent, and recently sold.
- Real keyword filter: narrow a location to listings that mention a term like pool or waterfront.
- Two input modes: build searches from simple fields, or paste search and property URLs you already have. Multiple URLs supported with automatic pagination.
- Lean and predictable: detail mode is off by default, so a standard run is fast and low cost. One result cap (Max items) keeps every run predictable.
- Resume & recurring updates: continue one interrupted crawl with `resumeFromRunId`, or turn on `incrementalMode` to schedule this same search daily/weekly and get back only what's NEW, UPDATED, or REAPPEARED.
- Optional export: pipe results straight into Notion, Linear, Airtable, or Apify via MCP connectors.

### Data you get

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

| Field | Example |
| --- | --- |
| id | 00000001 |
| url | https://www.trulia.com/home/sample-address-ny-10000-00000001 |
| listingType | FOR\_SALE |
| propertyType | SINGLE\_FAMILY\_HOME |
| price | 750000 |
| formattedPrice | $750,000 |
| streetAddress | 123 Sample Street |
| city | Sampletown |
| state | NY |
| zipCode | 10000 |
| neighborhood | Sample Heights |
| latitude | 40.0000 |
| longitude | -73.0000 |
| bedrooms | 3 |
| bathrooms | 2 |
| floorSpace | 1,800 sqft |
| listingAgentName | Agent Name |
| brokerName | Sample Realty |
| heroImage | https://www.trulia.com/pictures/thumbs\_4/zillowstatic/fp/00000000-full.jpg |
| photos | \[ "https://www.trulia.com/pictures/thumbs/_6/zillowstatic/fp/00000000-full.jpg" ] |
| tags | \[ "NEW", "OPEN SUN" ] |
| scrapedAt | 2026-01-01T00:00:00.000Z |

With Fetch property details enabled, each record also includes: priceHistory, taxes, features, openHouses, providerDetails (agent and broker contact), localProtections, affordability, estimatedRent, totalPhotoCount, plus the full homeDetails object.

**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 listing was first observed by this monitoring campaign |
| `lastSeenAt` | When this listing was last observed |

An `EXPIRED` row is a tombstone (the same fields a normal record carries, minus a couple of per-fetch MLS refresh-stamp leaves), not a re-fetch: it reflects the last snapshot this actor collected before the listing stopped appearing.

### How to use

Basic search:

```json
{
  "mode": "search",
  "searchLocation": "Austin,TX",
  "searchListingType": "FOR_SALE",
  "maxItems": 50
}
```

Search with keyword and sort:

```json
{
  "mode": "search",
  "searchLocation": "Miami,FL",
  "searchListingType": "FOR_SALE",
  "searchKeyword": "pool",
  "searchSort": "PRICE_LOW_TO_HIGH",
  "maxItems": 100
}
```

For rent, with full detail enrichment:

```json
{
  "mode": "search",
  "searchLocation": "Seattle,WA",
  "searchListingType": "FOR_RENT",
  "fetchDetails": true,
  "maxItems": 40
}
```

URL mode (search URLs are paginated, property URLs return one record each):

```json
{
  "mode": "url",
  "startUrls": [
    "https://www.trulia.com/sold/Denver,CO/",
    "https://www.trulia.com/home/sample-address-co-80000-00000002"
  ],
  "maxItems": 60
}
```

### Input parameters

| Parameter | Type | Default | Description |
| --- | --- | --- | --- |
| mode | string | search | search (build URLs from filters) or url (paste links) |
| searchLocation | string | New\_York,NY | Search mode. US location as City,STATE |
| searchListingType | string | FOR\_SALE | FOR\_SALE, FOR\_RENT, or SOLD |
| searchKeyword | string | (empty) | Search mode. Narrows to listings mentioning this term |
| searchSort | string | RECOMMENDED | RECOMMENDED, NEWEST, PRICE\_LOW\_TO\_HIGH, PRICE\_HIGH\_TO\_LOW |
| startUrls | array | (empty) | URL mode. Trulia search or property URLs |
| fetchDetails | boolean | false | Add price history, taxes, features, open houses, agent contact |
| maxItems | integer | 20 | Run cap on listings. 0 for unlimited |
| maxPages | integer | none (unlimited) | Optional safety cap on pages per search (40 listings per page). Leave empty to walk every result page; the run stops at Max items |
| resumeFromRunId | string | none | Continue one specific previous run/dataset: listings already collected there are skipped, so this run only returns new ones. For recurring daily monitoring of the same search, use `incrementalMode` instead -- see "Resume & recurring updates" 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 | none | Optional name for a monitoring campaign, so its incremental state stays stable or is deliberately shared. Auto-derived from location/URLs, listing type, keyword, sort, and fetchDetails when left empty |
| emitUnchanged | boolean | false | Incremental mode only. Also return listings unchanged since the last run, marked `UNCHANGED`. Adds and bills extra rows you already have |
| emitExpired | boolean | false | Incremental mode only. Also return listings 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 | Residential US is required; datacenter is refused |

### Resume & recurring updates

There are two different things here -- pick the one that matches what you're doing:

| Need | Use |
| --- | --- |
| A crawl 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 crawl: paste a run ID or dataset ID and this run skips listings already collected there, returning only the remaining new listings. 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` listings by default -- duplicates and unchanged listings are suppressed (and not charged, even in detail mode -- see below). Turn on `emitUnchanged` or `emitExpired` only when you also want those rows returned (and billed for). State is isolated per location/URL, listing type, keyword, sort, and fetchDetails setup automatically; set `stateKey` to name or deliberately share a monitoring campaign. It reuses the same listing identity (`id`, falling back to `url`) that `resumeFromRunId` dedups on, so the two features never disagree about what "the same listing" means.

With `fetchDetails` on, a listing's property page is still fetched every run (its fingerprint needs the detail data), but the `detail-enrichment` charge only applies to listings actually returned -- a suppressed `UNCHANGED` listing is neither pushed nor billed.

Scheduled-run example -- same search, run daily:

Day 1 (first run ever for this search):

```json
{ "mode": "search", "searchLocation": "Austin,TX", "searchListingType": "FOR_SALE", "incrementalMode": true }
```

-> every listing comes back with `"changeType": "NEW"`.

Day 2 (same input, run again the next day):

```json
{ "mode": "search", "searchLocation": "Austin,TX", "searchListingType": "FOR_SALE", "incrementalMode": true }
```

-> only listings that are new, changed price/status/agent/etc., or reappeared come back; everything else is skipped and not billed.

### Send results into your apps (MCP connectors)

Optionally pipe each result into the tools you already use. Authorize a connector under Apify, Settings, Integrations, then select it in the mcpConnectors field. For Notion, also set notionParentPageUrl. The connector receives a condensed, human-readable summary per item (address plus key fields); the complete record always stays in the Apify dataset. Leave the field empty to skip. Supported connectors: Notion, Linear, Airtable, and Apify.

### Output example

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

```json
{
  "id": "00000001",
  "url": "https://www.trulia.com/home/sample-address-ny-10000-00000001",
  "listingType": "FOR_SALE",
  "propertyType": "SINGLE_FAMILY_HOME",
  "price": 750000,
  "formattedPrice": "$750,000",
  "streetAddress": "123 Sample Street",
  "city": "Sampletown",
  "state": "NY",
  "zipCode": "10000",
  "neighborhood": "Sample Heights",
  "latitude": 40.0000,
  "longitude": -73.0000,
  "bedrooms": 3,
  "bathrooms": 2,
  "floorSpace": "1,800 sqft",
  "listingAgentName": "Agent Name",
  "brokerName": "Sample Realty",
  "heroImage": "https://www.trulia.com/pictures/thumbs_4/zillowstatic/fp/00000000-full.jpg",
  "photos": ["https://www.trulia.com/pictures/thumbs_6/zillowstatic/fp/00000000-full.jpg"],
  "tags": ["NEW"],
  "scrapedAt": "2026-01-01T00:00:00.000Z"
}
```

### Plan requirement

Trulia admits United States residential connections only, so this actor needs an Apify Residential proxy pinned to the US (the default). Datacenter exits are refused. Free Apify plans do not include residential proxy; use a Starter plan or higher (https://apify.com/pricing). The actor rotates US then CA residential exits and can fail over to a backup gateway. If a run returns zero items, check that the proxy is set to Residential US.

# Actor input Schema

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

How to start. 'Search by location' builds Trulia search URLs from the filters below. 'Direct URLs' takes Trulia search or property URLs you already have.

## `searchLocation` (type: `string`):

Search mode only. A US location as City,STATE (e.g. New\_York,NY or Austin,TX). Spaces are converted to underscores automatically.

## `searchListingType` (type: `string`):

Search mode only. For sale, for rent, or recently sold listings.

## `searchKeyword` (type: `string`):

Search mode only. Narrows results to listings whose text mentions this keyword (e.g. pool, waterfront, garage). Leave empty for no keyword filter.

## `searchSort` (type: `string`):

Search mode only. Result ordering. Note: Trulia may place a few promoted listings ahead of the chosen order.

## `startUrls` (type: `array`):

URL mode only. Trulia search URLs (paginated automatically) or property/home URLs (fetched as single detail records). Multiple URLs supported.

## `fetchDetails` (type: `boolean`):

Include full property details: price history, tax history, full feature list, open houses, listing agent and broker contact, affordability and more. Slower and uses more proxy traffic; billed per enriched listing. Leave off for fast search-result data, which already includes price, beds, baths, address, GPS, photos and agent name.

## `maxItems` (type: `integer`):

Maximum number of listings to scrape across all searches/URLs. This is the run cap. Set 0 for unlimited.

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

Optional safety cap on search pages to walk per location/URL (40 listings per page). Leave empty to walk every result page; the run stops at Max items.

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

Paste a previous run ID (or dataset ID) from this actor to continue a large crawl without returning or charging for listings already collected there. Use this after an interrupted run. For recurring daily monitoring of the same search, use Incremental mode below instead.

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

Turn this on for daily or recurring monitoring. The first run returns all matching listings as NEW. Later runs normally return only NEW, UPDATED, and REAPPEARED listings. Turn on "Emit unchanged" or "Emit expired" only when you also want those listings returned (and billed). State is kept separately per location/URL, listing type, keyword, sort, and fetchDetails setup; use State key when you want to name or deliberately share a monitoring campaign. To continue one specific interrupted run instead, use Resume from a previous run above.

## `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 location/URLs, listing type, keyword, sort, and fetchDetails settings -- different searches then never mix state with each other.

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

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

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

Off by default. Turn on to also return listings that were present in a previous run but are no longer found, marked EXPIRED, once a run has fully scanned the search (not capped, not a resume). Adds and bills extra synthetic rows, so leave it off unless you need expiry tracking.

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

Trulia admits US residential connections. Residential (US) is required; datacenter exits are refused. The actor rotates US then CA residential exits and can fail over to a backup gateway. Free Apify plans do not include residential proxy.

## `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 (address + 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",
  "searchLocation": "New_York,NY",
  "searchListingType": "FOR_SALE",
  "searchSort": "RECOMMENDED",
  "startUrls": [
    "https://www.trulia.com/for_sale/Austin,TX/"
  ],
  "fetchDetails": false,
  "maxItems": 20,
  "incrementalMode": false,
  "emitUnchanged": false,
  "emitExpired": false,
  "proxy": {
    "useApifyProxy": true,
    "apifyProxyGroups": [
      "RESIDENTIAL"
    ],
    "apifyProxyCountry": "US"
  },
  "maxNotifyListings": 50
}
```

# Actor output Schema

## `overview` (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",
    "searchLocation": "New_York,NY",
    "startUrls": [
        "https://www.trulia.com/for_sale/Austin,TX/"
    ],
    "maxItems": 20,
    "maxPages": 0,
    "incrementalMode": false,
    "emitUnchanged": false,
    "emitExpired": false,
    "proxy": {
        "useApifyProxy": true,
        "apifyProxyGroups": [
            "RESIDENTIAL"
        ],
        "apifyProxyCountry": "US"
    }
};

// Run the Actor and wait for it to finish
const run = await client.actor("abotapi/trulia-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",
    "searchLocation": "New_York,NY",
    "startUrls": ["https://www.trulia.com/for_sale/Austin,TX/"],
    "maxItems": 20,
    "maxPages": 0,
    "incrementalMode": False,
    "emitUnchanged": False,
    "emitExpired": False,
    "proxy": {
        "useApifyProxy": True,
        "apifyProxyGroups": ["RESIDENTIAL"],
        "apifyProxyCountry": "US",
    },
}

# Run the Actor and wait for it to finish
run = client.actor("abotapi/trulia-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",
  "searchLocation": "New_York,NY",
  "startUrls": [
    "https://www.trulia.com/for_sale/Austin,TX/"
  ],
  "maxItems": 20,
  "maxPages": 0,
  "incrementalMode": false,
  "emitUnchanged": false,
  "emitExpired": false,
  "proxy": {
    "useApifyProxy": true,
    "apifyProxyGroups": [
      "RESIDENTIAL"
    ],
    "apifyProxyCountry": "US"
  }
}' |
apify call abotapi/trulia-scraper --silent --output-dataset

```

## MCP server setup

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