Every serious real estate investor eventually builds some version of the same spreadsheet. It has a cell for the purchase price, cells for your financing assumptions, a section that calculates NOI, and a row at the bottom that spits out cap rate and cash-on-cash return. The spreadsheet is fine for analyzing one deal at a time when a broker sends you a pretty PDF. It completely falls apart when you are trying to screen 30 or 50 deals a week from MLS feeds, when the broker's rent roll is three years old, or when you need to compare projected rent growth across five different markets before committing to an acquisition strategy.

The missing piece is always the same: where do you get reliable, current rent data for a property you do not own yet? You cannot call a leasing office and ask. The broker's pro forma is marketing material, not underwriting. Zillow will give you a rough neighborhood estimate that is often hundreds of dollars off for a specific unit type. The only repeatable answer is a rental data API - and once you have one, you can build the investment analysis tool your spreadsheet was always trying to be.

This tutorial walks through exactly that. We will cover the metrics that matter, the API calls that power them, and the full Python implementation for an investment analysis tool you can run against any address in the US.

What Rental Investors Actually Analyze Before Buying

Before writing a single line of code, it is worth being precise about which numbers drive buy/no-buy decisions. Experienced investors focus on four metrics, roughly in order of importance:

Gross Rental Yield

Gross yield is the simplest measure: annual gross rent divided by purchase price, expressed as a percentage. A property you buy for $350,000 that rents for $2,400/month has a gross yield of 8.23% ($28,800 / $350,000). It is a quick filter metric - you know within seconds whether a deal is even worth analyzing further. Most serious investors in 2026 are targeting 7%+ gross yield as an entry threshold in secondary markets; primary markets often clear 4-5% and justify that with appreciation potential.

Cap Rate (Net Operating Income / Purchase Price)

Cap rate is the workhorse of commercial-style real estate analysis. It is independent of financing, which makes it comparable across deals and markets. The formula:

NOI = Gross Rent * (1 - vacancy_rate) - operating_expenses
Cap Rate = NOI / purchase_price

The tricky part is that both gross rent and vacancy rate are estimates for a deal you have not yet closed. That is precisely where a rental data API earns its keep - you get a market-calibrated rent estimate and a local vacancy rate to plug in, rather than accepting whatever the seller tells you.

Cash-on-Cash Return

Cash-on-cash (CoC) is cap rate's leveraged counterpart. It measures annual pre-tax cash flow divided by the actual equity you put in - your down payment plus closing costs. Two deals with the same cap rate can have dramatically different CoC returns depending on the financing terms. In a rising-rate environment like 2025-2026, deals that penciled at 7% CoC with 3% rates are showing negative cash flow at 7.5% debt service. The formula:

Annual Cash Flow = NOI - Annual Debt Service
CoC Return = Annual Cash Flow / Total Cash Invested

5-Year IRR Projection

IRR captures the full time value of the investment - initial equity out, cash flows in each year, and the projected sale proceeds at the end of your hold period. It is more complex to calculate but captures something cap rate and CoC miss: the compounding effect of rent growth over time. If rents in your target market are growing 4.2% per year, that meaningfully changes the 5-year IRR compared to a flat-rent assumption. The rental trends endpoint gives you that growth rate, calibrated to the actual market around the subject property.

The Data Problem: Getting Accurate Rents for a Property You Do Not Own

Most deal analysis tools either ask you to enter the rent yourself (garbage in, garbage out) or use HUD Fair Market Rent figures (which lag actual market conditions by 12-18 months and are not address-specific). Neither approach is defensible for serious underwriting.

A rental data API solves this with three distinct data sources that you should layer together:

  1. Comparable transaction data (POST /comps): The rent you should be able to achieve, based on what similar units in the immediate area are currently leasing for. This is your primary rent input.
  2. Rent growth trends (GET /trends): The year-over-year rent change rate for the submarket around the property. This drives your 3-5 year rent growth projections in the IRR model.
  3. HUD Fair Market Rent (GET /fair-market-rent): A floor check. If your comp-based estimate is significantly below FMR, either your comps are wrong or the property has characteristics that suppress rent. Either way, the discrepancy is a flag worth investigating.

