npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

@propraven/sdk

v0.3.0

Published

Official TypeScript/JavaScript SDK for the PropRaven property intelligence API (US parcels, owners, permits, deeds, deals, market data).

Readme

PropRaven TypeScript SDK

npm

The official TypeScript / JavaScript client for the PropRaven property intelligence API: US parcels, owners, permits, deeds, deal screens, market data and webhooks.

  • Typed methods for every operation in the API's OpenAPI spec, generated from openapi.json.
  • Zero runtime dependencies (uses the global fetch), Node.js 18+, ESM and CommonJS, with type definitions.
  • Retries with backoff, RFC 7807 errors as typed exceptions, auto-pagination, webhook verification and rate-limit info.

Docs: propraven.com/docs/typescript · REST reference: propraven.com/docs/v1 · Developer hub: propraven.com/developers · Hosted MCP server: propraven.com/docs/mcp

Server-side only. The REST API sends no CORS headers and API keys are secret, so the client refuses to run in a browser (it throws if window and document exist) unless you pass dangerouslyAllowBrowser: true. Call PropRaven from your server, a serverless function or a script.

Install

npm install @propraven/sdk

Quick start

import PropRaven from '@propraven/sdk';

const client = new PropRaven(); // reads PROPRAVEN_API_KEY from the environment

const parcel = await client.parcels.get('37:119:12104406');
console.log(parcel.address, parcel.total_assessed_value);

const permits = await client.parcels.permits('37:119:12104406', { shape: 'envelope' });

const page = await client.search.parcels({
  bounds: { north: 35.85, south: 35.75, east: -78.55, west: -78.7 },
  filters: { absenteeOnly: true },
  limit: 25,
});

CommonJS works too: const { PropRaven } = require('@propraven/sdk');.

Calling convention

Every method takes its path parameters positionally (in path order), then one params object that holds the query parameters, header parameters and JSON body fields, then optional per-request options:

client.parcels.permits(id, { shape: 'envelope' }, { timeout: 10_000 });
client.search.parcels({ bounds, filters, limit });         // body fields
client.verify.get({ parcel_id: '37:119:12104406', fields: 'year_built', preview: 'true' });

Parameter names are the wire names (county_fips, state_fips, min_value). Header parameters get friendly names: X-CREDIT-TOKEN is creditToken and X-PAYMENT is payment. The SDK never signs x402 payments; a payment requirement surfaces as PaymentRequiredError.

Responses are typed: ParcelsGetResponse, DealsAbsenteeResponse, ... (every component schema is exported as well, e.g. Parcel, Owner, Permit). Numbers are JSON numbers; identifiers (parcel_id, apn, county_fips, state_fips, zip) are strings. CSV endpoints (search.export) return a string.

Authentication

Pass apiKey, or set PROPRAVEN_API_KEY. The key is sent as Authorization: Bearer <key>.

const client = new PropRaven({ apiKey: process.env.MY_PROPRAVEN_KEY });

PropRaven keys start with pz_; the client logs a warning (and still sends it) when a key does not. A missing key is allowed — several endpoints are key-optional (anonymous calls are limited to 100 a day and 60 a minute per IP) — and the server answers 401 where a key is required.

Errors

Non-2xx responses throw a subclass of APIError, parsed from the API's RFC 7807 problem body (older {error} bodies and x402 402 envelopes are understood too):

import PropRaven, { NotFoundError, RateLimitError, APIError } from '@propraven/sdk';

try {
  await client.parcels.get('37:119:0000');
} catch (err) {
  if (err instanceof NotFoundError) {
    console.log('no such parcel');
  } else if (err instanceof RateLimitError) {
    console.log(`retry in ${err.retryAfter}s`);
  } else if (err instanceof APIError) {
    console.log(err.status, err.code, err.detail, err.errors, err.requestId);
  } else {
    throw err;
  }
}

| Status | Class | | --- | --- | | 400 | BadRequestError (errors lists each bad {param, message}) | | 401 | AuthenticationError | | 402 | PaymentRequiredError (accepts holds the x402 requirements, empty otherwise) | | 403 | PermissionDeniedError | | 404 | NotFoundError | | 405 | MethodNotAllowedError | | 409 | ConflictError | | 413 | PayloadTooLargeError | | 422 | UnprocessableEntityError | | 429 | RateLimitError (retryAfter in seconds, or null) | | 503 | ServiceUnavailableError | | 504 | GatewayTimeoutError | | other 5xx | InternalServerError | | no response | APIConnectionError, APITimeoutError (a subclass) | | aborted via signal | APIUserAbortError |

