NEWPermit Search is live — three years of North Carolina permits, free to start. Search permits →

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. /coverage and /me count 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-Quota and X-Quota-Period (month or total).

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.

ParameterWhat it does
officeOne or more office ids from /coverage, e.g. raleigh,cary.
countyA county name, e.g. Wake or Wake County: every office in it.
cityA city or town as the permit records it, e.g. Apex.
zip3 to 5 digits; a prefix matches every ZIP under it (276 = 276xx).
categoryOne or more of: demolition, accessory_adu, garage, deck_porch, fence, pool_spa, roofing, electrical, plumbing, mechanical, signage, addition, new_build, alteration, commercial, residential, other.
stageOne or more of: submitted, in_review, approved, issued, completed, withdrawn_rejected, other.
since, untilFiling-date range, YYYY-MM-DD, inclusive.
limitRecords per page, 1 to 500. Default 100.
cursorThe 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
}
  • category is our normalized category; permit_type and status are the office's own words; stage is the status normalized.
  • valuation is the value stated on the permit, in dollars; null where the office doesn't record one.
  • contractor is null when the permit names none. phone and email appear only on keys licensed for contacts.
  • named_contractor is 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:

ParameterWhat it does
400A bad parameter: bad_limit, bad_zip, bad_since, bad_category, bad_cursor…
401missing_key or invalid_key.
403revoked_key, expired_key, or outside_key_scope (an office your key doesn't cover).
404not_found.
429rate_limited (wait a minute) or quota_exhausted.
5xxOur 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.