# Trulia Property Scraper (US) (`scrapyx/trulia-properties-scraper`) Actor

Scrapes homes for sale, for rent and recently sold from Trulia — Zillow Group's US portal. Search any US city with price, bedroom and property-type filters; returns price, address, coordinates, beds/baths, photos, full descriptions, features, price history, taxes and Trulia's value estimate.

- **URL**: https://apify.com/scrapyx/trulia-properties-scraper.md
- **Developed by:** [Ibnu Adzim](https://apify.com/scrapyx) (community)
- **Categories:** Real estate
- **Stats:** 2 total users, 1 monthly users, 100.0% runs succeeded, 0 bookmarks
- **User rating**: No ratings yet

## Pricing

from $1.26 / 1,000 results

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 Property Scraper (US)

Scrapes homes **for sale**, **for rent** and **recently sold** from
**[Trulia](https://www.trulia.com)** — Zillow Group's US portal.

Public data only. No login, no cookies, no browser.

### The two things you need to know before using this

#### 1. A misspelled city returns the whole STATE, silently

Trulia does not reject an unknown city. `/CA/Not_A_Real_City_Xyz/` answers
**HTTP 200 carrying all 126,601 California listings** — real homes, correct
schema, completely the wrong place. Nothing in the rows themselves would
ever reveal it.

This actor checks Trulia's own `canonicalUrl` echo before paging: if the
site resolved your city to a state (or anything else), it **stops
immediately**, emits a single `SEARCH_SUMMARY` with `locationApplied:
false` and `upstreamResolvedLocation` showing what Trulia actually
searched. A typo costs you one row, not a dataset full of wrong-city
listings.

The same applies to **property type** — an unrecognised type segment
returns the unfiltered baseline rather than an error, so only the nine
verified values are accepted and anything else is refused before the run
starts.

#### 2. Each query is capped at 1,000 rows, and past the cap it repeats

Page 25 is the last genuine page. **Page 26 re-serves page 1 verbatim** —
not an empty list, not a 404. A crawler that stops on "empty page" would
loop forever collecting duplicates.

This actor stops when a page contributes no new listing ids, and reports
`paginationClamped: true` when that happens. If upstream claims more
matches than 1,000, `resultCapReached` is set and a warning tells you to
split the area up. **Several narrow cities return far more data than one
broad one.**

### What you get

Three record types share one dataset, told apart by `recordType`.

#### `PROPERTY` — one row per listing

Search rows already carry price, full street address with coordinates,
beds, baths, floor space, lot size, tags and photos. With
**Fetch full property details** on (the default), each row also gets
`propertyDetails`: full description, every feature, price history, tax
history, schools, neighbourhood info, all photos and Trulia's own value
estimate — typically 150–600 KB per home.

#### `SEARCH_SUMMARY` — one row per location searched

Trulia's own `totalHomes`, how deep this run paged, the filters requested,
and the three honesty flags: `locationApplied`, `resultCapReached`,
`paginationClamped`.

#### `ERROR` — one row per input that could not be processed

Every input maps to at least one row, so nothing disappears silently.

### Input

| Field | What it does |
| --- | --- |
| **Market side** | `for_sale`, `for_rent` or `sold` — each uses a different Trulia path grammar, built for you |
| **Locations** | `City, ST` — the **state is required** (a bare city is ambiguous and Trulia resolves it arbitrarily) |
| **Property type** | one of nine verified values, or any |
| **Min beds / min price / max price** | verified to actually move the result count |
| **Property URLs** | scrape specific listings directly, skipping search |
| **Max properties per location** | `0` = unlimited, meaning up to Trulia's own 1,000 cap |

#### Example

```json
{
  "searchType": "for_sale",
  "locations": ["Austin, TX", "Boise, ID"],
  "propertyType": "SINGLE-FAMILY_HOME",
  "minBeds": 3,
  "minPrice": 300000,
  "maxPrice": 600000,
  "maxItems": 200,
  "includePropertyDetails": true
}
```

### Notes on reliability

- **Active WAF: PerimeterX** (`_pxAppId: PXYO6YjwLb`, behind CloudFront). It
  is a strict TLS allowlist, not a solvable challenge — a non-allowlisted
  fingerprint gets a flat 403 every time, forever.
- **The allowlist differs per surface.** Of 17 profiles tried, `chrome120`
  and `chrome123` pass search and `/home/` but are **deterministically
  blocked on `/building/`** (where rental communities live). This actor's
  pool is the intersection that passes all three surfaces:
  `chrome99_android`, `safari17_0`, `safari17_2_ios`, `safari18_0`.
- **Residential proxy pinned to the US** is the default. Trulia is a
  US-only portal; a geo-mismatched exit IP is exactly the signal such a site
  scores against.
- **De-listed homes answer HTTP 410 Gone**, not 404. That is handled as a
  normal outcome (`_detailError`), so one home going off-market mid-crawl
  never costs the rest of the run.
- Retries rotate **both** the exit IP and the fingerprint before giving up.

### Known limits

- 1,000 rows per query, whatever the match count says. Split by city,
  price band or property type to go deeper.
- `sold` listings frequently have no public sale price
  (`HOME_EmptySoldPrice`) — that is Trulia's data, not a scrape failure.
- Rental communities (`/building/`) carry no `priceHistory` or `taxes`; they
  are buildings with a rent range, not individual homes.

# Actor input Schema

## `searchType` (type: `string`):

For sale, for rent, or recently sold. Each uses a different Trulia path grammar — the actor builds the right one for you.

## `locations` (type: `array`):

One search per entry, each with its own SEARCH\_SUMMARY row. Write them as 'San Francisco, CA', 'Austin, TX', 'New York, NY' — the state is REQUIRED because a bare city name is ambiguous across states.

If Trulia does not recognise a city it does NOT return an error: it silently returns that whole STATE's listings instead (California alone is 126,601 homes). The actor detects this via Trulia's own canonicalUrl echo, refuses to page it, and reports locationApplied: false — so a typo costs you one row, not a dataset full of wrong-city listings.

Because Trulia caps each query at 1,000 rows, several narrow cities return far more data than one broad one.

## `propertyType` (type: `string`):

Only these values are real Trulia type segments. Anything else is NOT rejected upstream — Trulia answers HTTP 200 with the UNFILTERED baseline, so a typo would look like a successful filtered search. Values outside this list are refused before the run starts.

## `minBeds` (type: `integer`):

Leave empty or 0 for any. Trulia is honest at the edges here — an impossible value returns a clean zero-match search rather than widening.

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

Leave empty or 0 for no minimum. For rentals this is monthly rent.

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

Leave empty or 0 for no maximum. For rentals this is monthly rent.

## `propertyUrls` (type: `array`):

Scrape specific listings directly, skipping search. Accepts a full Trulia home URL (https://www.trulia.com/home/123-main-st-austin-tx-78701-12345678). A bare numeric id does NOT work — Trulia's detail URL is an address slug. Can be combined with a location search in the same run.

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

Stop paginating a location after this many properties. Set to 0 for unlimited — note Trulia itself stops at 1,000 rows per query (25 pages × 40) however many matches it reports, and past that it silently re-serves page 1. Split into smaller cities to reach the rest.

## `includePropertyDetails` (type: `boolean`):

Fetch the complete listing (full description, all features, price and tax history, schools, neighbourhood info, every photo, Trulia's own value estimate) via one extra request per property. Turn this off for a fast listing-only crawl — search rows already carry price, address, coordinates, beds, baths, floor space and tags.

## `maxConcurrency` (type: `integer`):

Upper bound on requests in flight at once, across location pagination and detail fetches.

## `proxyConfiguration` (type: `object`):

Trulia runs PerimeterX and is a US-only portal, so this defaults to Residential pinned to the US. Do not remove the country pin without re-verifying — a geo-mismatched exit IP is exactly the signal a US-only real-estate site scores against.

## Actor input object example

```json
{
  "searchType": "for_sale",
  "locations": [
    "Austin, TX"
  ],
  "propertyType": "",
  "minBeds": 0,
  "minPrice": 0,
  "maxPrice": 0,
  "propertyUrls": [],
  "maxItems": 100,
  "includePropertyDetails": true,
  "maxConcurrency": 4,
  "proxyConfiguration": {
    "useApifyProxy": true,
    "apifyProxyGroups": [
      "RESIDENTIAL"
    ],
    "apifyProxyCountry": "US"
  }
}
```

# Actor output Schema

## `items` (type: `string`):

One row per scraped record. See the dataset's default view for field definitions.

# 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 = {
    "locations": [
        "Austin, TX"
    ]
};

// Run the Actor and wait for it to finish
const run = await client.actor("scrapyx/trulia-properties-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 = { "locations": ["Austin, TX"] }

# Run the Actor and wait for it to finish
run = client.actor("scrapyx/trulia-properties-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 '{
  "locations": [
    "Austin, TX"
  ]
}' |
apify call scrapyx/trulia-properties-scraper --silent --output-dataset

```

## MCP server setup

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