191.3M US property records. RESTful JSON API with parcel search, owner intelligence, permits, deeds, and market analytics.
PropRaven is one parcel dataset delivered three ways — this REST API, a Snowflake Marketplace share (in review), and bulk Parquet and CSV delivery. Field availability and release timing can differ between those channels. The API is the right door when you need a parcel, an owner or a cohort inside an application: one HTTPS call returns the owner of record, assessed value, last sale, permits, deed history and hazard risk wherever the county publishes them, with an official client in Python, TypeScript and Go and a hosted Model Context Protocol server for AI agents.
The v1 surface is organised into the endpoint groups below — the tags of the published OpenAPI 3.1 specification (version 1.2.1, which describes 87 paths and 92 operations). Every response is JSON, and every parcel is addressed by its canonical state_fips:county_fips:parcel_id identifier (for example 37:119:11906402). The parcel build currently served is v=2026-10-12.
| Group | What it returns |
|---|---|
| Parcels | One parcel by ID, plus its owner, permits, deed history, property-tax delinquency status, risk assessment, traffic history, GeoJSON polygons for a bounding box, and the paid dossier / comp pack / risk score. |
| Search | Parcel search by bounds, filters and sorting; full paginated text + attribute search; address / place / parcel autocomplete; licensed-firm search in a place; CSV export of a result set. |
| Coverage | Coverage statistics — where PropRaven has parcel data, by state and county. |
| Deals | Absentee owners, flips, contractors by permit activity, entity-owned parcels, high land-to-improvement ratio, lender profiles, long-held parcels, portfolio owners, county-quarter transaction summaries. |
| Leads | A lead feed priced per lead, with a free masked preview of count, price and sample. |
| Market | County market statistics, a single-county detail view, market trends, and flip activity grouped by county. |
| Owners | Owner search by name, owner profile, the owner's properties, portfolio summary, recorded deed transactions, and the paid owner intelligence report. |
| Webhooks | Create, list, inspect and disable webhook endpoints, and read recent delivery attempts. |
| Account | Current-period usage and quota for your key. |
| Storefront | The sealed field catalog and try-before-buy availability quotes — the free discovery surface agents read first. |
| Property Intelligence | Evidence-backed property signals for a parcel, plus the runs, named scenarios and investigation handoffs your key owns. |
| Credits | Fund and read a prepaid credit balance over x402. |
| Watch | Create a watch on a parcel or cohort (free) and poll it for new changes. |
| Verify | Verify facts for one parcel, or in batch, with a free preview. |
The Free plan is $0 with 1,000 lookups a month and no card required. New accounts land on /developers/welcome with the key on screen.
Include your API key in the Authorization header:
Authorization: Bearer pz_your_key_here
Keys are prefixed pz_ and are the only credential the API uses — there is no OAuth dance for server-to-server calls, and the same key authenticates the SDKs and the hosted MCP server. A key is required on every endpoint except the free discovery surface (coverage statistics, the storefront catalog and availability quotes, and lead-feed previews) and the x402-paid resources, which accept a signed wallet payment instead of a key. Being authenticated is not the same as being entitled: a free-tier key asking for a paid dossier gets the same HTTP 402 an anonymous caller does.
Keys are created and revoked self-serve under Settings > API Keys. Treat them as secrets — never ship one in a browser bundle. For the supply-chain side (SLSA provenance on every SDK and MCP release, OIDC trusted publishing) see Security.
The raw HTTP surface needs nothing but a key. The single-parcel lookup below is the first call most integrations make; the ID is the state FIPS, county FIPS and assessor parcel number.
curl -H "Authorization: Bearer pz_your_key" \
"https://propraven.com/api/v1/parcels/37:119:11906402"import requests
resp = requests.get(
"https://propraven.com/api/v1/parcels/37:119:11906402",
headers={"Authorization": "Bearer pz_your_key"}
)
parcel = resp.json()
print(parcel["address"], parcel["total_assessed_value"])const res = await fetch(
"https://propraven.com/api/v1/parcels/37:119:11906402",
{ headers: { Authorization: "Bearer pz_your_key" } }
);
const parcel = await res.json();
console.log(parcel.address, parcel.total_assessed_value);All three clients are generated from the same OpenAPI 3.1 document (/openapi.json) by generators that live in each SDK repo, and a scheduled job opens a regeneration pull request whenever the spec changes. Method names come from the spec's x-sdk-group / x-sdk-method (client.parcels.get is GET /api/v1/parcels/{id}). Every client reads PROPRAVEN_API_KEY from the environment, retries 429, 503 and 504 with backoff (honouring Retry-After, never retrying a 402), raises a typed error built from the problem body, iterates paginated lists for you, and verifies webhook signatures. npm and PyPI releases are published from GitHub Actions with trusted publishing and carry provenance. Source: jdw2111/propraven-python, jdw2111/propraven-typescript and jdw2111/propraven-go.
pip install propravenfrom propraven import PropRaven
client = PropRaven() # reads PROPRAVEN_API_KEY from env
parcel = client.parcels.get("37:119:12104406")
print(parcel["owner_name"], parcel["total_assessed_value"])
for row in client.deals.absentee_iter(county_fips="37119", max_items=500):
print(row["parcel_id"], row["total_assessed_value"])Python 3.9+. Responses are plain dicts with TypedDict hints; AsyncPropRaven is the asyncio client.
npm install @propraven/sdkimport { PropRaven } from "@propraven/sdk";
const client = new PropRaven(); // reads PROPRAVEN_API_KEY from env
const parcel = await client.parcels.get("37:119:12104406");
console.log(parcel.owner_name, parcel.total_assessed_value);
for await (const row of client.deals.absenteeAll({ county_fips: "37119" }, { maxItems: 500 })) {
console.log(row.parcel_id, row.total_assessed_value);
}Node 18+, zero dependencies, ESM + CJS. Server-side only: the REST API sends no CORS headers and keys are secret, so the client refuses to start in a browser.
go get github.com/jdw2111/propraven-go@latestpackage main
import (
"context"
"fmt"
"log"
propraven "github.com/jdw2111/propraven-go"
)
func main() {
client := propraven.NewClient() // reads PROPRAVEN_API_KEY from env
ctx := context.Background()
parcel, err := client.Parcels.Get(ctx, "37:119:12104406", nil)
if err != nil {
log.Fatal(err)
}
fmt.Println(*parcel.OwnerName, *parcel.TotalAssessedValue)
}Go 1.22+, standard library only. propraven.IsNotFound(err), IsRateLimited and friends classify errors.
Plans are metered on lookups per month; that monthly cap is the enforced meter, and a per-key burst limit sits on top of it. The four tiers below are the same ladder the pricing page publishes.
| Tier | Lookups | Price | Includes |
|---|---|---|---|
| Free | 1,000 / mo | $0 | Full records in the web app, self-serve API keys. No card required. |
| Developer | 100,000 / mo | $99/mo · 14-day free trial | Full API, SDKs & webhooks, MCP connector, owner & sponsor graph. |
| Team | 1,000,000 / mo | $1,000/mo | Everything in Developer, bulk export, priority support. |
| Data license | Unmetered (bulk) | Custom | Full state or national tables via Snowflake, Databricks or Parquet. Annual license, invoiced. |
Unauthenticated responses and every 429 carry the rate-limit headers X-RateLimit-Limit, X-RateLimit-Remaining, X-RateLimit-Reset. Read your current-period consumption from GET /api/v1/account/usage. Sustained throughput above the Team cap, or whole-state and national tables, is the data license path rather than a bigger meter.
The API has a second front door built for software that reasons rather than renders. The hosted Model Context Protocol server at https://propraven.com/mcp exposes 47 typed tools — parcel lookup, search and compare, owner pierce, hazard score, valuation estimate, permit and sales history, the deal finders, and the storefront pair — to Claude Desktop, Claude Code, Cursor, Grok, ChatGPT and any MCP-compatible client. The same pz_ key authenticates it as a bearer header; connector UIs such as Claude and Grok sign in with OAuth instead (rotating refresh tokens, no client id to enter). A local @propraven/mcp package on npm speaks stdio for latency-sensitive work. Setup for each client is on the MCP page, which is generated from the live tool registry.
{
"mcpServers": {
"propraven": {
"url": "https://propraven.com/mcp",
"headers": { "Authorization": "Bearer pz_your_real_key_here" }
}
}
}Agents that arrive with a wallet instead of an account use x402. The flow is discover → preview → pay: 3 tools (get_catalog and check_availability) are free and return the sealed field catalog with measured per-state coverage and the exact price of a specific parcel's dossier before anything is spent. Requesting the paid resource without payment returns HTTP 402 with the precise USDC-on-Base amount to sign; sign an EIP-3009 transferWithAuthorization, retry with the X-PAYMENT header, and the 200 carries the dossier plus an on-chain settlement receipt. A failed build is never charged. The dossier price is value-tiered and auditable — clamp($5 × V × R × F, $2, $20) — and lead feeds, owner reports, comp packs and risk scores follow the same 402 contract (owner reports, like every owner-name endpoint, also require your API key: a wallet alone is refused). The full walkthrough is on Connect your agent.
191.3M parcels · 110.0M mapped, counted on the 2026-08-30 map build, across 51 states (all 50 plus DC), described by 620 catalogued columns (44 rebuilt every epoch and 576 carried from an April 2026 reference export; 56 of the 620 are withheld from sale while they are rebuilt). The parcel build currently served is v=2026-10-12; per-state releases in the app database can lag it. The collected facts include 157M+ permit records, 10M+ title transfers, 27M+ resolved owners and 229M+ parcel records with a site address. 110.0M distinct locations carry a coordinate the map can draw. Coverage is uneven by design of the sources — county assessors, recorders and permit offices publish different fields — so check GET /api/v1/coverage or the storefront catalog for the jurisdiction you care about rather than trusting a national average. The data page publishes live counts, the schema explorer and per-field fill rates; parcel data explains what a parcel record contains.
Twelve of the 92 operations in the spec, as a map of the territory. Paths are relative to https://propraven.com.
| Method | Path | Description | Auth |
|---|---|---|---|
| GET | /api/v1/parcels/{id} | Get parcel by ID | API key |
| GET | /api/v1/parcels/{id}/owner | Get parcel owner details and portfolio | API key |
| GET | /api/v1/parcels/{id}/permits | Get parcel permits | API key |
| GET | /api/v1/parcels/{id}/deeds | Get parcel deed history | API key |
| POST | /api/v1/search | Search parcels by bounds, filters, and sorting | API key |
| GET | /api/v1/search/export | Export search results as CSV | API key |
| GET | /api/v1/coverage | Get coverage statistics | Key optional |
| GET | /api/v1/deals/absentee | Find absentee owners | API key |
| GET | /api/v1/owners/{name}/portfolio | Get owner portfolio summary | API key |
| GET | /api/v1/storefront/catalog | Machine Storefront — sealed field catalog | None |
| GET | /api/v1/parcels/{id}/report | Parcel dossier (paid, provenance-first) | Key or x402 |
| GET | /api/v1/account/usage | Current-period usage and quota | API key |
Polling is the wrong shape for change detection across 191.3M parcels, so the API pushes. Register an endpoint with POST /api/v1/webhooks and PropRaven delivers parcel.sold, parcel.permit_filed and parcel.owner_changed events signed with HMAC-SHA256 in the X-PropRaven-Signature header; each event carries a deterministic event_id for idempotency. Verify each delivery with a constant-time HMAC check and a five-minute replay window — plain Node.js below, plain Python on the webhooks page, or the helper every SDK ships (TypeScript verifyWebhook, Python verify_webhook, Go VerifyWebhook, shown below).
import { createHmac, timingSafeEqual } from "node:crypto";
// header: the X-PropRaven-Signature value, e.g. "t=1715882400000,v1=ab12..."
// rawBody: the exact request body string, before any JSON parsing
// secret: the whsec_... value returned when you created the endpoint
function verifyPropRavenSignature(header, rawBody, secret, toleranceMs = 300000) {
const parts = {};
for (const kv of String(header).split(",")) {
const i = kv.indexOf("=");
if (i > 0) parts[kv.slice(0, i).trim()] = kv.slice(i + 1).trim();
}
const t = Number(parts.t);
if (!Number.isFinite(t) || !parts.v1) return false;
if (Math.abs(Date.now() - t) > toleranceMs) return false; // replay window
const expected = createHmac("sha256", secret).update(parts.t + "." + rawBody).digest();
const given = Buffer.from(parts.v1, "hex");
return given.length === expected.length && timingSafeEqual(given, expected);
}import (
"io"
"net/http"
"os"
propraven "github.com/jdw2111/propraven-go"
)
func handler(w http.ResponseWriter, r *http.Request) {
rawBody, _ := io.ReadAll(r.Body) // the exact request body, before any JSON parsing
event, err := propraven.VerifyWebhook(rawBody,
r.Header.Get("X-PropRaven-Signature"), os.Getenv("PROPRAVEN_WEBHOOK_SECRET"))
if err != nil {
http.Error(w, "invalid signature", http.StatusUnauthorized)
return
}
_ = event // json.RawMessage: unmarshal into your own struct; the name is in event_type
w.WriteHeader(http.StatusNoContent)
}Every error is an RFC 7807 problem (application/problem+json) with a stable code. A bad parameter is a 400 invalid_parameter that names each one in errors; error bodies never contain database text:
{
"type": "https://api.propraven.com/errors/invalid-parameter",
"title": "Bad Request",
"status": 400,
"detail": "limit: must be >= 1",
"code": "invalid_parameter",
"errors": [{ "param": "limit", "message": "must be >= 1" }],
"request_id": "iad1::abc123"
}| Status | Meaning |
|---|---|
| 400 | Bad request (missing or invalid parameters) |
| 401 | Invalid or missing API key |
| 402 | Payment required — the body is an x402 offer naming the exact amount |
| 403 | API key is inactive or expired |
| 404 | Resource not found |
| 429 | Rate limit exceeded (check Retry-After header) |
| 405 | Method not allowed (the Allow header lists the methods) |
| 500 | Internal server error |
| 503 / 504 | Source temporarily unavailable, or the query ran out of time (query_timeout); retry after Retry-After |
List endpoints take limit and offset and return {data, total, limit, offset}: ask for the next page with offset + limit until a page comes back shorter than limit. GET /api/v1/search/full uses a cursor instead: pass the previous page's nextCursor as after until it is null. Since API 1.2.0, every field the spec types as a number is a JSON number (values such as total_assessed_value used to arrive as strings like "19388200.00"); identifiers such as parcel_id, county_fips and zip stay strings.
The full OpenAPI specification is available at /openapi.json(OpenAPI 3.1, version 1.2.1, 92 operations, validated in CI against recorded production responses). Import it into Insomnia or any OpenAPI-compatible tool, load the generated Postman collection, or point a ChatGPT Custom GPT Action at it with Bearer authentication.