Advanced
25 min

Underwrite STR Properties with API Data

Build DSCR calculations and lender-ready market rent analyses for short-term rental lending using real comparable data and market metrics.

1

Understand DSCR Lending for STR Properties

DSCR (Debt Service Coverage Ratio) lenders need third-party market data to validate short-term rental income projections. Unlike traditional mortgages that rely on W-2 income, DSCR loans qualify based on the property's ability to service its own debt.

Formula

NOI / Debt
DSCR = Net Operating Income / Annual Debt Service

Minimum

1.25x
Most lenders require DSCR at or above 1.25

Haircut

20%
Industry standard reduction to gross STR income

How DSCR is calculated:

1. Start with gross STR income (use P50 median as base case)
2. Apply 20% reduction for conservatism: Adjusted Income = Gross x 0.80
3. Subtract operating expenses (insurance, tax, HOA, maintenance, utilities)
4. Result is Net Operating Income (NOI)
5. DSCR = NOI / Annual Debt Service (mortgage principal + interest)

2

Pull Revenue Estimate

Call GET /calculator/estimate with the subject property address and specifications. The response includes revenue at all percentiles (avg, p25, p50, p75, p90). Use P50 (median) as the base case and P25 as the stress case for conservative underwriting.

Python

import requests

response = requests.get(
    "https://api.airroi.com/calculator/estimate",
    headers={"X-API-KEY": "YOUR_API_KEY"},
    params={
        "address": "742 Evergreen Terrace, Nashville, TN 37203",
        "bedrooms": 3,
        "baths": 2,
        "guests": 6,
        "currency": "usd",
    },
)

data = response.json()
percentiles = data["percentiles"]["revenue"]

print(f"Average Revenue:  ${percentiles['avg']:,.0f}")
print(f"P25 (Stress):     ${percentiles['p25']:,.0f}")
print(f"P50 (Base Case):  ${percentiles['p50']:,.0f}")
print(f"P75 (Upside):     ${percentiles['p75']:,.0f}")
print(f"P90 (Top Perf.):  ${percentiles['p90']:,.0f}")

3

Validate with Comparables

The /calculator/estimate response includes up to 25 comparable listings. Extract their TTM revenue, occupancy, and ADR to verify that the estimate is grounded in real comparable performance. This gives lenders confidence that the projection is data-driven, not speculative.

Python

# The /calculator/estimate response includes comparable listings
comps = data.get("comparable_listings", [])

print(f"Comparable Listings Used: {len(comps)}")
print(f"{'ID':<12} {'Revenue':>10} {'Occ':>6} {'ADR':>8} {'Dist':>6}")
print("-" * 48)

for comp in comps[:10]:
    print(
        f"{comp['listing_id']:<12} "
        f"${comp['revenue']:>8,.0f} "
        f"{comp['occupancy']:>5.0%} "
        f"${comp['average_daily_rate']:>6,.0f} "
        f"{comp.get('distance_miles', 0):>4.1f}mi"
    )

# Calculate comp statistics
revenues = [c["revenue"] for c in comps]
print(f"\nComp Revenue Range: ${min(revenues):,.0f} - ${max(revenues):,.0f}")
print(f"Comp Revenue Median: ${sorted(revenues)[len(revenues)//2]:,.0f}")

4

Market Context

Call POST /markets/summary for the local market to contextualize the property estimate. This reveals whether the market is healthy (high occupancy, stable ADR), growing (increasing listings), or at risk of oversaturation. Lenders want to see that the subject property operates in a viable market.

Python

import requests

response = requests.post(
    "https://api.airroi.com/markets/summary",
    headers={
        "X-API-KEY": "YOUR_API_KEY",
        "Content-Type": "application/json",
    },
    json={
        "market": {
            "country": "us",
            "region": "tennessee",
            "locality": "nashville",
        },
        "currency": "usd",
    },
)

