API reference
North Carolina building permits as JSON: filter by office, county, city, ZIP, category, stage and date, page through the results, and see who pulled each permit.
Base URL: https://www.tarsignal.com/api/v1
Quick start
Get a trial key on the Data & API page (it arrives by email in a minute), then ask for the five newest permits:
curl -H "Authorization: Bearer $TARSIGNAL_KEY" \
"https://www.tarsignal.com/api/v1/permits?limit=5"In Python:
import os, requests
r = requests.get(
"https://www.tarsignal.com/api/v1/permits",
headers={"Authorization": f"Bearer {os.environ['TARSIGNAL_KEY']}"},
params={"county": "Wake", "category": "new_build", "since": "2026-10-01"},
)
r.raise_for_status()
for permit in r.json()["data"]:
print(permit["filed_date"], permit["address"], permit["valuation"])In JavaScript:
const res = await fetch("https://www.tarsignal.com/api/v1/permits?zip=27519&stage=issued", {
headers: { Authorization: `Bearer ${process.env.TARSIGNAL_KEY}` },
});
const { data, next_cursor } = await res.json();Authentication
Send your key in the Authorization header as Bearer <key> (or in X-API-Key). Keys start ts_trial_ or ts_live_. Keep them on a server: anyone with a key can spend its quota. We store only a hash, so a lost key can't be shown again; we'll issue a new one and revoke the old.
The API is read-only, over HTTPS, and answers JSON in UTF-8. CORS is open, but don't put a key in a public web page.
Keys, scope and quotas
- Trial keys cover one permit office, last 7 days, return up to 1,000 records in total and allow 30 requests a minute. Contractor phone and email aren't included.
- Production keys are set up with you: the offices you license, a monthly record quota, a request rate, and contractor phone and email if your license includes them.
- Each permit or contractor returned counts as one record.
/coverageand/mecount nothing. A page is trimmed to the records your key has left, so a request never runs past your quota. - Every data response carries
X-Records-Remaining,X-Records-QuotaandX-Quota-Period(monthortotal).
GET /coverage
The permit offices we cover, with their id (what office takes), county, the range of filing dates we hold, the number of permits and when we last checked the office. No key needed.
curl "https://www.tarsignal.com/api/v1/coverage"GET /me
The calling key: its kind, the offices it covers (or "all"), whether contacts are included, its quota, the records left and when it expires.
GET /permits
Permits, newest first by filing date (issue date where the office doesn't record a filing date). Every filter is optional; lists take commas or repeated parameters.
| Parameter | What it does |
|---|---|
| office | One or more office ids from /coverage, e.g. raleigh,cary. |
| county | A county name, e.g. Wake or Wake County: every office in it. |
| city | A city or town as the permit records it, e.g. Apex. |
| zip | 3 to 5 digits; a prefix matches every ZIP under it (276 = 276xx). |
| category | One or more of: demolition, accessory_adu, garage, deck_porch, fence, pool_spa, roofing, electrical, plumbing, mechanical, signage, addition, new_build, alteration, commercial, residential, other. |
| stage | One or more of: submitted, in_review, approved, issued, completed, withdrawn_rejected, other. |
| since, until | Filing-date range, YYYY-MM-DD, inclusive. |
| limit | Records per page, 1 to 500. Default 100. |
| cursor | The next_cursor from the previous page. |
The response is { "data": [permits], "has_more": true, "next_cursor": "…" }. Pass next_cursor back as cursor until has_more is false; the cursor holds your place even as new permits arrive.
GET /permits/{id}
One permit by its id. Ids are stable: the same permit keeps its id as its status changes.
GET /contractors
Contractors with permits in the area, busiest first: name, license, number of permits, total stated value, latest permit date and the offices they filed with (and phone and email, if your key includes contacts). Takes office, county, city, zip, since (default: a year ago), until, limit (1 to 200, default 100) and cursor.
The permit record
An example; the address and contractor are placeholders.
{
"id": "9f45e8da-7246-4bd5-8968-cb81d555644c",
"permit_number": "WAKE-RBPR-000123-2026",
"office": { "id": "wake_county", "name": "Wake County" },
"address": "1234 OAK RIDGE DR",
"city": "WENDELL",
"zip": "27591",
"category": "new_build",
"permit_type": "Residential - New One- and Two-Family Dwelling",
"description": "New single-family dwelling",
"status": "In Review",
"stage": "in_review",
"valuation": 295750,
"square_footage": null,
"filed_date": "2026-10-13",
"issued_date": null,
"contractor": {
"name": "Example Homes LLC",
"license": "L.12345",
"phone": "(919) 555-0123",
"email": "permits@example.com"
},
"named_contractor": null
}categoryis our normalized category;permit_typeandstatusare the office's own words;stageis the status normalized.valuationis the value stated on the permit, in dollars; null where the office doesn't record one.contractoris null when the permit names none.phoneandemailappear only on keys licensed for contacts.named_contractoris another company the office's record names for the permit's work, as the office wrote it: on Mecklenburg County commercial permits, the trade contractor named in the county's project name, usually working under the general contractor of record. Null on every other record.
Errors
Errors come back as { "error": { "code": "…", "message": "…" } } with the status:
| Parameter | What it does |
|---|---|
| 400 | A bad parameter: bad_limit, bad_zip, bad_since, bad_category, bad_cursor… |
| 401 | missing_key or invalid_key. |
| 403 | revoked_key, expired_key, or outside_key_scope (an office your key doesn't cover). |
| 404 | not_found. |
| 429 | rate_limited (wait a minute) or quota_exhausted. |
| 5xx | Our side. Retry with backoff. |
Bulk CSV exports
For a full history or a one-off load, we send a download link for a CSV of your scope instead of paging the API. The link works for a set number of days. Columns:
id, permit_number, office, address, city, zip, category, permit_type, description, status, stage, valuation, square_footage, filed_date, issued_date, contractor_name, contractor_phone, contractor_email, contractor_license, named_contractor
Versioning
This is v1. Within v1 we only add: new fields, endpoints and filter values. Anything that would break a client gets a new version, with notice to every key holder.
Questions: support@tarsignal.com.
