Every rental property investment comes down to one number: how much money does this property make after expenses?
That number is the NOI. Net operating income. It strips away financing and taxes to show what the property actually earns from operations. Two investors can buy the same property with different mortgages and different tax situations. The NOI is the same for both of them.
NOI is also the numerator in the cap rate formula, which is the most common metric for comparing investment properties. You can’t calculate cap rate without NOI. You can’t calculate cash flow without NOI. It’s the foundation of every rental investment analysis.
Here’s how to calculate NOI for any rental property using API data, and how to scale that calculation across entire markets and portfolios.
What is the NOI formula?
The formula has three layers:
Gross Rental Income - Vacancy Loss = Effective Rental Income
Effective Rental Income - Operating Expenses = Net Operating Income (NOI)In practice, most investors simplify this to:
NOI = Annual Rent - Vacancy - Taxes - Insurance - Maintenance - ManagementMortgage payments are not included. Income taxes are not included. Capital expenditures (roof replacement, HVAC system) are not included. NOI measures the property’s operating performance independent of how you finance it or what tax bracket you’re in.
What data do I need to calculate NOI?
The API returns three fields that drive the calculation:
| API field | What it gives you | Role in NOI |
|---|---|---|
| rentZestimate | Monthly rent estimate | Gross income |
| taxAnnualAmount | Annual property tax | Operating expense |
| zestimate | Property market value | Insurance estimate base |
You estimate the remaining expenses from industry benchmarks:
| Expense | Typical range | Common estimate |
|---|---|---|
| Vacancy | 3-10% of gross rent | 5% |
| Insurance | 0.3-0.6% of property value | 0.4% |
| Maintenance | 5-15% of gross rent | 10% |
| Property management | 0-10% of gross rent | 8% |
These percentages work for quick screening. For a property you’re seriously considering, replace the estimates with actual quotes from insurance agents, property managers, and local contractors.
How do I calculate NOI with the API?
One API call per property. The function pulls the data and runs the numbers:
import requests, os
API_KEY = os.environ["ZILLAPI_KEY"]HEADERS = {"Authorization": f"Bearer {API_KEY}"}
def calculate_noi(address, vacancy_rate=0.05, insurance_rate=0.004, maintenance_rate=0.10, management_rate=0.08): """ Calculate NOI for a rental property.
All rates are expressed as decimals: - vacancy_rate: % of gross rent lost to vacancy - insurance_rate: % of property value for annual insurance - maintenance_rate: % of gross rent for repairs and maintenance - management_rate: % of gross rent for property management """ r = requests.get( "https://api.zillapi.com/v1/properties/by-address", params={ "address": address, "fields": "rentZestimate,taxAnnualAmount,zestimate,bedrooms,bathrooms,livingArea,yearBuilt,homeType,address", }, headers=HEADERS, ) d = r.json()["data"]
monthly_rent = d.get("rentZestimate", 0) annual_tax = d.get("taxAnnualAmount", 0) property_value = d.get("zestimate", 0)
if not monthly_rent or not property_value: return None
# Income gross_annual_rent = monthly_rent * 12 vacancy_loss = gross_annual_rent * vacancy_rate effective_income = gross_annual_rent - vacancy_loss
# Expenses insurance = property_value * insurance_rate maintenance = gross_annual_rent * maintenance_rate management = gross_annual_rent * management_rate total_expenses = annual_tax + insurance + maintenance + management
# NOI noi = effective_income - total_expenses
# Derived metrics cap_rate = (noi / property_value * 100) if property_value else 0 expense_ratio = (total_expenses / gross_annual_rent * 100) if gross_annual_rent else 0
return { "address": d["address"]["streetAddress"], "beds": d.get("bedrooms", 0), "baths": d.get("bathrooms", 0), "sqft": d.get("livingArea", 0), "year_built": d.get("yearBuilt", 0), "property_value": property_value, "monthly_rent": monthly_rent, "gross_annual_rent": gross_annual_rent, "vacancy_loss": round(vacancy_loss), "effective_income": round(effective_income), "expenses": { "taxes": annual_tax, "insurance": round(insurance), "maintenance": round(maintenance), "management": round(management), "total": round(total_expenses), }, "noi": round(noi), "monthly_noi": round(noi / 12), "cap_rate": round(cap_rate, 1), "expense_ratio": round(expense_ratio, 1), }
result = calculate_noi("17 Zelma Dr, Greenville, SC 29617")if result: print(f"Property: {result['address']}") print(f"Type: {result['beds']}bd/{result['baths']}ba | {result['sqft']:,} sqft") print(f"Value: ${result['property_value']:,}") print(f"Monthly Rent: ${result['monthly_rent']:,}") print() print("Income:") print(f" Gross Annual Rent: ${result['gross_annual_rent']:,}") print(f" Vacancy Loss (5%): -${result['vacancy_loss']:,}") print(f" Effective Income: ${result['effective_income']:,}") print() print("Expenses:") print(f" Property Taxes: ${result['expenses']['taxes']:,}") print(f" Insurance: ${result['expenses']['insurance']:,}") print(f" Maintenance: ${result['expenses']['maintenance']:,}") print(f" Management: ${result['expenses']['management']:,}") print(f" Total Expenses: ${result['expenses']['total']:,}") print() print(f"NOI: ${result['noi']:,}/year (${result['monthly_noi']:,}/month)") print(f"Cap Rate: {result['cap_rate']}%") print(f"Expense Ratio: {result['expense_ratio']}%")One credit. You get the full NOI breakdown with cap rate and expense ratio.
How do I compare NOI across properties?
Screening multiple properties by NOI reveals which ones generate the most income relative to their cost:
import time
addresses = [ "17 Zelma Dr, Greenville, SC 29617", "100 Main St, Greenville, SC 29601", "45 Augusta St, Greenville, SC 29601", "220 N Pleasantburg Dr, Greenville, SC 29607", "15 Overbrook Cir, Greenville, SC 29607",]
results = []for addr in addresses: noi_data = calculate_noi(addr) if noi_data: results.append(noi_data) time.sleep(0.35)
# Sort by cap rate (best deals first)results.sort(key=lambda x: x["cap_rate"], reverse=True)
print(f"{'Address':<35} {'Value':>10} {'Rent/mo':>8} {'NOI':>8} {'Cap':>5}")print("-" * 70)for r in results: print(f"{r['address']:<35} ${r['property_value']:>9,} ${r['monthly_rent']:>7,} " f"${r['noi']:>7,} {r['cap_rate']:>4}%")Five properties. Five credits. You see which property has the highest cap rate and which one generates the most absolute NOI. A property with a high cap rate but low absolute NOI might be a small single-family home. A property with a lower cap rate but high absolute NOI might be a larger multi-unit.
How do I account for actual expenses instead of estimates?
The estimates above work for screening. When you’re analyzing a specific property, replace the percentages with real numbers:
def noi_with_actuals(address, actual_expenses): """ Calculate NOI using actual expense data instead of estimates.
actual_expenses should be a dict with annual amounts: { "taxes": 2340, "insurance": 1200, "maintenance": 1800, "management": 0, # self-managed "hoa": 0, "utilities": 600, # owner-paid water/sewer "pest_control": 360, "landscaping": 480, } """ r = requests.get( "https://api.zillapi.com/v1/properties/by-address", params={ "address": address, "fields": "rentZestimate,zestimate,address", }, headers=HEADERS, ) d = r.json()["data"]
monthly_rent = d.get("rentZestimate", 0) property_value = d.get("zestimate", 0)
gross_annual = monthly_rent * 12 vacancy = gross_annual * 0.05 effective_income = gross_annual - vacancy
total_expenses = sum(actual_expenses.values()) noi = effective_income - total_expenses cap_rate = (noi / property_value * 100) if property_value else 0
return { "address": d["address"]["streetAddress"], "effective_income": round(effective_income), "total_expenses": total_expenses, "noi": round(noi), "cap_rate": round(cap_rate, 1), "expense_breakdown": actual_expenses, }
# Use actual expenses for a property you ownresult = noi_with_actuals("17 Zelma Dr, Greenville, SC 29617", { "taxes": 2340, "insurance": 1150, "maintenance": 1500, "management": 0, "hoa": 0, "utilities": 720, "pest_control": 360, "landscaping": 480,})
if result: print(f"Property: {result['address']}") print(f"Effective Income: ${result['effective_income']:,}") print(f"Total Expenses: ${result['total_expenses']:,}") for expense, amount in result["expense_breakdown"].items(): print(f" {expense}: ${amount:,}") print(f"NOI: ${result['noi']:,}") print(f"Cap Rate: {result['cap_rate']}%")Self-managed properties have zero management fees, which can add 1 to 2 percentage points to the cap rate. The API provides the rent estimate and property value. You fill in the actual expenses from your records.
How do I scan a market for high-NOI properties?
Search for properties in an area and calculate NOI for each one to find the best opportunities:
def market_noi_scan(bbox, status="FOR_SALE"): """Scan a market and rank properties by cap rate.""" listings = requests.post( "https://api.zillapi.com/v1/search", json={ "bbox": bbox, "listingStatus": status, "homeType": ["SINGLE_FAMILY"], }, headers=HEADERS, ).json()["data"]
results = [] for p in listings: rent = p.get("rentZestimate", 0) value = p.get("zestimate") or p.get("price", 0) tax = p.get("taxAnnualAmount", 0)
if not rent or not value: continue
gross = rent * 12 vacancy = gross * 0.05 insurance = value * 0.004 maintenance = gross * 0.10 management = gross * 0.08 expenses = tax + insurance + maintenance + management noi = (gross - vacancy) - expenses cap_rate = noi / value * 100
results.append({ "address": p["address"]["streetAddress"], "price": p.get("price", value), "rent": rent, "noi": round(noi), "cap_rate": round(cap_rate, 1), })
results.sort(key=lambda x: x["cap_rate"], reverse=True) return results
# Scan Greenville, SCdeals = market_noi_scan( bbox={"north": 34.90, "south": 34.80, "east": -82.35, "west": -82.45})
print(f"Found {len(deals)} properties with NOI data\n")print(f"{'Address':<35} {'Price':>10} {'Rent':>7} {'NOI':>8} {'Cap':>5}")print("-" * 70)for d in deals[:10]: print(f"{d['address']:<35} ${d['price']:>9,} ${d['rent']:>6,} ${d['noi']:>7,} {d['cap_rate']:>4}%")One search call returns up to 40 properties with enough data to calculate NOI for each one. No extra detail lookups needed. One credit for the entire market scan.
How do I track portfolio NOI over time?
For landlords and property managers, monthly NOI tracking reveals whether your portfolio is improving or declining:
import jsonfrom datetime import datetime
NOI_HISTORY_FILE = "noi_history.json"
def load_history(): try: with open(NOI_HISTORY_FILE) as f: return json.load(f) except FileNotFoundError: return []
def save_history(history): with open(NOI_HISTORY_FILE, "w") as f: json.dump(history, f, indent=2)
def monthly_portfolio_snapshot(portfolio): """ Take a monthly snapshot of portfolio NOI. portfolio: list of {"address": "...", "actual_rent": 1700, "monthly_expenses": 850} """ snapshot = { "date": datetime.now().strftime("%Y-%m"), "properties": [], "totals": {"income": 0, "expenses": 0, "noi": 0}, }
for prop in portfolio: r = requests.get( "https://api.zillapi.com/v1/properties/by-address", params={ "address": prop["address"], "fields": "zestimate,rentZestimate,address", }, headers=HEADERS, ) d = r.json()["data"]
actual_rent = prop["actual_rent"] monthly_expenses = prop["monthly_expenses"] monthly_noi = actual_rent - monthly_expenses zestimate = d.get("zestimate", 0) market_rent = d.get("rentZestimate", 0)
entry = { "address": d["address"]["streetAddress"], "actual_rent": actual_rent, "market_rent": market_rent, "rent_gap": actual_rent - market_rent, "expenses": monthly_expenses, "noi": monthly_noi, "value": zestimate, "cap_rate": round((monthly_noi * 12) / zestimate * 100, 1) if zestimate else 0, } snapshot["properties"].append(entry) snapshot["totals"]["income"] += actual_rent snapshot["totals"]["expenses"] += monthly_expenses snapshot["totals"]["noi"] += monthly_noi
time.sleep(0.35)
# Save to history history = load_history() history.append(snapshot) save_history(history)
return snapshot
# Monthly tracking for a 3-property portfolioportfolio = [ {"address": "17 Zelma Dr, Greenville, SC 29617", "actual_rent": 1700, "monthly_expenses": 850}, {"address": "100 Main St, Greenville, SC 29601", "actual_rent": 1400, "monthly_expenses": 680}, {"address": "45 Augusta St, Greenville, SC 29601", "actual_rent": 2100, "monthly_expenses": 1050},]
snap = monthly_portfolio_snapshot(portfolio)print(f"Portfolio Snapshot: {snap['date']}")print(f"Total Monthly Income: ${snap['totals']['income']:,}")print(f"Total Monthly Expenses: ${snap['totals']['expenses']:,}")print(f"Total Monthly NOI: ${snap['totals']['noi']:,}")print(f"Annual Portfolio NOI: ${snap['totals']['noi'] * 12:,}")Three credits per monthly snapshot. Over a year, that’s 36 credits ($0.18) for complete NOI tracking with market value updates. The rent gap column shows whether you’re charging above or below market rate for each property.
How does NOI relate to other investment metrics?
NOI is the starting point for three other metrics investors use:
Cap rate equals NOI divided by property value. A property worth $300,000 with $18,000 annual NOI has a 6% cap rate. The API gives you both numbers in one call. See the investor guide for the complete cap rate workflow.
Cash flow equals NOI minus mortgage payment. If that $18,000 NOI property has a $14,400 annual mortgage payment, the cash flow is $3,600 per year ($300 per month). The API provides the NOI inputs. You add your mortgage terms.
Cash-on-cash return equals annual cash flow divided by total cash invested. If you put $75,000 down on that property and net $3,600 per year in cash flow, your cash-on-cash return is 4.8%. This measures the return on the actual money you put in, not the total property value.
How much does NOI analysis cost?
| Workflow | API calls | Credits | Cost |
|---|---|---|---|
| Single property NOI | 1 | 1 | $0.005 |
| Compare 5 properties | 5 | 5 | $0.025 |
| Market scan (40 properties) | 1 | 1 | $0.005 |
| Portfolio tracking (10 units/month) | 10 | 10 | $0.05 |
| Annual portfolio tracking (10 units) | 120 | 120 | $0.60 |
The market scan is the best value. One search call returns enough data to calculate NOI for every property in the results. No per-property detail lookups needed.
Start calculating NOI in 60 seconds
Go to zillapi.com and sign up. Get 100 free credits with no credit card.
Look up a rental property you own or one you’re considering. Check the rent estimate and tax amount. Run the NOI calculation. Compare the cap rate to your target. If it hits your threshold, dig deeper with actual expense numbers.
For the full investment analysis with cash-on-cash returns, see the investor guide. For ARV calculations on fix-and-flip deals, see the ARV guide. For market-level analytics, see the data analytics guide. For getting your API key, follow the step-by-step walkthrough.
Frequently asked questions
What is NOI in real estate?
NOI stands for net operating income. It is the annual income a rental property generates after subtracting all operating expenses but before subtracting mortgage payments and income taxes. The formula is gross rental income minus vacancy losses minus operating expenses. Operating expenses include property taxes, insurance, maintenance, property management fees, and utilities paid by the owner. NOI is the standard measure for comparing rental property profitability.
How do I calculate NOI with the Zillow API?
Pull the rent estimate and tax amount from the Zillapi property response. Estimate insurance at 0.4% of property value, maintenance at 10% of gross rent, vacancy at 5% of gross rent, and management at 8% of gross rent. Subtract all operating expenses from the annual rent. One API call returns the rent estimate, tax amount, and property value needed for the calculation. Total cost is 1 credit ($0.005).
What is a good NOI for a rental property?
NOI itself is a dollar amount, not a ratio. Investors evaluate NOI through the cap rate, which is NOI divided by property value. A cap rate of 5 to 8% is considered healthy for most residential rental markets. A cap rate below 4% means the property is expensive relative to its income. Above 8% suggests higher risk or a less desirable market. The right target depends on the market, property type, and investor strategy.
What expenses are included in NOI?
NOI includes property taxes, property insurance, maintenance and repairs, property management fees, utilities paid by the owner, landscaping, pest control, and common area maintenance. NOI does not include mortgage payments, income taxes, depreciation, capital expenditures, or tenant improvements. The distinction matters because NOI measures the property’s operating performance independent of how it is financed.
How do I calculate cap rate from NOI?
Cap rate equals NOI divided by property value, expressed as a percentage. If a property generates $15,000 in annual NOI and is worth $250,000, the cap rate is 6%. The API returns both the rent estimate (for calculating NOI) and the Zestimate (for the property value denominator). One API call gives you both numbers for any U.S. property.
Can I track NOI for a portfolio of rental properties?
Yes. Loop through your property addresses, pull the rent estimate and tax data for each one, calculate NOI with your actual operating expenses, and aggregate the results. The API returns fresh data on every call so your portfolio NOI stays current. For a 20-unit portfolio, the monthly tracking costs 20 credits ($0.10). Store the results monthly to track NOI trends over time.