The Permity API answers one question well: what building permits are on record for an address, by trade, with the year of the most recent one. The concepts, authentication, response codes and a sample call are below, then the full reference.
The Permity API is REST over HTTPS. Requests and responses are JSON unless you ask for a PDF or a CSV, and every response carries a request_id (and an X-Request-Id header) to quote if you need help with a call.
Every request starts with this base URL:
https://permityusa.com/v1
Send your API key in a header named X-API-Key:
"X-API-Key": "pk_live_your_key_here"
Tools that only know how to send Authorization: Bearer … can send the key that way instead. A key in a query string is never accepted, because URLs end up in logs. Every key on an account draws on the same monthly allowance, so a second key is a convenience, not more lookups.
To check a key without spending anything, call GET /v1/me. It answers 200 with the account the key belongs to and what it has left, or 401 when the key is wrong. It is free, never runs a lookup, and is the call to make from a settings screen’s “Test” button.
curl https://permityusa.com/v1/me -H "X-API-Key: pk_live_your_key_here"
{
"account": { "name": "Example Roofing", "plan": "solo" },
"key": { "name": "Settings" },
"monthly_allowance": 50,
"allowance_remaining": 42,
"credits_remaining": 5,
"lookups_remaining": 47,
"terms": { "accepted": true, … },
"meta": { "request_id": "req_…", "quota_remaining": 47, "charged": 0 }
}lookups_remaining is the monthly allowance left plus credits, the same number as meta.quota_remaining on a lookup. A value that is not shaped like a key is answered 401 with “Malformed API key. Keys start with pk_live_.” A lookup checks the key before it reads the body, so a 401 is always about the key; and a 400 or 422 is refused before anything runs, so it never charges.
Permity has a private Zapier app, shared on request: ask for an invitation and we email you a link that adds it to your Zapier account. It signs in with an API key, returns the flat answer, and works in any CRM that connects to Zapier.
A lookup comes out of your monthly allowance first, and out of purchased credits only once the allowance is used. Opening a report you already have is free. Running it again is charged.
Each recipe is a Zap: the trigger, the Permity step, then what happens next. Only a Look Up step uses a lookup.
Cost: Step 2 uses 1 lookup from your monthly allowance (purchased credits only once the allowance is used), or nothing when it reuses a report from the last 30 days. Step 3 is free, because opening a report you already have is free.
Address List Finished sees lists started with the same Permity connection.
Cost: Step 1 uses up to 1 lookup per address, from your monthly allowance first; addresses beyond the lookups left are skipped, not charged. The trigger and the Slack post cost nothing, and opening any report in the list afterwards is free.
Cost: Step 2 uses 1 lookup from your monthly allowance (purchased credits only once the allowance is used), or nothing when it reuses a report from the last 30 days, so the same address sent twice in a month is looked up once. Step 3 is free, because opening a report you already have is free.
Cost: Step 2 uses 1 lookup from your monthly allowance (purchased credits only once the allowance is used), or nothing when it reuses a report from the last 30 days. Opening a report from its link in the sheet is free.
Momentum has no Zapier app yet, so both Momentum steps call its own API, which needs API access on your Momentum account. Ask and we help set it up.
Cost: Step 2 uses 1 lookup from your monthly allowance (purchased credits only once the allowance is used), or nothing when it reuses a report from the last 30 days. The Webhooks steps cost nothing on Permity, and opening the report later is free.
The app is not required. Webhooks by Zapier, Custom Request reaches the same API directly, for anyone with a key:
Method POST
URL https://permityusa.com/v1/permits/lookup
Headers X-API-Key | pk_live_…
Content-Type | application/json
Data {"address": "{{address_from_previous_step}}"}For a Zap, add ?format=flat to the URL and "on_repeat": "reuse" to the data. The flat answer maps field by field (Roof Status, Roof Year), and a property your account already has comes back for nothing instead of being charged again. Add a Filter on Roof Status = found before whatever comes next, so a property with no roofing permit does not fall through as if it had a roof from year zero.
Without format=flat, Zapier flattens the nested JSON itself, so trades.roofing.year appears as Trades Roofing Year and the filter goes on Trades Roofing Status.
200 means the request worked, and that includes a property with no permits on record: that is a real answer, not an error. Anything else carries a JSON body with an error code you can branch on and a message a person can read.
200 | A successful lookup, including none_found. |
202 | Accepted for async work (you sent a callback_url). The result is POSTed to you when ready. |
400 | malformed_request: the body is not valid JSON. |
401 | unauthorized: missing, wrong or revoked key. Checked before anything else, so it is never about the request body. See Testing a key. |
402 | payment_required: the subscription is not active. Distinct from 401 on purpose: billing, not credentials. |
403 | terms_not_accepted: nobody on the account has accepted the current terms yet, so lookups are paused and nothing is charged. accept_url is where a person on the account accepts them; retrying will not help until they do. Reading reports keeps working. |
403 | names_not_enabled_for_key: the key asked for owner names on an export (names=1) but was not created with the owner-names option. No file was made and nothing was charged. keys_url is where a person on the account creates a key with the option, or turns it on for this one. Not yet answered: owner names on exports are not available yet. |
422 | invalid_request: valid JSON, unusable values. issues names each field. Refused before the lookup runs, so nothing is charged. An address with no house number in its street part ("Arvada, CO") is one, unless a geo_id is sent. |
429 | quota_exceeded: the monthly allowance is used up. Retry-After says when it resets. rate_limited on the free area search means too many outlines in a minute; nothing was used up, and Retry-After says when to draw again. |
409 | request_in_progress: a request with the same Idempotency-Key is still running. Nothing was charged for this one. Retry with the same key in a few seconds to receive its answer. |
502 | upstream_unavailable: permit data is unavailable right now. Retry. |
503 | lookup_timeout: the lookup took too long and was stopped before any charge. Nothing was charged. Retry in a minute; the retry is faster. |
When the terms change, responses to your key carry a Permity-Terms-Update header until someone on the account accepts the new version: the current version, where to accept it, the date after which lookups pause (once one is set) and the version the account last accepted, for example Permity-Terms-Update: 2026-10-02; accept=https://permityusa.com/terms/accept; by=2026-11-01; accepted=2026-09-28.
{
"error": "invalid_request",
"message": "Invalid request body.",
"issues": [{ "field": "from", "message": "from must be YYYY-MM-DD" }],
"meta": { "request_id": "req_7c1e0a9b2f4d4e3a8b6c5d21" }
}Here is a complete call that looks up one property. Paste your key into the box and the sample below is written with it, ready to copy.
Stays in this browser tab. Nothing on this page sends it anywhere, stores it or runs a lookup. Create or find a key on your API keys page.
// Node 18+ or any server. A key in a web page is a key anyone can read.
const res = await fetch("https://permityusa.com/v1/permits/lookup", {
method: "POST",
headers: {
"X-API-Key": "pk_live_your_key_here",
"Content-Type": "application/json",
},
body: JSON.stringify({ address: "123 Main St, Denver, CO 80202" }),
});
if (res.status === 402) throw new Error("Subscription is not active");
if (res.status === 429) throw new Error("Monthly allowance used up");
const data = await res.json();
if (data.status === "ambiguous") {
// More than one property matched. Ask, or re-send with a candidate's geo_id.
console.log(data.candidates);
} else if (data.status === "ok") {
const roof = data.trades.roofing;
// `year` is null when nothing was found. Check `status`, not truthiness:
// "no permit" and "no coverage here" mean opposite things.
console.log(roof.status, roof.year, roof.year_source);
}/v1/permits/lookupThe trade summary and full permit history for one address.
| Field | ||
|---|---|---|
address | required | Free-form. Normalised server-side. |
property_type | optional | residential, commercial, industrial or agricultural. A filter, not a declaration: the detected type is always returned. |
from / to | optional | Date window, YYYY-MM-DD. Defaults to 1970-01-01 through today. |
geo_id | optional | Resolve an ambiguous lookup by passing back a candidate’s id. |
callback_url | optional | Run asynchronously: answer 202 now and POST the result here when it is ready. See Async and retries. |
on_repeat | optional | "run" (the default), "prompt" or "reuse". With "prompt", a property your account has run in the last 30 days comes back as status: "already_run" instead of being run again: the prior answer, a link to the report you already have (free to open), whether anything has been published since, and what a re-run would cost. A prompt charges nothing; running it again always charges, however recently you ran it. No other value of this field can produce that status. With "reuse", for a caller that cannot answer a question (a Zap, a script): a property your account has run in the last 30 days comes back as the report you already have, charged nothing, with reused: true and previous.ran_at saying when it was run; anything else runs and is charged as usual. Not with callback_url. |
confirm | optional | The rerun.confirm token from an already_run response. Runs the lookup and charges for it. It is bound to your account and that property, and expires in 30 minutes; a stale one is answered with a fresh prompt rather than an error. |
Send Accept: application/pdf to get the report document instead of the JSON, in the same call and for the same price. An address that does not resolve to one property has no document, so it answers with the JSON either way.
To keep both, the data and the document, from one lookup: take the JSON and then download report.url, the stored report of that same call. Opening it costs nothing more. If the download fails, run only that step again: the lookup is already paid, and running the whole example again is a second lookup.
curl -s -X POST https://permityusa.com/v1/permits/lookup \
-H "X-API-Key: pk_live_your_key_here" \
-H "Content-Type: application/json" \
-d '{"address":"123 Main St, Denver, CO 80202"}' -o lookup.json
URL=$(grep -o '"url": *"[^"]*/v1/reports/[^"]*"' lookup.json | cut -d'"' -f4)
if [ -n "$URL" ]; then curl -s "$URL" -o report.pdf; else cat lookup.json; fiAdd ?format=flat to the lookup URL, or to a report’s json_url, and the answer comes back as one level of named fields, every one present every time and null when unknown. It is built for tools that map fields, such as Zapier. It cannot be combined with on_repeat: "prompt" or callback_url, and it answers in JSON whatever the Accept header says.
status | ok, ambiguous or address_not_found. Only ok is an answer; the others carry nulls, never zeros, and candidates for ambiguous. |
roof_status, hvac_status, electrical_status, plumbing_status | found, found no date, none found or not covered. Read the status before the year: a blank year means three different things, and only the status says which. |
roof_year, roof_year_confidence, roof_permit_count (and the same for each trade) | The newest year on record for the trade, how sure that year is (high, medium, low), and how many permits. |
has_building_record, building_record_match | Whether the county assessor’s record of the building was found, and how: exact parcel or address. |
year_built, square_feet, lot_square_feet, bedrooms, bathrooms, stories, construction_type, basement_square_feet, basement_finished_square_feet, roof_type, garage, heating, cooling, fireplaces, property_use | The building, as the record states it. Areas are square feet. property_use is the parcel's recorded use, in a national land-use classification ("Mobile/manufactured home", "Duplex", "Condominium unit"). |
roofing_material, roofing_material_label, roofing_material_impact_resistant, roofing_material_permit_number, roofing_material_permit_date | The roof material named on the newest roofing permit that names one, and that permit’s number and date, the same as material_on_record in the full answer. What the permit says, not the roof today. All null when no roofing permit names a material. roof_type above is the building record’s own field, and the two are never mixed. |
owner_name | The owner of record, where the public record names one, as recorded and not verified. Null otherwise. |
valuation_estimate, valuation_low, valuation_high, comparable_sales_value, comparable_sales_per_sqft, comparable_sales_count, assessor_market_value, assessor_market_year, assessed_value, assessed_year | What the property is worth, in whole dollars, null where not given: the estimate and its range, what comparable sales imply, the assessor’s market value and the assessed value tax is figured from, each with its year. An estimate of market value, not an appraisal. Market value is not the cost to rebuild, so it is not a guide to dwelling coverage. |
permit_count, latest_permit_date, latest_permit_work, permits | Every permit on record, newest first, each with date, work, description, status, permit_number, issued_by and value. |
coverage | One sentence: whose records these are and how far they reach. |
point_parcel_address, point_parcel_use | Only on address_not_found: the recorded parcel the address’s map point falls inside, and its recorded use. Not matched to the address asked about; nothing was looked up for it. Null otherwise. |
address, geo_id, report_id, ran_at, reused | The property, the report, when the answer was produced, and whether it is a report you already had. |
report_url, pdf_url, report_link_expires_at | The signed report link (opens in a browser), the same report as a file download, and when both links stop working. |
charged | Lookups this call spent: 1, 0.5 for an address that is not one property, 0 for a report you already had. |
request_id, quota_remaining, charged_to | The same as meta in the full answer: the id to quote if you need help with this call, the lookups your account has left after it (monthly allowance plus credits, as GET /v1/me counts them), and what paid for it (allowance, credit, or null when nothing was charged). Opened through a report’s json_url, quota_remaining is null: the link works without a key, so it does not carry the account’s balance. |
Which roof field is which. Three families of field start with roof, from two different records. roof_year, roof_status, roof_year_confidence and roof_permit_count are the roofing trade, read from roofing permits, like hvac_year and the rest. roofing_material and the fields that start with it are the material a roofing permit names. roof_type is the building record’s own description of the roof, from the county assessor, and is never filled from a permit. The two records can differ and both be accurate.
/v1/properties/lookupWhat the building is, and no permits: year built, floor area, lot, stories, bedrooms, bathrooms, exterior, roof, units, class, structure and basement, from the county assessor’s record of the parcel. It costs half a lookup when facts come back, from your monthly allowance first and from credits only once the allowance is used. An address we cannot place, one that matches more than one parcel, or one in a county we do not hold costs nothing.
| Field | ||
|---|---|---|
address | required | House number and street, then city and state; a ZIP helps. Read as typed: it is not corrected, so spell the town as the post office does. |
apn | optional | The parcel number, to pick one candidate of an ambiguous answer. |
unit | optional | A suite or unit, when it is not in the address line. |
on_repeat | optional | As on /v1/permits/lookup: "run" (the default), "prompt" or "reuse", over the same 30 days. A prompt or a reuse charges nothing; running it again is charged. |
confirm | optional | The rerun.confirm token from an already_run answer. |
status is ok, ambiguous (with candidates: an address, county and apn each; we never pick one), address_not_found or not_covered. matched_by says how the parcel was found (apn, unit, address or street), and as_of is when the county last edited its record, or null. A field the county does not publish is null. No owner or other name, no sale price, no tax and no mortgage or lien is ever in this answer.
An ok answer can also carry valuation when figures are available, in the same shape as on a permits lookup (see The response), at no extra cost. An estimate of market value, not an appraisal. Market value is not the cost to rebuild, so it is not a guide to dwelling coverage.
curl -s -X POST https://permityusa.com/v1/properties/lookup \
-H "X-API-Key: pk_live_your_key_here" \
-H "Content-Type: application/json" \
-H "Idempotency-Key: facts-8-honey-locust" \
-d '{"address":"8 Honey Locust, Littleton, CO 80127"}'
{
"status": "ok",
"property": { "address": "8 HONEY LOCUST", "city": "Littleton", "state": "CO",
"zip": "80127", "county": "Jefferson", "apn": "5931310004" },
"facts": { "year_built": 1986, "living_area": 2111, "lot_area": null, "stories": null,
"bedrooms": null, "bathrooms": null, "exterior": null, "roof_material": null,
"units": null, "property_class": null, "structure_type": "2 Story",
"property_use": "Single family residential",
"basement_area": 587, "basement_finished": null },
"matched_by": "address",
"as_of": null,
"meta": { "quota_remaining": 41, "charged": 0.5, "charged_to": "allowance" }
}GET /v1/reports lists the reports your account already has, newest first, for nothing. Add ?address= to keep the ones for one address, or ?geo_id= for one property. The address is matched to the reports on file by house number, street, unit and ZIP (or town); it is not looked up, so it costs nothing, and a spelling a lookup would have corrected finds nothing. Each row’s json_url takes &format=flat.
The list pages: send a response’s next back as ?before= for the page after, until next is null. ?ids= takes up to 50 report ids and hands back fresh links for the ones still on file; the rest are listed in missing.
/v1/areas keeps the areas you draw around a block, with each house and the report it produced, so every device on the account and the web show the same list. GET /v1/areas/{id}?include=facts adds each house’s roof, HVAC, electrical and plumbing years, worked out from its report when you ask. Saving, listing and opening areas is free, and no names are stored in an area.
GET /v1/areas/{id}/export?format=<csv|kml|summary-pdf|doorknock-pdf|json>&houses=<id>,<id> is the area as a file, the same bytes the web and the app download. Free.
Owner names on an export are not available yet. When they are, add names=1 to the spreadsheet, map file or either PDF to include the owner of record for each house, as the county wrote it, where the house’s state allows it; the json file never carries names. The key must have been created with "This key may receive owner names" ticked on your API keys page, or had it turned on there: that tick is a person on your account accepting the names notice in our terms (section 5). A key without it is answered 403 names_not_enabled_for_key and no file is made. Each names export is recorded against the account and the key. Names are copied from county records as written and can be out of date; using a name to decide whether an individual is eligible for credit, insurance, employment or housing is not permitted, and contacting owners is yours to do within the law.
GET /v1/me says which account a key belongs to and what it has left: allowance_remaining, then credits_remaining, and lookups_remaining, their sum. Free, and the call to make to test a key.
status is the first thing to read. It is ok (one property, answered), ambiguous (more than one property matched; candidates lists them, and you re-send with one’s geo_id) or address_not_found. Two more appear only when you ask for them: accepted for an async call and already_run for a repeat prompt.
An address_not_found may carry point_parcel: the recorded parcel the address’s map point falls inside, with its address, city, zip, apn and recorded property_use. It is information for you to weigh: it was not matched to the address you asked about, nothing was looked up for it, and the miss is charged as a miss.
The shape of an ok answer, trimmed. Values are illustrative, not a real property; hvac, electrical and plumbing have the same shape as roofing.
{
"status": "ok",
"query": { "address": "123 Main St, Denver, CO 80202" },
"resolved": {
"geo_id": "…",
"address": "123 MAIN ST",
"city": "DENVER", "state": "CO", "zip": "80202",
"county": "Denver",
"property_type": "single_family",
"property_class": "residential",
"year_built": 1962,
"year_built_source": "property_record"
},
"coverage": {
"jurisdiction": "Denver",
"status": "full",
"permits_through": "2026-09-01"
},
"trades": {
"roofing": {
"status": "found",
"year": 2019,
"year_source": "file_date",
"year_confidence": "high",
"permit_count": 2,
"permits": [ … ],
"material_on_record": {
"material": "asphalt_shingle",
"label": "Asphalt shingle",
"impact_resistant": true,
"permit_number": "…",
"permit_date": "2019-06-11",
"status": "…",
"text": "Class 4 composition shingle"
}
},
"hvac": { "status": "none_found", "year": null, … },
"electrical": { … },
"plumbing": { … }
},
"permits": [
{
"permit_id": "…",
"date": "2019-06-11",
"date_source": "file_date",
"year": 2019,
"tags": ["roofing"],
"description": "Re-roof, tear off, Class 4 composition shingle",
"type": "…", "subtype": null,
"contractor": null,
"status": "…",
"jurisdiction": "Denver",
"fees": null
}
],
"untagged_permit_count": 0,
"report": {
"report_id": "…",
"url": "https://permityusa.com/v1/reports/…",
"json_url": "https://permityusa.com/v1/reports/….json?…",
"expires_at": "…"
},
"meta": {
"request_id": "req_…",
"quota_remaining": 412,
"charged": 1,
"charged_to": "allowance"
}
}year_source | Which date the year came from, in order of preference: file_date, issue_date, final_date, start_date. The application is when the work happened; a final can land years later. |
year_confidence | high, medium or low. Absent when year is null. |
inferred | Present only when there is a real basis for a guess (a new-construction permit, or the year built), with its strength. Never copied into year. |
before_rebuild | Present only when the address was rebuilt (a demolition permit paired with new-construction permits) and this trade has permits dated before the demolition. Those permits belong to the building that was removed, so they are counted here (permit_count, newest_year, boundary_date) and set none of status, year or permit_count above. They stay in permits. |
material_on_record | Roofing only. The roof material named on the newest roofing permit that names one: material (one of asphalt_shingle, wood_shake, metal, concrete_tile, clay_tile, tile, slate, synthetic, tpo, epdm, pvc, modified_bitumen, built_up, roll_roofing, coating), its label, the permit_number, permit_date and status of that permit so you can check it, and the text it was read from. It is what the permit says, not an inspection of the roof today. impact_resistant is true only when the permit says Class 4, impact resistant or IR, and null otherwise. When a permit names the roof coming off and the one going on, the one going on is reported. Expired, void and other lapsed permits never count. null when no roofing permit names a material, which is not evidence of anything. It is separate from the building record’s roof, and never filled from it. |
untagged_permit_count | Permits no trade could be read from. One of them could be the roof, so it is reported rather than hidden. |
contractor | The contractor named on the permit, where the record names one, as recorded and not verified. Null otherwise. |
report | A signed link to the report built from this answer, PDF or .json. It opens without a key, so it can be forwarded, and it expires. |
coverage.permits_through | The most recent date this jurisdiction’s records reach. A permit filed after it cannot be here yet. |
valuation | What the property is worth, by four measures, in whole dollars: estimate (value, low, high), comparable_sales (value, per_sqft, count), assessor_market (value, year), assessed (value, year; the figure tax is figured from, not a market value) and as_of, the date of the estimate. Present only when a figure is available, with null for any it does not give; kept with the report, so its link returns the figures it was filed with. An estimate of market value, not an appraisal. Market value is not the cost to rebuild, so it is not a guide to dwelling coverage. |
On 2026-10-12 the fields that described how an answer was put together, rather than what is on record, stopped being sent. Read these instead: meta.authorities, coverage.authorities, trades.*.authorities, trades.*.records_read_in_part (true when the trade rests on an authority whose records we hold only in part), jurisdiction.records, jurisdiction.records_url, jurisdiction.records_access, and on each permit authority_id and record_url. The Deprecation, Sunset and Permity-Deprecated-Fields headers that announced them are no longer sent.
Ids. A geo_id starts with pg_ and a permit_id with pp_. Both are stable: the same property or permit has the same id on every call. Send a geo_id back exactly as you received it. An id in the earlier form is still accepted until 2027-01-10. permit_number is the number the issuing authority prints, or null when none is known; quote it to the city.
This is the part worth reading twice. Collapsing these to a null year loses the distinction that matters most.
found | A permit and a year. |
none_found | No permit, and this jurisdiction reports well. Real signal: probably no permitted work. |
found_undated | A permit exists, no usable date. Work happened; when is unknown. |
no_coverage | The jurisdiction barely reports. Absence here is not evidence. |
A permit on record says work was permitted. No permit on record does not say no work was done, and no_coverage says nothing either way.
Each lookup is charged to your monthly allowance first, and to purchased credits only once the allowance is used. An address that is not found, or that matches more than one property, costs half a lookup. Building facts only costs half a lookup when facts come back and nothing otherwise. Running a property again is charged like the first time; opening a report you already have, through its report link or with on_repeat: "reuse", is free. Every call is checked against your balance before it runs, so with nothing left a reuse is refused too; finding the report through GET /v1/reports is not.
meta.charged says what the call cost (1, 0.5 or 0), meta.charged_to says what paid for it (allowance, credit or null), and meta.quota_remaining is the number that decides whether your next call runs. Address suggestions, the houses in an area, job status and reports cost nothing.
Add a callback_url and the lookup answers 202 at once with a job_id and a status_url. When the result is ready it is POSTed to your URL with an X-Permity-Signature: sha256=… header, an HMAC of the raw body under your webhook secret (on your API keys page). A caller that cannot receive a POST can poll GET /v1/jobs/{id} instead.
Send an Idempotency-Key header and a retry of the same request returns the original answer instead of running (and charging) again. A failure is never stored against the key, so retrying after a 502 or 429 runs for real.
/v1/permits/lookupLook up one propertyThe whole trade summary and permit history for an address. Add geo_id to resolve an ambiguous result, from/to to narrow the window.
curl -X POST https://permityusa.com/v1/permits/lookup \
-H 'X-API-Key: pk_live_your_key_here' \
-H "Content-Type: application/json" \
-d '{"address":"123 Main St, Denver, CO 80202"}'/v1/properties/lookupBuilding facts onlyWhat the building is (year built, floor area, lot, stories, rooms, exterior, roof, basement) and no permits. Uses half a lookup when facts come back, from your monthly allowance first; nothing otherwise. Add apn to pick one parcel of an ambiguous result.
curl -X POST https://permityusa.com/v1/properties/lookup \
-H 'X-API-Key: pk_live_your_key_here' \
-H "Content-Type: application/json" \
-d '{"address":"123 Main St, Denver, CO 80202"}'/v1/permits/lookupGet the PDF directlySame call, same quota, same stored report — Accept decides whether you get the data or the document. Saves a round trip when all you want is the file.
curl -X POST https://permityusa.com/v1/permits/lookup \
-H 'X-API-Key: pk_live_your_key_here' \
-H "Accept: application/pdf" \
-H "Content-Type: application/json" \
-d '{"address":"123 Main St, Denver, CO 80202"}' \
-o report.pdf/v1/permits/lookupLook up and save the PDFOne lookup gives you both: the data in lookup.json and the report saved as report.pdf. Opening the report link costs nothing more. If the second step fails, run only it again: the lookup is already paid.
curl -s -X POST https://permityusa.com/v1/permits/lookup \
-H 'X-API-Key: pk_live_your_key_here' \
-H "Content-Type: application/json" \
-d '{"address":"123 Main St, Denver, CO 80202"}' -o lookup.json
URL=$(grep -o '"url": *"[^"]*/v1/reports/[^"]*"' lookup.json | cut -d'"' -f4)
if [ -n "$URL" ]; then curl -s "$URL" -o report.pdf; else cat lookup.json; fi/v1/permits/lookupRun it asynchronouslyReturns 202 with a job_id immediately and POSTs the finished result to your callback_url, signed. Use it when a caller cannot wait, or when the chain might be slow.
curl -X POST https://permityusa.com/v1/permits/lookup \
-H 'X-API-Key: pk_live_your_key_here' \
-H "Content-Type: application/json" \
-H "Idempotency-Key: renewal-1001" \
-d '{"address":"123 Main St, Denver, CO 80202","callback_url":"https://hooks.example.com/permity"}'/v1/jobs/{id}Check an async jobStatus, the result once it is ready, and every webhook delivery attempt with its response time. Reading a job costs no quota.
curl https://permityusa.com/v1/jobs/job_9f2c \ -H 'X-API-Key: pk_live_your_key_here'
/v1/permits/streetCompare to the streetDeprecatedHow many properties on the same block pulled a permit for each trade, and in which years. The answer to what a permit record cannot see.
Stops answering on 30 September 2027. Until then it works exactly as it does today, and every response carries Deprecation and Sunset headers saying so. Nothing on the API replaces it. If you depend on this, say so before the sunset date and it can be reconsidered.
curl -X POST https://permityusa.com/v1/permits/street \
-H 'X-API-Key: pk_live_your_key_here' \
-H "Content-Type: application/json" \
-d '{"address":"123 Main St, Denver, CO 80202"}'/v1/batchesRun a whole listPost a CSV. Only the address column is required; every other column comes back untouched and in its original order.
curl -X POST 'https://permityusa.com/v1/batches?name=Q3%20renewals' \ -H 'X-API-Key: pk_live_your_key_here' \ -H "Content-Type: text/csv" \ --data-binary @book.csv
/v1/batches/{id}.csvDownload batch resultsYour columns, then four trade years each with its status beside it — because a blank year means two opposite things and the CSV has to say which.
curl 'https://permityusa.com/v1/batches/BATCH_ID.csv' \ -H 'X-API-Key: pk_live_your_key_here' -o results.csv
/v1/reports/{id}Fetch a report againThe PDF, or add .json for the data it was rendered from. Signed and expiring, so it opens without a key — which is what makes it forwardable.
curl 'https://permityusa.com/v1/reports/REPORT_ID?expires=…&signature=…' \ -o report.pdf
/v1/permits/lookupLook up for an automationon_repeat "reuse" hands back the report your account already has for a property looked up in the last 30 days, for nothing, and runs (one lookup) otherwise. format=flat answers in one level of named fields, ready to map.
curl -X POST 'https://permityusa.com/v1/permits/lookup?format=flat' \
-H 'X-API-Key: pk_live_your_key_here' \
-H "Content-Type: application/json" \
-d '{"address":"123 Main St, Denver, CO 80202","on_repeat":"reuse"}'/v1/reportsFind reports for an addressThe reports your account already has for an address (or a geo_id), newest first. Free: it matches the reports on file and never runs a lookup, so a spelling a lookup would have corrected finds nothing.
curl --get 'https://permityusa.com/v1/reports' \ -H 'X-API-Key: pk_live_your_key_here' \ --data-urlencode 'address=123 Main St, Denver, CO 80202'
/v1/meCheck your keyThe account this key belongs to and the lookups it has left: monthly allowance first, then credits. Free, and the way to test a key: 200 means it works, 401 that it does not.
curl 'https://permityusa.com/v1/me' \ -H 'X-API-Key: pk_live_your_key_here'
/v1/addresses/in-areaFind the houses in an areaThe house addresses inside an outline you draw, one row per building, in walking order, each with this account's report when it already has one. Free: it never runs a lookup. An outline is 3 to 200 [longitude, latitude] points and at most 600 m across; coverage says whether addresses could be listed here ("live"), not here ("none"), or not right now ("unavailable").
curl -X POST https://permityusa.com/v1/addresses/in-area \
-H 'X-API-Key: pk_live_your_key_here' \
-H "Content-Type: application/json" \
-d '{"polygon":[[-105.2755,40.0138],[-105.2715,40.0138],[-105.2715,40.0158],[-105.2755,40.0158]]}'/v1/areasSave an areaKeeps an area on your account so every device and the web show it: the outline, and each house with its result and report id. Send your own client_id and sending it again returns the saved area rather than a second one. PATCH /v1/areas/{id} renames it or merges a later pass by house id; DELETE removes it and never a report. Free. No names are stored.
curl -X POST https://permityusa.com/v1/areas \
-H 'X-API-Key: pk_live_your_key_here' \
-H "Content-Type: application/json" \
-d '{"client_id":"6f1c2e9a-area-0001","outline":[[-105.2755,40.0138],[-105.2715,40.0138],[-105.2715,40.0158],[-105.2755,40.0158]],"houses":[{"id":"1630 17th st boulder co 80302","address":"1630 17TH ST","city":"Boulder","state":"CO","zip":"80302","lat":40.0159,"lng":-105.2722,"result":"done","report_id":"REPORT_ID"}]}'/v1/areas/{id}Open an area with its roof yearsOne saved area. With include=facts each house carries fresh report links and its roof, HVAC, electrical and plumbing years with what each year is based on, worked out from its report when you ask. Where no roof permit is on record the roof year is the year built, and says so. A report past its retention shows as expired. GET /v1/areas lists your areas, newest first. Free.
curl 'https://permityusa.com/v1/areas/AREA_ID?include=facts' \ -H 'X-API-Key: pk_live_your_key_here'
/v1/reportsReopen reports by idUp to 50 of your reports by id, with fresh links; ids no longer on file come back in missing. Every list of reports also pages: send its next value as before to get the page after. Free.
curl --get 'https://permityusa.com/v1/reports' \ -H 'X-API-Key: pk_live_your_key_here' \ --data-urlencode 'ids=REPORT_ID_1,REPORT_ID_2'
/v1/addresses/autocompleteSuggest addressesNormalised suggestions as someone types. Costs no lookup quota — suggestions are keystrokes, not lookups.
curl 'https://permityusa.com/v1/addresses/autocomplete?q=2054+E+167th' \ -H 'X-API-Key: pk_live_your_key_here'