Used together, these three calls give you a rent estimate, a directional confidence check, and a growth rate - everything you need to populate an investment model with market-sourced inputs rather than seller-supplied assumptions.

Architecture of the Investment Analysis Tool

Here is the shape of what we are building. The tool takes structured deal parameters as input, makes three API calls, runs the financial model, and returns a complete analysis object.

Inputs:

API calls:

Outputs:

Example API Response: What the Data Looks Like

Before diving into the analysis code, here is what the combined API data looks like for a subject property - a 3/2 SFR in Indianapolis at a $295,000 purchase price:

# POST /v1/comps response
{
  "estimate": {
    "rent_low": 1780,
    "rent_mid": 1925,
    "rent_high": 2070,
    "confidence_score": 0.82,
    "comp_count": 9,
    "median_price_per_sqft": 1.47,
    "data_freshness_days": 22
  },
  "comps": [
    {
      "address": "4812 N Ritter Ave, Indianapolis, IN 46226",
      "distance_miles": 0.31,
      "bedrooms": 3,
      "bathrooms": 2,
      "sqft": 1340,
      "listed_rent": 1895,
      "normalized_rent": 1908,
      "status": "active",
      "listed_date": "2026-02-18",
      "amenity_match_score": 0.94
    }
  ]
}

# GET /v1/trends response
{
  "zip": "46226",
  "bedrooms": 3,
  "yoy_change_pct": 3.8,
  "vacancy_rate_pct": 6.2,
  "median_income": 52400,
  "population_density": 3840,
  "trend_direction": "increasing",
  "months_of_data": 24
}

# GET /v1/fair-market-rent response
{
  "zip": "46226",
  "bedrooms": 3,
  "fmr": 1612,
  "metro": "Indianapolis-Carmel-Anderson, IN HUD Metro FMR Area",
  "effective_year": 2026
}

A few things worth noting in this response. The confidence_score of 0.82 is strong - we have 9 comps with fresh data. The vacancy_rate_pct of 6.2% is a market-measured input, not a guess. The FMR of $1,612 is well below the comp estimate of $1,925, which confirms this is a market-rate area where rents have outpaced HUD's survey. That gap between FMR and market is actually informative for Section 8 analysis (this property would be an above-FMR unit and ineligible for standard voucher programs without special approval).

Python Code: Full Investment Analysis Using API Data

Here is the complete implementation. It is structured as a single function that takes deal parameters, calls the three endpoints, and returns a fully populated analysis dictionary. The financial math is straightforward - no external libraries required beyond requests and the standard library's numpy for IRR.

import requests
import numpy as np
from dataclasses import dataclass
from typing import Optional

API_BASE = "https://api.rentcompapi.com/v1"
API_KEY = "rc_live_xxxxxxxxxxxxxxxx"

HEADERS = {
    "Authorization": f"Bearer {API_KEY}",
    "Content-Type": "application/json"
}


@dataclass
class DealInputs:
    address: str
    city: str
    state: str
    zip_code: str
    bedrooms: int
    bathrooms: float
    sqft: int
    amenities: dict
    purchase_price: float
    down_payment_pct: float   # e.g. 0.25 for 25%
    interest_rate: float      # e.g. 0.075 for 7.5%
    loan_term_years: int      # e.g. 30
    annual_expenses: float    # taxes + insurance + maintenance + mgmt


