Set up Python and your RapidAPI key
You need Python 3.9 or newer and a Foreclosure Finder subscription on RapidAPI. The default example uses BASIC-compatible parameters and the Python standard library, so there are no packages to install. Choose BASIC to test core listing data, then copy the key for your subscribed RapidAPI application.
Set the key in your terminal rather than putting it in the script. On macOS or Linux:
export RAPIDAPI_KEY="YOUR_RAPIDAPI_KEY"In Windows PowerShell, use $env:RAPIDAPI_KEY="YOUR_RAPIDAPI_KEY". Keep the key out of public repositories and browser code. Customer requests go to the RapidAPI hostname used below; the website’s domain is not an alternative customer API endpoint.
Search a ZIP code and inspect one result
Save the following as foreclosure_lookup.py. The default request searches the five supported sources for ZIP 30067 within an explicit 25-mile radius, requests five rows and fetches details for the first returned listing. It makes one search request and, when a usable result exists, one detail request. It does not fetch the whole market or automatically retry.
import argparse
import json
import os
import sys
from urllib.error import HTTPError, URLError
from urllib.parse import quote, urlencode
from urllib.request import Request, urlopen
HOST = "foreclosure-finder1.p.rapidapi.com"
SOURCES = {"auction", "hud", "fanniemae", "freddiemac", "redfin"}
def api_get(path, params, key):
request = Request("https://" + HOST + path + "?" + urlencode(params), headers={
"X-RapidAPI-Key": key, "X-RapidAPI-Host": HOST,
"Accept": "application/json",
})
try:
with urlopen(request, timeout=60) as response:
data = json.load(response)
except HTTPError as error:
try:
body = json.loads(error.read())
except (ValueError, UnicodeError):
body = {}
if not isinstance(body, dict):
body = {}
message = "HTTP " + str(error.code)
for field in ("code", "action", "requestId"):
if body.get(field):
message += " | " + field + ": " + str(body[field])
if error.headers.get("Retry-After"):
message += " | Retry-After: " + error.headers["Retry-After"]
raise RuntimeError(message) from None
if not isinstance(data, dict):
raise RuntimeError("Expected a JSON object from the API.")
return data
def show_warnings(meta, label):
if meta.get("partial") or meta.get("failedSources") or meta.get("skippedStates"):
print(label + ": incomplete coverage or fields; review the warnings below.")
if meta.get("failedSources"):
print("Unavailable sources:", ", ".join(meta["failedSources"]))
if meta.get("skippedStates"):
print("Skipped states:", json.dumps(meta["skippedStates"]))
for warning in meta.get("warnings") or []:
print("Warning:", warning.get("code"), warning.get("action", ""))
def main():
parser = argparse.ArgumentParser()
parser.add_argument("--zipcode", default="30067")
parser.add_argument("--radius", type=int, default=25)
parser.add_argument("--paid-screen", action="store_true")
args = parser.parse_args()
key = os.environ.get("RAPIDAPI_KEY", "").strip()
if not key:
raise RuntimeError("Set RAPIDAPI_KEY before running this script.")
if not 1 <= args.radius <= 100:
raise RuntimeError("Choose a whole-number radius from 1 to 100 miles.")
params = {"zipcode": args.zipcode, "radius": args.radius,
"limit": 5, "sort": "price_asc"}
source = "all"
if args.paid_screen:
source = "auction"
params.update(sort="discount_desc", minDiscount=20)
data = api_get("/zipcode/" + source, params, key)
meta = data.get("meta") or {}
rows = data.get("listings")
if not isinstance(rows, list):
raise RuntimeError("Expected a listings array from the search.")
show_warnings(meta, "Search")
print(meta.get("totalCount", len(rows)), "matches;", len(rows), "returned")
for row in rows:
print(row.get("source"), row.get("listingId"), row.get("address"),
"opening bid / asking price:", row.get("openingBid"))
if not rows:
print("No rows returned. Check filters and warnings; no detail request made.")
return
first = rows[0]
source = first.get("source")
listing_id = first.get("listingId")
if source not in SOURCES or listing_id is None or str(listing_id) == "":
raise RuntimeError("The first result has no usable source or listing ID.")
if args.paid_screen:
print("First result's available deal metrics:")
print(json.dumps(first.get("deal"), indent=2))
params = {"url": first["propertyLink"]} if first.get("propertyLink") else {}
full = api_get("/listing/" + source + "/" + quote(str(listing_id), safe=""),
params, key)
show_warnings(full.get("meta") or {}, "Details")
details = (full.get("listing") or {}).get("details") or {}
print("Available photos:", len(details.get("photos") or []))
print((details.get("description") or "No description supplied.")[:500])
print("Source listing:", first.get("propertyLink") or "Not supplied")
if __name__ == "__main__":
try:
main()
except (HTTPError, URLError, TimeoutError, ValueError, RuntimeError) as error:
print("Request stopped:", error, file=sys.stderr)
sys.exit(1)
Run the default search, or choose your own ZIP and radius. Use py instead of python3 on Windows if that is how Python is installed.
python3 foreclosure_lookup.py
python3 foreclosure_lookup.py --zipcode 77002 --radius 40The 60-second timeout applies to each network operation; it is not a promised API response time or an overall deadline for both requests. Each call, including a cached result or a later manual retry, counts toward your RapidAPI allowance.
Read the search and detail responses
| Value | How to use it |
|---|---|
| meta.totalCount | Matches before pagination. This example asks for only five rows; the count is not a count of unique physical properties. |
| source + listingId | Save this pair as the record key. Different sources can represent the same property. Use the matching pair in the detail route. |
| openingBid | Auction opening bid or REO/MLS asking price. Standard price filters and price_asc use this field, which may be missing or nominal. |
| propertyLink | The original listing URL. Pass this matching URL to the detail request when it is available. |
| listing.details | Detail data nested inside the listing object, rather than at the response root. Missing fields stay unknown. |
BASIC detail responses include available descriptions, photos, contacts, documents and selected simple property features. Expanded structured facts and source extras, including bidding, occupancy, valuations and rental estimates, require a paid plan. No plan can supply a field the source did not publish.
To retrieve another search page, keep the same parameters and add offset=5 to params; the example keeps limit=5. Stop at the reported count. Every page is another call, and the result can change after cache expiry. Keep the detail lookup to the records your application actually needs.
Keep incomplete data visible to your users
HTTP 200 can contain usable but incomplete data. The example prints meta.partial, failed sources, skipped states and warning codes before displaying the available results. PARTIAL_RESULTS identifies missing source or neighboring-state coverage; SOURCE_FIELDS_UNAVAILABLE identifies missing optional fields. They are different conditions.
A Redfin radius warning can indicate that the collected ZIP-page inventory does not cover every property inside the radius. Inspect all warnings, including on a single-source search or detail response. An empty or partial result is not evidence that no foreclosure exists in the market.
For an application, keep these diagnostics alongside the rows and show a clear incomplete-data state. This starter preserves available results; it does not label them a complete market scan. Search caching and source publication delays also mean a fetched bid is a snapshot, not a live bidding feed.
Optional: screen Auction.com listings by discount
With PRO, ULTRA or MEGA, add --paid-screen. The script switches to Auction.com, requests minDiscount=20 and sort=discount_desc, and prints the first result’s available deal object before fetching its details. BASIC returns 403 for these paid filter and sort parameters; run without the flag to keep the free-plan workflow.
python3 foreclosure_lookup.py --zipcode 30067 --paid-screendiscountToEstimate is the percentage difference between the source estimate and the selected bid basis. The API uses an eligible current high bid, otherwise the opening bid, and excludes a basis below $1,000 from deal calculations. deal.basis identifies which price was used. A value of 20 means 20%, not 0.20.
grossYield, when present, is estimated monthly rent multiplied by 12, divided by the bid basis and expressed as a percentage. It excludes expenses, financing and acquisition costs. These are screening calculations based on source figures, not guaranteed returns, an appraisal or a recommendation to bid.
Paid plans give the same expanded available fields with different request allowances. Test the core workflow and your target market first; upgrade when your application needs the paid fields, filters or volume. Review current pricing and overage terms on RapidAPI.
Resolve errors before adding retries
- 400: correct the named parameter. Radius accepts any whole number from 1 to 100; use a five-digit ZIP code.
- 401 or 403: check the RapidAPI host, application key and subscription. On BASIC, remove the paid-screen flag. Gateway authentication errors can have a different body from provider errors.
- 404 on details: refresh the search and use a current listing ID and its matching propertyLink. A listing can disappear between search and detail calls.
- 429: check gateway usage and rate limits. A provider 503 with SOURCE_CAPACITY_EXCEEDED instead identifies shared source capacity; upgrading your plan does not bypass it.
- 5xx or timeout: review the error code and action. Respect Retry-After when present; if adding retries, use a small maximum attempt count and a delay. Do not repeatedly retry invalid input or a removed listing.
The example stops on an HTTP or network error and prints the provider’s request ID when supplied. Save the request ID, status, endpoint path and time for support, without sharing your API key. A non-JSON gateway error still produces an HTTP-status message.