market = response.json()
print(f"Market: Nashville, TN")
print(f"Active Listings: {market['active_listings_count']:,}")
print(f"Avg Occupancy:   {market['occupancy']:.0%}")
print(f"Avg ADR:         ${market['average_daily_rate']:,.0f}")
print(f"Avg Revenue:     ${market['revenue']:,.0f}")
print(f"Avg RevPAR:      ${market['revpar']:,.0f}")

# Market health indicators
print(f"\nMarket Health Indicators:")
print(f"  Occupancy {'above' if market['occupancy'] > 0.55 else 'below'} "
      f"55% threshold: {'Healthy' if market['occupancy'] > 0.55 else 'Caution'}")

5

Calculate DSCR

Now assemble the DSCR calculation. Start with the P50 gross revenue, apply the 20% haircut, subtract operating expenses to get NOI, then divide by annual debt service.

Line ItemAmount
Gross STR Revenue (P50)$85,000
20% Haircut-$17,000
Adjusted Income$68,000
Operating Expenses-$20,000
Net Operating Income (NOI)$48,000
Annual Debt Service ($450K at 7.25%, 30yr)$36,848
DSCR1.30

Python

# DSCR Calculation
# Industry standard: 20% reduction to gross STR income

gross_str_revenue = percentiles["p50"]  # Base case: $85,000
haircut = 0.80                          # 20% reduction
adjusted_income = gross_str_revenue * haircut

# Operating expenses
expenses = {
    "Insurance":      2400,
    "Property Tax":   6000,
    "HOA":            3600,
    "Maintenance":    3000,
    "Utilities":      3600,
    "Platform Fees":  1400,  # ~2% of adjusted income
}
total_expenses = sum(expenses.values())

# Net Operating Income
noi = adjusted_income - total_expenses

# Debt service (annual mortgage P&I)
loan_amount = 450000
interest_rate = 0.0725
loan_term_months = 360
monthly_payment = (
    loan_amount
    * (interest_rate / 12)
    * (1 + interest_rate / 12) ** loan_term_months
    / ((1 + interest_rate / 12) ** loan_term_months - 1)
)
annual_debt_service = monthly_payment * 12

# DSCR
dscr = noi / annual_debt_service

print(f"Gross STR Revenue (P50):  ${gross_str_revenue:,.0f}")
print(f"20% Haircut Applied:      ${gross_str_revenue - adjusted_income:,.0f}")
print(f"Adjusted Income:          ${adjusted_income:,.0f}")
print(f"Operating Expenses:       ${total_expenses:,.0f}")
print(f"Net Operating Income:     ${noi:,.0f}")
print(f"Annual Debt Service:      ${annual_debt_service:,.0f}")
print(f"DSCR:                     {dscr:.2f}")
print(f"Meets 1.25 Threshold:     {'Yes' if dscr >= 1.25 else 'No'}")

6

Sensitivity Analysis

Show how DSCR changes across revenue scenarios (P25, P50, P75) and interest rates. This helps lenders understand the risk profile and identify the break-even point where the property no longer meets the 1.25x DSCR threshold.

RateP25 ($62K)P50 ($85K)P75 ($102K)
6.50%0.861.361.73
7.00%0.831.311.67
7.25%0.811.301.64
7.50%0.791.281.62
8.00%0.761.241.56

Key Findings:

P25 (stress case) fails at all rates tested — the property cannot service debt at bottom-quartile performance.
P50 (base case) passes at rates up to ~7.75% before breaking the 1.25x threshold.
P75 (upside) comfortably passes at all rates tested.

Python

import itertools

# Revenue scenarios from API percentiles
revenue_scenarios = {
    "P25 (Stress)": percentiles["p25"],  # $62,000
    "P50 (Base)":   percentiles["p50"],  # $85,000
    "P75 (Upside)": percentiles["p75"],  # $102,000
}

# Interest rate scenarios
rate_scenarios = [0.065, 0.070, 0.0725, 0.075, 0.080]