def fetch_rent_data(deal: DealInputs) -> dict:
    """Call all three endpoints and return combined data."""

    # 1. Comp estimate
    comp_payload = {
        "address": deal.address,
        "city": deal.city,
        "state": deal.state,
        "zip": deal.zip_code,
        "unit_type": "house",
        "bedrooms": deal.bedrooms,
        "bathrooms": deal.bathrooms,
        "sqft": deal.sqft,
        "amenities": deal.amenities,
        "comp_radius_miles": 0.75,
        "max_comp_age_days": 90
    }
    comp_resp = requests.post(f"{API_BASE}/comps", json=comp_payload, headers=HEADERS)
    comp_resp.raise_for_status()
    comp_data = comp_resp.json()

    # 2. Rent trends
    trend_resp = requests.get(
        f"{API_BASE}/trends",
        params={"zip": deal.zip_code, "bedrooms": deal.bedrooms},
        headers=HEADERS
    )
    trend_resp.raise_for_status()
    trend_data = trend_resp.json()

    # 3. Fair market rent (floor check)
    fmr_resp = requests.get(
        f"{API_BASE}/fair-market-rent",
        params={"zip": deal.zip_code, "bedrooms": deal.bedrooms},
        headers=HEADERS
    )
    fmr_resp.raise_for_status()
    fmr_data = fmr_resp.json()

    return {
        "comps": comp_data,
        "trends": trend_data,
        "fmr": fmr_data
    }


def calculate_monthly_payment(principal: float, annual_rate: float, years: int) -> float:
    """Standard amortizing mortgage payment."""
    monthly_rate = annual_rate / 12
    n_payments = years * 12
    if monthly_rate == 0:
        return principal / n_payments
    payment = principal * (monthly_rate * (1 + monthly_rate) ** n_payments) / \
              ((1 + monthly_rate) ** n_payments - 1)
    return payment


def analyze_deal(deal: DealInputs, hold_years: int = 5) -> dict:
    """
    Run a full investment analysis using live rental data.
    Returns cap rate, CoC, 5-year projections, and IRR.
    """
    # Fetch API data
    api_data = fetch_rent_data(deal)

    # Extract key figures
    rent_mid = api_data["comps"]["estimate"]["rent_mid"]
    confidence_score = api_data["comps"]["estimate"]["confidence_score"]
    comp_count = api_data["comps"]["estimate"]["comp_count"]
    vacancy_rate = api_data["trends"]["vacancy_rate_pct"] / 100
    yoy_growth = api_data["trends"]["yoy_change_pct"] / 100
    fmr = api_data["fmr"]["fmr"]

    # Financing
    loan_amount = deal.purchase_price * (1 - deal.down_payment_pct)
    equity_invested = deal.purchase_price * deal.down_payment_pct
    # Rough closing costs: 2% of purchase price
    closing_costs = deal.purchase_price * 0.02
    total_cash_in = equity_invested + closing_costs

    monthly_payment = calculate_monthly_payment(
        loan_amount, deal.interest_rate, deal.loan_term_years
    )
    annual_debt_service = monthly_payment * 12

    # Year 1 metrics
    gross_annual_rent = rent_mid * 12
    effective_gross_income = gross_annual_rent * (1 - vacancy_rate)
    noi_year1 = effective_gross_income - deal.annual_expenses
    cap_rate = noi_year1 / deal.purchase_price
    gross_yield = gross_annual_rent / deal.purchase_price
    annual_cash_flow_year1 = noi_year1 - annual_debt_service
    coc_return = annual_cash_flow_year1 / total_cash_in

    # Multi-year projection
    projections = []
    cash_flows = [-total_cash_in]  # Year 0: equity out

    current_rent = rent_mid
    for year in range(1, hold_years + 1):
        if year > 1:
            current_rent = current_rent * (1 + yoy_growth)

        yr_gross = current_rent * 12
        yr_egi = yr_gross * (1 - vacancy_rate)
        yr_noi = yr_egi - deal.annual_expenses
        yr_cash_flow = yr_noi - annual_debt_service

        projections.append({
            "year": year,
            "monthly_rent": round(current_rent, 0),
            "gross_rent": round(yr_gross, 0),
            "egi": round(yr_egi, 0),
            "noi": round(yr_noi, 0),
            "cash_flow": round(yr_cash_flow, 0)
        })
        cash_flows.append(yr_cash_flow)

    # Terminal value at end of hold: assume same cap rate, sell at exit NOI / cap_rate
    exit_noi = projections[-1]["noi"]
    exit_value = exit_noi / cap_rate
    remaining_loan = loan_amount  # simplified - full amortization calc omitted
    # Approximate remaining balance using a simple factor
    fraction_paid = hold_years / deal.loan_term_years
    remaining_loan = loan_amount * (1 - fraction_paid * 0.3)
    net_sale_proceeds = exit_value - remaining_loan - (exit_value * 0.06)  # 6% selling costs
    cash_flows[-1] += net_sale_proceeds

    # IRR via numpy
    irr = np.irr(cash_flows) if hasattr(np, 'irr') else float(
        np.real(np.roots(list(reversed(cash_flows)))[0]) - 1
    )
    # numpy.irr deprecated in newer versions - use numpy_financial or manual calc
    try:
        import numpy_financial as npf
        irr = npf.irr(cash_flows)
    except ImportError:
        # Fallback: simple approximation
        total_return = sum(cash_flows[1:]) / abs(cash_flows[0])
        irr = (1 + total_return) ** (1 / hold_years) - 1

    # Confidence flag
    if confidence_score >= 0.75 and comp_count >= 5:
        confidence_flag = "AUTO_APPROVE"
    elif confidence_score >= 0.50 and comp_count >= 3:
        confidence_flag = "REVIEW"
    else:
        confidence_flag = "MANUAL_REQUIRED"

    return {
        "address": f"{deal.address}, {deal.city}, {deal.state} {deal.zip_code}",
        "purchase_price": deal.purchase_price,
        "rent_estimate": rent_mid,
        "confidence_score": confidence_score,
        "confidence_flag": confidence_flag,
        "fmr_floor": fmr,
        "fmr_premium_pct": round((rent_mid - fmr) / fmr * 100, 1),
        "gross_yield_pct": round(gross_yield * 100, 2),
        "cap_rate_pct": round(cap_rate * 100, 2),
        "coc_return_pct": round(coc_return * 100, 2),
        "noi_year1": round(noi_year1, 0),
        "annual_debt_service": round(annual_debt_service, 0),
        "total_cash_in": round(total_cash_in, 0),
        "irr_5yr_pct": round(irr * 100, 2),
        "vacancy_rate_pct": round(vacancy_rate * 100, 1),
        "rent_growth_rate_pct": round(yoy_growth * 100, 1),
        "projections": projections,
        "neighborhood": {
            "median_income": api_data["trends"]["median_income"],
            "population_density": api_data["trends"]["population_density"],
            "trend_direction": api_data["trends"]["trend_direction"]
        }
    }


