County Property Records API — County Assessor & Property Tax
Pricing
from $42.50 / 1,000 property record resolveds
County Property Records API — County Assessor & Property Tax
County property records, property tax records: who owns this property? Property owner lookup by address, parcel lookup by ID, parcel API, real estate owner search, LLC property ownership, property appraisal, assessed value, sales history. Tax assessor data and assessor search in 2,100+ US counties.
Pricing
from $42.50 / 1,000 property record resolveds
Rating
0.0
(0)
Developer
Scott Helvick
Maintained by CommunityActor stats
1
Bookmarked
149
Total users
41
Monthly active users
11 hours ago
Last modified
Categories
Share
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,369 county and county-equivalent routes in 51 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,369 registry routes across 51 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) | 11 | counties | yes | yes | partial |
| California (CA) | 47 | 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) | 3 | counties | yes | yes | no |
| Iowa (IA) | 82 | counties | yes | yes | partial |
| Idaho (ID) | 44 | counties | yes | yes | yes |
| Illinois (IL) | 72 | counties | yes | partial | partial |
| Indiana (IN) | 92 | counties | yes | yes | yes |
| Kansas (KS) | 7 | counties | yes | partial | no |
| Kentucky (KY) | 99 | counties | yes | partial | no |
| Louisiana (LA) | 33 | 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) | 60 | counties | yes | partial | partial |
| Minnesota (MN) | 74 | counties | yes | yes | partial |
| Missouri (MO) | 54 | counties | partial | partial | partial |
| Mississippi (MS) | 82 | counties | yes | yes | yes |
| Montana (MT) | 56 | counties | yes | yes | yes |
| North Carolina (NC) | 100 | counties | partial | yes | yes |
| 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) | 35 | counties | partial | partial | partial |
| Oklahoma (OK) | 61 | counties | yes | partial | partial |
| Oregon (OR) | 8 | counties | yes | yes | partial |
| Pennsylvania (PA) | 14 | counties | yes | yes | partial |
| Rhode Island (RI) | 5 | counties | yes | no | no |
| South Carolina (SC) | 35 | counties | yes | yes | partial |
| South Dakota (SD) | 15 | counties | yes | yes | no |
| Tennessee (TN) | 91 | counties | yes | yes | partial |
| Texas (TX) | 253 | counties | partial | partial | yes |
| Utah (UT) | 29 | counties | yes | yes | no |
| Virginia (VA) | 41 | counties | yes | partial | partial |
| Vermont (VT) | 14 | counties | yes | yes | yes |
| Washington (WA) | 15 | 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.
{"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.
Texas: address and parcel lookups cover every county except Donley and six counties whose published records carry no street numbers (Floyd, Hopkins, Live Oak, Montague, Red River, Runnels — owner search only). Outside the counties with their own appraisal-district source, records come from the statewide parcel collection: market value, no assessed value; annual 2025 roll.
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 NameorSTATE/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
nullandfield_notesexplaining 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:
{"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
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
from apify_client import ApifyClientbatch = {"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
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_addresswith{"address": "..."}(optionalcounty,state,includeHistory,expandUnits)POST /lookup_parcelwith{"parcel": "ST/County/ID"}, or a bare parcel ID plusstateandcountyPOST /owner_searchwith{"owner": "ST/County/Owner Name"}and optionalmaxRecords(default 20, maximum 100)
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 byparcelLookups, thenownerLookups. Every output row carries the corresponding zero-basedinput_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 acountyhint alone, provided only one covered state has a county by that name; otherwise addstate(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_indexvalues. maxRecordsis the most records one run delivers and charges, default 50, maximum 1000. Every charged row counts: address results (including each unit returned withexpandUnits), 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 asfailedrows whoseerrornamesmaxRecords. RaisemaxRecordsor resubmit that remainder.- If nonblank submitted lookups exceed
maxRecords, the run fails before any lookup runs; no input is silently dropped. RaisemaxRecordsor 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 exampleAPT 4B) to get that unit, or setexpandUnits: trueto return every unit, each charged as a property record and counted towardmaxRecords. 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:
{"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, andowner_searchstate 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:
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 infailed.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 asfailedrows with abudget-exhaustederror, never delivered or charged.max_records_limited: records or lookups withheld because the run reachedmaxRecords— returned asfailedrows whose error namesmaxRecords, 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 retaininput_indexfor 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-matchgenerally 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. -
acknowledged: the run (or request) carried only a note; nothing was searched and no results were charged.
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 itsmaxRecordslimit, was reached before this record. Theerrornames 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 setexpandUnitsto true for a building's units.not_covered— the county is not yet supported; checkCOVERAGE.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
Tell us what's missing. If a run doesn't give you what you need, add a feedback note to your next run: what was missing or wrong, or which tool you're switching to and why. A run with only a note isn't charged for results, and the maintainers read every note.
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 | the property owner is an LLC or corporation and you need the entity behind it — status, officers, UCC liens |
| Property Deed & Lien Records Search | 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 | 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. |