Wholesaling real estate is a numbers game. You look at dozens of properties to find one deal worth pursuing. You pull comps, estimate repairs, run the 70% rule, and make an offer. Then you do it again tomorrow.

The bottleneck is never the negotiation. It is the research.

Most wholesalers spend hours each day scrolling Zillow, cross-referencing tax records, and manually calculating ARV. Some pay $99 per month for PropStream. Others cobble together spreadsheets from three different data sources.

There is a faster way. A single API call returns the property data, tax records, Zestimate, and comparable sales you need to analyze any deal in seconds. Here is how to build a complete wholesaling toolkit with Python and the Zillow API.

What data do wholesalers need from an API?

Every wholesale deal follows the same analysis pattern. You need specific data points at each step, and all of them come from a single property lookup.

Wholesaling stepData neededAPI field
Find dealsListing status, days on markethomeStatus, daysOnZillow
Estimate valueZestimate, tax assessmentzestimate, taxAssessedValue
Calculate ARVRecent comps, sale pricesSearch endpoint (RECENTLY_SOLD)
Set max offerPrice, repairs estimate, ARVprice, zestimate, comps
Build buyer listRecent cash sales in areaSearch endpoint (RECENTLY_SOLD)
Package the dealPhotos, beds, baths, sqft, lot sizephotos, bedrooms, bathrooms, livingArea

One API call returns 300+ fields for any U.S. property. That single call costs 1 credit ($0.005).

For your API key setup, follow the step-by-step walkthrough.

How do I find wholesale deals with the API?

Wholesalers look for motivated sellers. That means properties sitting on the market too long, priced below tax assessment, or listed well under the Zestimate.

This script searches an area and flags potential deals:

import requests, os
API_KEY = os.environ["ZILLAPI_KEY"]
HEADERS = {"Authorization": f"Bearer {API_KEY}"}
def find_wholesale_leads(bbox, max_price=300000):
"""Search for properties that show signs of motivated sellers."""
r = requests.post(
"https://api.zillapi.com/v1/search",
json={
"bbox": bbox,
"listingStatus": "FOR_SALE",
"homeType": ["SINGLE_FAMILY"],
"maxPrice": max_price,
},
headers=HEADERS,
)
listings = r.json()["data"]
print(f"Found {len(listings)} listings in area\n")
leads = []
for prop in listings:
zpid = prop.get("zpid")
if not zpid:
continue
detail = requests.get(
"https://api.zillapi.com/v1/properties/by-zpid",
params={
"zpid": zpid,
"fields": "address,price,zestimate,taxAssessedValue,daysOnZillow,homeStatus,bedrooms,bathrooms,livingArea,lotAreaValue,yearBuilt",
},
headers=HEADERS,
).json()["data"]
price = detail.get("price", 0)
zestimate = detail.get("zestimate", 0)
tax_value = detail.get("taxAssessedValue", 0)
days = detail.get("daysOnZillow", 0)
# Flag deals: priced below Zestimate or on market 60+ days
discount = 0
if zestimate and price:
discount = round((1 - price / zestimate) * 100, 1)
if discount >= 10 or days >= 60:
leads.append({
"address": detail["address"]["streetAddress"],
"price": price,
"zestimate": zestimate,
"discount": f"{discount}%",
"days_on_market": days,
"beds": detail.get("bedrooms"),
"baths": detail.get("bathrooms"),
"sqft": detail.get("livingArea"),
})
return leads
# Search Greenville, SC
bbox = {"north": 34.90, "south": 34.80, "east": -82.35, "west": -82.45}
leads = find_wholesale_leads(bbox)
print(f"Found {len(leads)} potential wholesale leads:\n")
for lead in leads:
print(f" {lead['address']}")
print(f" Price: ${lead['price']:,} | Zestimate: ${lead['zestimate']:,}")
print(f" Discount: {lead['discount']} | Days: {lead['days_on_market']}")
print(f" {lead['beds']}bd/{lead['baths']}ba | {lead['sqft']:,} sqft\n")

The script flags two types of leads. Properties priced 10% or more below the Zestimate suggest a motivated seller. Properties sitting 60+ days on market suggest the seller may be open to a lower offer.

You can tighten or loosen those thresholds based on your market. In competitive areas, try 5% below Zestimate. In slower markets, raise the bar to 15%.

How do I calculate ARV from comps?

After repair value is the price a property would sell for after a full renovation. You calculate it by pulling comparable sales nearby and averaging their prices.

