API Reference
Permit history data and full PDF records via REST. Built for title companies, inspection firms, insurance carriers, and mortgage lenders who need permit data at scale — across 18+ metro areas and counting.
Get your free API key →Quickstart
Make your first permit lookup in under 60 seconds. You'll need a sandbox key — grab one free at /signup.
-
Get a sandbox key
Sign up at permitreport.ai/signup. Your key is shown immediately on the dashboard — no email confirmation gate.
-
Run your first lookup
Paste your key into the curl command below and run it:
curl -G "https://permitreport.ai/api/lookup" \ -H "Authorization: Bearer YOUR_API_KEY" \ --data-urlencode "address=123 Main St" \ --data-urlencode "metro=chicago"
const params = new URLSearchParams({ address: "123 Main St", metro: "chicago", }); const res = await fetch( `https://permitreport.ai/api/lookup?${params}`, { headers: { "Authorization": "Bearer YOUR_API_KEY" } } ); const data = await res.json(); console.log(data.permits);
import requests resp = requests.get( "https://permitreport.ai/api/lookup", headers={"Authorization": "Bearer YOUR_API_KEY"}, params={"address": "123 Main St", "metro": "chicago"}, ) data = resp.json() print(data["permits"])
require "net/http" require "uri" require "json" uri = URI("https://permitreport.ai/api/lookup") uri.query = URI.encode_www_form( address: "123 Main St", metro: "chicago" ) req = Net::HTTP::Get.new(uri) req["Authorization"] = "Bearer YOUR_API_KEY" res = Net::HTTP.start(uri.host, uri.port, use_ssl: true) { |h| h.request(req) } data = JSON.parse(res.body) puts data["permits"].inspect
-
Read the response
You'll get back a JSON object with a
permitsarray. Each permit has apermitNumber,issueDate,status,workType,estimatedCost, and more. See the endpoint reference for the full schema.
Authentication
Every API request requires a bearer token in the Authorization header.
Authorization: Bearer YOUR_API_KEY
Sandbox vs production
Keys are environment-scoped. Sandbox keys (prefix pr_sandbox_) hit real government data sources but are rate-limited and not billed. Use them for integration and testing. Production keys are issued after account review — email hello@permitreport.ai.
Key rotation
Rotate keys at any time from the dashboard — the old key is revoked immediately. Rotated keys return 403 Forbidden with {"error":"API key revoked"}.
GET /api/lookup
Returns permit history for a single address. Queries the jurisdiction's open-data source in real time and caches results for 24 hours. See a live example →
Query parameters
| Parameter | Type | Required | Description |
|---|---|---|---|
| address | string | required | Full street address including house number and street name. Example: 1600 Pennsylvania Ave NW |
| metro | string | optional | Metro key to target a specific jurisdiction. Defaults to chicago if omitted. See Coverage for all valid keys. |
Code samples
curl -G "https://permitreport.ai/api/lookup" \ -H "Authorization: Bearer pr_sandbox_demo_key_replace_me" \ --data-urlencode "address=1600 Pennsylvania Ave NW" \ --data-urlencode "metro=washington-dc"
const params = new URLSearchParams({ address: "1600 Pennsylvania Ave NW", metro: "washington-dc", }); const res = await fetch( `https://permitreport.ai/api/lookup?${params}`, { headers: { "Authorization": "Bearer pr_sandbox_demo_key_replace_me" } } ); const data = await res.json(); console.log(data.permits);
import requests resp = requests.get( "https://permitreport.ai/api/lookup", headers={"Authorization": "Bearer pr_sandbox_demo_key_replace_me"}, params={ "address": "1600 Pennsylvania Ave NW", "metro": "washington-dc", }, ) data = resp.json() print(data["permits"])
require "net/http" require "uri" require "json" uri = URI("https://permitreport.ai/api/lookup") uri.query = URI.encode_www_form( address: "1600 Pennsylvania Ave NW", metro: "washington-dc" ) req = Net::HTTP::Get.new(uri) req["Authorization"] = "Bearer pr_sandbox_demo_key_replace_me" res = Net::HTTP.start(uri.host, uri.port, use_ssl: true) { |h| h.request(req) } data = JSON.parse(res.body) puts data["permits"].inspect
Example response
{
"address": "1600 Pennsylvania Ave NW",
"metro": "washington-dc",
"metroLabel": "Washington, DC",
"count": 3,
"fromCache": false,
"permits": [
{
"permitNumber": "B2024-00132",
"issueDate": "2024-03-15",
"workType": "Building",
"estimatedCost": 125000,
"contractor": "Capitol Contractors LLC",
"status": "Finaled",
"description": "Interior renovation, kitchen and bathrooms",
"address": "1600 PENNSYLVANIA AVE NW",
"sourceLink": "https://dcra.dc.gov/service/building-permits"
}
]
}
Response schema
| Field | Type | Description |
|---|---|---|
| address | string | The address queried (echoed back) |
| metro | string | Metro key used for the query |
| metroLabel | string | Human-readable metro name |
| count | number | Total permits returned (capped at 50) |
| fromCache | boolean | True if served from 24h cache; false if freshly fetched |
| permits[].permitNumber | string|null | Permit number as issued by the jurisdiction |
| permits[].issueDate | string|null | ISO 8601 issue date (YYYY-MM-DD) |
| permits[].workType | string|null | Permit category: Building, Electrical, Plumbing, Mechanical, etc. |
| permits[].estimatedCost | number|null | Declared valuation in USD, where reported |
| permits[].contractor | string|null | Contractor of record where available |
| permits[].status | string|null | Permit status: Finaled, Open, Expired, Active, Issued, etc. |
| permits[].description | string|null | Scope-of-work description from the permit application |
| permits[].address | string|null | Normalized address as recorded in the source data |
| permits[].sourceLink | string | URL to the original record on the government portal |
Bulk Lookup (CSV → Zip)
Pro and Enterprise customers can run permit lookups across an entire address portfolio in one batch. Upload a CSV, get back a zip with per-address JSON files and a combined results.csv.
Accepts CSV text via textarea or file upload. Each row should include an address; city and state are optional but improve metro routing accuracy.
| Accepted column headers | Description |
|---|---|
| address / addr / street / full_address | Street address — at least one of these is required |
| city / municipality / town | City name — optional but recommended |
| state / st / state_code | Two-letter state abbreviation — optional but recommended |
Returns {"status":"processing","completedRows":42,"totalRows":100}. Poll until status === "done" or "failed".
Returns a zip archive. Contains permits/{n}_{address}.json per address plus results.csv with columns: address, metro, total_permits, open_permits, last_permit_date, total_declared_value, status.
GET /order/:id
Poll the status of a research order by its short ID (PR-XXXXXX). Once delivered, the response includes a download URL for the assembled PDF report.
The :short_id is the PR-XXXXXX identifier returned when an order is created. Use the public tracking page at /track/:token for a customer-facing status page (no auth required with a valid HMAC token).
Code samples
curl "https://permitreport.ai/order/PR-00001A" \ -H "Authorization: Bearer pr_sandbox_demo_key_replace_me"
const res = await fetch( "https://permitreport.ai/order/PR-00001A", { headers: { "Authorization": "Bearer pr_sandbox_demo_key_replace_me" } } ); const order = await res.json(); // Poll until order.customerStatus === "delivered" if (order.customerStatus === "delivered") { console.log(order.deliveredPdfUrl); }
resp = requests.get(
"https://permitreport.ai/order/PR-00001A",
headers={"Authorization": "Bearer pr_sandbox_demo_key_replace_me"},
)
order = resp.json()
if order["customerStatus"] == "delivered":
print(order["deliveredPdfUrl"])
req = Net::HTTP::Get.new(URI("https://permitreport.ai/order/PR-00001A")) req["Authorization"] = "Bearer pr_sandbox_demo_key_replace_me" # … same Net::HTTP boilerplate as above … order = JSON.parse(res.body) puts order["deliveredPdfUrl"] if order["customerStatus"] == "delivered"
Example response
{
"shortId": "PR-00001A",
"metro": "chicago",
"address": "123 Main St, Chicago, IL",
"tier": "premium",
"customerStatus": "delivered",
"statusUpdatedAt": "2026-06-21T14:30:00Z",
"deliveredPdfUrl": "https://cdn.permitreport.ai/reports/PR-00001A.pdf"
}
The customerStatus lifecycle:
| Status | Meaning |
|---|---|
| received | Order intake confirmed, queued for research |
| researching | Permit records are being retrieved and assembled |
| delivered | Report complete — deliveredPdfUrl is populated (Premium tier) |
Rate Limits & Quotas
Two limits apply: a per-minute rate limit and a monthly quota. Both are enforced per API key.
Per-minute rate limits
| Plan | Requests / minute | Notes |
|---|---|---|
| Sandbox | 10 | Free, no billing |
| Starter | 60 | Monthly quota applies |
| Pro | 200 | Monthly quota applies |
| Enterprise | Custom | Contact us for volume SLA |
Monthly quotas
| Plan | Monthly lookups | Bulk rows / job |
|---|---|---|
| Sandbox | 100 | 10 |
| Starter | 1,000 | 100 |
| Pro | 10,000 | 1,000 |
| Enterprise | Unlimited | 10,000 |
View pricing → for plan details and upgrade options.
What happens at the limit
Rate-limited requests receive 429 Too Many Requests with a Retry-After header. Use exponential backoff with jitter — the example below retries up to 3 times:
async function lookupWithRetry(params, apiKey, maxRetries = 3) { for (let attempt = 0; attempt < maxRetries; attempt++) { const res = await fetch( `https://permitreport.ai/api/lookup?${new URLSearchParams(params)}`, { headers: { "Authorization": `Bearer ${apiKey}` } } ); if (res.status !== 429) return res.json(); const wait = parseInt(res.headers.get("Retry-After") || "5") * 1000; await new Promise(r => setTimeout(r, wait * (2 ** attempt) + Math.random() * 500)); } throw new Error("Rate limit retries exhausted"); }
Quota exhaustion returns 429 with {"error":"Monthly quota exceeded"}. Upgrade your plan or contact us to add quota mid-cycle.
Error Codes
All error responses share the same envelope:
{
"error": "Human-readable description",
"docs": "https://permitreport.ai/docs"
}
| Status | Meaning | How to fix |
|---|---|---|
| 200 | Success | Read the permits array. count: 0 means no records found (not an error). |
| 400 | Bad request — missing required field, malformed address, unknown metro key | Check the error string. Verify address and metro are present and valid. |
| 401 | Missing Authorization header |
Add Authorization: Bearer YOUR_KEY to every request. |
| 403 | Invalid or revoked API key | Rotate a new key from the dashboard or request one via email. |
| 404 | Order or resource not found | Verify the PR-XXXXXX short ID is correct. |
| 429 | Rate limit or monthly quota exceeded | Check the Retry-After header. Use exponential backoff. Upgrade plan if quota is exhausted. |
| 502 | Upstream government data source temporarily unavailable | Retry after 30–60 seconds. Response body includes the metro that failed. |
| 503 | Service temporarily unavailable | Retry after 15 seconds. If persistent, email hello@permitreport.ai. |
200 with count: 0 and an empty permits array — your integration won't throw on missing records.
Coverage
The metro parameter accepts these keys. All metros query live government data unless noted.
* Atlanta and Houston return empty results — no public address-queryable API is available from those jurisdictions. Your integration won't break; you'll receive count: 0.
| Metro | Data source | Source API type |
|---|---|---|
| chicago | Chicago Data Portal (Socrata ydr8-5enu) | Socrata Open Data |
| nyc | NYC DOB (Socrata ipu4-2q9a) | Socrata Open Data |
| la | LA Building & Safety (LADBS) | LADBS proprietary + Socrata |
| sf | SF DataSF (Socrata i98e-djp9) | Socrata Open Data |
| seattle | Seattle Open Data | Socrata Open Data |
| boston | Boston Open Data (Socrata 8u6b-8mc5) | Socrata Open Data |
| austin | Austin Open Data (Socrata 3syk-w9eu) | Socrata Open Data |
| philadelphia | Philadelphia Open Data (Socrata) | Socrata Open Data |
| denver | Denver Open Data (Socrata 2qa3-7ye9) | Socrata Open Data |
| portland | Portland Maps ArcGIS BDS_Permit FeatureServer/22 | ArcGIS FeatureServer |
| miami | Miami-Dade RER ArcGIS FeatureServer/0 | ArcGIS FeatureServer |
| san-diego | San Diego County (Socrata dyzh-7eat) | Socrata Open Data |
| dallas | Dallas Open Data (Socrata e7gq-4sah) | Socrata Open Data |
| phoenix | Phoenix PDD ArcGIS FeatureServer/0 | ArcGIS FeatureServer |
| washington-dc | DC Open Data ArcGIS DCRA MapServer/1 | ArcGIS MapServer |
| san-antonio | CoSA ArcGIS BuildingPermits FeatureServer/0 | ArcGIS FeatureServer |
Webhooks Planned
Webhooks are on the roadmap for Enterprise customers. When released, they'll fire on order status transitions so you don't have to poll /order/:id.
Planned event types
| Event | Fires when |
|---|---|
| order.received | Order intake confirmed and queued for research |
| order.researching | Fulfillment pipeline has started retrieving permits |
| order.delivered | Report assembled and ready for download |
| bulk_job.done | A CSV batch job has completed all rows |
| bulk_job.failed | A CSV batch job failed (partial results may exist) |
Planned payload shape
{
"event": "order.delivered",
"timestamp": "2026-06-21T14:30:00Z",
"data": {
"shortId": "PR-00001A",
"customerStatus": "delivered",
"deliveredPdfUrl": "https://cdn.permitreport.ai/reports/PR-00001A.pdf"
}
}
Changelog
Recent API additions, in reverse chronological order.
-
2026-06-21
metroSan Antonio — live ArcGIS adapter added. Metro key:
san-antonio. Source: CoSA DSD BuildingPermits FeatureServer/0. -
2026-06-21
newBulk Lookup (CSV) —
POST /bulk/submit+ status polling + zip download. Pro/Enterprise feature. Up to 10,000 rows per job for Enterprise plans. -
2026-06-21
newSelf-serve API keys — sign up at
/signup, get a sandbox key immediately. Dashboard at/dashboardfor usage stats, key rotation, request log. -
2026-06-21
fixAustin adapter — migrated from deprecated dataset
3k5u-5s5vto3syk-w9eu(Issued Construction Permits) with richer fields. -
2026-06-21
metroWashington DC, San Diego, Miami, Phoenix — live ArcGIS and Socrata adapters. Metro keys:
washington-dc,san-diego,miami,phoenix. -
2026-05-15
metroPortland, Dallas, Denver, Philadelphia — live adapters added. Metro keys:
portland,dallas,denver,philadelphia. -
2026-04-10
metroAustin, Boston, Seattle — Socrata adapters added. Metro keys:
austin,boston,seattle. - 2026-03-01 metroChicago, NYC, LA, SF — initial launch with four metro adapters and 24h response caching.