Skip to content

Foreclosure Data API Tutorial: Build an Internal County Watchlist

Build a county watchlist with the Foreclosure Data Hub API. Follow a Node.js example for unified search, source-record identities, pagination, missing fields, and usage limits.

7 min read
Foreclosure Data Hub Research Team

Research & Analysis

The Foreclosure Data Hub Research Team builds and maintains the pipeline that aggregates U.S. foreclosure, REO, and pre-foreclosure records from public-record and auction sources nationwide. The team works with this data every day and writes about what it shows.

More from Foreclosure Data Hub Research Team
Blue home connected to database cylinders and property records on a navy background.

Turn a County Search into a Repeatable Research Workflow

A foreclosure data API becomes useful when it answers a recurring question: which records in your target county need another look? An internal watchlist gives your team one place to review addresses, reported sale dates, source identifiers, and changes since its last research session.

This guide uses Foreclosure Data Hub's unified search endpoint to retrieve a small county sample and save it locally. You can build on the same pattern for an internal dashboard or research application. Start with a bounded request, inspect the fields, and expand only after you understand the coverage and your account's allowance.

1. Get an API Key and Choose Your Scope

Retail API access is available with an eligible Pro or Business subscription, including the applicable trial limits. Open API key management and check your current usage before making requests. The API documentation is the reference for current eligibility, response limits, and supported parameters.

Keep your key in a server environment variable named FDH_API_KEY. Do not put it in frontend JavaScript, a public repository, or a browser request from your website. If you are evaluating redistribution or a customer-facing data product, review commercial data licensing before designing the integration.

For this example, use Florida and Miami-Dade County. This is a geographic query example, not a promise that a particular property or number of results will be available when you run it.

ParameterExamplePurpose
modeunifiedReturns a single normalized records array
stateFLDisambiguates the county's state
countyMiami-DadeBounds the research area
page1Starts at the first page
page_size5Keeps the first request small

At least one location filter is required. Pairing state and county also makes the intended scope clear when county names repeat across states.

2. Make a Small Request

After setting the environment variable securely in your terminal or server, try:

curl --fail-with-body --silent --show-error \
  -H "x-api-key: $FDH_API_KEY" \
  "https://www.foreclosuredatahub.com/api/v1/properties/search?mode=unified&state=FL&county=Miami-Dade&page=1&page_size=5"

Successful unified responses contain data.records and pagination metadata such as meta.page, meta.per_page, and meta.total_pages. A valid empty records array means the query returned no records at that moment. It does not establish that the county has no foreclosure activity.

3. Build a Small Local Watchlist

Save the following as county-watchlist.mjs and run it with Node.js 20 or later. The script makes one request by default, merges records into a local JSON file, and prints only a count and allowance information. Run this example separately from the cURL check if your remaining trial calls are limited.

import { readFile, writeFile } from "node:fs/promises";

const apiKey = process.env.FDH_API_KEY;
if (!apiKey) throw new Error("Set FDH_API_KEY on the server first.");

const file = "miami-dade-watchlist.json";
const pageSize = 5;
const maxPages = 1; // Increase deliberately after checking your allowance.
let saved = [];
try {
  saved = JSON.parse(await readFile(file, "utf8"));
  if (!Array.isArray(saved)) throw new Error("Watchlist must be an array.");
} catch (error) {
  if (error.code !== "ENOENT") throw error;
}

const watchlist = new Map(saved.map((row) => [row.watchlist_key, row]));
let received = 0;