The ARV guide covers the full formula. Here is the wholesaler-specific version:

def calculate_arv(address, radius_sqft=300):
"""Pull comps and calculate ARV for a wholesale deal."""
# Get subject property details
subject = requests.get(
"https://api.zillapi.com/v1/properties/by-address",
params={
"address": address,
"fields": "zpid,address,bedrooms,bathrooms,livingArea,zestimate,latitude,longitude",
},
headers=HEADERS,
).json()["data"]
sqft = subject.get("livingArea", 1500)
beds = subject.get("bedrooms", 3)
lat = subject["latitude"]
lng = subject["longitude"]
# Search for recently sold comps nearby
comps = requests.post(
"https://api.zillapi.com/v1/search",
json={
"bbox": {
"north": lat + 0.01,
"south": lat - 0.01,
"east": lng + 0.01,
"west": lng - 0.01,
},
"listingStatus": "RECENTLY_SOLD",
"homeType": ["SINGLE_FAMILY"],
"minBeds": max(beds - 1, 1),
"maxBeds": beds + 1,
},
headers=HEADERS,
).json()["data"]
# Filter comps by square footage similarity
valid_comps = []
for comp in comps:
comp_sqft = comp.get("livingArea", 0)
if comp_sqft and abs(comp_sqft - sqft) <= radius_sqft:
sale_price = comp.get("price", 0)
if sale_price > 0:
valid_comps.append({
"address": comp["address"]["streetAddress"],
"price": sale_price,
"sqft": comp_sqft,
"price_per_sqft": round(sale_price / comp_sqft, 2),
})
if not valid_comps:
return None
# Sort by price per sqft and take top 5
valid_comps.sort(key=lambda c: c["price_per_sqft"], reverse=True)
top_comps = valid_comps[:5]
arv = round(sum(c["price"] for c in top_comps) / len(top_comps))
print(f"Subject: {address}")
print(f" {beds}bd | {sqft:,} sqft | Zestimate: ${subject.get('zestimate', 0):,}")
print(f"\nComps used ({len(top_comps)}):")
for c in top_comps:
print(f" {c['address']} - ${c['price']:,} ({c['sqft']:,} sqft, ${c['price_per_sqft']}/sqft)")
print(f"\nARV: ${arv:,}")
return arv
arv = calculate_arv("17 Zelma Dr, Greenville, SC 29617")

This pulls recently sold homes within a tight radius, filters by size and bedroom count, and averages the top comps. The result is your after repair value.

For a deeper breakdown of ARV methodology, including renovation-adjusted calculations, see the complete ARV guide.

How do I apply the 70% rule?

The 70% rule is the core formula every wholesaler uses to set a maximum offer. It works like this:

MAO = (ARV x 0.70) - Repair Costs - Assignment Fee

MAO is your Maximum Allowable Offer. The 30% buffer covers the end buyer’s profit, closing costs, holding costs, and surprises.

def wholesale_analyzer(address, repair_cost, assignment_fee=10000):
"""Full wholesale deal analysis with 70% rule."""
arv = calculate_arv(address)
if not arv:
print("Could not calculate ARV. Not enough comps.")
return None
mao = (arv * 0.70) - repair_cost - assignment_fee
print(f"\n{'='*50}")
print(f"WHOLESALE DEAL ANALYSIS")
print(f"{'='*50}")
print(f"ARV (from comps): ${arv:,}")
print(f"70% of ARV: ${round(arv * 0.70):,}")
print(f"Repair costs: ${repair_cost:,}")
print(f"Your assignment fee: ${assignment_fee:,}")
print(f"{'='*50}")
print(f"MAX OFFER (MAO): ${round(mao):,}")
print(f"{'='*50}")
# Deal quality check
if mao <= 0:
print("\nVerdict: NO DEAL. Numbers don't work at any price.")
elif mao < arv * 0.50:
print(f"\nVerdict: TOUGH DEAL. You need to buy at {round(mao/arv*100)}% of ARV.")
print("Only works with a very motivated seller.")
else:
print(f"\nVerdict: POTENTIAL DEAL. Offer at ${round(mao):,} or below.")
print(f"That's {round(mao/arv*100)}% of ARV. Reasonable for a motivated seller.")
return {
"arv": arv,
"mao": round(mao),
"repair_cost": repair_cost,
"assignment_fee": assignment_fee,
"offer_to_arv_ratio": round(mao / arv * 100, 1),
}
# Analyze a deal: $30K in repairs, $10K assignment fee
deal = wholesale_analyzer("17 Zelma Dr, Greenville, SC 29617", repair_cost=30000, assignment_fee=10000)

