# Rent Roll to Excel: Yardi, AppFolio & RealPage Normalizer (`hanielazevedo/rent-roll-normalizer`) Actor

Rent roll to Excel: normalize Yardi, AppFolio, RealPage, Entrata, Buildium or Rent Manager rent rolls (XLSX, CSV, PDF) into one standard unit table plus occupancy, loss-to-lease, rent by floorplan and lease expirations, checked against the report's own totals. Rules-based, no AI.

- **URL**: https://apify.com/hanielazevedo/rent-roll-normalizer.md
- **Developed by:** [Haniel Azevedo](https://apify.com/hanielazevedo) (community)
- **Categories:** Real estate, Automation, Developer tools
- **Security:** Limited permissions
- **Stats:** 2 total users, 1 monthly users, 100.0% runs succeeded, 0 bookmarks
- **User rating**: No ratings yet

## Pricing

from $20.00 / 1,000 unit normalizeds

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

## Rent Roll Normalizer: Yardi, AppFolio, RealPage, Entrata, Buildium, Rent Manager to one clean Excel

Drop in a rent roll export in **XLSX, XLS, CSV or text PDF**. You get back:

1. **One standard unit-level table**: unit, floorplan, sq ft, beds/baths, status (occupied / notice / vacant / model / down), market rent, lease rent, other charges by type (concession, pet, parking, storage, utility, insurance, fees, other), lease start/end, move-in/out, balance and deposit.
2. **An underwriting summary**: physical, economic and sq ft occupancy; gross potential rent; vacancy loss; **loss-to-lease**; **average in-place vs market rent by floorplan**; and **lease expirations by month**, with cumulative %.
3. **Validation against the report's own totals.** The Actor re-adds every column and compares the result with the report's grand-total and summary rows: unit count, sq ft, market rent, lease rent, deposits, balances and occupancy. Each file is marked `validated` or `needs_review`, and you can see the reason.
4. **CSV, JSON and a formatted XLSX** file per rent roll. The XLSX has these sheets: Units, Summary, Floorplans, Expirations, Validation, Charges, Future residents.

The extraction is **rules-based: no AI, no LLM, no third-party APIs**. Your rent roll is not sent anywhere, and the same file always gives the same output.

**Pricing:** $0.02 per unit, charged only for rent rolls that reconcile with their own totals (`validated`). A 200-unit roll costs $4.00. Failed files, `needs_review` files and the demo file are free.

### Who it is for

- Multifamily **acquisitions analysts and underwriters** who re-key seller rent rolls into their model.
- **Lenders, brokers and appraisers** who receive rent rolls from many different property-management systems.
- **Asset managers** who compare properties that run on different software.

Re-keying and checking one 200-unit rent roll by hand usually takes 1–3 hours. This Actor takes about 2 seconds.

### Supported formats

| System | Typical report | Detected by |
|---|---|---|
| Yardi Voyager | Rent Roll / Rent Roll with Lease Charges | Headers, charge-code rows, "Current/Notice/Vacant Residents" summary |
| AppFolio | Rent Roll | Headers ("Rent", "Market Rent", "Past Due"…), "Total" rows |
| RealPage OneSite | Rent Roll | Headers ("Lease Rent", "Mkt + Addl"…) |
| Entrata | Rent Roll | Headers ("Lease Status", "Scheduled Rent"…) |
| Buildium | Rent Roll | Headers |
| Rent Manager | Rent Roll | Headers |
| Anything else | Any table with a unit column and a rent column | Generic column rules |

The parser handles **multi-row headers, subtotal and total rows, one-row-per-charge-code layouts** (Yardi), **vacant, model, down and admin units**, **future/applicant residents** (kept apart; add them with `includeFutureResidents`), and columns in any order or with extra columns. Set `sourceSystem` if auto-detection picks the wrong system.

### How to use it

1. Paste one or more **public links** to rent roll files into `fileUrls`. For confidential files, upload them to one of your **Apify key-value stores**, pick that store in `fileStore`, and list the record keys in `fileKeys`. The Actor gets read-only access to that store only.
2. Optional: set `asOfDate` (otherwise it is read from the report, or today's date is used), `propertyName`, and `redactTenantNames`. Redaction replaces names with `[redacted]` in every output.
3. Run it. Open the **Files** view for links to each XLSX and CSV, or the **Units** view for the rows.

### Example input

The Console form is prefilled with a real public rent roll: a 4-unit AppFolio PDF published by Triumph Property Management. Click **Start** without changing anything:

```json
{
  "fileUrls": [
    "https://triumphpropertymanagement.com/wp-content/uploads/2024/03/Rent-Roll-Sample.pdf"
  ]
}
```

### Example output

That run takes about 5 seconds, detects AppFolio, marks the file `validated` and is free (demo file). Summary (run of 1 Nov 2026):

```json
{"asOfDate": "2024-03-05", "unitCount": 4, "occupiedUnits": 4, "physicalOccupancyPct": 100,
 "grossPotentialRent": 4145, "inPlaceRent": 5570, "lossToLease": -130, "lossToLeasePct": -3.14,
 "avgMarketRent": 1381.67, "avgInPlaceRent": 1392.5, "unitsMissingMarketRent": 1,
 "totalBalance": 2975.42, "totalDeposits": 4840}
```

One unit row, with `redactTenantNames: true`:

```json
{"unit": "Unit 102", "floorplan": "2BR/2BA", "sqft": 994, "beds": 2, "baths": 2, "tenantName": "[redacted]",
 "status": "occupied", "marketRent": 1450, "contractRent": 1475, "totalCharges": 1475,
 "leaseStart": "2023-02-01", "leaseEnd": "2024-07-31", "moveIn": "2022-09-01", "balance": 1550.42, "deposit": 750}
```

Validation for that file: 14 of 14 checks passed. Sq ft 3,976, market rent 4,145, lease rent 5,570, deposits 4,840, balance 2,975.42, 4 units and 100% occupancy all match the report's totals.

### Accuracy (measured)

- **Public rent rolls** (6 real reports from Yardi and AppFolio, 4 to 232 units, published in bond disclosures and by property managers): every unit was found, and all 6 files reconciled to their own totals (`validated`).
- **Synthetic held-out set**: 480 files across 7 systems × 3 formats (XLSX, CSV, PDF) with known ground truth, generated after the rules were frozen. 100% unit recall, 0 extra units, 100% field accuracy over 193,456 fields, and 100% system detection. The same score held on a stress variant with shuffled columns and extra noise columns.
- The synthetic files are modelled on the same layouts the rules target, so treat the public-file result as the real-world evidence. Always check the Validation sheet before you rely on a number.

### Use from code (apify-client) or an AI agent (MCP)

```python
from apify_client import ApifyClient
client = ApifyClient("<YOUR_APIFY_TOKEN>")
run = client.actor("hanielazevedo/rent-roll-normalizer").call(run_input={
    "fileUrls": ["https://triumphpropertymanagement.com/wp-content/uploads/2024/03/Rent-Roll-Sample.pdf"], "redactTenantNames": True})
units = client.dataset(run["defaultDatasetId"]).list_items().items
summary = client.key_value_store(run["defaultKeyValueStoreId"]).get_record("OUTPUT")["value"]
```

```js
import { ApifyClient } from 'apify-client';
const client = new ApifyClient({ token: process.env.APIFY_TOKEN });
const run = await client.actor('hanielazevedo/rent-roll-normalizer').call({ fileUrls: ['https://triumphpropertymanagement.com/wp-content/uploads/2024/03/Rent-Roll-Sample.pdf'] });
```

With the **Apify MCP server** (`https://mcp.apify.com`), add this Actor as a tool. An agent can then call it with `fileUrls` and read the `OUTPUT` record.

### Pricing

Pay per event, with platform usage included:

| Event | Price | When |
|---|---|---|
| `unit-normalized` | **$0.02** per current unit | Once per rent roll, after its outputs are saved, and only when the file is `validated` |
| Actor start | $0.00005 per GB of memory | Once per run |

| Rent roll | Cost |
|---|---|
| 50 units | $1.00 |
| 200 units | $4.00 |
| 1,000 units | $20.00 |

Files that fail, files marked `needs_review` and the prefilled demo file are never charged. If your maximum total charge does not cover a whole file, that file is skipped without charge.

### FAQ

**Is my data used to train AI or sent to a third party?** No. Everything runs inside your Apify run, with rules and no AI. Use `redactTenantNames` to keep names out of the outputs.

**What does `needs_review` mean?** The units were extracted, but at least one total did not match the report's own totals, or the report has no totals to check against. You get every output so you can check them, and **you are not charged for that file**.

**Scanned PDFs?** They are not supported (no OCR). Export the report to Excel, CSV or a text PDF from your PM system.

**Is T-12 / operating statement support planned?** Yes. It is planned as a second version.

**What happens with a negative loss-to-lease?** Leases above market give a negative loss-to-lease, which the Actor reports as is. Units with no market rent are left out of loss-to-lease and economic occupancy, and are counted in `unitsMissingMarketRent`.

### Limitations

- Text PDFs only. Scanned images are not read (no OCR).
- One property per file works best. A portfolio roll with several properties is parsed, but its totals only reconcile when each property has its own total rows.
- Columns the rules cannot place are left out of the standard table. Charge codes that are not recognised go to `charge_other`, with the raw codes kept in `chargeDetail`.
- Real-world testing so far covers Yardi and AppFolio public reports. RealPage, Entrata, Buildium and Rent Manager are covered by their documented column layouts and synthetic tests. If one of your files fails, you are not charged for it.
- Maximum file size is 100 MB (default 50 MB).

### Other Actors by Haniel

- [Property Owner Lookup & Absentee Owner List (PA, SF, CT, DC)](https://apify.com/hanielazevedo/property-details-by-address): Property details by address and absentee owner lists from official assessor data for Pittsburgh (Allegheny County), San Francisco, Connecticut and DC. $0.008 per lookup.
- [Insurance Loss Run Extractor: Loss Run PDF to Excel & CSV](https://apify.com/hanielazevedo/insurance-loss-run-extractor): Extract claims and policy-year totals from carrier loss run PDFs into Excel, CSV and JSON, each row checked against the report totals. $0.04 per validated claim.
- [Floor Plan Takeoff: PDF Room Areas, Wall LF & Door Counts](https://apify.com/hanielazevedo/floor-plan-takeoff): Construction takeoff from vector floor-plan PDFs: room square footage, wall linear feet and door counts, with a scale self-check. $2.50 per sheet.
- [PDF to Markdown Converter & PDF Text Extractor (OCR, DOCX)](https://apify.com/hanielazevedo/document-to-markdown-ocr): PDF, scanned PDF, DOCX, HTML and images to clean Markdown or text for RAG and AI agents. $3 per 1,000 documents.

# Actor input Schema

## `fileUrls` (type: `array`):

Public http(s) links to rent roll exports: Excel (.xlsx, .xls), CSV or text PDF. Scanned PDFs are not OCRed. To use files from your Apify account, put them in a key-value store and select it below: this Actor runs with limited permissions and cannot read a store you have not selected. The prefilled public demo file (a 4-unit AppFolio sample) is processed free of charge.

## `fileStore` (type: `string`):

Your Apify key-value store holding the rent roll files (Console > Storage > Key-value stores). Selecting it grants this run read-only access to that one store. Without record keys, every record whose key ends in .xlsx, .xls, .csv, .txt, .tsv or .pdf is processed.

## `fileKeys` (type: `array`):

Optional. Keys of the rent roll records in the selected store, e.g. "oak-street-rent-roll.xlsx". Leave empty to use every supported file in the store.

## `asOfDate` (type: `string`):

Optional, YYYY-MM-DD. Used for lease-expiration buckets and month-to-month detection. Blank = the date printed on the report ("As Of"), else today.

## `propertyName` (type: `string`):

Optional. Overrides the property name read from the report header.

## `sourceSystem` (type: `string`):

Which property management system produced the export. Auto-detection works for standard reports; set it if detection picks the wrong one.

## `redactTenantNames` (type: `boolean`):

Replace resident names with \[redacted] in every output (dataset, CSV, Excel, JSON). Use before sharing with lenders or investors.

## `includeFutureResidents` (type: `boolean`):

Yardi and similar reports list future residents/applicants below the current roll. They are never counted in occupancy or charged; turn on to also list them in the outputs.

## `maxFileSizeMb` (type: `integer`):

Larger files are skipped (not charged).

## Actor input object example

```json
{
  "fileUrls": [
    "https://triumphpropertymanagement.com/wp-content/uploads/2024/03/Rent-Roll-Sample.pdf"
  ],
  "sourceSystem": "auto",
  "redactTenantNames": false,
  "includeFutureResidents": false,
  "maxFileSizeMb": 50
}
```

# Actor output Schema

## `units` (type: `string`):

No description

## `files` (type: `string`):

No description

## `summary` (type: `string`):

No description

## `store` (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 = {
    "fileUrls": [
        "https://triumphpropertymanagement.com/wp-content/uploads/2024/03/Rent-Roll-Sample.pdf"
    ]
};

// Run the Actor and wait for it to finish
const run = await client.actor("hanielazevedo/rent-roll-normalizer").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 = { "fileUrls": ["https://triumphpropertymanagement.com/wp-content/uploads/2024/03/Rent-Roll-Sample.pdf"] }

# Run the Actor and wait for it to finish
run = client.actor("hanielazevedo/rent-roll-normalizer").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 '{
  "fileUrls": [
    "https://triumphpropertymanagement.com/wp-content/uploads/2024/03/Rent-Roll-Sample.pdf"
  ]
}' |
apify call hanielazevedo/rent-roll-normalizer --silent --output-dataset

```

## MCP server setup

```json
{
    "mcpServers": {
        "apify": {
            "type": "http",
            "url": "https://mcp.apify.com/?tools=fetch-actor-details,hanielazevedo/rent-roll-normalizer"
        }
    }
}
```

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/tYAw0KukJUbKyNgUR/builds/FNFG4bkk4bZFSqkXY/openapi.json
