Property Owner Search — Property Records Property Owner Portfolio — Property Records by Owner Name Portfolio by Name
Design notes for an owner-name asset profile: full-match-set parcel totals, matching business records, and recorded documents in one record.
Property Owner Search — Property Records Property Owner Portfolio — Property Records by Owner Name Portfolio by Name
Design notes for property-owner-portfolio, an Apify Actor for assembling an owner portfolio from public records. An owner name can appear across tax rolls, business registers, and recorded documents without those sources presenting a single asset profile. This Actor takes 1-50 owner queries, each with a name and state and optionally a county, and returns one aggregated record per owner query. It keeps the parcel totals separate from the bounded detail list, and it says which sources were searched rather than treating every missing result as an empty portfolio.
What this is
The starting question is what real estate holdings appear under this owner name in the covered public records? That is different from starting with a property address and asking who owns it. Here the output unit is the owner query: tax-roll parcels and their totals, matching business-register entities, and recorded documents naming the owner. Each source remains visible inside the profile. The Actor does not turn a shared name into a verified ownership relationship.
Parcel results include a count, assessed and market totals where published, and a breakdown by county. These aggregates cover the full matching set within the requested state or county. The individual parcel records are bounded by maxParcels, from 1 to 500, so an agent can request a short detail list without reducing the portfolio totals to that list. Missing published values remain unknown; the Actor does not estimate them.
The other sources provide context rather than an ownership graph. Business records supply the matching registered names, identifiers, types, standing, dates, and addresses of record. Recorded documents supply instrument identifiers, document types, dates, book/page references, descriptions, and named parties. A matching entity is not automatically the same owner, and a name on a recorded document does not establish current parcel ownership. Those distinctions belong in the interpretation of the result, not in a footnote after an ownership claim.
Every normal owner result includes query, status, boolean retry, coverage, parcels, entities, deeds, and notes. A no-match, an unavailable source, and an uncovered state are separate outcomes. Run-level counts are stored separately as OUTPUT in the run’s key-value store. They are not trailing dataset rows that a consumer must distinguish from portfolios.
Coverage is separate for each source
| Source | Covered states |
|---|---|
| State tax-roll parcels | FL, IN, NC, NY |
| Business register | AK, CO, FL, IA, IL, MS, RI, WV |
| Recorded documents | WV |
The statewide NY tax-roll source excludes the five NYC boroughs and some counties outside the city. A state-level flag therefore describes source availability, not complete county participation or uniform fields. Uncovered states return not_covered without an owner charge. A state covered only by the business register can still produce a completed profile with entity records and no parcel search. Reading the flags and notes is part of reading the answer.
Why I built it this way
The owner query is the useful unit
A batch owner portfolio API should not make its caller reconstruct an owner from a stream of parcels followed by a special summary row. That shape works for property-by-property processing, but it creates unnecessary bookkeeping when the question is about the owner. One profile makes the relationship between query, totals, detail records, and source limitations explicit.
It also fixes the batch boundary. An agent can submit several owner names and preserve each answer as one object. The batch accepts 1-50 queries. Each includes its own state and optional county, so a statewide REIT-name query can sit beside a county-constrained utility-name query without either changing the other’s scope. Names are search inputs, not promises of results. The returned legal names and parcel owner strings still need to be read.
A result limit must not become a valuation limit
The easiest aggregation mistake is to fetch a page of parcels, sum the page, and label that sum a portfolio. The mistake becomes larger as the owner becomes more interesting. A short list may contain useful examples while omitting most of the matching tax roll.
I separated the controls: maxParcels bounds detail records, while parcel_count, assessed_total, market_total, and by_county describe the full match set in scope. truncated explains when more parcels matched than were listed. The returned detail list favors the parcels with the highest assessed values. A low limit is useful when the question is about county concentration and total assessed value rather than every parcel identifier.
Full-match-set does not mean every asset the owner actually holds. It means every match in the searched source within the stated scope. Holdings under other names, missing counties, and publication lag remain outside that claim. Nor are published assessed values interchangeable with market estimates. Retaining the source’s value categories is more useful than compressing them into one apparently precise asset value.
County is a constraint, not a suggestion
A county-scoped search should answer the county question. Quietly expanding to the whole state after a miss would make an empty county look populated by unrelated results elsewhere. The Actor does not do that. If a caller wants a statewide query after a county miss, it can submit a separate query with the county omitted.
The business register is a different scope: it remains statewide even when the parcel and recorded-document searches are limited to a county. Applying the county filter there would suggest that registration and property location share the same geography. Notes state the scope of each searched source so that a statewide entity match is not mistaken for proof of an asset in the requested county.
Unknown is not zero
A source that answered with no matches returns an object containing an empty record list. A source that was not searched is null. An unavailable source can also be null, with status and notes explaining the failure. Collapsing those forms into an empty array would erase the difference between a legitimate miss and a question never answered.
The top-level classification preserves that distinction. completed means all requested sources that cover the state answered and at least one returned records. It does not mean all three sources are covered. partial means something was delivered while another covered source errored. source_error means nothing was delivered and at least one source failed. no_match is reserved for searched sources that answered without matches. not_covered means no source could be searched, including when the caller switched off every source available in that state.
Invalid runtime input is a separate correction case: one invalid_input record with null query values and guidance, not a claimed owner search. An unexpected run-level fault can also produce a correction record while preserving already delivered results. Neither correction should be interpreted as a portfolio. Normal owner results remain one record per query, with no trailing summary rows.
Retry advice should be usable without parsing prose
The retry boolean is a routing hint for an agent, while notes explain the result to a reader. source_error is retryable. A partial profile is retryable when the missing source had a timeout or retryable outage; other partial errors need not improve from an unchanged repeat. Settled no-matches and coverage gaps are not retry candidates. Invalid input needs correction rather than repetition.
Owners run sequentially, while an owner’s requested and covered sources run together. The configured run limit is 900 seconds, with time reserved for wrap-up. Owners that the run has no time to search receive uncharged, retryable source_error records. I do not publish a measured latency range without measurements; full batches should use asynchronous runs rather than rely on a synchronous response window.
The profile must survive before it is charged
One owner record is the billing unit as well as the output unit. The record is written first; only then is a charge attempted for a delivered profile. Empty searches, uncovered states, invalid input, and outage-only records do not incur an owner charge. A partial profile remains chargeable because it includes delivered data from another source.
That is deliberately not a charge for each parcel, entity, or document inside the profile. The detail limit should control response size, not force a caller to infer the cost of a large matching owner from the number of nested rows. Current rates belong on the Store Pricing tab, where subscriber discounts can be applied automatically without making these design notes stale.
How to use it
This batch combines a statewide REIT-name query with a county-scoped utility-name query. The names are realistic inputs, not a claim that either call will return a particular count. Both optional sources are enabled; coverage decides whether each is searched.
{
"owners": [
{"name": "INVITATION HOMES", "state": "FL"},
{"name": "DUKE ENERGY", "state": "NC", "county": "Wake"}
],
"maxParcels": 10,
"includeEntities": true,
"includeDeeds": true
}
Input contract
| Field | Type | Required | Default | Description |
|---|---|---|---|---|
owners |
array of objects | yes | none | 1-50 owner queries. |
owners[].name |
string | yes | none | Owner name as recorded, 2-120 characters. |
owners[].state |
string | yes | none | Two-letter US state code; each query names one state. |
owners[].county |
string | no | omitted | Limits parcels and recorded documents, not the statewide business register. |
maxParcels |
integer | no | 100 |
1-500 returned parcel records per owner; totals are not capped. |
includeEntities |
boolean | no | true |
Include matching business-register records where covered. |
includeDeeds |
boolean | no | true |
Include recorded documents where covered. |
No extra input fields are accepted. Parcel search is always requested. Set the optional switches to false only when that source’s results are not needed; switching off the only covering source produces not_covered, not a no-match.
REST and Python
curl -X POST \
"https://api.apify.com/v2/acts/shelvick~property-owner-portfolio/run-sync-get-dataset-items" \
-H "Authorization: Bearer $APIFY_TOKEN" \
-H "Content-Type: application/json" \
-d '{"owners":[{"name":"INVITATION HOMES","state":"FL"},{"name":"DUKE ENERGY","state":"NC","county":"Wake"}],"maxParcels":10,"includeEntities":true,"includeDeeds":true}'
For longer runs, start POST /v2/acts/shelvick~property-owner-portfolio/runs, poll the run, and retrieve /v2/datasets/{defaultDatasetId}/items. The Python client handles the run-and-dataset workflow:
import os
from apify_client import ApifyClient
client = ApifyClient(os.environ["APIFY_TOKEN"])
run = client.actor("shelvick/property-owner-portfolio").call(run_input={
"owners": [
{"name": "INVITATION HOMES", "state": "FL"},
{"name": "DUKE ENERGY", "state": "NC", "county": "Wake"},
],
"maxParcels": 10,
"includeEntities": True,
"includeDeeds": True,
})
for record in client.dataset(run["defaultDatasetId"]).iterate_items():
print(record["query"], record["status"], record["coverage"])
print(record["notes"])
if record["status"] in ("completed", "partial"):
print(record["parcels"], record["entities"], record["deeds"])
if record["retry"]:
print("Candidate for a later retry:", record["query"])
An MCP-enabled agent can call shelvick/property-owner-portfolio through the Apify MCP server with the same input object. It should branch on status before reading nullable source data and use the boolean retry field rather than trying to classify wording in the notes. Re-running a partial profile can deliver more data but can also incur a new owner charge; retain the previous result and retry only the affected queries.
Reading the returned record
Here is an illustrative no-match record, not a live result. The parcel source answered; the other sources do not cover this state. That distinction is visible without interpreting an empty portfolio as proof of no holdings:
{
"query": {"name": "EXAMPLE HOLDINGS LLC", "state": "NC", "county": null},
"status": "no_match",
"retry": false,
"coverage": {"parcels": true, "entities": false, "deeds": false},
"parcels": {
"total_matches": 0,
"truncated": false,
"parcel_count": 0,
"assessed_total": null,
"market_total": null,
"by_county": [],
"records": []
},
"entities": null,
"deeds": null,
"notes": [
"Searched property tax-roll parcels in North Carolina.",
"Business-register search is not available for North Carolina yet, so no company records were looked up.",
"Recorded-document search is not available for North Carolina yet, so no deeds, mortgages, or liens were looked up.",
"Nothing matched this name in the sources that were searched. Public records index an owner exactly as the name was written on the document, so a portfolio held under a slightly different legal name will not appear under this one.",
"Nothing was charged for this owner."
]
}
For delivered profiles, parcel records include identifiers, owner strings, location, published values, tax year, building and lot characteristics, and sale fields where available. The county breakdown contains county, count, and assessed_total; there is no separate per-county market-total field. Business results contain up to 25 records, and their total is the number returned, not an uncapped registry count. Recorded-document results also list up to 25 eligible entries; their total is the matching count before the list cap and filtering. Non-property vital and military records are excluded, so that count is not necessarily the number of eligible property documents.
The key-value store’s OUTPUT holds owners_requested, outcome counts for completed, partial, no_match, not_covered, and source_error, and the actual charged count. Keep that run-level reconciliation separate from each owner’s asset profile.
How it compares to separate record lookups
| Approach | Starting point | Result shape | Portfolio totals |
|---|---|---|---|
| Single-parcel assessor lookup | Address or parcel ID | One property record | Requires separate aggregation |
| Business-register lookup | Entity name | Registration details | No parcel totals |
| Recorder search | Party name or document identifier | Recorded documents | No tax-roll totals |
| Property Owner Portfolio | Owner name + state | One owner record with covered source results | Full-match-set parcel totals, capped detail list |
The distinction is the output unit, not a claim that every other lookup is unnecessary. Use County Property Records when you have a property address or parcel ID and need its single-parcel assessor record. Use Business Entity + UCC Lien Search for entity and secured-filing detail. Use Property Deed Records for document-level deed lookups. This Actor is for an owner-level asset profile with totals and source context in one object.
It does not infer ownership trees, create new valuations, certify title, return document images, or monitor changes automatically. Those are different tasks. A public-record profile is useful precisely when its scope and limits remain attached to the answer.
Pricing model
Pay per owner record that delivered something, charged after that record is written. completed and partial profiles qualify; no-match, uncovered, invalid-input, and outage-only records do not incur an owner charge. A partial profile is charged once because another source delivered data, not because the failed search itself earned a fee. There is no separate owner-profile charge per nested parcel or matching source.
Subscribers receive an automatic discount. Current rates and applicable charges are on the Apify Store Pricing tab, which remains authoritative. Lowering maxParcels changes the amount of parcel detail returned, not the full-match-set totals or the per-delivered-owner billing unit.
Open questions / future work
- Coverage expansion: which additional tax-roll and recorded-document sources can support owner-name queries with equally explicit county scope? A broader map should not weaken the meaning of the coverage flags.
- County fallback: should an explicitly requested second statewide query be easier to express? Today county remains a strict constraint, with no automatic widening after a miss.
- Detail limits: would independently configurable entity and document limits improve batch workflows? Today those lists are bounded separately from the parcel limit, and their count semantics remain distinct.