If the MAO comes back negative or below 50% of ARV, walk away. The numbers do not work. Move on to the next lead.

If the MAO is between 55% and 70% of ARV, you have a workable deal. Make your offer at or below the MAO.

How do I build a cash buyer list?

Your deal is only as good as your buyer list. Cash buyers are investors who purchase properties quickly without financing delays. You can find them by searching for recent sales in your target area.

def find_cash_buyers(bbox, min_purchases=1):
"""Find likely cash buyers from recent sales data."""
r = requests.post(
"https://api.zillapi.com/v1/search",
json={
"bbox": bbox,
"listingStatus": "RECENTLY_SOLD",
"homeType": ["SINGLE_FAMILY"],
},
headers=HEADERS,
)
sales = r.json()["data"]
print(f"Found {len(sales)} recent sales in area\n")
buyers = []
for sale in sales[:30]:
zpid = sale.get("zpid")
if not zpid:
continue
detail = requests.get(
"https://api.zillapi.com/v1/properties/by-zpid",
params={
"zpid": zpid,
"fields": "address,price,zestimate,datePostedString,daysOnZillow,homeStatus",
},
headers=HEADERS,
).json()["data"]
price = detail.get("price", 0)
zestimate = detail.get("zestimate", 0)
days = detail.get("daysOnZillow", 0)
# Investor signals: bought below Zestimate, short time on market
if zestimate and price and price < zestimate * 0.85:
buyers.append({
"address": detail["address"]["streetAddress"],
"sale_price": price,
"zestimate": zestimate,
"discount": round((1 - price / zestimate) * 100, 1),
"days_on_market": days,
})
print(f"Found {len(buyers)} likely investor purchases:\n")
for b in buyers:
print(f" {b['address']}")
print(f" Sold: ${b['sale_price']:,} | Zestimate: ${b['zestimate']:,}")
print(f" Bought {b['discount']}% below market | {b['days_on_market']} days on market\n")
return buyers
bbox = {"north": 34.90, "south": 34.80, "east": -82.35, "west": -82.45}
buyers = find_cash_buyers(bbox)

Properties that sold significantly below the Zestimate often went to investors. They bought at a discount because the seller was motivated, or the property needed work. Those buyers are your target.

Cross-reference the addresses with county records to find the buyer’s name and mailing address. That gives you a direct mail list of active cash buyers in your market.

How do I scan deals in bulk?

Analyzing one property at a time works when you are starting out. But serious wholesalers need to scan entire ZIP codes and neighborhoods every day.

This script scans multiple areas and ranks every property by deal potential:

def bulk_deal_scanner(areas, repair_per_sqft=25, assignment_fee=10000):
"""Scan multiple areas and rank properties by wholesale potential."""
all_deals = []
for area_name, bbox in areas.items():
print(f"\nScanning {area_name}...")
r = requests.post(
"https://api.zillapi.com/v1/search",
json={
"bbox": bbox,
"listingStatus": "FOR_SALE",
"homeType": ["SINGLE_FAMILY"],
},
headers=HEADERS,
)
listings = r.json()["data"]
print(f" {len(listings)} active listings")
for prop in listings[:15]:
zpid = prop.get("zpid")
if not zpid:
continue
detail = requests.get(
"https://api.zillapi.com/v1/properties/by-zpid",
params={
"zpid": zpid,
"fields": "address,price,zestimate,taxAssessedValue,livingArea,bedrooms,bathrooms,daysOnZillow,yearBuilt",
},
headers=HEADERS,
).json()["data"]
price = detail.get("price", 0)
zestimate = detail.get("zestimate", 0)
sqft = detail.get("livingArea", 0)
if not all([price, zestimate, sqft]):
continue
# Estimate ARV as Zestimate (quick proxy for bulk scanning)
arv = zestimate
repair_cost = sqft * repair_per_sqft
mao = (arv * 0.70) - repair_cost - assignment_fee
# Deal score: how far below MAO is the asking price?
if mao > 0:
spread = mao - price
score = round(spread / arv * 100, 1)
else:
spread = 0
score = -99
all_deals.append({
"area": area_name,
"address": detail["address"]["streetAddress"],
"price": price,
"arv": arv,
"mao": round(mao),
"spread": round(spread),
"score": score,
"beds": detail.get("bedrooms"),
"sqft": sqft,
"days": detail.get("daysOnZillow", 0),
})
# Sort by score (highest spread relative to ARV)
all_deals.sort(key=lambda d: d["score"], reverse=True)
print(f"\n{'='*60}")
print(f"TOP WHOLESALE OPPORTUNITIES (ranked by deal score)")
print(f"{'='*60}")
for deal in all_deals[:10]:
if deal["score"] <= 0:
continue
print(f"\n {deal['address']} ({deal['area']})")
print(f" Asking: ${deal['price']:,} | ARV: ${deal['arv']:,} | MAO: ${deal['mao']:,}")
print(f" Spread: ${deal['spread']:,} | Score: {deal['score']}%")
print(f" {deal['beds']}bd | {deal['sqft']:,} sqft | {deal['days']} days on market")
return all_deals
# Scan three neighborhoods
areas = {
"Downtown Greenville": {"north": 34.86, "south": 34.84, "east": -82.38, "west": -82.41},
"Taylors": {"north": 34.93, "south": 34.91, "east": -82.28, "west": -82.32},
"Mauldin": {"north": 34.80, "south": 34.77, "east": -82.28, "west": -82.32},
}
deals = bulk_deal_scanner(areas)