# --- Example usage ---
if __name__ == "__main__":
    deal = DealInputs(
        address="4801 N Central Ave",
        city="Indianapolis",
        state="IN",
        zip_code="46226",
        bedrooms=3,
        bathrooms=2.0,
        sqft=1380,
        amenities={"in_unit_laundry": False, "parking": "driveway", "pet_friendly": True},
        purchase_price=295000,
        down_payment_pct=0.25,
        interest_rate=0.075,
        loan_term_years=30,
        annual_expenses=9200   # taxes $4k, insurance $1.8k, maintenance $2k, mgmt 8%
    )

    result = analyze_deal(deal)

    print(f"Address:       {result['address']}")
    print(f"Rent Estimate: ${result['rent_estimate']:,.0f}/mo (confidence: {result['confidence_score']:.2f})")
    print(f"Gross Yield:   {result['gross_yield_pct']}%")
    print(f"Cap Rate:      {result['cap_rate_pct']}%")
    print(f"CoC Return:    {result['coc_return_pct']}%")
    print(f"5-Yr IRR:      {result['irr_5yr_pct']}%")
    print(f"Flag:          {result['confidence_flag']}")
    print()
    print("Year-by-Year Projections:")
    for yr in result['projections']:
        print(f"  Year {yr['year']}: Rent ${yr['monthly_rent']:,.0f}/mo | "
              f"NOI ${yr['noi']:,.0f} | Cash Flow ${yr['cash_flow']:,.0f}")

Using Rent Trends Data: Interpreting yoy_change_pct

