Every API here is available over the APIs.io API and to AI agents over MCP.
openapi: 3.2.0
info:
title: PropRaven Zoning API
version: 1.2.1
description: 'National property intelligence API: US parcels with ownership, valuation, permit, deed, risk and market data.
**Base URL.** `https://propraven.com/api/v1` (every path below starts with `/api/v1`; `https://api.propraven.com/api/v1` serves the same API). The REST API is for server-side use: it sends no CORS headers, and API keys are secret.
**Authentication.** `Authorization: Bearer pz_…`. Create a key in the dashboard. A few endpoints are key-optional (marked with an empty security requirement); anonymous calls to them are limited to 100 a day and 60 a minute per IP. The Free plan includes 1,000 API calls a month (60 a minute, 1,000 a day); `GET /api/v1/account/usage` reports your plan, period usage and limits.
**Numbers.** Since API 1.2.0, every field this spec types as `number` or `integer` is a JSON number. (Before 1.2.0, values read from Postgres `numeric` and `bigint` columns, such as `total_assessed_value`, were sent as strings like "19388200.00".) Identifiers stay strings: `parcel_id`, `apn`, `county_fips`, `state_fips`, `zip`.
**Errors.** Every error is an RFC 7807 problem (`application/problem+json`, schema `Problem`) with a stable `code`. Bad parameters are a 400 `invalid_parameter` naming each one in `errors`; an unknown endpoint is a 404 `not_found`; an unsupported method is a 405 with an `Allow` header; a query that runs out of time is a 504 `query_timeout` with `Retry-After`. Error bodies never contain database text. Paid endpoints answer an unpaid call with an x402 `402 Payment Required` envelope (`x402Version`, `accepts`).
**Pagination.** List endpoints take `limit` and `offset` and return `{data, total, limit, offset}`; request the next page with `offset + limit` until a page is shorter than `limit` or `offset` reaches `total`. `GET /api/v1/search/full` uses a keyset cursor: pass the previous page''s `nextCursor` as `after` until `nextCursor` is null.
**Rate limits.** Burst and daily windows by plan: Free 60 a minute / 1,000 a day; Developer 3,000 a minute / 100,000 a day; Team 12,000 a minute / 1,000,000 a day; key-optional endpoints called without a key, 60 a minute / 100 a day per IP. Authenticated responses carry `X-RateLimit-Limit` / `-Remaining` / `-Reset` (Unix seconds), all describing the window that will refuse you first. Every 429 and every 503 carries `Retry-After` (seconds): wait at least that long, then retry with backoff. A **429** means your plan''s window is used up (`rate_limit_exceeded`); or the key is cooling down for up to 15 minutes because its request rate spiked far above its own recent baseline (`key_cooldown`; it is not banned or revoked, and your other keys are unaffected); or one address failed authentication more than 60 times in a minute (`auth_failures_exceeded`). A **503** means PropRaven is briefly shedding load to protect the service (`load_shed`; calls without a key first, paid plans last) or the rate limiter is briefly unavailable. The MCP endpoint (`/mcp`) answers the same cases with the same status and `Retry-After`, plus a JSON-RPC error with code -32003.
**SDKs.** Official clients generated from this spec: `@propraven/sdk` (npm), `propraven` (PyPI) and `github.com/jdw2111/propraven-go`. Each retries 429/503/504 with backoff, raises typed errors on the `Problem` shape, paginates, and verifies webhook signatures. The `x-sdk-group`, `x-sdk-method` and `x-sdk-pagination` extensions name each operation in the SDKs.'
contact:
name: PropRaven Support
url: https://propraven.com/docs
email: support@propraven.com
license:
name: Commercial
url: https://propraven.com/terms#data-license
servers:
- url: https://propraven.com
description: Production. Every path starts with /api/v1. (https://api.propraven.com serves the same API.)
security:
- bearerAuth: []
tags:
- name: Zoning
description: 'Zoning from the jurisdiction that issues it: the raw code as published, overlays, and ordinance-parsed attributes with a citation per value, plus honest per-jurisdiction coverage. Account required.'
paths:
/api/v1/parcels/{id}/zoning:
get:
operationId: getParcelZoning
tags:
- Zoning
security:
- bearerAuth: []
summary: Zoning of a parcel, from the issuing jurisdiction
description: 'The parcel''s zoning code exactly as the issuing jurisdiction published it, the issuing body (a city or town, not the county), overlays as a set, split zoning, and, where the ordinance has been parsed, the underwriting attributes (minimum lot area, height, FAR/density, setbacks, by-right multifamily, ADU), each with its ordinance section, verbatim quote and link in `attributes.citations`. Null is never zero or false. `{ zoning: null, reason: "not_collected" }` means the parcel''s jurisdiction has not been collected yet; it says nothing about whether the parcel is zoned. A research starting point, not a legal determination. Account required.'
parameters:
- name: id
in: path
required: true
description: 'Parcel identifier: the canonical `state_fips:county_fips:parcel_id` form (e.g. `37:119:12104406`), the legacy 5-digit `county_fips5:parcel_id` form, or a PropRaven parcel UUID. URL-encode it.'
schema:
type: string
example: 37:119:12104406
responses:
'200':
description: The parcel's zoning, or not_collected.
headers:
X-RateLimit-Limit:
$ref: '#/components/headers/X-RateLimit-Limit'
X-RateLimit-Remaining:
$ref: '#/components/headers/X-RateLimit-Remaining'
X-RateLimit-Reset:
$ref: '#/components/headers/X-RateLimit-Reset'
content:
application/json:
schema:
type: object
required:
- zoning
properties:
zoning:
oneOf:
- $ref: '#/components/schemas/ParcelZoning'
- type: 'null'
reason:
type: string
enum:
- not_collected
coverage:
type: array
items:
$ref: '#/components/schemas/ZoningCoverageRow'
'400':
$ref: '#/components/responses/BadRequest'
'401':
$ref: '#/components/responses/Unauthorized'
'429':
$ref: '#/components/responses/RateLimitExceeded'
'500':
$ref: '#/components/responses/InternalError'
'404':
$ref: '#/components/responses/NotFound'
x-sdk-group: zoning
x-sdk-method: parcel
/api/v1/zoning/search:
get:
operationId: searchZoning
tags:
- Zoning
security:
- bearerAuth: []
summary: Screen parcels by zoning
description: Parcels by exact raw code (case-sensitive), base code, overlay, derived family, or parsed ordinance attribute. Attribute filters EXCLUDE parcels whose code is unparsed, so an empty result means unknown, not none; `dictionary_warning` and `coverage` say how much was parsed. Unknown parameters are a 400. Account required.
parameters:
- name: state_fips
in: query
schema:
type: string
- name: county_fips
in: query
schema:
type: string
- name: jurisdiction_id
in: query
schema:
type: string
- name: raw_code
in: query
schema:
type: string
description: Exact published code(s), comma-separated or repeated.
- name: base_code
in: query
schema:
type: string
- name: family
in: query
schema:
type: string
- name: has_overlay
in: query
schema:
type: string
- name: without_overlay
in: query
schema:
type: boolean
- name: multifamily_by_right
in: query
schema:
type: boolean
- name: adu_allowed
in: query
schema:
type: boolean
- name: min_lot_area_sqft_max
in: query
schema:
type: number
- name: max_height_ft_min
in: query
schema:
type: number
- name: limit
in: query
schema:
type: integer
minimum: 1
maximum: 500
default: 50
- name: offset
in: query
schema:
type: integer
minimum: 0
responses:
'200':
description: Matching parcels with the coverage of the jurisdictions searched.
headers:
X-RateLimit-Limit:
$ref: '#/components/headers/X-RateLimit-Limit'
X-RateLimit-Remaining:
$ref: '#/components/headers/X-RateLimit-Remaining'
X-RateLimit-Reset:
$ref: '#/components/headers/X-RateLimit-Reset'
content:
application/json:
schema:
type: object
required:
- results
- count
- limit
- offset
- coverage
properties:
results:
type: array
items:
$ref: '#/components/schemas/ParcelZoning'
count:
type: integer
limit:
type: integer
offset:
type: integer
coverage:
type: array
items:
$ref: '#/components/schemas/ZoningCoverageRow'
dictionary_warning:
type: string
'400':
$ref: '#/components/responses/BadRequest'
'401':
$ref: '#/components/responses/Unauthorized'
'429':
$ref: '#/components/responses/RateLimitExceeded'
'500':
$ref: '#/components/responses/InternalError'
x-sdk-group: zoning
x-sdk-method: search
/api/v1/zoning/coverage:
get:
operationId: getZoningCoverage
tags:
- Zoning
security:
- bearerAuth: []
summary: Zoning coverage per jurisdiction
description: One row per registered zoning body with its honest `servingStatus` (served / partial / no_zoning / failed / registered) and reason, parcels matched against a measured denominator, distinct codes, overlays, and how many codes have parsed ordinance attributes (zeros included). Account required; the same table is on https://propraven.com/coverage.
parameters:
- name: state_fips
in: query
schema:
type: string
- name: jurisdiction_id
in: query
schema:
type: string
responses:
'200':
description: Coverage rows and totals.
headers:
X-RateLimit-Limit:
$ref: '#/components/headers/X-RateLimit-Limit'
X-RateLimit-Remaining:
$ref: '#/components/headers/X-RateLimit-Remaining'
X-RateLimit-Reset:
$ref: '#/components/headers/X-RateLimit-Reset'
content:
application/json:
schema:
type: object
required:
- coverage
- totals
properties:
coverage:
type: array
items:
$ref: '#/components/schemas/ZoningCoverageRow'
totals:
type: object
additionalProperties:
type:
- number
- 'null'
'401':
$ref: '#/components/responses/Unauthorized'
'429':
$ref: '#/components/responses/RateLimitExceeded'
'500':
$ref: '#/components/responses/InternalError'
x-sdk-group: zoning
x-sdk-method: coverage
components:
headers:
X-RateLimit-Reset:
description: Unix timestamp (seconds) when the window described by X-RateLimit-Limit resets.
schema:
type: integer
example: 1711670400
Retry-After:
description: Seconds to wait before retrying.
schema:
type: integer
example: 5
X-RateLimit-Remaining:
description: Calls left in the window described by X-RateLimit-Limit.
schema:
type: integer
example: 997
X-Request-Id:
description: Support handle for this request; equals the problem body's `request_id`. Sent on every error response, including 401.
schema:
type: string
X-RateLimit-Limit:
description: Size of the rate-limit window that will refuse you first — the per-minute burst window or the per-day window, whichever has fewer calls left. Limit, Remaining and Reset always describe that same window. Sent on every authenticated response (and on anonymous responses of key-optional endpoints).
schema:
type: integer
example: 1000
responses:
NotFound:
description: The resource (or the endpoint) does not exist.
content:
application/problem+json:
schema:
$ref: '#/components/schemas/Problem'
example:
type: https://api.propraven.com/errors/not-found
title: Not Found
status: 404
detail: No API endpoint at /api/v1/doesnotexist.
code: not_found
headers:
X-Request-Id:
$ref: '#/components/headers/X-Request-Id'
BadRequest:
description: A query or body parameter is invalid. Nothing was read or charged.
headers:
X-RateLimit-Limit:
$ref: '#/components/headers/X-RateLimit-Limit'
X-RateLimit-Remaining:
$ref: '#/components/headers/X-RateLimit-Remaining'
X-RateLimit-Reset:
$ref: '#/components/headers/X-RateLimit-Reset'
X-Request-Id:
$ref: '#/components/headers/X-Request-Id'
content:
application/problem+json:
schema:
$ref: '#/components/schemas/Problem'
example:
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
Unauthorized:
description: Missing, malformed or invalid credential.
content:
application/problem+json:
schema:
$ref: '#/components/schemas/Problem'
example:
type: https://api.propraven.com/errors/authentication-required
title: Unauthorized
status: 401
detail: Provide an API key in the Authorization header ('Bearer pz_...') or sign in via the web dashboard.
code: authentication_required
headers:
X-Request-Id:
$ref: '#/components/headers/X-Request-Id'
InternalError:
description: Unexpected server error. The body never carries database or driver text.
content:
application/problem+json:
schema:
$ref: '#/components/schemas/Problem'
example:
type: https://api.propraven.com/errors/internal-error
title: Internal Server Error
status: 500
detail: An internal error occurred.
code: internal_error
request_id: iad1::abc123
headers:
X-Request-Id:
$ref: '#/components/headers/X-Request-Id'
RateLimitExceeded:
description: Rate limit exceeded.
headers:
X-RateLimit-Limit:
$ref: '#/components/headers/X-RateLimit-Limit'
X-RateLimit-Remaining:
$ref: '#/components/headers/X-RateLimit-Remaining'
X-RateLimit-Reset:
$ref: '#/components/headers/X-RateLimit-Reset'
Retry-After:
$ref: '#/components/headers/Retry-After'
X-Request-Id:
$ref: '#/components/headers/X-Request-Id'
content:
application/problem+json:
schema:
$ref: '#/components/schemas/Problem'
example:
type: https://api.propraven.com/errors/rate-limit-exceeded
title: Rate limit exceeded
status: 429
detail: Rate limit exceeded
code: rate_limit_exceeded
schemas:
ZoningAttributes:
type: object
required:
- attributeSource
- citations
description: 'The parsed ordinance dictionary for the code. EVERY value may be null: null means the ordinance has not been parsed for this code, or the text read does not state the value (or its quote did not verify). Null is never zero or false.'
properties:
minLotAreaSqft:
type:
- number
- 'null'
minLotWidthFt:
type:
- number
- 'null'
maxHeightFt:
type:
- number
- 'null'
maxStories:
type:
- number
- 'null'
maxFar:
type:
- number
- 'null'
maxDensityUnitsPerAcre:
type:
- number
- 'null'
maxLotCoveragePct:
type:
- number
- 'null'
setbackFrontFt:
type:
- number
- 'null'
setbackSideFt:
type:
- number
- 'null'
setbackRearFt:
type:
- number
- 'null'
minParkingPerUnit:
type:
- number
- 'null'
multifamilyByRight:
type:
- boolean
- 'null'
duplexByRight:
type:
- boolean
- 'null'
triplexQuadplexByRight:
type:
- boolean
- 'null'
aduAllowed:
type:
- boolean
- 'null'
shortTermRentalAllowed:
type:
- boolean
- 'null'
maxUnitsByRight:
type:
- number
- 'null'
permittedUseFamilies:
type:
- array
- 'null'
items:
type: string
conditionalUseFamilies:
type:
- array
- 'null'
items:
type: string
citations:
type: object
description: Attribute name (e.g. `min_lot_area_sqft`) -> every record read for it, each with its section, verbatim quote and link.
additionalProperties:
type: array
items:
$ref: '#/components/schemas/ZoningAttributeCitation'
ordinanceVersion:
type:
- string
- 'null'
attributeSource:
type: string
enum:
- ordinance_text
- publisher_table
- manual
- none
sourceCitation:
type:
- string
- 'null'
sourceUrl:
type:
- string
- 'null'
parsedAt:
type:
- string
- 'null'
reviewStatus:
type: string
enum:
- unreviewed
- machine
- human_verified
- disputed
ZoningAttributeCitation:
type: object
required:
- verified
properties:
value:
type:
- number
- boolean
- string
- array
- 'null'
items:
type: string
qualifier:
type:
- string
- 'null'
section:
type:
- string
- 'null'
description: The ordinance's own label for the place, e.g. `Table 4-1`.
quote:
type:
- string
- 'null'
description: Verbatim ordinance text, mechanically re-found in the authority's published text.
url:
type:
- string
- 'null'
description: Deep link to the section in the authority's text.
confidence:
type:
- number
- 'null'
verified:
type: boolean
description: True when the quote was found exactly once in the text read. Only verified values reach the typed attribute fields.
note:
type:
- string
- 'null'
ParcelZoning:
type: object
required:
- parcel
- matched
- rawCode
- overlays
- attributes
- match
properties:
parcel:
type: object
properties:
stateFips:
type: string
countyFips:
type: string
parcelId:
type: string
matched:
type: boolean
description: False when the parcel's centroid fell in no published district (a measurement, not an error).
jurisdiction:
type:
- object
- 'null'
required:
- jurisdictionId
- name
- type
- stateFips
properties:
jurisdictionId:
type: string
description: Registry id of the zoning body, e.g. `nc-37119-charlotte`.
name:
type: string
type:
type: string
enum:
- city
- town
- village
- borough
- township
- county
- unincorporated
- consolidated
- special_district
stateFips:
type: string
placeFips:
type:
- string
- 'null'
countyFips:
type: array
items:
type: string
zoningHomeUrl:
type:
- string
- 'null'
boundaryJurisdiction:
type:
- object
- 'null'
required:
- jurisdictionId
- name
- type
- stateFips
properties:
jurisdictionId:
type: string
description: Registry id of the zoning body, e.g. `nc-37119-charlotte`.
name:
type: string
type:
type: string
enum:
- city
- town
- village
- borough
- township
- county
- unincorporated
- consolidated
- special_district
stateFips:
type: string
placeFips:
type:
- string
- 'null'
countyFips:
type: array
items:
type: string
zoningHomeUrl:
type:
- string
- 'null'
rawCode:
type:
- string
- 'null'
description: 'THE FACT: the zoning code exactly as the issuing jurisdiction published it.'
baseCode:
type:
- string
- 'null'
codeName:
type:
- string
- 'null'
overlays:
type: array
items:
type: object
properties:
code:
type: string
kind:
type: string
name:
type:
- string
- 'null'
restrictionSummary:
type:
- string
- 'null'
splitZoned:
type:
- boolean
- 'null'
altCodes:
type: array
items:
type: string
zonedSince:
type:
- string
- 'null'
match:
type: object
properties:
method:
type: string
confidence:
type:
- number
- 'null'
overlaysAvailable:
type: boolean
attributes:
$ref: '#/components/schemas/ZoningAttributes'
attributesMatchedOn:
type:
- string
- 'null'
enum:
- raw_code
- base_code
- null
conditionalNote:
type:
- string
- 'null'
derivedFamily:
type: object
properties:
family:
type:
- string
- 'null'
method:
type: string
confidence:
type:
- number
- 'null'
unresolvedReason:
type: string
evidence:
type: string
provenance:
type: object
properties:
sourceUrl:
type:
- string
- 'null'
publisherRole:
type:
- string
- 'null'
harvestedAt:
type:
- string
- 'null'
epochVersion:
type:
- string
- 'null'
ZoningCoverageRow:
type: object
required:
- jurisdictionId
- jurisdictionName
- servingStatus
- parcelsMatched
- matchRate
- distinctCodes
- codesWithAttributes
- dictionaryCompleteness
properties:
jurisdictionId:
type: string
jurisdictionName:
type: string
jurisdictionType:
type: string
stateFips:
type: string
countyFips:
type: array
items:
type: string
servingStatus:
type: string
enum:
- served
- partial
- no_zoning
- failed
- registered
description: served = codes on parcels AND parsed ordinance attributes; partial = codes on parcels, attributes missing or incomplete; no_zoning = the body has no zoning ordinance; failed = could not be collected; registered = not yet attempted.
statusReason:
type:
- string
- 'null'
statusAsOf:
type:
- string
- 'null'
parcelsInJurisdiction:
type:
- integer
- 'null'
denominatorMethod:
type: string
enum:
- boundary_centroid
- publisher_count
- unknown
parcelsMatched:
type: integer
parcelsUnmatched:
type: integer
matchRate:
type:
- number
- 'null'
distinctCodes:
type: integer
overlaysAvailable:
type: boolean
distinctOverlays:
type: integer
parcelsWithOverlay:
type: integer
parcelsSplitZoned:
type:
- integer
- 'null'
parcelsWithAttributes:
type: integer
parcelAttributeRate:
type:
- number
- 'null'
codesWithAttributes:
type: integer
codesMissingAttributes:
type: integer
dictionaryCompleteness:
type:
- number
- 'null'
primarySourceUrl:
type:
- string
- 'null'
lastHarvestedAt:
type:
- string
- 'null'
Problem:
type: object
description: 'THE error body of every /api/v1 endpoint (RFC 7807 problem details, served as `application/problem+json`). `code` is the stable machine-readable identifier (e.g. invalid_parameter, authentication_required, account_required, monthly_cap_reached, query_timeout, not_found, method_not_allowed); `detail` is human-readable and never contains database or driver text. Validation failures add `errors: [{param, message}]`. `request_id` identifies the request for support; every error response carries it, and the same value in the `X-Request-Id` header. Per-code members (e.g. `reason`, `retry_after`, `used` / `limit` / `plan` / `upgrade`, `allow`) sit beside the core members. Exception: an x402 `402 Payment Required` keeps the x402 protocol envelope (`x402Version`, `accepts`).'
required:
- type
- title
- status
- detail
- code
properties:
type:
type: string
title:
type: string
status:
type: integer
detail:
type: string
code:
type: string
description: Stable machine-readable code.
example: invalid_parameter
reason:
type: string
description: Finer reason for `code`, e.g. people_data_requires_account.
errors:
type: array
description: Per-parameter validation failures (400 invalid_parameter).
items:
type: object
required:
- param
- message
properties:
param:
type: string
example: limit
message:
type: string
example: must be >= 1
request_id:
type: string
description: Support handle for this request.
additionalProperties: true
example:
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
securitySchemes:
bearerAuth:
type: http
scheme: bearer
bearerFormat: pz_*
description: 'API key with pz_ prefix. Pass as: Authorization: Bearer pz_your_api_key'
sessionCookie:
type: apiKey
in: cookie
name: sb-<project-ref>-auth-token
description: The signed-in web session (Supabase auth cookie) set by signing in at https://propraven.com/sign-in. Used only by account-scoped dashboard routes; an API key is not accepted on those routes.
externalDocs:
description: 'Connect your agent to PropRaven: the MCP server, the discover -> preview -> pay (x402) flow, and pricing.'
url: https://propraven.com/docs/agents