API reference

OpenPermits API

Paths and parameters follow the Shovels v2 API, so existing clients can switch by changing the base URL and key. Create a key under API keys and send it in the X-API-Key header from your servers. Money is in cents; dates are YYYY-MM-DD.

curl https://api.openpermits.ai/v2/permits/search \
  -H "X-API-Key: $OPENPERMITS_KEY" \
  -G -d geo_id=ga/atlanta -d permit_tags=SOLAR,-POOL_AND_HOT_TUB \
  -d permit_from=2026-01-01 -d permit_to=2026-12-31 -d size=100

Pages return next_cursor; pass it back as cursor for the next page. To keep a copy in sync, page through once, then ask only for what changed with updated_since (or first_seen_since for new permits only). For new permits without gaps, stop each poll at settled_through from /v2/meta/release (pass it as first_seen_before) and start the next one there: permits before it are fully geocoded and linked. The full machine-readable spec is at /v2/openapi.json.

Credits and limits

Data endpoints cost one credit per record returned, and one per single-record lookup. Empty results, include_count and the reference endpoints are free. Every charged response reports your allowance:

X-Credits-Charged: 100
X-Credits-Used: 12400
X-Credits-Limit: 50000
X-Credits-Remaining: 37600
X-Credits-Reset: 2026-10-25

Errors

Errors are JSON with a detail, a stable code where you might branch on it, and a request_id to quote to support.

401Missing, invalid or revoked key (code key_revoked)
402Monthly credits used up (code credits_exhausted); resets on the date in X-Credits-Reset
403No active subscription (code subscription_inactive)
404No such record
422A parameter is missing or invalid; detail says which
429Over your rate limit (code rate_limited); wait Retry-After seconds

Webhooks

Turn on webhooks for a saved search and add an endpoint under Settings → Webhooks. After each data refresh we POSTthe search's new permits to every endpoint that's on, up to 100 per event, in the same shape as /v2/permits/search. Each permit costs one credit. Reply with any 2xx within 10 seconds; anything else is retried with backoff for about a day, and redirects aren't followed. The same event can arrive twice, so use id to ignore repeats.

POST /your/endpoint
OP-Event-Id: evt_…
OP-Event-Type: permits.new
OP-Signature: t=1790000000,v1=5257a869e7ecebeda32affa62cdca3fa51cad7e77a0e56ff536d0ce8e108d8bd

{
  "id": "evt_…",
  "type": "permits.new",
  "created": "2026-09-25T13:15:04.120Z",
  "saved_search": { "id": "…", "name": "Roofing in 30306", "url": "https://openpermits.ai/search?…" },
  "window": { "first_seen_since": "2026-09-25T06:02:11.000Z", "first_seen_before": "2026-09-25T12:03:40.000Z" },
  "page": 0,
  "permits": [ { "id": "…", "number": "BLD2026-01234", "tags": ["ROOFING"], … } ]
}

Check the signature before trusting a request: HMAC-SHA256 of `${t}.${raw body}`with your endpoint's signing secret must equal v1, and t should be within five minutes of now.

import { createHmac, timingSafeEqual } from "node:crypto";

function verify(header, rawBody, secret) {
  const { t, v1 } = Object.fromEntries(header.split(",").map((p) => p.split("=")));
  if (Math.abs(Date.now() / 1000 - Number(t)) > 300) return false;
  const expected = createHmac("sha256", secret).update(`${t}.${rawBody}`).digest();
  const given = Buffer.from(v1 ?? "", "hex");
  return given.length === expected.length && timingSafeEqual(given, expected);
}

Permits

GET/v2/permits/export.csv

Export permits as CSV

The same filters as `/v2/permits/search`, streamed as one CSV file, newest first. Costs one credit per row. The export stops at your remaining allowance or `limit`, whichever is smaller; `X-Export-Row-Limit` says which. Text cells that a spreadsheet would run as a formula are prefixed with an apostrophe.

ParameterTypeDescription
permit_fromrequiredstringStart of the permit-date range (issue date, else file date).
permit_torequiredstring
geo_idstringA state (`GA`), 5-digit zip or county FIPS (`30319`, `13121`), jurisdiction slug (`ga/atlanta`), or any `geo_id` from `/v2/geography/search`.
permit_tagsstring
permit_statusstring
permit_qstring
permit_min_job_valueinteger
property_typestring
contractor_idstring
address_idstring
updated_sincestring
first_seen_sincestring
first_seen_beforestring
bboxstringOnly permits inside a map box: `minLon,minLat,maxLon,maxLat`, at most 2 degrees each way.
latnumberWith `lon` and `radius_m`: only permits within that distance of a point.
lonnumber
radius_mnumberMetres, up to 50,000.
limitintegerAt most this many rows. Default 50000.
GET/v2/permits/search