The deal score ranks properties by how far the asking price falls below your MAO. A positive score means the seller is already asking less than your maximum offer. Those are your hottest leads.

Run this script daily. Compare today’s results against yesterday’s to spot new listings and price drops.

How do I package a deal for buyers?

When you find a deal that works, you need to present it to your cash buyers. A professional deal package closes faster than a text message with an address.

def create_deal_package(address, repair_cost, assignment_fee=10000):
"""Generate a complete deal package for cash buyers."""
# Pull full property details
prop = requests.get(
"https://api.zillapi.com/v1/properties/by-address",
params={
"address": address,
"fields": "address,price,zestimate,taxAssessedValue,bedrooms,bathrooms,livingArea,lotAreaValue,yearBuilt,homeType,daysOnZillow,schools,photos,description,latitude,longitude",
},
headers=HEADERS,
).json()["data"]
price = prop.get("price", 0)
arv = prop.get("zestimate", 0)
sqft = prop.get("livingArea", 0)
mao = (arv * 0.70) - repair_cost - assignment_fee
print(f"{'='*60}")
print(f"WHOLESALE DEAL PACKAGE")
print(f"{'='*60}")
print(f"\nProperty: {prop['address']['streetAddress']}")
print(f"City: {prop['address'].get('city', '')}, {prop['address'].get('state', '')}")
print(f"ZIP: {prop['address'].get('zipcode', '')}")
print(f"\n--- Property Details ---")
print(f"Beds: {prop.get('bedrooms')} | Baths: {prop.get('bathrooms')}")
print(f"Sqft: {sqft:,} | Lot: {prop.get('lotAreaValue', 'N/A')}")
print(f"Year Built: {prop.get('yearBuilt', 'N/A')}")
print(f"Type: {prop.get('homeType', 'N/A')}")
print(f"\n--- Financial Summary ---")
print(f"Asking Price: ${price:,}")
print(f"ARV (Zestimate): ${arv:,}")
print(f"Repair Estimate: ${repair_cost:,}")
print(f"Assignment Fee: ${assignment_fee:,}")
print(f"Contract Price: ${round(mao):,}")
print(f"\n--- Buyer's Numbers ---")
end_buyer_cost = round(mao) + assignment_fee
buyer_profit = arv - end_buyer_cost - repair_cost
roi = round(buyer_profit / end_buyer_cost * 100, 1) if end_buyer_cost > 0 else 0
print(f"Buyer's All-In: ${end_buyer_cost + repair_cost:,}")
print(f"Buyer's Profit: ${buyer_profit:,}")
print(f"Buyer's ROI: {roi}%")
print(f"\n--- Photos ---")
photos = prop.get("photos", [])
print(f"{len(photos)} photos available")
print(f"\n--- Nearby Schools ---")
for school in prop.get("schools", [])[:3]:
name = school.get("name", "Unknown")
rating = school.get("rating", "N/A")
print(f" {name} - Rating: {rating}/10")
return {
"address": prop["address"]["streetAddress"],
"price": price,
"arv": arv,
"mao": round(mao),
"buyer_profit": buyer_profit,
"roi": roi,
"photos": len(photos),
}
package = create_deal_package("17 Zelma Dr, Greenville, SC 29617", repair_cost=25000)