Every APIError has status, type, title, detail, code, errors, requestId, headers, body and retryAfter; its message is "<status> <code>: <detail>". All errors extend PropRavenError. The classes are also available as statics (PropRaven.NotFoundError).

Retries and timeouts

Failed requests are retried up to 2 times (maxRetries) on network errors, timeouts, 429, 503 and 504 for any method, and on other 5xx only for GET/HEAD/DELETE/OPTIONS. Other 4xx — 402 in particular, whose Retry-After is in days — are never retried. The wait is the server's Retry-After (seconds or HTTP date), else the X-RateLimit-Reset of an exhausted window, else exponential backoff (0.5 s, 1 s, ... ±25% jitter). If the server asks for more than 60 s the error is raised instead of waiting.

The default timeout is 60 s (timeout, in milliseconds). Both can be set per client and per request:

const client = new PropRaven({ maxRetries: 4, timeout: 20_000 });
await client.deals.absentee({ county_fips: '37119' }, { maxRetries: 0, timeout: 5_000 });

Pagination

Every paginated method m has an auto-paginating sibling mAll that returns an AsyncIterable of items. Offset endpoints advance offset (in the query string, or in the JSON body for POST /search) until a short page, total, or has_more: false; search.full follows nextCursor via after.

for await (const parcel of client.deals.absenteeAll({ county_fips: '37119' }, { pageSize: 100, maxItems: 1000 })) {
  console.log(parcel.parcel_id);
}

const hits = await client.search.fullAll({ q: 'main st', state: 'NC' }, { maxItems: 200 }).toArray();

pageSize is sent as limit; maxItems bounds the total. The second argument also accepts the per-request options.

Webhooks

Deliveries carry X-PropRaven-Signature: t=<unix_ms>,v1=<hex>, an HMAC-SHA256 of `${t}.${rawBody}` keyed with your webhook secret (whsec_..., used exactly as issued). Verify against the raw body:

import { verifyWebhook, WebhookVerificationError } from '@propraven/sdk/webhooks'; // or from '@propraven/sdk'

app.post('/webhooks/propraven', express.raw({ type: 'application/json' }), (req, res) => {
  try {
    const event = verifyWebhook({
      payload: req.body, // Buffer or string, exactly as received
      signature: req.header('X-PropRaven-Signature'),
      secret: process.env.PROPRAVEN_WEBHOOK_SECRET!,
    });
    console.log(event);
    res.sendStatus(200);
  } catch (err) {
    if (err instanceof WebhookVerificationError) return res.sendStatus(400);
    throw err;
  }
});

Timestamps more than toleranceSeconds (default 300) from now are rejected; several v1= entries are accepted (secret rotation); comparison is constant-time. The verifier is synchronous and dependency-free.

Rate-limit info

client.lastRateLimit holds { limit, remaining, reset } from the most recent response that carried X-RateLimit-* headers (reset is Unix epoch seconds), or null. For one call, use .withResponse():

const { data, response, rateLimit, requestId } = await client.account.usage().withResponse();
console.log(rateLimit?.remaining, response.status);

Configuration

| Option | Default | | | --- | --- | --- | | apiKey | process.env.PROPRAVEN_API_KEY | pz_... key; may be omitted | | baseURL | process.env.PROPRAVEN_BASE_URL or https://propraven.com | paths include /api/v1 | | timeout | 60000 | milliseconds | | maxRetries | 2 | | | defaultHeaders | {} | sent on every request | | fetch | global fetch | custom fetch implementation | | dangerouslyAllowBrowser | false | see the server-side note above |

Per-request options: timeout, maxRetries, headers (a null value removes a header), query, signal.

Methods

70 operations in 19 namespaces.