The yoy_change_pct field from the trends endpoint is not just a directional indicator - it is a direct input to your rent growth assumption in the 5-year projection. Using 3.8% annual growth on a $1,925 starting rent means you are projecting $2,304 by Year 5. That $379/month difference in rent produces roughly $3,400 more NOI annually in Year 5 compared to a flat-rent assumption. Compounded across a 5-year hold with a terminal sale, the IRR difference can be 2-3 percentage points.

A few interpretive notes on the field:

Vacancy Rate as an Input: Using vacancy_rate_pct

The most common mistake in DIY investment models is using a fixed 5% vacancy assumption regardless of the market. In some Indianapolis submarkets the actual vacancy rate is 6.2%. In downtown Austin it might be 3.8%. In certain secondary markets in the Midwest it can be 9-11%. Each percentage point of vacancy costs you approximately 1% of gross rent - on a $23,100/year gross rent property, the difference between 5% and 8% vacancy is $693/year, which at a 6 cap translates to $11,550 of value.

The vacancy_rate_pct field from the trends endpoint is the market vacancy rate for the zip code and bedroom count combination. Plug it directly into your effective gross income calculation rather than using an arbitrary assumption. This is one of the highest-leverage accuracy improvements you can make to any investment model - and it is a single field substitution.

Neighborhood Context: Median Income and Population Density as Demand Proxies

The median_income and population_density fields in the trends response are not directly used in the financial calculation, but they are powerful context inputs for deal quality assessment.

Median income acts as a rent affordability ceiling. The standard housing affordability benchmark is 30% of gross income toward housing. If the area median income is $52,400 ($4,367/month), the market affordability ceiling for a single-income household is roughly $1,310/month. Your comp-estimated rent of $1,925 is above that ceiling - which means your tenant pool is likely dual-income households or higher earners who choose this neighborhood. That is a quality signal worth noting: it tells you something about tenant profile and the likely durability of that rent level.

Population density is a proxy for demand depth. Higher density means more potential tenants competing for units, which supports both occupancy and rent growth. A 3,840/sqmi density puts this neighborhood in a dense-suburban range - plenty of demand, not so much turnover pressure as a hyper-urban core. In thin-density exurban markets (under 1,000/sqmi), you are taking on more vacancy risk that the vacancy_rate_pct may not fully capture if the market is transitioning.

Deal screening at scale: The real leverage of this tool is not analyzing one deal - it is running 50 deals per day through an automated pipeline. A typical MLS data feed delivers 20-100 new listings per day in a single metro. With the RentComp API, you can enrich each listing with a comp-based rent estimate, vacancy rate, and growth trend, then auto-score every deal against your investment criteria before a human analyst ever looks at it. Deals that pass the automated screen (confidence score above 0.75, cap rate above your threshold, positive CoC) go into a priority review queue. Everything else gets logged and archived. An analyst who used to manually review 5-8 deals per day can now review 30-40 pre-screened deals with full rent data already populated.

Building a Deal Screening Pipeline at Scale

Scaling the single-deal analysis function into a batch screening pipeline requires three additional components: an MLS feed or deal source, a scheduler, and a results store. Here is the structure:

import time
from typing import List

