US building permit data

Thousands of permit portals. One schema. One API key.

Every US jurisdiction publishes building permits differently — different columns, different date formats, different names for the same party. We normalize them into one stable schema and serve it as a filterable REST API and a semantic search endpoint your agents can query directly.

Metered per 1,000 requests · No minimum · OpenAPI schema included

GET /api/v1/records
curl -H "X-API-Key: $EXHAUST_API_KEY" \
  "https://permitsapi.com/api/v1/records\
?state=WA&work_class=solar&min_valuation=25000"

{
  "data": [
    {
      "permit_number": "6912345-CN",
      "jurisdiction": "City of Seattle",
      "state": "WA",
      "address_line": "1420 NW 85th St",
      "postal_code": "98117",
      "work_class": "solar",
      "work_description": "Install 8.2kW rooftop PV",
      "contractor_name": "Blue Ridge Construction",
      "owner_name": "M. Ellison",
      "valuation_usd": "31500.00",
      "status_text": "Permit Issued",
      "issued_date": "2025-09-02",
      "source_url": "https://cos-data.seattle…",
      "scraped_at": "2025-09-03T04:12:07Z"
    }
  ],
  "meta": { "returned": 50, "has_more": true }
}
The problem

Public data is not usable data.

Permit records are public almost everywhere. That is exactly why nobody has them in a form you can query. The work is not access — it is the thousand small incompatibilities between one jurisdiction and the next.

Every column is named differently

One city's issued_date is another's DateIssued is another's permit_issue_dt. The same field means different things depending on who published it.

Dates lie about their format

03/11/2024 is March 11th in most of the country and November 3rd in others. Guessing wrong silently corrupts every time-series you build on it.

Parties get mixed up

The "permittee" is the contractor in one dataset and the property owner in the next. Mapping them onto the wrong field misattributes work to the wrong company.

What you get

A schema you can build on.

One normalized record type

Addresses parsed into components, dates resolved against each jurisdiction's own convention, valuations as decimals, parties assigned to the role they actually held. Field names are stable API surface — we version the schema rather than renaming columns under you.

Semantic search, not just filters

Ask for "rooftop solar installs over $25k" and get ranked results with cosine similarity — no embedding cost on your side. Filters are applied inside the vector query, so a narrow filter still returns a full page of hits.

Provenance on every row

Each record carries the source_url it came from, the source_name of the publisher and the scraped_at timestamp. When a customer or a regulator asks where a number came from, you can answer with a link.

Incremental sync built in

Pass updated_since and pull only what changed. Records are keyed by a content hash, so a republished page updates in place instead of duplicating — your mirror stays consistent without a reconciliation job.

The API

Two endpoints. Both boring, on purpose.

Authenticate with an X-API-Key header. Every response is JSON, every error carries a request_id, and the full OpenAPI schema is published at /docs.

# Filter structured records. Every parameter is optional.
curl -H "X-API-Key: $EXHAUST_API_KEY" \
  "https://permitsapi.com/api/v1/records?\
jurisdiction=City+of+Seattle&\
work_class=roofing&\
issued_after=2025-01-01&\
min_valuation=10000&\
order_by=issued_date&\
limit=50"

# Same query, same filters, as a spreadsheet.
curl -H "X-API-Key: $EXHAUST_API_KEY" -H "Accept: text/csv" \
  "https://permitsapi.com/api/v1/records?state=WA&work_class=roofing" \
  -o wa-roofing.csv

# Pull only what changed since your last sync.
curl -H "X-API-Key: $EXHAUST_API_KEY" \
  "https://permitsapi.com/api/v1/records?\
updated_since=2025-09-01T00:00:00Z&limit=200"
Who buys this

A permit is a signal that someone is about to spend money.

Contractor lead gen

A pulled roofing permit means a homeowner is mid-project. Filter by trade, ZIP and valuation, and route the lead the same day it is published.

Property intelligence

Renovation history is the missing column in most property datasets. Join on parcel id or address and know what was actually done to a building.

Insurance & risk

Unpermitted work, roof age, electrical upgrades. Underwriting signals that show up in permits years before they show up in a claim.

AI agents & RAG

The search endpoint returns ranked chunks with metadata, ready to hand to a retriever. Your agent asks a question in English and gets sourced records back.

Coverage

Growing jurisdiction by jurisdiction.

We add a jurisdiction only once every essential field maps cleanly. A dataset that is missing issue dates or mislabels the contractor is worse than no dataset at all, so it does not ship until it is right.

Permit records
Jurisdictions
States
Daily
Refresh cadence
JurisdictionStateEarliest recordStatus
City of SeattleWALive
More jurisdictions in onboarding — ask us which ones you need.

Need a specific city or county? Tell us. Adding a jurisdiction that already publishes an open dataset is usually days, not months.

Pricing

Metered. No seats, no annual lock-in.

You are billed for what you pull, in blocks of 1,000. Semantic queries are metered separately from record reads because they cost us more to serve.

Evaluation

Prove it fits before you pay.

$0

10,000 records, one-time

  • Full schema, no sampling
  • Both endpoints enabled
  • 60 requests / minute
  • Email support
Get a trial key
Bulk & enterprise

Whole-corpus mirrors.

Custom

Volume rates and flat-fee options

  • Full historical backfill
  • Jurisdiction requests prioritized
  • Custom rate limits
  • Redistribution terms available
Talk to us
Questions

Before you ask.

Where does the data come from?

Public records published by the jurisdictions themselves — open data portals and municipal permit systems. Every record links back to the exact source page it came from via source_url, so nothing we serve is unverifiable.

How fresh is it?

Sources are re-pulled on a daily schedule. Freshness is bounded by the publisher: a city that updates its dataset weekly cannot be fresher than weekly no matter how often we poll it. Each record's scraped_at tells you exactly when we last saw it.

Can I redistribute the data?

Standard plans license the API for use inside your product. Redistributing the raw corpus — reselling it, or publishing it as a dataset of its own — needs different terms. Ask and we will sort it out; it is not a hard no.

What happens when I hit the rate limit?

You get a 429 with a Retry-After header, and every response carries X-RateLimit-Limit and X-RateLimit-Remaining so you can back off before you get there. Rate limits are per key and adjustable on request.

Do you have the jurisdiction I need?

Check the coverage table, and if it is not there, ask. If the jurisdiction publishes an open dataset we can usually onboard it in days. If it only has a search portal with no export, it takes longer and we will tell you honestly which one it is.

Why not just scrape it myself?

You can — it is public data. The cost is not access, it is the long tail: date formats that differ by county, party roles that swap meaning between datasets, portals that change their HTML without warning, and the ongoing job of noticing when one breaks. That maintenance is the product.

Start with a trial key.

Tell us what you are building and which jurisdictions you need. We will send a key and enough records to prove the schema fits before you spend anything.