PropRaven Python SDK
Sync and async clients for the PropRaven API, generated from its OpenAPI 3.1 spec. Python 3.9+, httpx is the only dependency; responses are plain dicts with TypedDict hints. Published to PyPI from GitHub Actions with trusted publishing.
The current release was generated from OpenAPI 1.2.0 and wraps 70 of the 92 operations in the live spec (version 1.2.1). These 22 operations are not in the SDK yet; call them over REST with the same API key:
GET /api/v1/data/databricks/grantGET /api/v1/intelligence/runs/{runId}GET /api/v1/intelligence/runs/{runId}/handoffGET /api/v1/licensees/firmsGET /api/v1/licensees/individualsGET /api/v1/market/zillow/compareGET /api/v1/market/zillow/contextGET /api/v1/market/zillow/timeseriesGET /api/v1/neighborhood/distressGET /api/v1/owners/pierceGET /api/v1/parcels/{id}/distressGET /api/v1/parcels/{id}/observationsGET /api/v1/parcels/{id}/signalsGET /api/v1/parcels/{id}/tax-statusGET /api/v1/parcels/{id}/zoningGET /api/v1/permits/searchGET /api/v1/permits/{permitId}/eventsGET /api/v1/r2-credentialsGET /api/v1/sources/{sourceId}/observationsGET /api/v1/zoning/coverageGET /api/v1/zoning/searchPOST /api/v1/intelligence/scenarios
Install
pip install propravenQuickstart
from propraven import PropRaven
client = PropRaven() # reads PROPRAVEN_API_KEY
parcel = client.parcels.get("37:119:12104406")
print(parcel["owner_name"], parcel["total_assessed_value"]) # numbers are numbers
permits = client.parcels.permits("37:119:12104406", shape="envelope")
owner = client.owners.get("MARKEY ENTERPRISES INC")AsyncPropRaven has the same methods (await client.parcels.get(...)); both clients work as context managers.
The default base URL is https://propraven.com (every path starts with /api/v1); override it withPROPRAVEN_BASE_URL. Parcel ids take the canonicalstate:county:parcel form, the legacy 5-digit form or a PropRaven UUID.
Errors
The API answers every error with an RFC 7807 problem body; the SDK raises a typed error per status (400, 401, 402, 403, 404, 405, 409, 413, 422, 429, 503, 504, other 5xx) carryingcode, detail, the per-parameter errors and the request id.
import propraven
try:
client.parcels.get("37:119:00000000000")
except propraven.NotFoundError as err:
print(err.code, err.detail, err.request_id)
except propraven.RateLimitError as err:
print("retry after", err.retry_after, "s")
except propraven.PaymentRequiredError as err:
print(err.accepts) # x402 offerRetries, timeouts and rate limits
Network errors, 429, 503 and 504 are retried twice by default (other 5xx only for GET and DELETE), waiting for Retry-After or the rate-limit reset when the server names one, otherwise backing off exponentially. A wait longer than 60 seconds is raised instead of slept, and a 402 is never retried.
client = PropRaven(max_retries=3, timeout=30)
client.search.full(q="Tryon", state="NC", max_retries=0) # per request
print(client.last_rate_limit)Pagination
for row in client.deals.absentee_iter(county_fips="37119", page_size=100, max_items=1000):
print(row["parcel_id"], row["total_assessed_value"])
# search.full follows the keyset cursor (nextCursor -> after) for you:
rows = list(client.search.full_iter(q="Tryon", state="NC", max_items=200))Webhook signature verification
Checks X-PropRaven-Signature (t=<unix_ms>,v1=<hex>, HMAC-SHA256 overt.raw_body) in constant time with a 5-minute replay window. A dependency-free verifier is on thewebhooks page.
import os
from flask import Flask, request
from propraven import verify_webhook, WebhookVerificationError
app = Flask(__name__)
@app.post("/webhooks/propraven")
def propraven_webhook():
try:
event = verify_webhook(
request.get_data(), # the RAW body bytes
request.headers.get("X-PropRaven-Signature", ""),
os.environ["PROPRAVEN_WEBHOOK_SECRET"],
)
except WebhookVerificationError:
return "", 401
if event["event_type"] == "parcel.sold":
print("sold:", event["parcel_id"], event.get("sale_price_usd"))
return "", 204