# Fixed assumptions
loan_amount = 450000
loan_term = 360
total_expenses = 20000
haircut = 0.80

print(f"{'Scenario':<16} {'Rate':>6} {'Revenue':>10} {'NOI':>10} "
      f"{'Debt':>10} {'DSCR':>6} {'Pass':>5}")
print("-" * 70)

for rev_name, revenue in revenue_scenarios.items():
    for rate in rate_scenarios:
        adjusted = revenue * haircut
        noi = adjusted - total_expenses
        monthly = (
            loan_amount * (rate / 12)
            * (1 + rate / 12) ** loan_term
            / ((1 + rate / 12) ** loan_term - 1)
        )
        debt = monthly * 12
        dscr = noi / debt

        print(
            f"{rev_name:<16} {rate:>5.1%} ${revenue:>8,.0f} "
            f"${noi:>8,.0f} ${debt:>8,.0f} {dscr:>5.2f} "
            f"{'Yes' if dscr >= 1.25 else 'NO':>5}"
        )
    print()

# Identify break-even
for rev_name, revenue in revenue_scenarios.items():
    adjusted = revenue * haircut
    noi = adjusted - total_expenses
    for rate in [r / 1000 for r in range(50, 100)]:
        monthly = (
            loan_amount * (rate / 12)
            * (1 + rate / 12) ** loan_term
            / ((1 + rate / 12) ** loan_term - 1)
        )
        if noi / (monthly * 12) < 1.25:
            print(f"{rev_name} breaks 1.25 DSCR at {rate:.1%} rate")
            break

7

Build the Market Rent Analysis Document

Assemble all data points into a lender-ready market rent analysis. This document should include: subject property details, revenue projection with source citation, comparable properties table, market health indicators, DSCR calculation, and risk factors.

Subject Property

Address, specs, location

Revenue Projection

P25/P50/P75/P90 with source

Comparables Table

10-25 comp listings with metrics

Market Health

Occupancy, ADR, supply trends

DSCR Calculation

NOI / Debt Service with breakdown

Risk Factors

Regulatory, seasonal, market risks

Python

def build_market_rent_analysis(property_data, estimate, market, comps):
    """Generate a lender-ready market rent analysis document."""
    report = []
    report.append("=" * 60)
    report.append("MARKET RENT ANALYSIS - SHORT-TERM RENTAL")
    report.append("=" * 60)

    # Section 1: Subject Property
    report.append("\n1. SUBJECT PROPERTY")
    report.append(f"   Address:    {property_data['address']}")
    report.append(f"   Bedrooms:   {property_data['bedrooms']}")
    report.append(f"   Bathrooms:  {property_data['baths']}")
    report.append(f"   Max Guests: {property_data['guests']}")

    # Section 2: Revenue Projection
    p = estimate["percentiles"]["revenue"]
    report.append("\n2. REVENUE PROJECTION")
    report.append(f"   Source: AirROI Enterprise API (airroi.com)")
    report.append(f"   Date:   {datetime.now().strftime('%Y-%m-%d')}")
    report.append(f"   P25 (Conservative): ${p['p25']:,.0f}")
    report.append(f"   P50 (Median):       ${p['p50']:,.0f}")
    report.append(f"   P75 (Above Avg):    ${p['p75']:,.0f}")
    report.append(f"   P90 (Top Perf):     ${p['p90']:,.0f}")

    # Section 3: Comparable Properties
    report.append("\n3. COMPARABLE PROPERTIES")
    report.append(f"   {'ID':<12} {'Rev':>10} {'Occ':>6} {'ADR':>8}")
    for c in comps[:10]:
        report.append(
            f"   {c['listing_id']:<12} "
            f"${c['revenue']:>8,.0f} "
            f"{c['occupancy']:>5.0%} "
            f"${c['average_daily_rate']:>6,.0f}"
        )

    # Section 4: Market Health
    report.append("\n4. MARKET HEALTH INDICATORS")
    report.append(f"   Market:          {market.get('market_name', 'N/A')}")
    report.append(f"   Active Listings: {market['active_listings_count']:,}")
    report.append(f"   Avg Occupancy:   {market['occupancy']:.0%}")
    report.append(f"   Avg ADR:         ${market['average_daily_rate']:,.0f}")
    report.append(f"   Avg Revenue:     ${market['revenue']:,.0f}")

    # Section 5: DSCR Calculation
    report.append("\n5. DSCR CALCULATION")
    report.append(f"   Gross Revenue (P50):  ${p['p50']:,.0f}")
    report.append(f"   20% Haircut:          ${p['p50'] * 0.20:,.0f}")
    report.append(f"   Adjusted Income:      ${p['p50'] * 0.80:,.0f}")

    # Section 6: Risk Factors
    report.append("\n6. RISK FACTORS")
    report.append("   - STR regulatory changes in municipality")
    report.append("   - Seasonal demand fluctuation")
    report.append("   - Market supply growth / saturation")
    report.append("   - Platform policy changes")

    report.append("\n" + "=" * 60)
    report.append("Data sourced from AirROI Enterprise API (airroi.com)")
    report.append("Tracking 20M+ properties across 190+ countries")
    report.append("=" * 60)

    return "\n".join(report)

