Search and retrieve building permits
- search_permits - Search Permits
- export_permits - Export Permits
- sync_permits - Sync Permits
- list_permit_events - List Permit Events
- get_permit - Get Permit
- get_permits_by_address - Get Permits By Address
- get_coverage_stats - Get Coverage Stats
- list_plays - List Plays
- system_age - System Age
- battery_retrofit_candidates - Battery Retrofit Candidates
- reroof_due - Reroof Due
- orphan_recovery - Orphan Recovery
- list_covered_jurisdictions - List Covered Jurisdictions
Search building permits by location, date, category, contractor and more.
Filters combine with AND. Supply at least one narrowing filter -- a city, ZIP, jurisdiction, date range or lat/lng radius -- for a query that returns quickly.
Results are paginated and ordered newest-first on the permit's best available date
(date_issued, falling back to date_filed). total is exact up to 10,000; beyond
that it stops counting and returns total: 10000 with total_capped: true, meaning
"10,000 or more". If the count cannot finish in time the response carries
total: null with total_unknown: true -- an unknown total is reported as unknown
and never as a number. Rows are unaffected either way; page through them normally.
category=hvac matches HVAC and MECHANICAL together, because AC and furnace work is
filed under either depending on the city. Unknown parameter names are rejected with a
400 rather than ignored, so a typo can never silently return unfiltered results.
Free-tier keys are limited to a recent window; paid tiers have full history.
import os
from permitstack import Permitstack
with Permitstack(
api_key=os.getenv("PERMITSTACK_API_KEY", ""),
) as p_client:
res = p_client.permits.search_permits(radius_miles=5.0, fields="summary", record_kind="permit", page=1, per_page=25, count_only=False)
# Handle response
print(res)| Parameter | Type | Required | Description |
|---|---|---|---|
zip_code |
OptionalNullable[str] | ➖ | 5-digit ZIP code |
city |
OptionalNullable[str] | ➖ | City name |
state |
OptionalNullable[str] | ➖ | 2-letter state code |
jurisdiction |
OptionalNullable[str] | ➖ | Jurisdiction name (e.g. 'Wake County', 'Tacoma') or partial match |
address |
OptionalNullable[str] | ➖ | Street address, partial + case-insensitive (e.g. '1600 Pennsylvania') — indexed, fast |
lat |
OptionalNullable[float] | ➖ | Latitude for radius search |
lng |
OptionalNullable[float] | ➖ | Longitude for radius search |
radius_miles |
Optional[float] | ➖ | Radius in miles (used with lat/lng) |
fields |
Optional[str] | ➖ | 'summary' (default) or 'full'. With 'full' each result is a PermitDetail rather than a PermitSummary -- the same nine extra columns GET /v1/permits/{id} returns (record_kind, date_expired, fee_amount, stories, units, square_footage, applicant_name, contractor_license, created_at) -- so you do not fetch them one permit at a time. See the PermitDetail schema for their types; the declared response schema here is PermitSummary, which is the default shape. Developer plan and above. |
bbox |
OptionalNullable[str] | ➖ | Map-viewport bounding box 'minLng,minLat,maxLng,maxLat'. Returns permits whose location falls inside the box (geocoded permits only). |
polygon |
OptionalNullable[str] | ➖ | A drawn area as a GeoJSON Polygon geometry (URL-encoded), e.g. {"type":"Polygon","coordinates":[[[lng,lat],...]]}. Returns permits inside the polygon (geocoded permits only). |
category |
OptionalNullable[str] | ➖ | Permit category (e.g. solar, SOLAR, roofing, hvac — case insensitive) |
status |
OptionalNullable[models.PermitStatus] | ➖ | Permit status (e.g. issued, filed, final) |
property_type |
OptionalNullable[models.PropertyType] | ➖ | Property type (e.g. residential, commercial) |
tag |
OptionalNullable[str] | ➖ | Filter by tag |
record_kind |
Optional[str] | ➖ | Record kind: 'permit' (default, building permits only), 'contractor', 'tag', 'non_building', 'admin', or 'all' |
filed_after |
datetime | ➖ | Filed on or after this date |
filed_before |
datetime | ➖ | Filed on or before this date |
issued_after |
datetime | ➖ | Issued on or after this date |
issued_before |
datetime | ➖ | Issued on or before this date |
date_after |
datetime | ➖ | On or after this date, matched against whichever date a record has (issued, else filed). Use this when a source populates only one of issued/filed — e.g. issued-only feeds vs filed-only feeds. |
date_before |
datetime | ➖ | On or before this date, matched against whichever date a record has (issued, else filed). |
parcel |
OptionalNullable[str] | ➖ | Parcel number / APN / folio (formatting ignored). Returns permits on that parcel where the source publishes one. |
min_value |
OptionalNullable[float] | ➖ | Minimum estimated value |
max_value |
OptionalNullable[float] | ➖ | Maximum estimated value |
min_solar_kw |
OptionalNullable[float] | ➖ | Minimum extracted solar system size (kW DC) |
max_solar_kw |
OptionalNullable[float] | ➖ | Maximum extracted solar system size (kW DC) |
min_sqft |
OptionalNullable[float] | ➖ | Minimum square footage mentioned (from enrichment) |
has_enrichment |
OptionalNullable[bool] | ➖ | Only permits that have (true) or lack (false) LLM enrichment |
scope |
OptionalNullable[str] | ➖ | Substring match on the enriched work scope |
q |
OptionalNullable[str] | ➖ | Case-insensitive substring match across description, address, and permit number. Terms under 3 characters are matched but not counted: total is null with total_unknown=true (a 1-2 character term has no trigram index and the count would cost tens of seconds). |
contractor_name |
OptionalNullable[str] | ➖ | Contractor name (partial match) |
owner_filed |
OptionalNullable[bool] | ➖ | true = owner-filed permits only (no contractor on record but an owner name is present — often a DIY/homeowner lead, though for feeds that don't capture contractors the owner may be a builder or institution); false = permits that have a contractor |
has_owner_address |
OptionalNullable[bool] | ➖ | true = only permits that carry a property-owner mailing address; false = only those that do not. Coverage varies sharply by jurisdiction (we hold the county assessor roll for some and not others), so this is a targeting filter rather than a defect: it returns exactly the rows that are actionable. The address itself is visible on Business and above. |
page |
Optional[int] | ➖ | N/A |
per_page |
Optional[int] | ➖ | Results per page. Above your plan's maximum this is clamped, not rejected; the response echoes the per_page actually applied. |
keyword |
OptionalNullable[str] | ➖ | : warning: ** DEPRECATED **: This will be removed in a future release, please migrate away from it as soon as possible. Alias of q. |
date_from |
datetime | ➖ | : warning: ** DEPRECATED **: This will be removed in a future release, please migrate away from it as soon as possible. Alias of date_after. |
date_to |
datetime | ➖ | : warning: ** DEPRECATED **: This will be removed in a future release, please migrate away from it as soon as possible. Alias of date_before. |
start_date |
datetime | ➖ | : warning: ** DEPRECATED **: This will be removed in a future release, please migrate away from it as soon as possible. Alias of date_after. |
end_date |
datetime | ➖ | : warning: ** DEPRECATED **: This will be removed in a future release, please migrate away from it as soon as possible. Alias of date_before. |
limit |
OptionalNullable[int] | ➖ | : warning: ** DEPRECATED **: This will be removed in a future release, please migrate away from it as soon as possible. Alias of per_page. Clamped to your plan's maximum, not rejected. |
count_only |
Optional[bool] | ➖ | Return only the total for these filters -- no permit records. Skips the row fetch entirely, so it is markedly cheaper for both of us when you are sizing a query rather than reading it. results comes back empty and total_capped / total_unknown mean exactly what they always do. |
retries |
Optional[utils.RetryConfig] | ➖ | Configuration to override the default retry behavior of the client. |
| Error Type | Status Code | Content Type |
|---|---|---|
| errors.FeatureLockedError | 403 | application/json |
| errors.HTTPValidationError | 422 | application/json |
| errors.ErrorDetail | 401, 429 | application/json |
| errors.PermitstackDefaultError | 4XX, 5XX | */* |
Export permits matching filters as CSV. Tier-gated row limits.
import os
from permitstack import Permitstack
with Permitstack(
api_key=os.getenv("PERMITSTACK_API_KEY", ""),
) as p_client:
res = p_client.permits.export_permits(record_kind="permit", limit=1000)
# Handle response
print(res)| Parameter | Type | Required | Description |
|---|---|---|---|
zip_code |
OptionalNullable[str] | ➖ | N/A |
city |
OptionalNullable[str] | ➖ | N/A |
state |
OptionalNullable[str] | ➖ | N/A |
category |
OptionalNullable[str] | ➖ | N/A |
status |
OptionalNullable[models.PermitStatus] | ➖ | N/A |
property_type |
OptionalNullable[models.PropertyType] | ➖ | N/A |
tag |
OptionalNullable[str] | ➖ | N/A |
record_kind |
Optional[str] | ➖ | N/A |
filed_after |
datetime | ➖ | N/A |
filed_before |
datetime | ➖ | N/A |
issued_after |
datetime | ➖ | N/A |
issued_before |
datetime | ➖ | N/A |
date_after |
datetime | ➖ | On or after this date, matched against whichever date a record has (issued, else filed). |
date_before |
datetime | ➖ | On or before this date, matched against whichever date a record has (issued, else filed). |
parcel |
OptionalNullable[str] | ➖ | Parcel number / APN / folio (formatting ignored). |
min_value |
OptionalNullable[float] | ➖ | N/A |
max_value |
OptionalNullable[float] | ➖ | N/A |
min_solar_kw |
OptionalNullable[float] | ➖ | N/A |
max_solar_kw |
OptionalNullable[float] | ➖ | N/A |
min_sqft |
OptionalNullable[float] | ➖ | N/A |
has_enrichment |
OptionalNullable[bool] | ➖ | N/A |
scope |
OptionalNullable[str] | ➖ | N/A |
q |
OptionalNullable[str] | ➖ | N/A |
contractor_name |
OptionalNullable[str] | ➖ | N/A |
owner_filed |
OptionalNullable[bool] | ➖ | true = owner-filed permits only (no contractor on record, owner name present — often DIY/homeowner leads, though for feeds without contractor capture the owner may be a builder/institution); false = permits that have a contractor |
jurisdiction |
OptionalNullable[str] | ➖ | A jurisdiction's id (from /v1/jurisdictions) or its name, partial and case-insensitive -- the same matching as /v1/permits/search. Lets a full load be partitioned along the coverage list. |
limit |
Optional[int] | ➖ | Rows to return, up to your plan's export maximum (a larger value is refused with 403, never silently lowered). If more rows match than limit, the response header X-Permitstack-Truncated is true: narrow the filter or split the date range and export again. |
keyword |
OptionalNullable[str] | ➖ | : warning: ** DEPRECATED **: This will be removed in a future release, please migrate away from it as soon as possible. Alias of q. |
date_from |
datetime | ➖ | : warning: ** DEPRECATED **: This will be removed in a future release, please migrate away from it as soon as possible. Alias of date_after. |
date_to |
datetime | ➖ | : warning: ** DEPRECATED **: This will be removed in a future release, please migrate away from it as soon as possible. Alias of date_before. |
start_date |
datetime | ➖ | : warning: ** DEPRECATED **: This will be removed in a future release, please migrate away from it as soon as possible. Alias of date_after. |
end_date |
datetime | ➖ | : warning: ** DEPRECATED **: This will be removed in a future release, please migrate away from it as soon as possible. Alias of date_before. |
retries |
Optional[utils.RetryConfig] | ➖ | Configuration to override the default retry behavior of the client. |
| Error Type | Status Code | Content Type |
|---|---|---|
| errors.FeatureLockedError | 403 | application/json |
| errors.HTTPValidationError | 422 | application/json |
| errors.ErrorDetail | 401, 429 | application/json |
| errors.PermitstackDefaultError | 4XX, 5XX | */* |
Incremental data feed (change-data-capture). Page the dataset ordered by
(updated_at, id). Omit cursor for the initial full load; persist next_cursor and pass
it back after each nightly ingest to receive only new + changed permits. Upsert-only
(deletes are not emitted).
import os
from permitstack import Permitstack
with Permitstack(
api_key=os.getenv("PERMITSTACK_API_KEY", ""),
) as p_client:
res = p_client.permits.sync_permits(limit=5000, record_kind="permit", fields="summary")
# Handle response
print(res)| Parameter | Type | Required | Description |
|---|---|---|---|
cursor |
OptionalNullable[str] | ➖ | Opaque cursor from the previous response's next_cursor. Omit to start the initial full sync from the beginning. |
since |
OptionalNullable[str] | ➖ | Alternative start point: an ISO-8601 UTC timestamp; returns permits with updated_at >= since. Ignored when cursor is supplied. |
limit |
Optional[int] | ➖ | Max permits per page (cursor page size, up to 50,000). |
record_kind |
Optional[str] | ➖ | 'permit' (default), a specific record_kind, or 'all'. |
state |
OptionalNullable[str] | ➖ | Optional 2-letter state filter to scope the feed. |
category |
OptionalNullable[str] | ➖ | Optional category filter (e.g. solar, roofing). |
fields |
Optional[str] | ➖ | 'summary' (default) or 'full'. 'full' adds record_kind, date_expired, fee_amount, stories, units, square_footage, applicant_name, contractor_license and created_at to every record, so a mirror does not have to fetch them one permit at a time. OPT-IN: the default payload is unchanged apart from contractor_id, added on 2026-09-10 to every permit surface; widening a feed under a consumer with a strict schema is otherwise avoided. |
retries |
Optional[utils.RetryConfig] | ➖ | Configuration to override the default retry behavior of the client. |
| Error Type | Status Code | Content Type |
|---|---|---|
| errors.FeatureLockedError | 403 | application/json |
| errors.HTTPValidationError | 422 | application/json |
| errors.ErrorDetail | 401, 429 | application/json |
| errors.PermitstackDefaultError | 4XX, 5XX | */* |
What CHANGED, instead of re-reading everything: permit transitions (new_permit, status_change, issued, completed, expired) built from the daily ingest diff, filterable by city, state, category and jurisdiction.
This is the endpoint to poll on a schedule, and cursor is how to poll it.
Save the next_cursor from each response and pass it back as cursor on the
next call; you then receive only what you have not already seen, in the order it
was detected, with no duplicates and no gap. Page until a response comes back
shorter than per_page, save that last next_cursor, and resume there tomorrow.
WHERE YOU START MATTERS, AND THE FIRST CALL DOES NOT START AT THE BEGINNING.
A call with NO cursor returns the NEWEST events and its next_cursor is the
frontier -- a watermark at "now". Pass it straight back and you correctly get 0
results, because you have just declared yourself caught up. That is exactly what
a poller wants: from here on you receive everything new. It is NOT a backfill,
and it will not walk history for you -- there are 75,544 events behind that
watermark for the Tampa filter below alone.
To start somewhere else, build the cursor yourself: it is
<ISO-8601 timestamp>|<event id>, and |0 is a valid id floor.
# start polling from now (typical):
GET /v1/permits/events?city=Tampa&state=FL&category=roofing
&event_type=new_permit -> save next_cursor
# ...then, from the next call onward:
GET ...&cursor=2026-09-08T04%3A09%3A16.381111%2B00%3A00%7C51823904
# start from a chosen point instead (backfill the last 30 days, then keep polling):
GET ...&cursor=2026-08-11T00%3A00%3A00%2B00%3A00%7C0
One call answers what a paged re-pull of /v1/permits/search costs hundreds of. Measured 2026-09-09, that Tampa query returns 38 records in a single call; the same question asked by re-pulling search pages 1-100 twelve times a day costs ~9,600 requests and runs into the daily cap.
detected_after still works and is fine for an ad-hoc look at a window, but it
is the wrong tool for a poller: it is INCLUSIVE, and one ingest batch stamps
thousands of events with a single identical detected_at (measured 2026-09-10:
3,688 timestamps carry over 1,000 events each, the largest 1,370,561). A poller
that stores the newest detected_at and passes it back therefore re-receives
that whole batch every time. cursor breaks the tie on the event id and does not.
Supplying a cursor returns events oldest-unseen first, which is what lets a
batch be paged safely; without one you get the newest first, for browsing.
cursor and page are alternatives -- passing both is a 400 rather than a
silently skipped page.
Ingest runs once a day, so a daily poll is enough; detected_at is when WE saw
the change, not when the city recorded it. For push instead of poll, register a
webhook (developer tier and above) and skip the polling entirely.
import os
from permitstack import Permitstack
with Permitstack(
api_key=os.getenv("PERMITSTACK_API_KEY", ""),
) as p_client:
res = p_client.permits.list_permit_events(page=1, per_page=50)
# Handle response
print(res)| Parameter | Type | Required | Description |
|---|---|---|---|
event_type |
OptionalNullable[str] | ➖ | Filter by event type: new_permit, status_change, issued, completed, expired (the permit's expiration date has passed without it being finished; emitted when that date arrives) |
category |
OptionalNullable[str] | ➖ | Permit category (e.g. solar, battery) |
city |
OptionalNullable[str] | ➖ | City name |
state |
OptionalNullable[str] | ➖ | 2-letter state code |
jurisdiction |
OptionalNullable[str] | ➖ | Jurisdiction name (partial) |
permit_id |
OptionalNullable[str] | ➖ | Events for a single permit id |
detected_after |
date | ➖ | Only events detected on/after this UTC timestamp (ISO 8601). Inclusive, so a batch sharing one timestamp is re-delivered; prefer cursor for polling. |
cursor |
OptionalNullable[str] | ➖ | Opaque cursor from the previous response's next_cursor. Resumes exactly where you stopped, with no duplicates and no gap. Omit on the first call. |
page |
Optional[int] | ➖ | N/A |
per_page |
Optional[int] | ➖ | N/A |
limit |
OptionalNullable[int] | ➖ | : warning: ** DEPRECATED **: This will be removed in a future release, please migrate away from it as soon as possible. Alias of per_page. Clamped to 100, not rejected. |
retries |
Optional[utils.RetryConfig] | ➖ | Configuration to override the default retry behavior of the client. |
| Error Type | Status Code | Content Type |
|---|---|---|
| errors.HTTPValidationError | 422 | application/json |
| errors.ErrorDetail | 401, 429 | application/json |
| errors.PermitstackDefaultError | 4XX, 5XX | */* |
Get full details for a single permit.
import os
from permitstack import Permitstack
with Permitstack(
api_key=os.getenv("PERMITSTACK_API_KEY", ""),
) as p_client:
res = p_client.permits.get_permit(permit_id="8b0513b0-82c1-4220-9ede-cf81253853d0")
# Handle response
print(res)| Parameter | Type | Required | Description |
|---|---|---|---|
permit_id |
str | ✔️ | N/A |
retries |
Optional[utils.RetryConfig] | ➖ | Configuration to override the default retry behavior of the client. |
| Error Type | Status Code | Content Type |
|---|---|---|
| errors.HTTPValidationError | 422 | application/json |
| errors.ErrorDetail | 401, 429 | application/json |
| errors.PermitstackDefaultError | 4XX, 5XX | */* |
Get all permits for a specific address (partial match).
import os
from permitstack import Permitstack
with Permitstack(
api_key=os.getenv("PERMITSTACK_API_KEY", ""),
) as p_client:
res = p_client.permits.get_permits_by_address(address="5701 Alexanne Point", page=1, per_page=25, record_kind="permit")
# Handle response
print(res)| Parameter | Type | Required | Description |
|---|---|---|---|
address |
str | ✔️ | N/A |
page |
Optional[int] | ➖ | N/A |
per_page |
Optional[int] | ➖ | N/A |
record_kind |
Optional[str] | ➖ | 'permit' (default), a specific record_kind, or 'all'. |
retries |
Optional[utils.RetryConfig] | ➖ | Configuration to override the default retry behavior of the client. |
| Error Type | Status Code | Content Type |
|---|---|---|
| errors.HTTPValidationError | 422 | application/json |
| errors.ErrorDetail | 401, 429 | application/json |
| errors.PermitstackDefaultError | 4XX, 5XX | */* |
Coverage statistics: total permits, every jurisdiction we hold, and the counties they fall in.
Each jurisdiction carries data_through (the newest permit we actually hold, measured),
plus county / county_fips (the county most of its permits fall in) and counties
(the full measured mix with a share of the sample per county, since some cities straddle
a county line). counties at the top level is the same data keyed by county, listing the
jurisdictions whose permits fall in each one. A null county means not measured, which is
the case for statewide feeds that publish no coordinates.
import os
from permitstack import Permitstack
with Permitstack(
api_key=os.getenv("PERMITSTACK_API_KEY", ""),
) as p_client:
res = p_client.permits.get_coverage_stats()
# Handle response
print(res)| Parameter | Type | Required | Description |
|---|---|---|---|
retries |
Optional[utils.RetryConfig] | ➖ | Configuration to override the default retry behavior of the client. |
| Error Type | Status Code | Content Type |
|---|---|---|
| errors.ErrorDetail | 401, 429 | application/json |
| errors.PermitstackDefaultError | 4XX, 5XX | */* |
List available trade plays and their parameters.
import os
from permitstack import Permitstack
with Permitstack(
api_key=os.getenv("PERMITSTACK_API_KEY", ""),
) as p_client:
res = p_client.permits.list_plays()
# Handle response
print(res)| Parameter | Type | Required | Description |
|---|---|---|---|
retries |
Optional[utils.RetryConfig] | ➖ | Configuration to override the default retry behavior of the client. |
| Error Type | Status Code | Content Type |
|---|---|---|
| errors.ErrorDetail | 401, 429 | application/json |
| errors.PermitstackDefaultError | 4XX, 5XX | */* |
Addresses whose LATEST permit in the trade is min–max years old with nothing since — the system is statistically due for replacement. Only trades with a real lifecycle are supported (roofing, hvac, mechanical, solar=repower, pool).
import os
from permitstack import Permitstack
with Permitstack(
api_key=os.getenv("PERMITSTACK_API_KEY", ""),
) as p_client:
res = p_client.permits.system_age(trade="<value>", page=1, per_page=25)
# Handle response
print(res)| Parameter | Type | Required | Description |
|---|---|---|---|
trade |
str | ✔️ | roofing | hvac | mechanical | solar | pool |
state |
OptionalNullable[str] | ➖ | 2-letter state code |
city |
OptionalNullable[str] | ➖ | City name |
zip_code |
OptionalNullable[str] | ➖ | 5-digit ZIP |
jurisdiction |
OptionalNullable[str] | ➖ | Jurisdiction name (partial) |
min_age_years |
OptionalNullable[float] | ➖ | Override the trade's default minimum age |
max_age_years |
OptionalNullable[float] | ➖ | Override the trade's default maximum age |
page |
Optional[int] | ➖ | N/A |
per_page |
Optional[int] | ➖ | N/A |
retries |
Optional[utils.RetryConfig] | ➖ | Configuration to override the default retry behavior of the client. |
| Error Type | Status Code | Content Type |
|---|---|---|
| errors.FeatureLockedError | 403 | application/json |
| errors.HTTPValidationError | 422 | application/json |
| errors.ErrorDetail | 401, 429 | application/json |
| errors.PermitstackDefaultError | 4XX, 5XX | */* |
Solar permits aged between min/max years whose address has NO battery permit on file — the storage-retrofit lead list. Requires a location filter.
import os
from permitstack import Permitstack
with Permitstack(
api_key=os.getenv("PERMITSTACK_API_KEY", ""),
) as p_client:
res = p_client.permits.battery_retrofit_candidates(min_age_years=2.0, max_age_years=7.0, page=1, per_page=25)
# Handle response
print(res)| Parameter | Type | Required | Description |
|---|---|---|---|
state |
OptionalNullable[str] | ➖ | 2-letter state code |
city |
OptionalNullable[str] | ➖ | City name |
zip_code |
OptionalNullable[str] | ➖ | 5-digit ZIP |
jurisdiction |
OptionalNullable[str] | ➖ | Jurisdiction name (partial) |
min_age_years |
Optional[float] | ➖ | Minimum age of the PV permit, in years |
max_age_years |
Optional[float] | ➖ | Maximum age of the PV permit, in years |
page |
Optional[int] | ➖ | N/A |
per_page |
Optional[int] | ➖ | N/A |
retries |
Optional[utils.RetryConfig] | ➖ | Configuration to override the default retry behavior of the client. |
| Error Type | Status Code | Content Type |
|---|---|---|
| errors.FeatureLockedError | 403 | application/json |
| errors.HTTPValidationError | 422 | application/json |
| errors.ErrorDetail | 401, 429 | application/json |
| errors.PermitstackDefaultError | 4XX, 5XX | */* |
Roofing permits aged min-max years whose address has NO newer roofing permit — homes whose roof is statistically due for replacement. Alias of /v1/plays/system-age?trade=roofing (kept as the discoverable roofing-first name).
import os
from permitstack import Permitstack
with Permitstack(
api_key=os.getenv("PERMITSTACK_API_KEY", ""),
) as p_client:
res = p_client.permits.reroof_due(min_age_years=12.0, max_age_years=25.0, page=1, per_page=25)
# Handle response
print(res)| Parameter | Type | Required | Description |
|---|---|---|---|
state |
OptionalNullable[str] | ➖ | 2-letter state code |
city |
OptionalNullable[str] | ➖ | City name |
zip_code |
OptionalNullable[str] | ➖ | 5-digit ZIP |
jurisdiction |
OptionalNullable[str] | ➖ | Jurisdiction name (partial) |
min_age_years |
Optional[float] | ➖ | Minimum age of the roofing permit, in years |
max_age_years |
Optional[float] | ➖ | Maximum age of the roofing permit, in years |
page |
Optional[int] | ➖ | N/A |
per_page |
Optional[int] | ➖ | N/A |
retries |
Optional[utils.RetryConfig] | ➖ | Configuration to override the default retry behavior of the client. |
| Error Type | Status Code | Content Type |
|---|---|---|
| errors.FeatureLockedError | 403 | application/json |
| errors.HTTPValidationError | 422 | application/json |
| errors.ErrorDetail | 401, 429 | application/json |
| errors.PermitstackDefaultError | 4XX, 5XX | */* |
PV permits whose installer (contractor, applicant, or owner) matches installer
in a territory — a named competitor's customer base. Requires a location filter.
import os
from permitstack import Permitstack
with Permitstack(
api_key=os.getenv("PERMITSTACK_API_KEY", ""),
) as p_client:
res = p_client.permits.orphan_recovery(installer="<value>", page=1, per_page=25)
# Handle response
print(res)| Parameter | Type | Required | Description |
|---|---|---|---|
installer |
str | ✔️ | Installer name to match (contractor/applicant/owner) |
state |
OptionalNullable[str] | ➖ | 2-letter state code |
city |
OptionalNullable[str] | ➖ | City name |
zip_code |
OptionalNullable[str] | ➖ | 5-digit ZIP |
jurisdiction |
OptionalNullable[str] | ➖ | Jurisdiction name (partial) |
page |
Optional[int] | ➖ | N/A |
per_page |
Optional[int] | ➖ | N/A |
retries |
Optional[utils.RetryConfig] | ➖ | Configuration to override the default retry behavior of the client. |
| Error Type | Status Code | Content Type |
|---|---|---|
| errors.FeatureLockedError | 403 | application/json |
| errors.HTTPValidationError | 422 | application/json |
| errors.ErrorDetail | 401, 429 | application/json |
| errors.PermitstackDefaultError | 4XX, 5XX | */* |
Every jurisdiction we cover, with how current and how complete each one is.
data_through is the newest permit we actually hold, measured -- not when we last
polled the source. freshness_label and completeness_label are the same sentences the
export picker and the delivered manifest use, so this can never disagree with them.
import os
from permitstack import Permitstack
with Permitstack(
api_key=os.getenv("PERMITSTACK_API_KEY", ""),
) as p_client:
res = p_client.permits.list_covered_jurisdictions()
# Handle response
print(res)| Parameter | Type | Required | Description |
|---|---|---|---|
retries |
Optional[utils.RetryConfig] | ➖ | Configuration to override the default retry behavior of the client. |
| Error Type | Status Code | Content Type |
|---|---|---|
| errors.ErrorDetail | 401, 429 | application/json |
| errors.PermitstackDefaultError | 4XX, 5XX | */* |