ATTOM Data APIapi.developer.attomdata.com ↗
Access ATTOM property details, owner records, neighborhood POI, and sales trend data via 5 structured endpoints covering addresses, geographies, and more.
What is the ATTOM Data API?
The ATTOM Data API exposes 5 endpoints covering property search, detailed property attributes, owner records, neighborhood points of interest, and geographic sales trends. get_property_detail_owner returns current ownership data alongside full property records, while get_transaction_sales_trend aggregates yearly or monthly transaction metrics for a given geography — useful for market analysis pipelines that need structured, address-level real estate data.
curl -X GET 'https://api.parse.bot/scraper/7d4d00ce-c23d-4935-a089-995a4e9497d6/get_property_address?page=1&pagesize=2&postalcode=90210' \ -H 'X-API-Key: $PARSE_API_KEY'
Typed, relational, agent-ready
A generated client with real types, enums, and the links between objects — the structure a flat JSON response can't carry. Autocompletes in your editor and reads cleanly to coding agents.
- Fully typed · autocompletes
- Objects link to objects
- Typed errors & pagination
Typed Python client. Set up the SDK in your uv project, then pull this API’s typed client:
uv add parse-sdk uv run parse init uv run parse add --marketplace api-developer-attomdata-com-api
uv run parse add --marketplace pulls a pinned snapshot of this canonical API — it won’t change underneath you. To customize it, subscribe and swap to your own copy.
"""
ATTOM Data API Client
A Python client for accessing comprehensive property data, ownership information,
and sales trends through the ATTOM Data API.
Get your API key from: https://parse.bot/settings
"""
import os
import requests
from typing import Any, Dict, List, Optional
class ParseClient:
"""
Client for interacting with the ATTOM Data API through Parse Bot.
Attributes:
base_url: The base URL for the API
scraper_id: The unique scraper identifier
api_key: The API key for authentication
"""
def __init__(self, api_key: Optional[str] = None):
"""
Initialize the ParseClient.
Args:
api_key: API key for authentication. If not provided, reads from PARSE_API_KEY env var.
"""
self.base_url = "https://api.parse.bot"
self.scraper_id = "7d4d00ce-c23d-4935-a089-995a4e9497d6"
self.api_key = api_key or os.getenv("PARSE_API_KEY")
if not self.api_key:
raise ValueError("API key must be provided or set in PARSE_API_KEY environment variable")
def _call(self, endpoint: str, method: str = "POST", **params) -> Dict[str, Any]:
"""
Make an API call to the Parse Bot scraper.
Args:
endpoint: The endpoint name (e.g., 'get_property_address')
method: HTTP method ('GET' or 'POST')
**params: Parameters to send with the request
Returns:
Response JSON as a dictionary
Raises:
requests.RequestException: If the API call fails
"""
url = f"{self.base_url}/scraper/{self.scraper_id}/{endpoint}"
headers = {
"X-API-Key": self.api_key,
"Content-Type": "application/json"
}
try:
if method.upper() == "GET":
response = requests.get(url, headers=headers, params=params)
elif method.upper() == "POST":
response = requests.post(url, headers=headers, json=params)
else:
raise ValueError(f"Unsupported HTTP method: {method}")
response.raise_for_status()
return response.json()
except requests.RequestException as e:
print(f"API call failed: {e}")
raise
def get_property_address(
self,
address1: Optional[str] = None,
address2: Optional[str] = None,
postalcode: Optional[str] = None,
latitude: Optional[float] = None,
longitude: Optional[float] = None,
radius: Optional[float] = None,
propertytype: Optional[str] = None,
page: int = 1,
pagesize: int = 100
) -> Dict[str, Any]:
"""
Search for properties within a specified area or matching address criteria.
Args:
address1: First line of property address
address2: Second line (city, state, zip)
postalcode: ZIP code to search
latitude: Latitude for radius search
longitude: Longitude for radius search
radius: Search radius in miles
propertytype: Property type filter
page: Page number for pagination (default: 1)
pagesize: Number of results per page (default: 100)
Returns:
Response containing property array and status
"""
params = {
k: v for k, v in {
"address1": address1,
"address2": address2,
"postalcode": postalcode,
"latitude": latitude,
"longitude": longitude,
"radius": radius,
"propertytype": propertytype,
"page": page,
"pagesize": pagesize
}.items() if v is not None
}
return self._call("get_property_address", method="GET", **params)
def get_property_detail(
self,
attomid: Optional[str] = None,
address1: Optional[str] = None,
address2: Optional[str] = None
) -> Dict[str, Any]:
"""
Get rich property details for a specific property.
Args:
attomid: ATTOM unique property identifier
address1: First line of address
address2: Second line of address (city, state, zip)
Returns:
Response containing detailed property information
"""
params = {
k: v for k, v in {
"attomid": attomid,
"address1": address1,
"address2": address2
}.items() if v is not None
}
return self._call("get_property_detail", method="GET", **params)
def get_property_detail_owner(self, attomid: str) -> Dict[str, Any]:
"""
Get detailed property data including current owner information.
Args:
attomid: ATTOM unique property identifier (required)
Returns:
Response containing property details with owner information
"""
return self._call("get_property_detail_owner", method="GET", attomid=attomid)
def get_transaction_sales_trend(
self,
geoidv4: str,
startyear: int,
endyear: int,
interval: str = "yearly",
propertytype: Optional[str] = None,
page: int = 1,
pagesize: int = 100
) -> Dict[str, Any]:
"""
Get sales trends for a given geography.
Args:
geoidv4: Geographic V4 identifier (required, 32-char hex hash)
startyear: Start year for trend analysis (required)
endyear: End year for trend analysis (required)
interval: Trend interval - 'yearly' or 'monthly' (default: 'yearly')
propertytype: Property type filter
page: Page number (default: 1)
pagesize: Results per page (default: 100)
Returns:
Response containing sales trends array
"""
params = {
k: v for k, v in {
"geoidv4": geoidv4,
"startyear": startyear,
"endyear": endyear,
"interval": interval,
"propertytype": propertytype,
"page": page,
"pagesize": pagesize
}.items() if v is not None
}
return self._call("get_transaction_sales_trend", method="GET", **params)
def get_poi_search(
self,
address: Optional[str] = None,
zipcode: Optional[str] = None,
point: Optional[str] = None,
radius: Optional[float] = None,
searchdistance: Optional[float] = None,
categoryname: Optional[str] = None,
categoryid: Optional[str] = None,
lineofbusinessname: Optional[str] = None,
industryname: Optional[str] = None,
recordlimit: Optional[int] = None,
page: int = 1,
pagesize: int = 10
) -> Dict[str, Any]:
"""
Search for points of interest in proximity to a location.
Args:
address: Street address to search near
zipcode: ZIP code to search near
point: Geographic point in format POINT(longitude,latitude)
radius: Search radius in miles
searchdistance: Search distance in miles
categoryname: POI category name filter
categoryid: POI category ID filter
lineofbusinessname: Line of business filter
industryname: Industry name filter
recordlimit: Maximum number of results
page: Page number (default: 1)
pagesize: Results per page (default: 10)
Returns:
Response containing POI array
"""
params = {
k: v for k, v in {
"address": address,
"zipcode": zipcode,
"point": point,
"radius": radius,
"searchdistance": searchdistance,
"categoryname": categoryname,
"categoryid": categoryid,
"lineofbusinessname": lineofbusinessname,
"industryname": industryname,
"recordlimit": recordlimit,
"page": page,
"pagesize": pagesize
}.items() if v is not None
}
return self._call("get_poi_search", method="GET", **params)
def main():
"""
Practical workflow example: Find properties in an area, retrieve detailed
information including owner data, and identify nearby points of interest.
"""
# Initialize the client
client = ParseClient()
print("=" * 80)
print("ATTOM Data API - Property Research & Neighborhood Analysis Workflow")
print("=" * 80)
# Step 1: Search for properties by address
print("\n[Step 1] Searching for properties in Beverly Hills...")
search_response = client.get_property_address(
address1="Beverly Hills",
address2="CA",
pagesize=3
)
if search_response.get("status", {}).get("code") != 0:
print(f"Error: {search_response.get('status', {}).get('msg')}")
return
properties = search_response.get("property", [])
total_found = search_response.get("status", {}).get("total", 0)
print(f"\nFound {total_found} properties. Analyzing first {len(properties)}:\n")
property_details = []
# Step 2: Get detailed information and owner data for each property
print("[Step 2] Retrieving detailed property and owner information...\n")
for idx, prop in enumerate(properties, 1):
prop_id = prop.get("identifier", {}).get("attomId")
address_oneline = prop.get("address", {}).get("oneLine", "N/A")
print(f"Property {idx}: {address_oneline}")
if not prop_id:
print(" (No ATTOM ID available)\n")
continue
# Get owner information
try:
owner_response = client.get_property_detail_owner(attomid=str(prop_id))
if owner_response.get("status", {}).get("code") == 0:
owner_data = owner_response.get("property", [{}])[0]
owner_info = owner_data.get("owner", {}).get("owner1", {})
owner_name = owner_info.get("fullname", "N/A")
print(f" ATTOM ID: {prop_id}")
print(f" Current Owner: {owner_name}")
# Store for later analysis
property_details.append({
"attomid": prop_id,
"address": address_oneline,
"owner": owner_name
})
else:
print(f" ATTOM ID: {prop_id}")
print(" Owner Info: Unable to retrieve")
except Exception as e:
print(f" ATTOM ID: {prop_id}")
print(f" Owner Info: Error - {str(e)[:60]}")
print()
# Step 3: Find points of interest in the area
print("[Step 3] Searching for points of interest near the properties...\n")
try:
poi_response = client.get_poi_search(
address="Beverly Hills, CA",
searchdistance=1,
recordlimit=10
)
if poi_response.get("status", {}).get("code") == 0:
pois = poi_response.get("poi", [])
total_pois = poi_response.get("status", {}).get("total", 0)
if pois:
print(f"Found {total_pois} points of interest. Showing first {len(pois)}:\n")
print(f"{'Business Name':<40} {'Category':<30} {'Distance':<10}")
print("-" * 80)
for poi in pois[:5]:
biz_name = poi.get("businessLocation", {}).get("businessStandardName", "N/A")
category = poi.get("category", {}).get("condensedHeading", "N/A")
distance = poi.get("details", {}).get("distance", "N/A")
# Truncate long names for display
biz_name_display = (biz_name[:39] + "...") if len(biz_name) > 40 else biz_name
category_display = (category[:29] + "...") if len(category) > 30 else category
print(f"{biz_name_display:<40} {category_display:<30} {distance:<10}")
print()
else:
print("No points of interest found in this area.\n")
else:
print("Unable to retrieve POI data.\n")
except Exception as e:
print(f"POI search failed: {str(e)[:80]}\n")
# Step 4: Summary
print("=" * 80)
print("Summary of Found Properties:")
print("=" * 80)
if property_details:
print(f"\n{'Address':<50} {'Owner':<25}")
print("-" * 75)
for prop in property_details:
address_display = (prop["address"][:49] + "...") if len(prop["address"]) > 50 else prop["address"]
owner_display = (prop["owner"][:24] + "...") if len(prop["owner"]) > 25 else prop["owner"]
print(f"{address_display:<50} {owner_display:<25}")
else:
print("\nNo property details retrieved.")
print("\n" + "=" * 80)
print("Workflow complete!")
print("=" * 80)
if __name__ == "__main__":
main()Returns properties within a specified area or matching address criteria.
| Param | Type | Description |
|---|---|---|
| page | integer | Page number |
| radius | number | Search radius in miles |
| address1 | string | Street address line 1 |
| address2 | string | City, state, zip |
| latitude | number | Latitude for radius search |
| pagesize | integer | Results per page |
| longitude | number | Longitude for radius search |
| postalcode | string | Postal/ZIP code to search |
| propertytype | string | Property type filter |
{
"type": "object",
"fields": {
"status": "object",
"property": "array"
},
"sample": {
"data": {
"status": {
"msg": "SuccessWithResult",
"code": 0,
"page": 1,
"total": 9429,
"version": "1.0.0",
"pagesize": 2,
"transactionID": "f4bf202a669467a2d0cdb7760cc83e59",
"responseDateTime": "2026-02-26T00:22:43.312Z"
},
"property": [
{
"address": {
"line1": "502 N ALTA DR",
"line2": "BEVERLY HILLS, CA 90210",
"country": "US",
"oneLine": "502 N ALTA DR, BEVERLY HILLS, CA 90210",
"postal1": "90210",
"postal2": "3502",
"postal3": "C011",
"locality": "BEVERLY HILLS",
"matchCode": "ExaStr",
"countrySubd": "CA"
},
"vintage": {
"pubDate": "2025-10-14",
"lastModified": "2025-10-14"
},
"location": {
"geoid": "CO06037, CS0691750, ND0004780424, PL0606308, SB0000068678, SB0000068680, ZI90210",
"geoIdV4": {
"CO": "c901b0c78f08f2e777cce3d16936fd0e",
"CS": "c5d84093eb167032a75ea743fd5f2f81",
"DB": "938838cffdba658277c328179d6d9166",
"N1": "ba85655100a0573f28372da416303c28",
"PL": "759869c1bf6d2bca73c7291f9a7959f0",
"SB": "455e6b356ca9102619ffa1aa7dbbcae8, 3b1c5e204f0c6fd74e749b74c2449b0c",
"ZI": "ecf7b806ad7dea1def2c60b246364d06"
},
"accuracy": "Rooftop",
"distance": 0,
"latitude": "34.079958",
"longitude": "-118.392471"
},
"identifier": {
"Id": 111904,
"apn": "4341-003-001",
"fips": "06037",
"attomId": 111904
}
},
{
"address": {
"line1": "511 N BEDFORD DR",
"line2": "BEVERLY HILLS, CA 90210",
"country": "US",
"oneLine": "511 N BEDFORD DR, BEVERLY HILLS, CA 90210",
"postal1": "90210",
"postal2": "3213",
"postal3": "C026",
"locality": "BEVERLY HILLS",
"matchCode": "ExaStr",
"countrySubd": "CA"
},
"vintage": {
"pubDate": "2025-10-14",
"lastModified": "2025-10-14"
},
"location": {
"geoid": "CO06037, CS0691750, DB0604830, ND0004780424, PL0606308, ZI90210",
"geoIdV4": {
"CO": "c901b0c78f08f2e777cce3d16936fd0e",
"CS": "c5d84093eb167032a75ea743fd5f2f81",
"DB": "938838cffdba658277c328179d6d9166",
"N1": "ba85655100a0573f28372da416303c28",
"PL": "759869c1bf6d2bca73c7291f9a7959f0",
"SB": "518dd0b4740f4cc4327409bfa11c3ddb, 3b1c5e204f0c6fd74e749b74c2449b0c",
"ZI": "ecf7b806ad7dea1def2c60b246364d06"
},
"accuracy": "Rooftop",
"distance": 0,
"latitude": "34.070421",
"longitude": "-118.408060"
},
"identifier": {
"Id": 156593,
"apn": "4345-028-003",
"fips": "06037",
"attomId": 156593
}
}
]
},
"status": "success"
}
}About the ATTOM Data API
Property Search and Detail
get_property_address accepts street address components (address1, address2), a postal code, or a latitude/longitude pair with an optional radius (in miles) to return a paginated property array matching those criteria. get_property_detail narrows to a single property using either an attomid or a full address, returning a rich property array with detailed attribute fields. get_property_detail_owner requires an attomid and adds current owner information to that same structure — ownership data is only accessible through this dedicated endpoint, not through the general detail call.
Sales Trends by Geography
get_transaction_sales_trend returns a salesTrends array for a target geography defined by a geoidv4 — a 32-character hex identifier available in the geoIdV4 field of property responses. Required inputs are startyear, endyear, and geoidv4. The interval parameter accepts 'yearly' or 'monthly'. Geography type is encoded in the ID prefix: ZI for ZIP code, CO for county, PL for place, CS for census tract, and DB for district. An optional propertytype filter narrows results to a specific asset class.
Points of Interest
get_poi_search returns a poi array for locations near an address, ZIP code, or a WKT-style point string formatted as POINT(longitude,latitude). A categoryid parameter filters by POI type. The recordlimit parameter caps results (default 20), and standard page/pagesize parameters control pagination across larger result sets.
The ATTOM Data API is a managed, monitored endpoint for api.developer.attomdata.com — not a raw scraper you maintain. Every endpoint is automatically health-checked on a schedule, and when api.developer.attomdata.com changes and a check fails, the API is automatically queued for repair and re-verified. It is built to keep working as the site underneath it changes.
This isn't an official api.developer.attomdata.com API — it's an independent, maintained REST wrapper over public data. Where the source has no official API (or only a limited one), Parse gives you a stable contract over a source that never promised one, and keeps it current. Need a new endpoint or field? You can revise it yourself in plain English and the agent rebuilds it against the live site in minutes — contributing the change back to the shared API is free.
Will this API break when the source site changes?+
Is this an official API from the source site?+
Can I fix or extend this API myself if I need a new endpoint or field?+
What happens if I call an endpoint that has an issue?+
- Enriching property listings with ownership records pulled from
get_property_detail_owner - Building neighborhood scorecards by querying nearby amenities via
get_poi_searchwithcategoryidfilters - Generating market trend dashboards using
get_transaction_sales_trendwith monthly interval data over multi-year ranges - Geocoding-free property lookups by passing
address1andaddress2toget_property_detail - Radius-based property prospecting by supplying latitude, longitude, and a mile radius to
get_property_address - Comparing sales velocity across ZIP codes and counties by switching
geoidv4prefixes in trend queries - Populating CRM records with structured property attributes from the
propertyresponse array
| Tier | Price | Credits/month | Rate limit |
|---|---|---|---|
| Free | $0/mo | 100 | 5 req/min |
| Hobby | $30/mo | 1,000 | 20 req/min |
| Developer | $100/mo | 5,000 | 100 req/min |
One credit = one API call regardless of which marketplace API you call. Exceeding the rate limit returns a 429 response. Authenticate with the X-API-Key header.
Does ATTOM Data have an official developer API?+
How do I obtain the `geoidv4` value needed for sales trend queries?+
get_transaction_sales_trend requires a geoidv4 that comes from the geoIdV4 field returned in property responses — for example, from get_property_detail. The prefix of the 32-character hex hash encodes geography type: ZI for ZIP code, CO for county, PL for place, CS for census, and DB for district. There is no dedicated endpoint to look up geography IDs independently.Does the API return historical ownership or mortgage/lien data?+
get_property_detail_owner. Historical ownership chains, mortgage records, and lien data are not exposed by these endpoints. You can fork this API on Parse and revise it to add endpoints that cover those additional ATTOM data products.What is the difference between `get_property_detail` and `get_property_detail_owner`?+
get_property_detail accepts either an attomid or an address pair and returns property attribute data. get_property_detail_owner requires an attomid and returns that same property structure augmented with current owner information. Owner fields are not present in the base detail response.