PropRaven TypeScript SDK
Typed client for the PropRaven API, generated from its OpenAPI 3.1 spec. Zero runtime dependencies, ESM + CJS, Node 18+. Published to npm from GitHub Actions with provenance.
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
npm install @propraven/sdkQuickstart
import { PropRaven } from "@propraven/sdk";
const client = new PropRaven(); // reads PROPRAVEN_API_KEY
const parcel = await client.parcels.get("37:119:12104406");
console.log(parcel.owner_name, parcel.total_assessed_value); // numbers are numbers
const permits = await client.parcels.permits("37:119:12104406", { shape: "envelope" });
const owner = await client.owners.get("MARKEY ENTERPRISES INC");Server-side only: the REST API sends no CORS headers and keys are secret, so the client refuses to start in a browser unless you pass dangerouslyAllowBrowser: true.
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 { NotFoundError, RateLimitError, PaymentRequiredError, PropRavenError } from "@propraven/sdk";
try {
await client.parcels.get("37:119:00000000000");
} catch (err) {
if (err instanceof NotFoundError) console.log(err.code, err.detail, err.requestId);
else if (err instanceof RateLimitError) console.log("retry after", err.retryAfter, "s");
else if (err instanceof PaymentRequiredError) console.log(err.accepts); // x402 offer
else throw err;
}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.
const client = new PropRaven({ maxRetries: 3, timeout: 30_000 });
await client.search.full({ q: "Tryon", state: "NC" }, { maxRetries: 0 }); // per request
console.log(client.lastRateLimit); // { limit, remaining, reset }Pagination
for await (const row of client.deals.absenteeAll({ county_fips: "37119" }, { pageSize: 100, maxItems: 1000 })) {
console.log(row.parcel_id, row.total_assessed_value);
}
// search.full follows the keyset cursor (nextCursor -> after) for you:
const rows = await client.search.fullAll({ q: "Tryon", state: "NC" }, { maxItems: 200 }).toArray();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 { verifyWebhook, WebhookVerificationError } from "@propraven/sdk";
// verifyWebhook returns `unknown` unless you name the type you expect.
// Field reference: https://propraven.com/docs/webhooks
interface PropRavenEvent {
event_id: string;
event_type: "parcel.sold" | "parcel.owner_changed" | "parcel.permit_filed";
parcel_id: string;
state_fips: string;
county_fips: string;
[field: string]: unknown;
}
export async function POST(request: Request) {
const payload = await request.text(); // the RAW body
let event: PropRavenEvent;
try {
event = verifyWebhook<PropRavenEvent>({
payload,
signature: request.headers.get("X-PropRaven-Signature"),
secret: process.env.PROPRAVEN_WEBHOOK_SECRET ?? "",
});
} catch (err) {
if (err instanceof WebhookVerificationError) return new Response("Forbidden", { status: 401 });
throw err;
}
if (event.event_type === "parcel.sold") {
console.log("sold:", event.state_fips, event.county_fips, event.parcel_id, event.sale_price_usd);
}
return new Response(null, { status: 204 });
}