Search permits

Newest first by permit date (issue date, else file date).

ParameterTypeDescription
permit_fromrequiredstringStart of the permit-date range (issue date, else file date).
permit_torequiredstring
geo_idstringA state (`GA`), 5-digit zip or county FIPS (`30319`, `13121`), jurisdiction slug (`ga/atlanta`), or any `geo_id` from `/v2/geography/search`.
permit_tagsstringComma-separated tags (`/v2/list/tags`). Prefix a tag with `-` to exclude it.
permit_statusstringComma-separated statuses.
permit_qstringText in the description or type, 3 to 50 characters.
permit_min_job_valueintegerMinimum declared job value, in cents.
permit_numberstringExact permit number, case-insensitive. `%` and `_` act as wildcards.
property_typestringComma-separated property types (`/v2/list/property_types`).
contractor_idstring
address_idstringEvery permit at one address (`geo_ids.address_id` on a permit, or `id` from `/v2/addresses/search`).
updated_sincestringOnly permits created or changed since this time (RFC 3339 or a date). For incremental syncs.
first_seen_sincestringOnly permits we first saw since this time (RFC 3339 or a date). Unlike `updated_since`, status changes don't count.
first_seen_beforestringOnly permits we first saw before this time (exclusive). For gap-free polling, request `first_seen_since` = your last cutoff and `first_seen_before` = `settled_through` from `/v2/meta/release`, then keep that value as your next cutoff.
bboxstringOnly permits inside a map box: `minLon,minLat,maxLon,maxLat`, at most 2 degrees each way.
latnumberWith `lon` and `radius_m`: only permits within that distance of a point.
lonnumber
radius_mnumberMetres, up to 50,000.
sizeinteger Default 10.
cursorstring`next_cursor` from the previous page.
include_countbooleanAdd `total_count` to the first page (exact up to 10,000). Free. Default false.
GET/v2/permits/{permit_id}

Get a permit, with its status history

ParameterTypeDescription
permit_idrequiredpathstring

Contractors

GET/v2/contractors/search

Search contractors

Sorted by permit count, highest first. Returns one representative per dedup group unless `include_members`.

ParameterTypeDescription
geo_idstringA state (`GA`), 5-digit zip or county FIPS (`30319`, `13121`), jurisdiction slug (`ga/atlanta`), or any `geo_id` from `/v2/geography/search`.
contractor_qstringText in the name or DBA.
contractor_classificationstringComma-separated classes (`/v2/list/contractor_classifications`).
contractor_licensestringLicense number, punctuation-insensitive.
contractor_min_permit_countinteger
include_membersboolean Default false.
sizeinteger Default 10.
cursorstring`next_cursor` from the previous page.
include_countbooleanAdd `total_count` to the first page (exact up to 10,000). Free. Default false.
GET/v2/contractors/{contractor_id}

Get a contractor, with its group and jurisdictions

ParameterTypeDescription
contractor_idrequiredpathstring
GET/v2/contractors/{contractor_id}/permits

List a contractor's permits

ParameterTypeDescription
contractor_idrequiredpathstring
include_groupbooleanInclude every branch in the contractor's group. Default false.
permit_fromstring
permit_tostring
permit_tagsstring
permit_statusstring
sizeinteger Default 10.
cursorstring`next_cursor` from the previous page.
include_countbooleanAdd `total_count` to the first page (exact up to 10,000). Free. Default false.

Properties

GET/v2/addresses/{address_id}/systems

How old an address's roof, HVAC and other systems are

For each building system, the last permitted work on it and how far through its typical lifespan it is. Where no permit is on record, the building's year built (from its assessor parcel) stands in, and the basis says so. Lifespans: roof 20 years, HVAC 15, water heater 10, plumbing 40, electrical 30, windows and doors 25. One credit.

ParameterTypeDescription
address_idrequiredpathstring
GET/v2/properties/search

Search properties (assessor parcels)

ParameterTypeDescription
geo_idstringA state (`GA`), 5-digit zip or county FIPS (`30319`, `13121`), jurisdiction slug (`ga/atlanta`), or any `geo_id` from `/v2/geography/search`.
property_typestring
owner_typestring`individual`, `business`, `government`, ...
apnstring
min_year_builtinteger
max_year_builtinteger
with_permitsboolean Default false.
sizeinteger Default 10.
cursorstring`next_cursor` from the previous page.
GET/v2/properties/{parcel_id}

Get a property, with up to 50 of its permits and its system ages

ParameterTypeDescription
parcel_idrequiredpathstring

