Skip to content
SDK · Python

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/grant
  • GET /api/v1/intelligence/runs/{runId}
  • GET /api/v1/intelligence/runs/{runId}/handoff
  • GET /api/v1/licensees/firms
  • GET /api/v1/licensees/individuals
  • GET /api/v1/market/zillow/compare
  • GET /api/v1/market/zillow/context
  • GET /api/v1/market/zillow/timeseries
  • GET /api/v1/neighborhood/distress
  • GET /api/v1/owners/pierce
  • GET /api/v1/parcels/{id}/distress
  • GET /api/v1/parcels/{id}/observations
  • GET /api/v1/parcels/{id}/signals
  • GET /api/v1/parcels/{id}/tax-status
  • GET /api/v1/parcels/{id}/zoning
  • GET /api/v1/permits/search
  • GET /api/v1/permits/{permitId}/events
  • GET /api/v1/r2-credentials
  • GET /api/v1/sources/{sourceId}/observations
  • GET /api/v1/zoning/coverage
  • GET /api/v1/zoning/search
  • POST /api/v1/intelligence/scenarios

Install

pip install propraven

Quickstart

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 offer

Retries, 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

Resources