| Method | HTTP | Summary | | --- | --- | --- | | client.account.usage(params?) | GET /api/v1/account/usage | Current-period usage and quota | | client.cmbs.exposure(params?) | GET /api/v1/cmbs/exposure | CMBS loan exposure for a parcel or an owner | | client.cohorts.export(id, params?) | GET /api/v1/cohorts/{id}/export | Mail-merge export of one of your lists (account required; included for subscribers, per row otherwise) | | client.cohorts.list(params?) | GET /api/v1/cohorts | List your saved parcel lists (cohorts) | | client.coverage.get(params?) | GET /api/v1/coverage | Get coverage statistics | | client.coverage.map(params?) | GET /api/v1/coverage/map | County coverage map data | | client.credits.balance(params) | GET /api/v1/storefront/credits/balance | Read a prepaid credit balance + ledger | | client.credits.topup(params) | GET /api/v1/storefront/credits/topup | Fund a prepaid credit balance over x402 | | client.crime.lookup(params) | GET /api/v1/crime/lookup | Crime score near a point | | client.deals.absentee(params?)+ absenteeAll() iterator | GET /api/v1/deals/absentee | Find absentee owners | | client.deals.contractors(params?)+ contractorsAll() iterator | GET /api/v1/deals/contractors | Search contractors by permit activity | | client.deals.entities(params?)+ entitiesAll() iterator | GET /api/v1/deals/entities | Find entity-owned parcels (LLC, Corp, Trust, LP) | | client.deals.flips(params?)+ flipsAll() iterator | GET /api/v1/deals/flips | Find property flips | | client.deals.highLandRatio(params?)+ highLandRatioAll() iterator | GET /api/v1/deals/high-land-ratio | Find parcels with high land-to-improvement ratio | | client.deals.lenders(params?)+ lendersAll() iterator | GET /api/v1/deals/lenders | Search lender profiles | | client.deals.longHold(params?)+ longHoldAll() iterator | GET /api/v1/deals/long-hold | Find long-held parcels (10+ years) | | client.deals.market(params?)+ marketAll() iterator | GET /api/v1/deals/market | County-quarter transaction summary or affordability index | | client.deals.portfolioOwners(params?)+ portfolioOwnersAll() iterator | GET /api/v1/deals/portfolio-owners | Find portfolio investors (owners of 2+ properties) | | client.freshness.datasets(params?) | GET /api/v1/freshness/datasets | Per-dataset availability and freshness | | client.freshness.get(params?) | GET /api/v1/freshness | How fresh the served parcel snapshot is | | client.leads.find(params) | GET /api/v1/leads/find | Lead feed (paid, priced per lead) — with a FREE preview | | client.lookup.batch(params) | POST /api/v1/lookup/batch | Resolve up to 500 parcel queries in one call | | client.lookup.get(params) | GET /api/v1/lookup | Exact parcel lookup (UUID or APN) | | client.market.counties(params?)+ countiesAll() iterator | GET /api/v1/market/counties | Get county market statistics | | client.market.county(fips, params?) | GET /api/v1/market/counties/{fips} | Detailed view for a single county | | client.market.flips(params?)+ flipsAll() iterator | GET /api/v1/market/flips | Flip-activity summary grouped by county | | client.market.snapshot(params?) | GET /api/v1/market/snapshot | Market snapshot for a geography | | client.market.trends(params?) | GET /api/v1/market/trends | Get market trends | | client.owners.card(params?) | GET /api/v1/owners/card | Owner card -- the owner of record and their mailing contact (account required) | | client.owners.get(name, params?) | GET /api/v1/owners/{name} | Get owner profile | | client.owners.portfolio(name, params?) | GET /api/v1/owners/{name}/portfolio | Get owner portfolio summary | | client.owners.properties(name, params?)+ propertiesAll() iterator | GET /api/v1/owners/{name}/properties | Get owner's properties | | client.owners.report(name, params?) | GET /api/v1/owners/{name}/report | Owner intelligence report (paid, priced per resolution; account required) — with a free preview | | client.owners.search(params) | GET /api/v1/owners/search | Search property owners | | client.owners.transactions(name, params?) | GET /api/v1/owners/{name}/transactions | Recorded deed transactions for an owner | | client.parcels.assessmentHistory(id, params?) | GET /api/v1/parcels/{id}/assessment-history | Get recorded annual assessment history | | client.parcels.batch(params) | POST /api/v1/parcels/batch | Fetch up to 100 parcels by (state, county, parcel) tuple | | client.parcels.compPack(id, params?) | GET /api/v1/parcels/{id}/comp-pack | Comp pack (paid, priced per pack) — with a FREE preview | | client.parcels.comps(id, params?) | GET /api/v1/parcels/{id}/comps | Comparable sales for a parcel | | client.parcels.deeds(id, params?) | GET /api/v1/parcels/{id}/deeds | Get parcel deed history | | client.parcels.geojson(params) | GET /api/v1/parcels/geojson | Parcel polygons as GeoJSON for a bounding box | | client.parcels.get(id, params?) | GET /api/v1/parcels/{id} | Get parcel by ID | | client.parcels.occupants(id, params?) | GET /api/v1/parcels/{id}/occupants | Business occupants of a parcel | | client.parcels.owner(id, params?) | GET /api/v1/parcels/{id}/owner | Get parcel owner details and portfolio | | client.parcels.permits(id, params?) | GET /api/v1/parcels/{id}/permits | Get parcel permits | | client.parcels.pois(params) | GET /api/v1/parcels/poi | Business parcels in a small bounding box | | client.parcels.report(id, params?) | GET /api/v1/parcels/{id}/report | Parcel dossier (paid, provenance-first) | | client.parcels.risks(id, params?) | GET /api/v1/parcels/{id}/risks | Get parcel risk assessment | | client.parcels.riskScore(id, params?) | GET /api/v1/parcels/{id}/risk-score | Risk score (paid, priced per assessment) — with a FREE preview | | client.parcels.trafficHistory(id, params) | GET /api/v1/parcels/{id}/traffic-history | Nearest traffic station + AADT history | | client.parcels.violations(id, params?) | GET /api/v1/parcels/{id}/violations | Code violations on a parcel | | client.search.autocomplete(params) | GET /api/v1/search/autocomplete | Address / place / parcel autocomplete | | client.search.export(params?) | GET /api/v1/search/export | Export search results as CSV | | client.search.full(params)+ fullAll() iterator | GET /api/v1/search/full | Full paginated text + attribute search | | client.search.parcels(params?)+ parcelsAll() iterator | POST /api/v1/search | Search parcels | | client.storefront.availability(params?) | GET /api/v1/storefront/availability | Machine Storefront -- try-before-buy (jurisdiction coverage or per-parcel quote) | | client.storefront.catalog(params?) | GET /api/v1/storefront/catalog | Machine Storefront — sealed field catalog | | client.traffic.stations(params) | GET /api/v1/traffic/stations | Traffic count stations in a bounding box | | client.verify.batch(params) | POST /api/v1/verify | Verify facts (batch, paid per lookup) - FREE preview | | client.verify.get(params) | GET /api/v1/verify | Verify facts for one parcel | | client.watch.create(params) | POST /api/v1/watch | Create a watch (free) | | client.watch.delete(id, params) | DELETE /api/v1/watch/{id} | Delete a watch | | client.watch.list(params) | GET /api/v1/watch | List your watches | | client.watch.poll(id, params) | GET /api/v1/watch/{id} | Poll a watch for new changes (priced per delta) | | client.webhooks.create(params) | POST /api/v1/webhooks | Create a webhook endpoint | | client.webhooks.delete(id, params?) | DELETE /api/v1/webhooks/{id} | Soft-disable a webhook endpoint | | client.webhooks.deliveries(id, params?) | GET /api/v1/webhooks/{id}/deliveries | Recent delivery attempts for a webhook | | client.webhooks.get(id, params?) | GET /api/v1/webhooks/{id} | Get a single webhook endpoint | | client.webhooks.list(params?) | GET /api/v1/webhooks | List webhook endpoints | | client.webhooks.retryDelivery(id, deliveryId, params?) | POST /api/v1/webhooks/{id}/deliveries/{deliveryId}/retry | Re-queue a failed webhook delivery |

Regenerating from the spec

The typed layer (src/generated/) is generated from openapi.json by scripts/generate.mjs; the core (src/core/, src/webhooks.ts, src/client.ts) is hand-written.

npm run spec:update -- /path/to/openapi.json   # or a URL; default https://propraven.com/openapi.json
npm run generate
npm run typecheck && npm test

.github/workflows/regenerate.yml does this daily and opens a spec-sync PR when the live spec changes.

Requirements

Node.js 18 or later (or any runtime with a WHATWG fetch: Deno, Bun, Cloudflare Workers, Vercel Edge). TypeScript users need the DOM or @types/node typings for fetch/Response.

License

Apache-2.0. API data is licensed separately under the PropRaven terms.