# County Property Records API — County Assessor & Property Tax (`shelvick/county-property-records`) Actor

County property records and property tax records: who owns this property? Property owner lookup by address, parcel lookup by ID, real estate owner search for company and LLC property ownership, assessed value, appraisal and sales history. County assessor data from 1,750+ US counties in 48 states.

- **URL**: https://apify.com/shelvick/county-property-records.md
- **Developed by:** [Scott Helvick](https://apify.com/shelvick) (community)
- **Categories:** Real estate, Business, Developer tools
- **Stats:** 137 total users, 41 monthly users, 100.0% runs succeeded, 1 bookmarks
- **User rating**: No ratings yet

## Pricing

from $42.50 / 1,000 property record resolveds

This Actor is paid per event. You are not charged for the Apify platform usage, but only a fixed price for specific events.
Since this Actor supports Apify Store discounts, the price gets lower the higher subscription plan you have.

Learn more: https://docs.apify.com/actors/running/actors-in-store.md#pay-per-event

## What's an Apify Actor?

An Actor is a serverless cloud program that runs on the Apify platform. It has two run modes.
In Batch mode, an Actor accepts a well-defined JSON input, performs an action which can take anything from a few seconds to a few hours,
and optionally produces a well-defined JSON output, datasets with results, or files in key-value store.
In Standby mode, an Actor provides a web server which can be used as a website, API, or an MCP server.

Apify vocabulary and the platform model are defined once, in the agent quickstart at https://apify.com/agents.md.

## How to integrate an Actor?

If asked about integration, you help developers integrate Actors into their projects.
You adapt to their stack and deliver integrations that are safe, well-documented, and production-ready.

Do not guess an integration path. Every one of them is in the agent quickstart at https://apify.com/agents.md: the Apify MCP server, Agent Skills with the Apify CLI, the JavaScript and Python clients, the REST API, and the account-free path for an agent with no human to sign in. It also carries the rule on stating cost before the first paid run.

For examples already wired to this Actor's own input schema, see the [API](#api) section below.

Each client library has reference documentation the quickstart does not restate: [JavaScript/TypeScript](https://docs.apify.com/api/client/js/docs.md) (`npm install apify-client`) and [Python](https://docs.apify.com/api/client/python/docs.md) (`pip install apify-client`).

# README

County property records are public but fragmented across county assessor and recorder systems. This Actor turns county assessor records into one API: it accepts US property addresses, parcel IDs, and owner names, and returns one normalized record per accepted lookup — owner, assessed and market values, tax history, sale history, and property characteristics where published — usable as a single county property database by agents and pipelines.

The registry spans 2,027 county and county-equivalent routes in 50 states. That count describes registered routes, not identical capabilities: some counties support both address and parcel lookup, some parcel lookup only, some return partial records, and a small number are explicitly unavailable. Read the run's `COVERAGE` artifact for the authoritative capability snapshot.

Coverage totals: **2,027 registry routes across 50 states.**

| State | Covered routes or units | Level | Address lookup | Parcel lookup | Owner search |
|---|---:|---|---:|---:|---:|
| Alaska (AK) | 2 | counties | yes | partial | no |
| Alabama (AL) | 43 | counties | yes | partial | partial |
| Arkansas (AR) | 75 | counties | yes | yes | yes |
| Arizona (AZ) | 10 | counties | yes | yes | partial |
| California (CA) | 42 | counties | yes | partial | no |
| Colorado (CO) | 28 | counties | yes | yes | partial |
| Connecticut (CT) | 169 towns | towns (statewide layer) | yes | yes | yes |
| District of Columbia (DC) | 1 | counties | yes | yes | yes |
| Delaware (DE) | 3 | counties | yes | yes | partial |
| Florida (FL) | 67 | counties | yes | yes | yes |
| Georgia (GA) | 159 | counties | partial | partial | partial |
| Hawaii (HI) | 1 | counties | yes | yes | no |
| Iowa (IA) | 4 | counties | yes | yes | partial |
| Idaho (ID) | 44 | counties | yes | yes | yes |
| Illinois (IL) | 69 | counties | yes | partial | partial |
| Indiana (IN) | 92 | counties | yes | yes | yes |
| Kansas (KS) | 1 | counties | yes | no | no |
| Kentucky (KY) | 2 | counties | yes | yes | no |
| Louisiana (LA) | 29 | counties | yes | partial | partial |
| Massachusetts (MA) | 351 municipalities | municipalities (statewide layer) | yes | yes | yes |
| Maryland (MD) | 24 | counties | yes | yes | no |
| Maine (ME) | 16 | counties | yes | yes | no |
| Michigan (MI) | 55 | counties | yes | partial | partial |
| Minnesota (MN) | 59 | counties | yes | yes | yes |
| Missouri (MO) | 8 | counties | yes | partial | partial |
| Mississippi (MS) | 82 | counties | yes | yes | yes |
| Montana (MT) | 56 | counties | yes | yes | yes |
| North Carolina (NC) | 100 | counties | partial | yes | partial |
| North Dakota (ND) | 53 | counties | yes | yes | yes |
| Nebraska (NE) | 93 | counties | yes | yes | no |
| New Hampshire (NH) | 3 | counties | yes | no | no |
| New Jersey (NJ) | 21 | counties | yes | yes | partial |
| New Mexico (NM) | 13 | counties | yes | yes | partial |
| Nevada (NV) | 2 | counties | yes | yes | no |
| New York (NY) | 62 | counties | partial | partial | yes |
| Ohio (OH) | 16 | counties | yes | partial | partial |
| Oklahoma (OK) | 42 | counties | yes | partial | partial |
| Oregon (OR) | 7 | counties | yes | yes | partial |
| Pennsylvania (PA) | 14 | counties | yes | yes | partial |
| Rhode Island (RI) | 5 | counties | yes | no | no |
| South Carolina (SC) | 13 | counties | yes | yes | partial |
| Tennessee (TN) | 91 | counties | yes | yes | partial |
| Texas (TX) | 253 | counties | partial | partial | yes |
| Utah (UT) | 29 | counties | yes | yes | no |
| Virginia (VA) | 40 | counties | yes | partial | partial |
| Vermont (VT) | 14 | counties | yes | yes | yes |
| Washington (WA) | 12 | counties | yes | partial | partial |
| Wisconsin (WI) | 72 | counties | yes | yes | yes |
| West Virginia (WV) | 55 | counties | yes | partial | yes |
| Wyoming (WY) | 23 | counties | yes | yes | yes |

**Don't see your target?** Run the lookup anyway — lookups outside coverage are never charged, return an explicit not-covered result, and tell us where to expand next.

Shelby County, Tennessee (Memphis, Bartlett, Germantown, Collierville, Cordova, Arlington, Lakeland, and Millington) supports address and parcel-ID lookups, including owner and mailing address, appraised and assessed values, building details, and sales history where published. Owner-name search is not supported for this county.

```json
{
  "query": "827 Krenson Woods Ln, Lakeland, FL 33813",
  "query_type": "address",
  "input_index": 0,
  "status": "completed",
  "billing_eligible": true,
  "county": "Polk",
  "state": "FL",
  "owner_name": "PROGRESS TAMPA 1 LLC",
  "parcel_id": "232912140174000660",
  "assessed_value": 235922,
  "last_sale_date": "2024-05",
  "last_sale_price": 262500,
  "tax_history": [],
  "sale_history": [],
  "field_notes": []
}
```

Virginia coverage includes Accomack, Campbell, James City, and New Kent counties and Hampton, Hopewell, Lynchburg, and Petersburg cities. Crawford County, Pennsylvania, is also covered. These jurisdictions support address, parcel, and owner lookups against the locality's assessor records.

### What this does

- Resolves full street addresses to normalized public property records.
- Resolves exact parcel/account IDs supplied as `STATE/County/ParcelID`.
- Searches real estate owners by name (`STATE/Owner Name` or `STATE/County/Owner Name`) and returns each matching parcel plus a free portfolio summary row — suited to company and LLC holdings.
- Returns the same top-level schema across counties, with `null` and `field_notes` explaining unavailable data.
- Supports ordered batches and returns one dataset row per accepted input occurrence.
- Pushes each row before any corresponding charge and never bills misses.

Typical uses include portfolio enrichment, owner-of-record verification, assessment research, underwriting inputs, and multi-county property pipelines.

Records come straight from official county assessor and parcel systems, as-is. Rolls can lag recent sales and occasionally contain clerk-side errors — for anything with legal weight, verify against the official record at the source.

#### Real estate owner search (company and LLC portfolios)

`ownerLookups` runs a real estate owner search: every parcel a name holds in scope, plus one free `owner_portfolio` summary row with parcel count, per-county breakdown, assessed and market totals, and an absentee count. Statewide owner search covers Florida, North Carolina, New York, Indiana, Texas (every county except Donley; a county-scoped Texas search uses the county's own records where those offer owner search), and every whole-state layer (Arkansas, Connecticut, Idaho, Massachusetts, Mississippi, Montana, North Dakota, Vermont, Wisconsin, Wyoming, and the District of Columbia); county-scoped search covers standard parcel layers, covered Georgia counties with owner search, every county of Arkansas, Connecticut, Idaho, Massachusetts, Minnesota, Mississippi, Montana, North Dakota, Vermont, Wisconsin, Wyoming, and the District of Columbia, every Tennessee county served by the state assessment app, Cook County, Illinois, and selected county portals in Arkansas (Benton, Boone, Calhoun, Pulaski, Saline), Ohio (Butler), and Virginia (Montgomery, Williamsburg). Those Arkansas county records also include county-published appraised and assessed values, land and improvement values, and sales/deed history. The live per-county list is the `owner_search` flag in the `COVERAGE` artifact below — a county whose portal offers no owner-name search returns an explicit not-covered row rather than an empty result. California is excluded because its public rolls redact owner names. It is most useful on entity holdings — pass a company or LLC name to map its property portfolio across counties:

```json
{
  "query": "IH2 Property Florida",
  "query_type": "owner",
  "status": "completed",
  "owner_portfolio": {
    "parcel_count_returned": 3,
    "total_matches": 214,
    "truncated": true,
    "counties": { "Broward": 2, "Palm Beach": 1 },
    "assessed_value_total": 81399694,
    "totals_scope": "all_matches"
  }
}
```

Owner-search property records bill at the standard per-record event; the portfolio summary row is always free. The use limits below apply: no FCRA-regulated screening.

#### Assessed home value lookup and property sales history

Every completed record carries the county's published valuation set — `assessed_value`, `market_value`, `land_value`, `improvement_value`, and `tax_year` — so a batch doubles as an assessed home value lookup across jurisdictions. `sale_history`, `last_sale_price`, and `last_sale_date` return recorded property sales history where the county publishes it; when sales live in a separate clerk system, `field_notes` says so structurally instead of returning silent nulls.

### Batch API Quickstart

All three examples submit the same batch: two addresses followed by one parcel lookup. Each branches on `status` before reading nullable fields.

#### curl

```bash
curl -X POST \
  "https://api.apify.com/v2/acts/shelvick~county-property-records/run-sync-get-dataset-items?token=YOUR_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{
    "addresses": [
      "827 Krenson Woods Ln, Lakeland, FL 33813",
      "1001 Preston St, Houston, TX 77002"
    ],
    "parcelLookups": ["IL/Cook/17-09-100-001-0000"]
  }' | jq -r '.[] | if .status == "completed" then "\(.input_index): \(.owner_name // "owner unavailable")" elif .status == "failed" then "\(.input_index): retry or correct input — \(.error)" else "\(.input_index): not covered — \(.error)" end'
```

#### Python

```python
from apify_client import ApifyClient

batch = {
    "addresses": [
        "827 Krenson Woods Ln, Lakeland, FL 33813",
        "1001 Preston St, Houston, TX 77002",
    ],
    "parcelLookups": ["IL/Cook/17-09-100-001-0000"],
}

client = ApifyClient("YOUR_TOKEN")
run = client.actor("shelvick/county-property-records").call(run_input=batch)
for record in client.dataset(run["defaultDatasetId"]).iterate_items():
    if record["status"] == "completed":
        print(record["input_index"], record.get("owner_name"), record.get("assessed_value"))
    elif record["status"] == "failed":
        print(record["input_index"], "retry or correct input", record.get("error"))
    else:
        print(record["input_index"], "not covered", record.get("error"))
```

#### JavaScript

```javascript
import { ApifyClient } from 'apify-client';

const batch = {
  addresses: [
    '827 Krenson Woods Ln, Lakeland, FL 33813',
    '1001 Preston St, Houston, TX 77002',
  ],
  parcelLookups: ['IL/Cook/17-09-100-001-0000'],
};

const client = new ApifyClient({ token: 'YOUR_TOKEN' });
const run = await client.actor('shelvick/county-property-records').call(batch);
const { items } = await client.dataset(run.defaultDatasetId).listItems();
for (const record of items) {
  if (record.status === 'completed') {
    console.log(record.input_index, record.owner_name ?? null, record.assessed_value ?? null);
  } else if (record.status === 'failed') {
    console.log(record.input_index, 'retry or correct input', record.error ?? null);
  } else {
    console.log(record.input_index, 'not covered', record.error ?? null);
  }
}
```

### Single lookups over HTTP

You can also call this Actor over HTTP as a REST API, one lookup per request, with no run to start or dataset to fetch. Send a JSON body to one of three endpoints on `https://shelvick--county-property-records.apify.actor`:

- `POST /lookup_address` with `{"address": "..."}` (optional `county`, `state`, `includeHistory`, `expandUnits`)
- `POST /lookup_parcel` with `{"parcel": "ST/County/ID"}`, or a bare parcel ID plus `state` and `county`
- `POST /owner_search` with `{"owner": "ST/County/Owner Name"}` and optional `maxRecords` (default 20, maximum 100)

```bash
curl -X POST "https://shelvick--county-property-records.apify.actor/lookup_address" \
  -H "Authorization: Bearer <APIFY_TOKEN>" \
  -H "Content-Type: application/json" \
  -d '{"address": "827 Krenson Woods Ln, Lakeland, FL 33813"}'
```

Each response is `{"records": [...]}` in the same record shape as the batch dataset; owner search adds `portfolio`, `total_matches`, and `truncated`. Each lookup is billed per resolved record, exactly as in batch runs, and no-match and not-covered results are free. Branch on `status` as in the batch examples. Direct MCP clients can connect to the same lookups at `/mcp`.

### Batch contract and limits

- Accepted order is all nonblank `addresses`, followed by `parcelLookups`, then `ownerLookups`. Every output row carries the corresponding zero-based `input_index`; an owner query may emit multiple parcel rows plus one free portfolio summary row at that index.
- Blank entries are ignored when mixed with usable inputs. An all-blank request fails validation rather than producing an empty successful run.
- Parcel lookups must be `STATE/County/ParcelID`. A bare ID is also accepted with a `county` hint alone, provided only one covered state has a county by that name; otherwise add `state` (the error names the candidate states). Malformed parcel strings fail validation.
- Exact normalized duplicates resolve once and charge at most once, but still produce one row per submitted occurrence with distinct `input_index` values.
- `maxRecords` is the most records one run delivers and charges, default 50, maximum 1000. Every charged row counts: address results (including each unit returned with `expandUnits`), parcel lookups, and each property an owner search returns. Free rows (misses, notices, owner portfolio summaries) do not count. When the limit is reached the run stops cleanly: the remaining records and lookups are not delivered or charged and come back as `failed` rows whose `error` names `maxRecords`. Raise `maxRecords` or resubmit that remainder.
- If nonblank submitted lookups exceed `maxRecords`, the run fails before any lookup runs; no input is silently dropped. Raise `maxRecords` or split the batch.
- An address without a unit number (no `APT`, `UNIT`, `#` and so on) that matches more than 4 unit parcels, such as a condo building, returns one free notice row (`status: "failed"`, `error_class: "indeterminate"`) saying how many units share the address. Add the unit number (for example `APT 4B`) to get that unit, or set `expandUnits: true` to return every unit, each charged as a property record and counted toward `maxRecords`. Addresses matching 2 to 4 parcels, and addresses that include a unit, return each matching parcel as before.
- Each input array accepts at most 1000 strings. Larger workloads should be split into multiple async runs.

### Coverage contract

Coverage is capability-specific, not a binary promise. The `COVERAGE` key-value record written by each completed run is authoritative for that run:

```json
{
  "updated_at": "2026-08-01T12:00:00Z",
  "counties": [
    {
      "state": "TX",
      "county": "harris",
      "address_lookup": true,
      "parcel_lookup": true,
      "owner_search": true,
      "completeness": "full"
    }
  ]
}
```

- `address_lookup`, `parcel_lookup`, and `owner_search` state whether each lookup path is structurally available.
- `completeness: "full"` means the configured source can return its normal record shape; individual fields may still be null because counties publish different data.
- `completeness: "partial"` marks structurally thin coverage, such as parcel-only counties where records are frequently sparse.
- `completeness: "unavailable"` marks a registered county route that cannot currently return assessment data by either lookup type.

Read the artifact from the run's default key-value store:

```python
coverage_record = client.key_value_store(run["defaultKeyValueStoreId"]).get_record("COVERAGE")
coverage = coverage_record["value"]
```

Capability may improve, regress, or change as public sources change. Do not assume monotonic growth or infer address support from the registry count alone.

### Input reference

| Field | Type | Required | Default | Description |
|---|---|---:|---:|---|
| `addresses` | array of strings | one lookup source | `[]` | Full US street addresses; include city, state, and preferably ZIP. |
| `parcelLookups` | array of strings | one lookup source | `[]` | Exact `STATE/County/ParcelID` values. |
| `ownerLookups` | array of strings | one lookup source | `[]` | Owner searches as `STATE/Owner Name` or `STATE/County/Owner Name`; bare names require a state hint. |
| `address` | string | no | blank | Convenience alias appended after the `addresses` array. |
| `parcelLookup` | string | no | blank | Convenience alias appended after the `parcelLookups` array. |
| `ownerLookup` | string | no | blank | Convenience alias appended after the `ownerLookups` array. |
| `county` | string | no | blank | County hint, without the word “County”. |
| `state` | string | no | blank | Two-letter state hint; pair with `county` for bare parcel IDs. |
| `includeHistory` | boolean | no | `true` | Include history where published. Set false for a smaller current snapshot. |
| `maxResultsPerOwner` | integer | no | `50` | Maximum property records returned per owner query, 1–500. |
| `expandUnits` | boolean | no | `false` | Return every unit parcel when an address without a unit number matches more than 4 of them; otherwise one free notice row is returned. |
| `maxRecords` | integer | no | `50` | Most records one run delivers and charges, 1–1000; owner-search results count. More nonblank lookups than this fails validation. |

### Output contract

Every row always emits `query`, `query_type`, `input_index`, `status`, `error_class`, `billing_eligible`, `tax_history`, `sale_history`, and `field_notes`, including failed and not-covered rows. Other fields are nullable.

Every row also emits `retry`, derived from `error_class`: `null` on completed records, `later` to retry unchanged after a short delay, `with_changes` to correct the input first, or `never` to avoid repeating the same lookup.

`source_url` is null on failed/not-covered records and on completed records when the source provides no parcel-detail URL and no verified public link can be built, including Broward statewide records and free owner portfolio summaries; this does not indicate a failed lookup.

`field_notes` identifies structural omissions with `fields`, `reason`, and `detail`. It distinguishes data omitted from the public record from data not carried by the county source used for that row.

`nearby_addresses` is empty except on some address `no_match` rows where the county holds the street: it then lists up to 3 recorded addresses on that street (`situs_address` and `parcel_id` only), nearest house number first. They are other parcels, not a match, and are not charged; re-run with a `parcel_id` for the full record, including owner and values.

`near_matches` is empty except on some owner-search `no_match` rows for a business name: it then lists up to 5 other owner names on the same county roll that share the searched name without its legal-form and role words (for example, a partnership `LTD` holding title for its `GP, LLC`), closest first, each with `owner_name` and `parcel_count`. They are other owners, not a match, and are not charged; re-run owner search with one of those names to get its parcels.

Derived output fields are `absentee_owner` (a conservative situs-versus-mailing comparison), `county_fips` (the five-digit Census county code), and `owner_portfolio` (set only on the free summary row emitted after completed owner-search records).

The `OUTPUT` key-value record contains:

- `submitted`: accepted nonblank input occurrences.
- `deduplicated`: unique normalized lookups actually resolved.
- `invalid_input`: unparseable entries emitted as unbilled failed records before source resolution, also included in `failed`.
- `completed`, `failed`, `not_covered`: emitted property-record counts by status; free owner portfolio summaries are excluded.
- `thin`: completed rows that did not clear the billing answer bar.
- `billing_eligible`: unique delivered lookups that cleared the answer bar (each one charged).
- `charged`: unique eligible lookups charged.
- `budget_limited`: unique lookups stopped by the run's maximum cost — returned as `failed` rows with a `budget-exhausted` error, never delivered or charged.
- `max_records_limited`: records or lookups withheld because the run reached `maxRecords` — returned as `failed` rows whose error names `maxRecords`, never delivered or charged.
- `unit_stack_notices`: addresses without a unit number that returned the free shared-address notice instead of every unit.
- `portfolio_summaries`: free owner portfolio summary rows emitted separately from property-record status counts.

### Use limits

Records come from public county assessor and recorder data. This Actor must not be used for FCRA-regulated tenant, employment, credit, or insurance screening, and it returns no non-public personal data.

### Statuses and retry policy

- `completed`: a property record matched. Consume nullable fields defensively and retain `input_index` for correlation. Do not retry a completed input merely because optional fields are null.
- `failed`: either no match or a transient source/adapter failure. Transient errors are retry-safe; `no-match` generally means correct or enrich the input before retrying.
- `not_covered`: the requested capability is structurally unavailable. Do not retry unchanged input against the same coverage snapshot.

#### Failure taxonomy: when to retry

`status` alone cannot tell a county portal that is down for an afternoon from an
address that does not exist. Every non-completed row carries `error_class`, a stable
machine-routable value. Branch on it rather than on the prose in `error`, which may
be reworded at any time. It is `null` on `completed` rows.

Retry the same request later, unchanged:

- `source_error` — the county records source could not be reached. County portals
  have scheduled maintenance and unplanned outages, sometimes lasting a day; this is
  the source being unavailable, not a problem with the lookup.
- `budget_exhausted` — the run's maximum cost, or its `maxRecords` limit, was reached
  before this record. The `error` names which; resubmit the remainder in a new run, or
  raise that limit.

Do not retry unchanged; correct the input or the expectation first:

- `no_match` — the source answered and nothing matched.
- `malformed_input` — the entry could not be parsed.
- `unroutable` — the address could not be routed to a county.
- `address_mismatch` — the parcel found was not the address asked for.
- `indeterminate` — several candidates matched and none could be identified as the one
  asked for, including the shared-address notice; add a unit number or parcel ID, or set
  `expandUnits` to true for a building's units.
- `not_covered` — the county is not yet supported; check `COVERAGE`.
- `owner_search_requires_county` — name the county and search again.

Every retry is a new run and may be billable if it returns an eligible completed record. Pipeline callers should persist completed keys and deduplicate them before retrying a failed subset.

### Pricing and billing semantics

The Store Pricing tab is authoritative for current pricing.

Failed and not-covered lookups are never billed. A completed record is billing-eligible only when it returns an identifier, owner, value, or sale price the caller did not supply. A record that only echoes the searched identifier, or only adds secondary characteristics, is returned as a thin completed row with `billing_eligible: false`.

The Actor pushes rows before charging them. Exact normalized duplicates are resolved and charged at most once while each occurrence remains visible in the dataset. An answer is only ever delivered when it can be charged: once the run reaches its maximum cost, remaining lookups are not resolved and come back as `failed` rows with a `budget-exhausted` error, at no charge. The same holds for `maxRecords`: the records a run charges never exceed it, and what does not fit comes back uncharged with an error naming `maxRecords`. Set the maximum cost per run and `maxRecords` to cover the batch, or resubmit the stubbed remainder in a new run. Compare `billing_eligible`, `charged`, `budget_limited`, and `max_records_limited` in `OUTPUT` to reconcile the run.

### Automation: schedules, agents, and MCP

For scheduled or larger batches, start an async run, wait for terminal status, then retrieve the default dataset by `defaultDatasetId`. Read `OUTPUT` from `defaultKeyValueStoreId` and alert when `failed` or `not_covered` exceeds your threshold; keep the dataset rows for exact `input_index`-level remediation.

Agents can call the Actor through Apify's agent tooling. Instruct them to branch on `status`, treat nullable fields as optional, inspect `field_notes`, and consult the run's `COVERAGE` artifact before promising a lookup capability. Do not let an agent infer success from the presence of a dataset row alone.

### Performance and sizing

Lookups run with five-way concurrency. Typical records complete in roughly 1–3 seconds, but public sources have long tails and transient slowdowns.

Use the synchronous dataset endpoint for small batches; 25 or fewer lookups is a conservative ceiling for staying comfortably inside its five-minute response window under normal conditions. Use async runs for larger batches, retrieve completed chunks as they become visible, and split very large workloads so one slow source does not dominate recovery.

### High-volume needs

Running thousands of lookups a month, or need specific counties prioritized? Open a conversation on the Actor's Issues tab. High-volume feedback directly drives coverage and roadmap priorities.

### FAQ

**Why is a field null?**\
Counties publish different data. Inspect `field_notes` for structural omissions; an empty note list means no known structural explanation was attached.

**What happens for an unsupported county or lookup type?**\
The row returns `status: "not_covered"`, is not billed, and includes a machine-readable `error`. Check `COVERAGE` before retrying.

**Can I look up by parcel number?**\
Yes. Use `STATE/County/ParcelID`, or pass a bare ID with a `county` hint — add `state` as well when the county name is shared by several states.

**Are duplicate inputs removed from output?**\
No. They are deduplicated for resolution and billing, then expanded back to one row per occurrence with distinct `input_index` values.

### What this does not do

- Consumer listing or asking-price data.
- Guaranteed nationwide or uniform field coverage.
- Owner phone numbers, email addresses, or skip tracing.
- Guaranteed completeness for fields a county does not publish.

For active for-sale listings and asking prices, use a listings-portal scraper instead. For owner contact details or skip tracing, use a people-data enrichment tool instead. Use this Actor when you need the official county record behind an address or parcel: owner of record, assessed value, tax history, and recorded sales.

### Related Actors

| Actor | Use it when |
|---|---|
| [Secretary of State Business Search + UCC Liens](https://apify.com/shelvick/business-entity-lien-search) | the property owner is an LLC or corporation and you need the entity behind it — status, officers, UCC liens |
| [Property Deed & Lien Records Search](https://apify.com/shelvick/property-deed-records) | you need the recorded documents behind a property — deeds, mortgages, releases, and liens from county recorder indexes |
| [Property Owner Portfolio — Property Records by Owner Name](https://apify.com/shelvick/property-owner-portfolio) | you start with an owner name, not a property, and want one aggregated asset profile — full-match-set parcel totals by county plus matching business entities and recorded documents where covered — rather than a single-parcel assessor record. |

# Actor input Schema

## `addresses` (type: `array`):

Full US property street addresses to resolve to county property records, e.g. "827 Krenson Woods Ln, Lakeland, FL 33813". Each address is matched to the right county assessor/recorder and returned as one normalized record. Include city + state (and ZIP if known) for reliable county resolution. Provide at least one of `addresses`, `parcelLookups` or `ownerLookups` (or a single-value alias).

## `parcelLookups` (type: `array`):

Provide one parcel ID per entry (one parcel ID per lookup); do not combine IDs with commas or semicolons. Direct parcel or assessor account lookups when you already know the ID, one per line in the form STATE/County/ParcelOrAccount, e.g. "FL/Polk/232912140174000660". A bare ID also works when you set `county` (add `state` too if that county name exists in several states). Use this for exact-record retrieval; use `addresses` when you only have the street address. Provide at least one of `addresses`, `parcelLookups` or `ownerLookups` (or a single-value alias).

## `ownerLookups` (type: `array`):

Owner names to search, one per line as STATE/Owner Name or STATE/County/Owner Name (a bare name is accepted with a `state` hint). Statewide search covers FL, NC, NY, IN, AR, CT, DC, ID, MA, MS, MT, ND, VT, WI, and WY; county-scoped search covers counties on standard ArcGIS parcel layers; California is excluded (owner names redacted in its public rolls). Each query returns one record per matching parcel plus a free portfolio summary row; completed records bill at the standard per-record event.

## `address` (type: `string`):

Convenience single-address alias of `addresses`: pass one full US property street address here instead of a list, e.g. "827 Krenson Woods Ln, Lakeland, FL 33813". Folded into `addresses`; use `addresses` for a batch. Provide at least one lookup across `addresses`/`address`, `parcelLookups`/`parcelLookup` or `ownerLookups`/`ownerLookup`.

## `parcelLookup` (type: `string`):

Convenience single alias of `parcelLookups`: one STATE/County/ParcelOrAccount lookup, e.g. "FL/Polk/232912140174000660". Folded into `parcelLookups`; use `parcelLookups` for a batch.

## `ownerLookup` (type: `string`):

Convenience single alias of `ownerLookups`: one STATE/Owner Name or STATE/County/Owner Name query. Folded into `ownerLookups`; use `ownerLookups` for a batch.

## `county` (type: `string`):

Optional county name (without the word 'County') to scope or disambiguate lookups, e.g. "Polk". Useful when addresses are ambiguous across county lines. Leave blank to auto-resolve each address to its county.

## `state` (type: `string`):

Optional two-letter US state code to scope lookups, e.g. "FL". Combined with `county` to disambiguate same-named counties across states.

## `includeHistory` (type: `boolean`):

When true (default), each record includes full tax-year history and recorded sale history where the county publishes it. Set false for a current-snapshot-only record (owner, value, latest tax year) — faster and smaller.

## `maxResultsPerOwner` (type: `integer`):

Maximum property records returned for each owner-name query, from 1 to 500. The free portfolio summary reports whether results were truncated.

## `expandUnits` (type: `boolean`):

When an address without a unit number (no APT, UNIT, # and so on) matches more than 4 unit parcels, such as a condo building, the run returns one free notice row saying how many units share the address instead of every unit. Add the unit number (e.g. "APT 4B") to get that unit, or set this to true to return all of them, each charged as a property record and counted toward maxRecords. Addresses matching 2 to 4 parcels, and addresses that include a unit, are unaffected. Default false.

## `maxRecords` (type: `integer`):

The most records one run delivers and charges, counted across every row type: address results (including each unit returned under expandUnits), parcelLookups, and ownerLookups results (each owner-search property counts). When the limit is reached the run stops cleanly: remaining records and lookups are not delivered or charged and come back as failed rows whose error names maxRecords. Free rows (misses, notices, owner portfolio summaries) do not count. A batch with more nonblank lookups than maxRecords fails before any lookup runs, with an error naming both numbers. Default 50, maximum 1000.

## Actor input object example

```json
{
  "addresses": [
    "827 Krenson Woods Ln, Lakeland, FL 33813"
  ],
  "parcelLookups": [],
  "ownerLookups": [],
  "includeHistory": true,
  "maxResultsPerOwner": 50,
  "expandUnits": false,
  "maxRecords": 50
}
```

# Actor output Schema

## `results` (type: `string`):

Normalized property records for this run (one per accepted lookup).

## `output` (type: `string`):

Run-level outcome counts: lookups accepted, records delivered and charged.

## `coverage` (type: `string`):

Covered counties used for this run, with a last-updated timestamp.

# API

You can run this Actor programmatically using our API. Below are code examples in JavaScript, Python, and CLI, as well as the OpenAPI specification and MCP server setup.

## JavaScript example

```javascript
import { ApifyClient } from 'apify-client';

// Initialize the ApifyClient with your Apify API token
// Replace the '<YOUR_API_TOKEN>' with your token
const client = new ApifyClient({
    token: '<YOUR_API_TOKEN>',
});

// Prepare Actor input
const input = {
    "addresses": [
        "827 Krenson Woods Ln, Lakeland, FL 33813"
    ],
    "parcelLookups": [],
    "ownerLookups": [],
    "address": "",
    "parcelLookup": "",
    "ownerLookup": "",
    "county": "",
    "state": ""
};

// Run the Actor and wait for it to finish
const run = await client.actor("shelvick/county-property-records").call(input);

// Fetch and print Actor results from the run's dataset (if any)
console.log('Results from dataset');
console.log(`💾 Check your data here: https://console.apify.com/storage/datasets/${run.defaultDatasetId}`);
const { items } = await client.dataset(run.defaultDatasetId).listItems();
items.forEach((item) => {
    console.dir(item);
});

// 📚 Want to learn more 📖? Go to → https://docs.apify.com/api/client/js/docs

```

## Python example

```python
from apify_client import ApifyClient

# Initialize the ApifyClient with your Apify API token
# Replace '<YOUR_API_TOKEN>' with your token.
client = ApifyClient("<YOUR_API_TOKEN>")

# Prepare the Actor input
run_input = {
    "addresses": ["827 Krenson Woods Ln, Lakeland, FL 33813"],
    "parcelLookups": [],
    "ownerLookups": [],
    "address": "",
    "parcelLookup": "",
    "ownerLookup": "",
    "county": "",
    "state": "",
}

# Run the Actor and wait for it to finish
run = client.actor("shelvick/county-property-records").call(run_input=run_input)

# Fetch and print Actor results from the run's dataset (if there are any)
print(f"💾 Check your data here: https://console.apify.com/storage/datasets/{run.default_dataset_id}")
for item in client.dataset(run.default_dataset_id).iterate_items():
    print(item)

# 📚 Want to learn more 📖? Go to → https://docs.apify.com/api/client/python/docs/quick-start

```

## CLI example

```bash
echo '{
  "addresses": [
    "827 Krenson Woods Ln, Lakeland, FL 33813"
  ],
  "parcelLookups": [],
  "ownerLookups": [],
  "address": "",
  "parcelLookup": "",
  "ownerLookup": "",
  "county": "",
  "state": ""
}' |
apify call shelvick/county-property-records --silent --output-dataset

```

## MCP server setup

```json
{
    "mcpServers": {
        "apify": {
            "type": "http",
            "url": "https://mcp.apify.com/?tools=fetch-actor-details,shelvick/county-property-records"
        }
    }
}
```

The hosted server signs you in with OAuth on first connect, so no API token belongs in this config. Clients without OAuth support can send an `Authorization: Bearer <APIFY_API_TOKEN>` header instead, using a token from API & Integrations in Apify Console (https://console.apify.com/settings/integrations).

## OpenAPI specification

Download the OpenAPI definition: https://api.apify.com/v2/actors/9EezIhXgxEUIGgPN7/builds/TXAHr7UJjSCHZ3WTR/openapi.json
