# MHVillage Manufactured Home Investor API (`amazinglabs-dev/mhvillage-investor-api`) Actor

Search MHVillage.com manufactured and mobile home listings by state, city or ZIP, with built-in deal scoring that flags homes priced well below comparable listings in the same run.

- **URL**: https://apify.com/amazinglabs-dev/mhvillage-investor-api.md
- **Developed by:** [Tam Nguyen](https://apify.com/amazinglabs-dev) (community)
- **Stats:** 2 total users, 1 monthly users, 100.0% runs succeeded, 0 bookmarks
- **User rating**: No ratings yet

## Pricing

from $5.00 / 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.

Learn more: https://docs.apify.com/actors/running/actors-in-store.md#pay-per-event

## What's an Apify Actor?

An Actor is a serverless cloud program that runs on the Apify platform. It has two run modes.
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.

Apify vocabulary and the platform model are defined once, in the agent quickstart at https://apify.com/agents.md.

## 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.

Do not guess an integration path. Every one of them is in the agent quickstart at https://apify.com/agents.md: the Apify MCP server, Agent Skills with the Apify CLI, the JavaScript and Python clients, the REST API, and the account-free path for an agent with no human to sign in. It also carries the rule on stating cost before the first paid run.

For examples already wired to this Actor's own input schema, see the [API](#api) section below.

Each client library has reference documentation the quickstart does not restate: [JavaScript/TypeScript](https://docs.apify.com/api/client/js/docs.md) (`npm install apify-client`) and [Python](https://docs.apify.com/api/client/python/docs.md) (`pip install apify-client`).

# README

## 🏘️ MHVillage Manufactured Home Investor API

Most scrapers hand you rows. This one hands you a shortlist: manufactured and mobile home listings from **MHVillage.com** — the largest marketplace for manufactured housing in the US — pre-scored against each other on price-per-square-foot, so the underpriced listings are flagged before you've opened a single one.

Search a whole state across multiple cities or ZIPs in one run, with automatic pagination, optional full-description/year/manufacturer enrichment per listing, and a built-in fallback that shows you where the inventory actually is when you search too broadly. Pull it straight into a spreadsheet, a CRM, a deal-sourcing pipeline, or your own underwriting model via the API.

**Use cases:**

- **Deal sourcing** — scan a whole state for listings priced well below their comps
- **Market research** — pull comparable sales/asks for a submarket before pricing a unit
- **Inventory monitoring** — scheduled runs to catch new listings in target areas
- **Portfolio tracking** — watch specific cities or ZIPs for competitor pricing moves

### What data does it extract?

| Field | Description |
|---|---|
| 📍 `state`, `city` | The location this result came from |
| 🏷️ `listingType` | `for-sale` or `for-rent` |
| 🏠 `name`, `address` | Listing title and full street address |
| 💰 `price` | Listing price in USD, when it could be matched with certainty (see note below) |
| 🛏️ `bedrooms`, `bathrooms`, `sqft` | Home size |
| 📐 `pricePerSqft`, `groupMedianPricePerSqft` | This listing's $/sqft vs. the median for everything else pulled in the same run |
| 🚩 `isLikelyDeal` | `true` when priced at or below 85% of that median |
| 📝 `description`, `year`, `make` | Full listing description, model year, and manufacturer — only populated with `enrichWithDetailPage: true` |
| 🖼️ `image` | Listing photo URL |
| 🔗 `url`, `detailUrl` | Direct links to the listing |
| 🗂️ `resultType` | `listing`, or `city_directory` when a bare-state search returns city-level counts instead (see [Input](#input)) |

### Features

- **Multi-location in one run** — pass any number of `{ state, city }` / `{ state, zip }` entries; each is searched and paginated independently
- **Automatic deal scoring** — every listing's $/sqft is benchmarked against the state-level median from the same run, no comps database to maintain
- **State-level discovery** — search a bare state and get back a directory of every city with open inventory and how many listings each has, so you know where to aim before spending a run on it
- **Optional detail-page enrichment** — opt into full description, year, and manufacturer per listing at the cost of one extra page load each
- **Resilient extraction** — listings are read by data shape (structured data first, visible-text patterns as fallback), not hard-coded CSS selectors, so it degrades gracefully instead of breaking silently on minor site redesigns
- **Self-diagnosing** — any run that comes back empty automatically saves the page HTML and a screenshot to the Key-Value Store so you can see exactly what the Actor saw

### Input

```json
{
  "locations": [
    { "state": "TX", "city": "Austin" },
    { "state": "FL", "zip": "33801" }
  ],
  "listingType": "for-sale",
  "maxListingsPerLocation": 100,
  "maxPagesPerLocation": 3,
  "enrichWithDetailPage": false,
  "computeDealScore": true
}
```

| Field | Type | Default | Description |
|---|---|---|---|
| `locations` | array | *(required)* | One entry per search: `{ "state", "city" }`, `{ "state", "zip" }`, `{ "state" }` alone, or `{ "startUrl" }` with a direct MHVillage search URL for exact control. A bare `state` with no `city`/`zip` returns a city directory instead of listings — see [Features](#features). |
| `listingType` | string | `"for-sale"` | `"for-sale"` or `"for-rent"` |
| `maxListingsPerLocation` | integer | `100` | Stop collecting for a location once this many listings have been kept (1–2000) |
| `maxPagesPerLocation` | integer | `3` | Cap on result pages paged through per location, independent of `maxListingsPerLocation` (1–50) |
| `enrichWithDetailPage` | boolean | `false` | Visit each kept listing's own page for `description`, `year`, and `make`. Slower and uses more proxy bandwidth — one extra page load per kept listing, not per listing found. |
| `computeDealScore` | boolean | `true` | Compute `pricePerSqft` / `groupMedianPricePerSqft` / `isLikelyDeal` |
| `proxyConfiguration` | object | US Residential | Must resolve to a US IP — see [Proxy](#proxy) |

Each entry in `locations` also accepts `debugAlwaysCapture: true` to force-save the page HTML and a screenshot to the Key-Value Store for that location regardless of whether it returns results, useful when you're debugging a specific search that looks wrong.

### Running it

**Console** — click **Try for free**, fill in the input form, hit **Start**.

**API** (`amazinglabs-dev/mhvillage-investor-api`):

```bash
curl "https://api.apify.com/v2/acts/amazinglabs-dev~mhvillage-investor-api/run-sync-get-dataset-items?token=<YOUR_API_TOKEN>" \
  -X POST \
  -H "Content-Type: application/json" \
  -d '{
    "locations": [{ "state": "TX", "city": "Austin" }],
    "listingType": "for-sale",
    "maxListingsPerLocation": 100
  }'
```

```javascript
import { ApifyClient } from 'apify-client';

const client = new ApifyClient({ token: '<YOUR_API_TOKEN>' });

const run = await client.actor('amazinglabs-dev/mhvillage-investor-api').call({
  locations: [{ state: 'TX', city: 'Austin' }],
  listingType: 'for-sale',
  maxListingsPerLocation: 100,
});

const { items } = await client.dataset(run.defaultDatasetId).listItems();
```

```python
from apify_client import ApifyClient

client = ApifyClient("<YOUR_API_TOKEN>")

run = client.actor("amazinglabs-dev/mhvillage-investor-api").call(run_input={
    "locations": [{"state": "TX", "city": "Austin"}],
    "listingType": "for-sale",
    "maxListingsPerLocation": 100,
})

items = client.dataset(run["defaultDatasetId"]).list_items().items
```

Runs can also be scheduled (Console → Schedules) for recurring deal-sourcing, triggered via webhook on completion, or wired into Zapier/Make through Apify's standard integrations.

### Output

Results land in a Dataset — view as a table or JSON in the Console, or export to CSV, JSON, Excel, XML, or HTML. Pull it over the API (as above) straight into your own pipeline.

A real result searching Austin, TX:

```json
{
  "resultType": "listing",
  "state": "TX",
  "city": "Austin",
  "listingType": "for-sale",
  "name": "15027 Moss Phlox Cir , Pflugerville, TX 78660",
  "address": "15027 Moss Phlox Cir, Pflugerville, TX, 78660",
  "price": 139995,
  "bedrooms": 4,
  "bathrooms": 2,
  "sqft": 2280,
  "pricePerSqft": 61.4,
  "groupMedianPricePerSqft": 77.1,
  "isLikelyDeal": true,
  "url": "https://www.mhvillage.com/homes/3564145"
}
```

With `enrichWithDetailPage: true`, a real result also includes the full description, year, and manufacturer:

```json
{
  "resultType": "listing",
  "state": "TX",
  "city": "Austin",
  "year": 2021,
  "make": "Clayton",
  "description": "Newly Remodeled 4BR/3Bath home with a garage for rent! Welcome home to this beautifully remodeled 4 bedroom, 2 bath home offering comfortable living space and excellent value...",
  "detailUrl": "https://www.mhvillage.com/homes/3566169"
}
```

A bare-`state` search returns a city directory instead, so you can see where the inventory actually is before committing a run to it:

```json
{
  "resultType": "city_directory",
  "state": "FL",
  "city": "Alachua",
  "forSaleCount": 1,
  "url": "https://www.mhvillage.com/homes/fl/alachua"
}
```

**On price**: MHVillage's own listing markup doesn't include price directly — this Actor reads it from the rendered page separately and only attaches it when it can match prices to listings with certainty (position-for-position, same count on both sides). When that isn't possible, every other field is still returned with `price` simply omitted from that item, rather than risking the wrong number on the wrong home.

### Proxy

MHVillage blocks all non-US traffic at the network level, so this Actor requires a proxy that resolves to a US IP. It defaults to Apify's **US Residential** proxy group, which works out of the box on any plan with proxy access — no configuration needed.

### Troubleshooting

Extraction reads listings by data shape (structured data, then visible-text patterns) rather than fixed CSS selectors, so minor MHVillage redesigns shouldn't break it outright. If a run still comes back with zero results, check the Key-Value Store (Storage tab) for that run — it automatically saves the full page HTML and a screenshot whenever it finds nothing unexpectedly. The most common cause is a proxy not actually resolving to a US IP; confirm `apifyProxyCountry: "US"` is still set.

### Is this legal?

This Actor only reads listing pages that MHVillage serves to any visitor, logged in or not — no account, paywall, CAPTCHA, or bot-check sits in front of this data. The US-only proxy requirement exists because MHVillage blocks foreign IP ranges at the network level, which is a geographic restriction, not an anti-bot measure to route around. That said, this isn't legal advice — you're responsible for how you use the data you collect.

### Pricing

Billed per the model shown on this Actor's **Pricing** tab in Apify Store. Cost scales with the number of locations, pages per location, and whether `enrichWithDetailPage` is enabled (it adds one browser page load per kept listing).

### Roadmap

- **Photos, community name** — not every field MHVillage shows visually is in its structured data yet
- **Historical comps** — today's deal score compares listings within a single run; a persisted comps database across runs (compared against *actual recent sales*, not just today's other results) is the natural next step
- **New-listing alerts** — scheduled runs that only report listings posted since the last run
- **More marketplaces** — community-specific inventory and dealer-lot pages beyond the general search

### Support

Found a bug or have a feature request? Open an issue on the Actor's source repository, or reach out via the Apify Console.

# Actor input Schema

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

One entry per search. Use state (+ optional city), or zip, or paste a direct MHVillage search URL as startUrl if the guessed URL shape ever stops matching. A state with no city returns a directory of that state's cities and their listing counts instead of listings - use it to find out where the inventory actually is before running a city-by-city search.

## `listingType` (type: `string`):

Search for-sale or for-rent listings.

## `maxListingsPerLocation` (type: `integer`):

Stop collecting results for a location once this many listings have been found.

## `maxPagesPerLocation` (type: `integer`):

Caps how many pages of results this Actor will page through per location, regardless of maxListingsPerLocation.

## `enrichWithDetailPage` (type: `boolean`):

Slower and uses more proxy bandwidth, but fills in fields the search-results card doesn't show: full description text and the home's year and manufacturer.

## `computeDealScore` (type: `boolean`):

Compares each listing's price-per-sqft to the median for its state within this run and sets isLikelyDeal on standout-cheap listings.

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

MHVillage blocks all non-US traffic, so this must resolve to a US IP. Residential is the reliable choice.

## Actor input object example

```json
{
  "locations": [
    {
      "state": "TX",
      "city": "Austin"
    }
  ],
  "listingType": "for-sale",
  "maxListingsPerLocation": 100,
  "maxPagesPerLocation": 3,
  "enrichWithDetailPage": false,
  "computeDealScore": true,
  "proxyConfiguration": {
    "useApifyProxy": true,
    "apifyProxyGroups": [
      "RESIDENTIAL"
    ],
    "apifyProxyCountry": "US"
  }
}
```

# Actor output Schema

## `listings` (type: `string`):

No description

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

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

# Run the Actor and wait for it to finish
run = client.actor("amazinglabs-dev/mhvillage-investor-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 '{
  "locations": [
    {
      "state": "TX",
      "city": "Austin"
    }
  ]
}' |
apify call amazinglabs-dev/mhvillage-investor-api --silent --output-dataset

```

## MCP server setup

```json
{
    "mcpServers": {
        "apify": {
            "type": "http",
            "url": "https://mcp.apify.com/?tools=fetch-actor-details,amazinglabs-dev/mhvillage-investor-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/npkzrZRoW4Ko5wxQZ/builds/yJ4JTHiBDn6u8ctBW/openapi.json