def screen_deals_batch(deals: List[DealInputs],
                       cap_rate_threshold: float = 0.065,
                       coc_threshold: float = 0.08,
                       confidence_threshold: float = 0.75,
                       delay_seconds: float = 1.2) -> dict:
    """
    Screen a batch of deals. Returns bucketed results.
    delay_seconds: rate limit buffer between API calls.
    """
    results = {
        "auto_approve": [],
        "review": [],
        "reject": [],
        "errors": []
    }

    for i, deal in enumerate(deals):
        try:
            analysis = analyze_deal(deal)

            # Apply screening criteria
            passes_cap = analysis["cap_rate_pct"] / 100 >= cap_rate_threshold
            passes_coc = analysis["coc_return_pct"] / 100 >= coc_threshold
            passes_confidence = analysis["confidence_score"] >= confidence_threshold

            if passes_cap and passes_coc and passes_confidence:
                results["auto_approve"].append(analysis)
            elif passes_cap and passes_coc:
                # Financials pencil but low confidence - needs manual comp review
                results["review"].append(analysis)
            else:
                results["reject"].append({
                    "address": analysis["address"],
                    "cap_rate_pct": analysis["cap_rate_pct"],
                    "coc_return_pct": analysis["coc_return_pct"],
                    "confidence_score": analysis["confidence_score"]
                })

        except Exception as e:
            results["errors"].append({
                "address": deal.address,
                "error": str(e)
            })

        # Respect API rate limits
        if i < len(deals) - 1:
            time.sleep(delay_seconds)

    summary = {
        "total_analyzed": len(deals),
        "auto_approved": len(results["auto_approve"]),
        "needs_review": len(results["review"]),
        "rejected": len(results["reject"]),
        "errors": len(results["errors"])
    }
    results["summary"] = summary
    return results

A few practical notes on running this at volume. First, batch your API calls efficiently - the POST /comps call is the most expensive in terms of compute; space calls 1.2 seconds apart to stay within standard rate limits. Second, cache trend data by zip code - trends do not change hour to hour, so pulling GET /trends once per zip per day is sufficient. Third, persist the raw API responses alongside your analysis results - if you need to rerun the model with different assumptions, you want the raw data without burning more API calls.

Confidence Score Thresholds: When to Auto-Approve vs. Escalate

The confidence_score in the comps response reflects the quality of the underlying comp set - primarily comp count, data freshness, and amenity match score distribution. A score above 0.75 means you have a well-supported estimate that you can use with confidence in automated underwriting. Below 0.75, you have a weaker comp pool and should require human review before committing to a deal.

Here is a practical threshold framework:

For accurate rental comp estimates and the factors that affect confidence scoring, see our guide on rental comp accuracy best practices.

Exporting to a Deal Memo: API Data as Supporting Documentation

When you take a deal to investors, partners, or a lender, the underwriting assumptions need to be sourced and documented. "I ran the numbers on a spreadsheet" does not survive lender due diligence or investor scrutiny. API-sourced comp data does.

The comps response gives you everything you need for a rent section in a deal memo: the comp addresses, their distances from the subject, their listed rents, their normalized rents, and the confidence-weighted point estimate. Attach that as an exhibit. For DSCR loan analysis, lenders increasingly accept API-generated rent schedules as supporting documentation when the data source is credible and the methodology is documented.

A minimal PDF generation approach using Python's reportlab library or a Jinja2 HTML template rendered to PDF via weasyprint can turn the analysis dictionary into a formatted deal memo in seconds. The key fields to include: rent estimate with confidence score, comparable transaction table (address, distance, rent, normalized rent), vacancy rate source, rent growth assumption and its source, and the resulting cap rate / CoC / IRR with clearly labeled assumptions.

The goal is a document that answers the question "where did this rent number come from?" with a defensible, auditable answer - not just "the model said so."

Putting It Together

Real estate investment analysis is fundamentally a data problem. The financial math is not hard - cap rate, CoC, and IRR are straightforward calculations that any analyst can implement in an afternoon. The hard part has always been getting accurate, current, consistent rent inputs without spending hours on manual research for every deal.

A rental data API changes that. Three API calls replace 60-90 minutes of manual research per deal, apply consistent methodology across your entire deal pipeline, and produce a rent estimate with a confidence score that tells you exactly how much to trust it. At 50 deals per day, that is the difference between a one-person deal screening operation and a team of five analysts.

The tool we built here is a starting point. Add your own cap rate thresholds, plug in your market-specific expense assumptions, layer in appreciation models for different metro types, and wire it to your MLS feed. The architecture scales - and with the RentComp API as the data layer, the rent inputs stay accurate as markets move.

Start Analyzing Deals with Live Rental Data

Join the RentComp API waitlist and get founding member pricing locked in - 80% off for life. First 200 signups only.

Join the Waitlist