The deal package shows your buyer exactly what they are getting. ARV, repair estimate, their all-in cost, expected profit, and ROI. Professional packages with photos and school data close faster than a spreadsheet screenshot.

How much does this cost compared to PropStream?

Most wholesalers use PropStream for deal finding. It works, but it costs $99 per month for a single user. You get a web dashboard with no API access, no custom automation, and no way to build your own tools.

FeaturePropStreamZillapi
Monthly cost$99$5
Annual cost$1,188$54
Property lookupsUnlimited (manual)1,000/mo ($5 plan)
API accessNoYes
Custom automationNoYes (Python, JS, any language)
Bulk scanningManual onlyFully automated
ARV compsBuilt-inVia search endpoint
PhotosYesYes
ZestimatesNoYes
Free tierNo100 credits

A wholesaler analyzing 50 deals per month uses about 200 API credits. That is $1 per month. Even scanning 500 properties per month costs just $2.50.

The real value is automation. PropStream requires you to search manually. With the API, your script runs every morning and sends you a list of new leads before you finish breakfast.

What does a daily wholesaling workflow look like?

Here is how a complete automated wholesaling workflow runs with the API:

  1. The bulk scanner runs across your target ZIP codes every morning
  2. Any property priced 10%+ below Zestimate or sitting 60+ days gets flagged
  3. The script pulls comps for each flagged lead and calculates after repair value
  4. It applies the MAO formula with your standard repair estimates
  5. You get the top 5 deals ranked by score in your inbox
  6. When you put a property under contract, the deal packager builds the buyer sheet

Steps 1 through 5 run automatically. You only step in when a deal scores high enough to make an offer.

For the daily scheduling piece, you can use cron jobs, a cloud function, or a no-code tool. The agents guide shows how to set up automated alerts that run on a schedule.

How do I get started?

Go to zillapi.com and sign up. You get 100 free credits with no credit card required.

That is enough to scan a full neighborhood, analyze 20 deals, and build a starter cash buyer list.

For the full Python setup, see the Python tutorial. For property field documentation, see the property data API guide. For comparable sales methodology, see the comps API guide.

Frequently asked questions

Can I use the Zillow API for wholesaling?

Yes. Third-party REST APIs like Zillapi return property data that covers every step of the wholesaling workflow. You get listing status, Zestimates, tax assessments, comparable sales, and price history for any U.S. property. One API call costs 1 credit ($0.005). The free tier gives 100 credits at signup with no credit card required. That is enough to analyze 100 potential deals before spending a dollar.

How do I calculate ARV with an API?

Search for recently sold properties near the subject property using the search endpoint. Filter for homes with similar square footage, bedroom count, and condition. Average the sale prices of the three to five closest comps. That average is your after repair value. The ARV calculation uses the same comps data that appraisers and flippers rely on. Each search call costs 1 credit.

What is the 70% rule in wholesaling?

The 70% rule says your maximum offer should be 70% of the after repair value minus repair costs minus your assignment fee. For example, if ARV is $250,000, repairs cost $30,000, and your fee is $10,000, your max offer is $135,000. This leaves a 30% buffer for the end buyer to cover profit, closing costs, and holding costs.

How do I find cash buyers using property data?

Search for recently sold properties in your target area and filter for sales where the buyer paid cash or purchased at a price well below market value. Investors buying rental properties and flippers buying fixers both show up in recent sales data. Collect these buyer patterns to build a cash buyer list. Each search costs 1 credit ($0.005).

How much does a wholesaling API cost compared to PropStream?

PropStream costs $99 per month for a single user license. Zillapi costs $5 per month for 1,000 property lookups, or $54 per year for 12,000 lookups. The free tier gives 100 lookups at signup. For a wholesaler analyzing 50 to 100 deals per month, the API costs under $1 per month in credits. You also get full programmatic access to build custom tools.

Can I automate wholesale deal finding with an API?

Yes. Write a Python script that searches for properties in your target areas, pulls detailed data for each result, calculates ARV from comps, and applies the 70% rule. Run that script daily on a schedule. New deals that pass your criteria get flagged automatically. This replaces hours of manual searching on Zillow, Redfin, and PropStream with a single automated workflow.