# Generate the report
report = build_market_rent_analysis(
    property_data={"address": "742 Evergreen Terrace, Nashville, TN",
                    "bedrooms": 3, "baths": 2, "guests": 6},
    estimate=data,
    market=market,
    comps=comps,
)
print(report)

Continue Learning

Keep exploring the AirROI API with these related tutorials.

Frequently Asked Questions

Yes. Many DSCR and non-QM lenders accept third-party market rent analyses from platforms like AirROI as supporting documentation for STR income projections. The key is providing transparent data sources, comparable properties, and clear methodology. Always confirm specific documentation requirements with the lending institution.

Cite as: "Revenue projections sourced from AirROI Enterprise API (airroi.com), a third-party STR analytics platform tracking 20M+ properties across 190+ countries. Data retrieved [date]." Include the specific endpoint used, percentile level cited, and number of comparable properties analyzed.

Refresh data at each milestone: initial screening, formal application, and closing. If more than 30 days have passed since the last pull, refresh to capture any market shifts. AirROI data is updated on a rolling basis, so each API call returns the most current available data.

Yes. Use a loop to call /calculator/estimate for each property address, then call /markets/summary for each unique market. The API supports up to 10 requests per second, so a portfolio of 50 properties can be screened in under a minute.

Market rent for STR is the projected revenue based on comparable properties in the area, which is what the AirROI API provides. Actual rent is what a specific listing has earned historically. For underwriting, market rent (specifically the P50 median with a 20% haircut) is the conservative standard because it removes operator-specific performance variation.

The AirROI API returns occupancy rates that already reflect actual vacancy. When using P50 occupancy, you are using the median performance including real vacancy. The 20% haircut applied to gross revenue provides additional conservatism beyond the embedded vacancy. Do not double-count vacancy by both using P25 occupancy and applying a haircut.

STR operating expenses typically range from 25-40% of gross revenue depending on the management model. Self-managed properties run 20-25% (lower management fees, owner handles some tasks). Professionally managed properties run 35-40% (full management fee of 20-25%, plus all service costs). Key line items: property management, cleaning, maintenance, insurance, property tax, utilities, platform fees, and supplies.

Always research local STR regulations before underwriting. Check if the municipality requires permits/licenses, has occupancy limits, enforces minimum stay requirements, or has announced regulatory changes. Factor regulatory risk into your sensitivity analysis by modeling scenarios where STR income is partially or fully restricted. The market data from AirROI reflects current active listings, which indicates the existing regulatory environment.

Ready to Build?

Get your API key and start making calls in minutes.
made with