# AppFolio Scraper — US Rentals + Office Phone from Any PM Site (`memo23/appfolio-scraper`) Actor

Turn any AppFolio listings URL into live vacancies with the office phone the index hides. Rent, beds, baths, address, lat/lng, photos, apply URL, office name, phone. Paste {tenant}.appfolio.com/listings. HTTP only, no login. JSON, CSV or Excel. You bring the PM URL — not a weekly dump.

- **URL**: https://apify.com/memo23/appfolio-scraper.md
- **Developed by:** [Muhamed Didovic](https://apify.com/memo23) (community)
- **Categories:** Real estate, Lead generation
- **Stats:** 16 total users, 16 monthly users, 100.0% runs succeeded, 0 bookmarks
- **User rating**: No ratings yet

## Pricing

from $1.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

## AppFolio Scraper — US Rentals + Office Phone from Any PM Site

**Turn any AppFolio listings URL into structured vacancies — including the office phone the index page hides.** Paste `https://{tenant}.appfolio.com/listings` (or a detail URL, or a custom domain that redirects there) and get one row per unit: rent, beds, baths, address, lat/lng, photos, apply link, office name, and phone.

### Why Use This Scraper?

- **URL-in, live scrape.** You point it at a manager's own AppFolio site. It is not a weekly snapshot of "all AppFolio."
- **Office phone on every detail.** The listings index has no phone. The detail sidebar does (`ATLAS PROPERTY GROUP` + `(415) 216-4945` in the smoke test).
- **Map coordinates.** Lat/lng come from the listings page `googleMap.markers` JSON, not guessed from the address.
- **Apply + showing URLs.** Ready-to-use links onto that manager's application and showing forms.
- **No login, no browser.** Cheerio HTTP only. Proxy optional.

### Overview

One dataset row per vacancy on the AppFolio site you pasted. Typical use: a lead-gen or inventory job that already has a list of property-manager websites. There is no national "search by city" input — AppFolio does not publish a public directory of tenant hosts. Filter on the manager's site, copy the URL.

### Supported Inputs

| Input | What it is | Example |
|---|---|---|
| Listings index | That PM's current vacancies | `https://apg.appfolio.com/listings` |
| Detail URL | A single unit | `https://apg.appfolio.com/listings/detail/e4afc96a-ac2d-499b-80d8-729e87091806` |
| Custom domain | Marketing site that 302s onto AppFolio | `https://www.examplepm.com/listings/` |
| `startUrl` | Single-URL convenience field | same as one `startUrls` item |

**Unsupported:** AppFolio Property Manager (the logged-in SaaS), tenant portals, and "all AppFolio in California" with no URL. You must have a listings URL.

**Seed list:** this repo ships `seeds/start-urls.json` — **~2,760** `{tenant}.appfolio.com/listings` URLs mined from Common Crawl and live-checked. Paste a subset into `startUrls` when you want multi-PM coverage without hunting hosts yourself.

### Use Cases

| Audience | What they get |
|---|---|
| **Rental lead-gen** | Office phone + apply URL for the managers whose sites you already know |
| **PropTech / aggregators** | Live inventory from PM sites that never hit Zillow |
| **Market researchers** | Asking rents, beds, availability on a named manager |
| **Agencies** | A CRM-ready dump of one competitor's vacancies |

### How It Works

![How the AppFolio scraper works](https://raw.githubusercontent.com/muhamed-didovic/muhamed-didovic.github.io/main/assets/how-it-works-appfolio.png)

1. Paste one or more `{tenant}.appfolio.com/listings` URLs.
2. The actor reads every vacancy card and the map-marker JSON (rent, beds, address, photo, lat/lng).
3. With `scrapeDetails` on (default), it opens each detail page for office name, phone, full description, apply/showing URLs, and extra photos.
4. It stops at `maxItems`. One row per vacancy.

### Input Configuration

| Field | Type | Default | What it does |
|---|---|---|---|
| `startUrls` | string list | sample SF listings URL | AppFolio listings or detail URLs |
| `startUrl` | string | empty | Same, for a single URL |
| `maxItems` | integer | 100 | Cap on saved listings (free accounts hard-capped at 100) |
| `scrapeDetails` | boolean | true | Visit detail pages for phone / office name |
| `proxy` | proxy object | off | Optional. Direct fetch worked in recon. |

**Example — one manager, 25 listings**

```json
{
  "startUrls": ["https://apg.appfolio.com/listings"],
  "maxItems": 25
}
```

**Example — index only, no phones (faster)**

```json
{
  "startUrls": ["https://aaaprop.appfolio.com/listings"],
  "maxItems": 100,
  "scrapeDetails": false
}
```

### Output Overview

Each row is one vacancy. Index fields (rent, beds, address, coordinates, photo) are always present when the card parsed. Phone and office name require `scrapeDetails: true`.

### Output Samples

Listings URL `https://apg.appfolio.com/listings` (detail follow-up):

```json
{
  "platform": "appfolio",
  "id": "e4afc96a-ac2d-499b-80d8-729e87091806",
  "listingId": 428,
  "tenant": "apg",
  "url": "https://apg.appfolio.com/listings/detail/e4afc96a-ac2d-499b-80d8-729e87091806",
  "title": "Newer Studios with Outdoor Amenities!",
  "address": "345 6th St, Unit 805, San Francisco, CA 94103",
  "city": "San Francisco",
  "state": "CA",
  "zip": "94103",
  "latitude": 37.778165,
  "longitude": -122.404677,
  "rent": 2700,
  "rentFormatted": "$2,700/mo",
  "currency": "USD",
  "beds": 0,
  "baths": 1,
  "sqft": 350,
  "available": "NOW",
  "officeName": "ATLAS PROPERTY GROUP",
  "phone": "(415) 216-4945",
  "applyUrl": "https://apg.appfolio.com/listings/rental_applications/new?listable_uid=e4afc96a-ac2d-499b-80d8-729e87091806&source=Website"
}
```

Studio units report `beds: 0`.

### Key Output Fields

| Group | Fields |
|---|---|
| Identity | `platform`, `id` (UUID), `listingId`, `tenant`, `url` |
| Listing | `title`, `description`, `rent`, `rentFormatted`, `currency`, `beds`, `baths`, `sqft`, `available` |
| Location | `address`, `street`, `city`, `state`, `zip`, `latitude`, `longitude` |
| Media | `image`, `images[]` |
| Contact | `officeName`, `phone`, `applyUrl`, `showingUrl`, `contactUrl` |
| Extra | `amenities[]`, `utilities`, `petPolicy`, `scrapedAt` |

AppFolio listing sites do **not** publish a public email on the vacancy pages we scrape. Phone is on the detail sidebar. Email is a gap vs Buildium's XML feed.

### FAQ

**Can I scrape all AppFolio listings in a city without URLs?** Not via AppFolio search — there is none. Use `seeds/start-urls.json` (~2.7k live-checked tenant listings URLs from Common Crawl) and filter yourself, or ask for a custom geo-filtered seed. CRT.sh does not enumerate tenants (wildcard cert).

**Is this the same as housingfeed/appfolio-scraper?** No. That actor reads a pre-built database and filters by state/city. This actor live-scrapes the URL you give it, including phone.

**Why is email empty?** It is not on the public listings/detail HTML. Do not expect it.

**Do I need a proxy?** Recon loaded three tenants with a plain GET. Turn proxy on if you hit blocks.

### Support

Open an issue on the [actor page](https://apify.com/memo23/appfolio-scraper/issues/open). For custom work: see Additional Services below.

### Additional Services

Need a geo-filtered cut of the seed list, a combined AppFolio+Buildium run, or a private feed? Get in touch via the actor issues tab or [memo23's profile](https://apify.com/memo23). The raw seed files also live in this repo under `seeds/`.

### Explore More Scrapers

- [Buildium Scraper](https://apify.com/memo23/buildium-scraper) — same URL-in idea, public XML with **email + phone**
- [Apartments.com Scraper](https://apify.com/memo23/apartments-cheerio-ppe)
- [Zumper Scraper](https://apify.com/memo23/zumper-cheerio)
- [Zillow Scraper](https://apify.com/memo23/zillow-scraper-ppe)
- More at [apify.com/memo23](https://apify.com/memo23)

### 🤖 For AI Agents & LLM Apps

Compact reference for agents calling this actor via the [Apify MCP server](https://mcp.apify.com) or the Apify API (`memo23/appfolio-scraper`).

**Purpose:** Live-scrape rental vacancies from an AppFolio property-manager listings URL, including office phone from the detail page.

**Minimal input:**

```json
{
  "startUrls": ["https://apg.appfolio.com/listings"],
  "maxItems": 25
}
```

**Output fields:** `platform`, `id`, `listingId`, `tenant`, `url`, `title`, `description`, `address`, `street`, `city`, `state`, `zip`, `latitude`, `longitude`, `rent`, `rentFormatted`, `currency`, `beds`, `baths`, `sqft`, `available`, `amenities`, `utilities`, `petPolicy`, `image`, `images`, `applyUrl`, `showingUrl`, `contactUrl`, `officeName`, `phone`, `scrapedAt`.

**Behaviors an agent should know:**

- Always set `maxItems`. Default 100; free Apify accounts are capped at 100.
- Requires a real `{tenant}.appfolio.com/listings` (or detail) URL. No city-wide search.
- `scrapeDetails` default true adds phone + office name; false is index-only.
- `beds: 0` means studio.
- No public email on these pages.
- Proxy off by default.

### ⚠️ Disclaimer

This Actor is an independent tool and is not affiliated with, endorsed by, or sponsored by AppFolio, Inc. or any of its subsidiaries. All trademarks mentioned are the property of their respective owners.

The scraper accesses only publicly available rental-listing pages on `*.appfolio.com` — no authenticated Property Manager endpoints, tenant records, or content behind a login. Users are responsible for ensuring their use complies with AppFolio's Terms of Service, applicable data-protection law (GDPR, CCPA, etc.), and any contractual obligations of their own organization.

### SEO Keywords

appfolio scraper, appfolio listings api, scrape appfolio, appfolio rental listings, property manager listings scraper, appfolio.com/listings, US rental scraper, apartment vacancy scraper, appfolio office phone, property management inventory, appfolio lead generation, small landlord listings, off-market rentals scraper, appfolio data extraction, US multifamily listings

# Actor input Schema

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

One or more AppFolio listing-index or detail URLs. Example: https://apg.appfolio.com/listings or https://zenproperties.appfolio.com/listings/detail/3641585a-f89f-4f60-8f2d-18432b8cc17e. Default: a sample San Francisco listings page.

## `startUrl` (type: `string`):

Convenience alternative to startUrls when you have one URL. Merged with startUrls and de-duplicated. Example: https://apg.appfolio.com/listings. Default: empty.

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

Hard cap on listings saved across all start URLs. Controls how many detail pages are fetched when scrapeDetails is on. Free Apify accounts are capped at 100. Example: 5 for a smoke test. Default: 100.

## `scrapeDetails` (type: `boolean`):

When true (default), each listing's detail page is opened to add office name, phone, full description, apply URL and extra photos. When false, only the listings index is parsed — faster, no phone. Default: true.

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

Optional. AppFolio listing sites usually load without a proxy. Turn Apify Proxy on only if you see blocks. Default: off.

## Actor input object example

```json
{
  "startUrls": [
    "https://apg.appfolio.com/listings",
    "https://aaaprop.appfolio.com/listings",
    "https://zenproperties.appfolio.com/listings"
  ],
  "maxItems": 100,
  "scrapeDetails": true,
  "proxy": {
    "useApifyProxy": false
  }
}
```

# Actor output Schema

## `dataset` (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 = {
    "startUrls": [
        "https://apg.appfolio.com/listings",
        "https://aaaprop.appfolio.com/listings",
        "https://zenproperties.appfolio.com/listings"
    ]
};

// Run the Actor and wait for it to finish
const run = await client.actor("memo23/appfolio-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 = { "startUrls": [
        "https://apg.appfolio.com/listings",
        "https://aaaprop.appfolio.com/listings",
        "https://zenproperties.appfolio.com/listings",
    ] }

# Run the Actor and wait for it to finish
run = client.actor("memo23/appfolio-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 '{
  "startUrls": [
    "https://apg.appfolio.com/listings",
    "https://aaaprop.appfolio.com/listings",
    "https://zenproperties.appfolio.com/listings"
  ]
}' |
apify call memo23/appfolio-scraper --silent --output-dataset

```

## MCP server setup

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