Foreclosure Finder

Use a foreclosure API with Python: search, inspect and screen listings

Build a search-to-detail workflow with Python’s standard library. Use core listing data first, then add Auction.com discount filters when your application needs paid fields.

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:

Shell
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.

Python · standard library
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.

Shell
python3 foreclosure_lookup.py
python3 foreclosure_lookup.py --zipcode 77002 --radius 40

The 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

ValueHow to use it
meta.totalCountMatches before pagination. This example asks for only five rows; the count is not a count of unique physical properties.
source + listingIdSave this pair as the record key. Different sources can represent the same property. Use the matching pair in the detail route.
openingBidAuction opening bid or REO/MLS asking price. Standard price filters and price_asc use this field, which may be missing or nominal.
propertyLinkThe original listing URL. Pass this matching URL to the detail request when it is available.
listing.detailsDetail 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.

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.