Addresses

GET/v2/addresses/search

Search addresses

ParameterTypeDescription
address_qstringFree-text address; fuzzy-matched.
geo_idstringA state (`GA`), 5-digit zip or county FIPS (`30319`, `13121`), jurisdiction slug (`ga/atlanta`), or any `geo_id` from `/v2/geography/search`.
with_permitsbooleanOnly addresses with at least one permit. Default true.
sizeinteger Default 10.
cursorstring`next_cursor` from the previous page.

Decisions

Zoning and land-use agenda items from city and county bodies.

GET/v2/decisions/search

Search zoning and land-use decisions

ParameterTypeDescription
meeting_fromrequiredstring
meeting_torequiredstring
geo_idstringA state (`GA`), 5-digit zip or county FIPS (`30319`, `13121`), jurisdiction slug (`ga/atlanta`), or any `geo_id` from `/v2/geography/search`.
categorystringComma-separated (`/v2/list/decision_categories`).
statusstring
zoning_fromstring
zoning_tostring
qstringText in the title or description.
sizeinteger Default 10.
cursorstring`next_cursor` from the previous page.
include_countbooleanAdd `total_count` to the first page (exact up to 10,000). Free. Default false.
GET/v2/decisions/{decision_id}

Get a decision

ParameterTypeDescription
decision_idrequiredpathstring

Markets

Monthly permit activity and contractor market share for a geography, recomputed after every data refresh.

GET/v2/contractors/rankings

Contractors by market share

The contractors pulling the most permits in a geography and period, with their share of all permits there, including permits that name no contractor. One credit per contractor returned.

ParameterTypeDescription
geo_idrequiredstringA state (`GA`), county FIPS (`13121`), zip, jurisdiction slug (`ga/atlanta`), or a city `geo_id` from `/v2/geography/search`.
tagstringOne work-type tag, or `*` (the default) for all permits. Default *.
fromstringFirst month, `YYYY-MM`. Defaults to 24 months before `to` (12 for rankings).
tostringLast month, `YYYY-MM`. Defaults to this month.
sizeinteger Default 25.
GET/v2/markets/metrics

Monthly permit activity for a place

Permit counts, job value (total and median), average approval time and active contractors per month, overall or for up to six work-type tags. Months with no permits are omitted. One credit per row.

ParameterTypeDescription
geo_idrequiredstringA state (`GA`), county FIPS (`13121`), zip, jurisdiction slug (`ga/atlanta`), or a city `geo_id` from `/v2/geography/search`.
tagstringComma-separated work-type tags, or `*` (the default) for all permits. Default *.
fromstringFirst month, `YYYY-MM`. Defaults to 24 months before `to` (12 for rankings).
tostringLast month, `YYYY-MM`. Defaults to this month.

Signals

Ready-made lead and risk lists built from permit history.

GET/v2/signals/aging-systems

Addresses whose roof, HVAC or other system is getting old

Addresses in a place where a building system is older than `min_age_years` (by default its typical lifespan). The evidence is either the last permit for that system (`basis: permit`) or, where there is none, the building's year built (`basis: year_built`, wherever assessor parcels are loaded). One credit per address.

ParameterTypeDescription
geo_idrequiredstringA state (`GA`), county FIPS (`13121`), zip, jurisdiction slug (`ga/atlanta`), or a city `geo_id` from `/v2/geography/search`.
systemrequiredstring
min_age_yearsinteger
basisstring Default all.
property_typestringComma-separated (`/v2/list/property_types`).
sizeinteger Default 10.
cursorstring`next_cursor` from the previous page.

Reference

GET/v2/geography/search

Find a geo_id for a state, county, city, zip or jurisdiction

ParameterTypeDescription
qrequiredstring
kindstring
sizeinteger Default 20.
GET/v2/jurisdictions/search

List covered jurisdictions, with permit counts and date ranges

ParameterTypeDescription
qstring
sizeinteger Default 50.
GET/v2/list/contractor_classifications

Contractor classes

GET/v2/list/decision_categories

Decision categories

GET/v2/list/property_types

Property types

GET/v2/list/tags

Work-type tags

GET/v2/meta/coverage

How often each permit field is filled, for a place and period

ParameterTypeDescription
permit_fromrequiredstringStart of the permit-date range (issue date, else file date).
permit_torequiredstring
geo_idstringA state (`GA`), 5-digit zip or county FIPS (`30319`, `13121`), jurisdiction slug (`ga/atlanta`), or any `geo_id` from `/v2/geography/search`.
GET/v2/meta/release

When each part of the data was last refreshed

Account

GET/v2/usage

Your allowances in the current monthly window