for (let page = 1; page <= maxPages; page++) {
  const query = new URLSearchParams({
    mode: "unified",
    state: "FL",
    county: "Miami-Dade",
    page: String(page),
    page_size: String(pageSize),
  });
  const response = await fetch(
    `https://www.foreclosuredatahub.com/api/v1/properties/search?${query}`,
    {
      headers: { "x-api-key": apiKey },
      signal: AbortSignal.timeout(15000),
    }
  );
  if (!response.ok) {
    const retryAfter = response.headers.get("Retry-After");
    throw new Error(
      `HTTP ${response.status}; request ID: ` +
      `${response.headers.get("X-Request-ID") ?? "unavailable"}; ` +
      `Retry-After: ${retryAfter ?? "not supplied"}. Check API docs.`
    );
  }

  const payload = await response.json();
  const records = payload.data?.records;
  const totalPages = payload.meta?.total_pages;
  if (!Array.isArray(records) || !Number.isInteger(totalPages)) {
    throw new Error("Unexpected unified response format.");
  }
  const observedAt = new Date().toISOString();
  for (const record of records) {
    if (!record.source || record.row_id == null) {
      throw new Error("A record is missing its source identity.");
    }
    const key = `${record.source}:${record.row_id}`;
    watchlist.set(key, {
      ...record,
      watchlist_key: key,
      last_seen_by_app: observedAt,
    });
  }
  received += records.length;

  const remaining = response.headers.get("X-Export-Quota-Remaining");
  console.log({ page, received: records.length, remainingRows: remaining });
  if (page >= totalPages || records.length === 0) break;
  if (remaining !== null && Number(remaining) < pageSize) break;
}

await writeFile(file, JSON.stringify([...watchlist.values()], null, 2));
console.log({ received, stored: watchlist.size });

Run it with node county-watchlist.mjs. Keep the resulting file private: it contains property research data. This is a single-process starter script; a shared application should use a database, access controls, and a unique key on the source-record identity.

4. Understand What the Watchlist Preserves

Use the pair source and row_id to identify a delivered source record. An address alone is a poor key: spelling can vary, units matter, and several source records can refer to one property. This key does not establish a universal property identity across feeds or guarantee that upstream identifiers never change.

Preserve the raw reported values alongside your own notes. Fields such as owner names, lender names, estimated values, and sale dates can be missing. A missing mortgage amount is unknown, not zero; an estimate is not an appraisal or payoff statement.

The script merges observations and retains older records. It does not mark an absent property as sold or cancelled. With one page, you have a sample rather than a complete county inventory. Even after paging through a result set, records can change while requests are running. A production watchlist should record query scope, run completion, observation times, and the source's reported status before assigning a change label.

5. Handle Pagination and Allowances Deliberately

Unified search uses page_size; the single-source data endpoint uses per_page. Do not substitute one parameter for the other. Keep the page size fixed during a multi-page run and stop at the returned total_pages or your own smaller request budget.

Successful retail property responses expose remaining row allowance in headers including X-Export-Quota-Remaining. Despite the header name, it applies to these API responses. Repeated requests can consume rows again, so the example intentionally avoids automatic retries.

ResponseNext action
400Correct the filters or pagination before trying again
401Check whether the key is present and valid
403Read the error; check plan access or remaining allowance
429Stop and respect Retry-After when supplied
Network error or 5xxInvestigate, then retry only within a small explicit budget

For a 403 EXPORT_QUOTA_EXCEEDED response, use the reported remaining allowance and reset information. Do not keep resending the same oversized request. A timeout also leaves uncertainty about whether the server completed the request; local deduplication does not undo usage charges.

From Sample to Internal Integration

Once the sample is useful, decide how often your team actually needs fresh observations. Align your polling with research needs and source update patterns, rather than assuming a continuous live feed. Store successful observations before expanding the query, and check the original organizer when a sale decision depends on current status.

If you prefer a dashboard workflow, use saved searches and email alerts. If your team needs to understand ambiguous fields first, start with owner and lender verification.

Frequently Asked Questions

How do I access the Foreclosure Data Hub API?

Retail API access requires an eligible Pro or Business subscription, including applicable trial limits. Generate a key in the dashboard API section, keep it server-side, and check the current API documentation and account allowance before making requests.

How do I paginate unified foreclosure search results?

Use mode=unified with page and page_size. Read meta.total_pages, keep the page size fixed during a run, and stop at your own request budget or remaining allowance. The single-source endpoint uses per_page instead.

Does pre_foreclosure=true include every early-stage filing?

No. It selects scheduled-sale preforeclosures with a known sale date today or later in UTC and excludes REO-only feeds and REO asset types. Records without a known sale date are excluded.

Can I redistribute records returned by a retail API key?

Do not assume retail access grants redistribution rights. Review the applicable terms and commercial data licensing before building a customer-facing data product or redistributing records.

More Articles

Contact Support