v1

SitePath API

Programmatic, source-attributed access to county-level solar permitting research across every U.S. county — plus data-center and BESS regulatory intel, and the live change feed. Designed for server-to-server integration with your own pipelines, GIS, or BI tooling.

Base URL: https://www.sitepathintel.com/api/v1

Authentication

Every request (except /health) must include an API key in the Authorization header.

curl https://www.sitepathintel.com/api/v1/counties \
  -H "Authorization: Bearer sp_live_xxxxxxxxxxxxxxxxxxxxxxxx"
Treat API keys like passwords. They grant full read access to data your subscription covers. We never use them from the browser — embed them only in server-side code, environment variables, or a secrets manager. We deliberately keep CORS closed on the API to discourage browser usage.

Create a key

If your account has API access, open your account → API keys and click Create new key. Give it a descriptive name (e.g. "Production CRM"). The full key is shown once — copy it to a secrets manager immediately. We store only an irreversible hash; lost keys must be revoked and recreated.

Limits: up to 10 active keys per account (contact us to raise). Revoking a key takes effect within 60 seconds.

Versioning

The API is versioned two ways. The path version (/api/v1) changes only for a rewrite that breaks the URL surface. Day-to-day changes are governed by a dated version — the current one is 2026-08-01, returned on every response as the X-API-Version header.

We add fields and endpoints without a version bump, so write tolerant parsers (ignore unknown fields). A change that removes or renames a field ships under a new dated version; we announce it to active API customers and serve Deprecation and Sunset headers on the old behavior for a transition window before it's retired.

Request IDs

Every response includes a Request-Id header (e.g. req_0a1b2c…), and every error repeats it in the body as request_id. Log it. When you contact support about a specific call, quote the request id and we can find it immediately.

Pagination

List endpoints (/counties, /projects, /changes) return a consistent envelope. Records live in data; each carries its own object type.

{
  "object":      "list",
  "data":        [ { "object": "project", … }, … ],
  "has_more":    true,
  "next_cursor": "eyJvIjo1MDB9",   // opaque — pass back as ?cursor=
  "total":       1284
}

To page, pass limit and the previous response's next_cursor as ?cursor=. Stop when has_more is false (next_cursor is then null). Treat the cursor as opaque — don't construct or parse it.

Rate limits & quota

Two per-key limits apply. A short burst limit (sliding window) and a monthly request ceiling:

API accessBurstMonthly ceiling
Standard120 requests / minute100,000 requests / month
Partner300 requests / minute200,000 requests / month

Every 2xx response reports where you stand, so you can self-throttle instead of hitting errors:

HeaderMeaning
X-RateLimit-Limit / -Remaining / -Window / -ResetBurst budget, window length (seconds), and the epoch-seconds reset time.
RateLimit / RateLimit-PolicyThe same budget in the IETF standard format.
X-Monthly-Limit / -Remaining / -ResetMonthly ceiling, approximate remaining (≤60s stale), and the reset period.
ETagContent fingerprint — send it back as If-None-Match to get a cheap 304 when the data hasn't changed.
Link: …; rel="license"The API license (no redistribution of the dataset).

Exceed the burst limit and you get 429 rate_limited; exhaust the monthly ceiling and you get 429 monthly_quota_exceeded. Both include Retry-After (seconds) — back off and retry. The map layer's 429s (rate_limited, monthly_ceiling) carry it too.

A key may also carry its own scopes and monthly ceiling, which you set yourself under API keys on your account page — least privilege for a key handed to a contractor or pasted into one service. Before a limit applies we show you what it would have refused, based on what that key has actually called, so you never narrow a live integration blind. A restriction only ever narrows: it can't reach past what the account has licensed, and a per-key ceiling can only sit below the plan's. X-Monthly-Limit always reports the ceiling actually enforced on the key you're calling with, and GET /usage lists that key's scopes. Those two endpoints, plus /health and /openapi.json, are never scoped away — a throttled key can always see why.

Cost tripwire, not a billing meter. The monthly ceiling exists so a runaway script can't run up an unbounded bandwidth bill — you never pay per call. The burst limit is per-warm-instance, so spiky traffic across instances may allow a small burst past the stated cap. New to the API? Start with the integration guide.

Errors

Errors return a typed JSON object plus the request id. Branch on error.type (a stable class) or error.code (specific) — never on the message text.

{
  "error": {
    "type":    "authentication_error",
    "code":    "invalid_key",
    "message": "Invalid or revoked API key.",
    "doc_url": "https://www.sitepathintel.com/api-docs#errors"
  },
  "request_id": "req_0a1b2c3d…"
}

Types: authentication_error (401), permission_error (403), invalid_request_error (400/404/405), rate_limit_error (429), api_error (5xx). Codes you might see:

StatusCodeMeaning
400invalid_parameterA query parameter can't be read: a date filter (since, until, adoptedSince) that doesn't start with a real YYYY-MM-DD date, a number filter (minScore, maxScore, minSetbackPropertyFt, maxSetbackPropertyFt, minMw) that isn't a plain number, a fixed-choice filter (grade, moratorium, countyOwn, sourced) given a value outside its list, or a /lookup coordinate that is missing, out of range or not a plain decimal number. The message names the parameter and shows the expected form. Fix the value; retrying it unchanged returns the same 400.
401missing_keyNo Authorization header.
401invalid_keyKey didn't match. Could be revoked or typo'd.
401scoped_key_requiredA key sent as ?token= must be a scoped individual key. Send an unscoped key or an organization key as an Authorization: Bearer header instead. See the feature service.
403plan_requiredThe account doesn't include API access.
403addon_requiredThe account hasn't licensed that dataset. Contact support.
403scope_requiredThe account has it; this key wasn't granted it. The key's owner changes its scopes under API keys on their account page — no need to contact us.
403org_suspendedThe organization's API access is suspended. Contact support.
403org_key_requiredBulk delivery needs an organization key with the bulk scope.
403sandbox_not_allowedSandbox keys (sp_test_…) can't use bulk delivery or the feature service.
403publishable_key_layer_onlyA publishable key (sp_pub_…) was sent to the Bearer API. It works only on the data layer.
403publishable_token_requiredA live or sandbox key was put in a data-layer URL. The layer takes publishable tokens only; use the Bearer API with a live key.
403layer_forbiddenData layer: the token is invalid or revoked, or its organization is suspended.
403origin_requiredData layer: the request carries no browser origin. A publishable token serves only pages on the origins registered for its organization.
403origin_not_allowedData layer: the page's origin isn't registered for the token's organization.
404not_found / county_not_foundUnknown endpoint or FIPS.
404geometry_not_foundNo boundary geometry on file for that FIPS on /counties/:fips/geometry.
405method_not_allowedOnly GET is supported.
429rate_limitedSlow down. See Retry-After.
429monthly_quota_exceededMonthly ceiling reached. Resets on the 1st (UTC), or contact support to raise it.
429monthly_ceilingData layer: the organization's monthly ceiling is reached. The same limit as monthly_quota_exceeded on the Bearer API.
500internal_errorSomething failed on our side. Retry; if it persists, send support the request_id.
503data_unavailableThe dataset isn't loaded on this deploy. Retry shortly.

OpenAPI & tools

A machine-readable OpenAPI 3.1 spec describes every endpoint, parameter, schema, and error. It's public (no key needed):

curl https://www.sitepathintel.com/api/v1/openapi.json

Import it into Postman or Insomnia (File → Import → URL), or generate a typed client in your language with openapi-generator. The spec's version matches the dated X-API-Version.

Official libraries

Hand-crafted, zero-dependency clients for Python 3.8+ and Node 18+ with auto-pagination, retries, ETag caching, and typed errors that carry the request_id. They are delivered with your API onboarding as source you vendor into your project — they are not yet on PyPI or npm, and the sitepath name on PyPI belongs to an unrelated package. Ask support@sitepathintel.com for the current build, or generate a client from the OpenAPI spec above.

Map SDK & data layer Publishable token

The SitePath map as a product you drop into your own site. It is not a picture of our map: it is the same county choropleth and the same infrastructure points, with the controls the SitePath app has and a few it does not. Runs on a publishable token (sp_pub_…), which is safe to expose in a browser because it is locked to the origins registered on your organization.

Drop it in

<link rel="stylesheet" href="https://www.sitepathintel.com/vendor/leaflet/leaflet.css">
<script src="https://www.sitepathintel.com/vendor/leaflet/leaflet.js"></script>
<script src="https://www.sitepathintel.com/sitepath-symbology.js"></script>
<script src="https://www.sitepathintel.com/sitepath-map.js"></script>
<div id="map" style="height:640px"></div>
<script>
  const spm = SitePathMap.create('#map', { token: 'sp_pub_…', panel: 'left', theme: 'light' });
  spm.on('select', (fips, county) => console.log(fips, county.grade));
</script>

No code at all? Iframe /embed.html?token=sp_pub_…&panel=right&theme=dark. Try it on the live preview, or see it inside a customer's own portal, GIS and alerting.

What the panel gives your users

  • Layers — county permitting risk on or off; solar projects, battery storage, and data centers (where licensed) each on their own toggle.
  • Filters — state and county, nothing else. A county name, FIPS or "lat, lng" search flies to the match. Filters never remove a county: what falls outside them is greyed out, and the legend says so.
  • Symbology — counties always carry the SitePath A–F grade colours, so a map on your site reads the same as the map on ours; you adjust fill transparency and county borders.
  • County report — a click on a county opens its full report in the centre of the map: ordinance rows, permitting, zoning parameters, data centers (status and moratorium, data-center ordinance rows, reported moratoria, facilities, opposition, grid, incentives and constraints, for organizations licensed for data centers), community opposition, local news, governing board and meeting record. Sections start collapsed; a toggle row switches off the ones a reader does not want, and the choice is remembered. With the source_urls licence every item links to its source document (ordinance full text, minutes, the article, the petition); without it, no links.
  • Project points — off by default. Pass showProjects: true to add the solar, storage and (where licensed) data-center points with their own toggles and export.
  • Export & share — GeoJSON or CSV of the state or county chosen in Filters, never the whole country; a share link that restores the filter, transparency and the view.
  • Themes — light or dark, panel left or right, any basemap (CARTO light/dark, OpenStreetMap, none, or your own tile URL). Mobile folds the panel into a bottom sheet.

Programmatic API

setFilters(patch), setSymbology(patch), setLayers(patch), getState() / setState(state), flyTo(fips), openReport(fips), exportGeoJSON(), exportCsv('counties'|'points'), shareUrl(), on('ready'|'select'|'report'|'change'|'error', fn), destroy(). The Leaflet map is exposed as spm.map for your own overlays.

The layer resources underneath

The SDK reads plain GeoJSON you can also load into Mapbox, ArcGIS, QGIS or your own renderer: /api/v1/layer/<token>/counties.geojson (every county with score, grade, rank, ordinance status, moratorium, trajectory, population, saturation and a precomputed fill), points.geojson (the infrastructure points), bess.geojson, style.json (the grade ramp), meta.json, and county/<fips>.json (the county report). Data centers ride only for organizations licensed for them. Every response carries the required "Data © SitePath Intelligence" attribution; the SDK renders it and exports carry it.

Feature service — SitePath as a native ArcGIS / QGIS layer

GET/arcgis/rest/services/SitePath/FeatureServerAPI key (?token= or Bearer)

An ArcGIS-compatible feature service (GeoServices REST). ArcGIS Pro, ArcGIS Online, ArcGIS Enterprise and QGIS add it by URL, draw it with SitePath's own symbology, query it server-side and refresh on their own schedule. Layer 0 is county permitting risk (polygons: score, grade, rank, ordinance status, moratorium, trajectory, population, saturation, profile and county-page links); layer 1 is the infrastructure points (solar; battery storage with the storage, data-center & meetings add-on; data centers with the data-centers licence).

Standard where clauses (=, <>, <, >, IN, LIKE, IS NULL, AND/OR/NOT), envelope filtering, outFields, orderByFields, paging (1000 per page), outSR 4326 or 102100, f=json|pjson|geojson, returnCountOnly, returnIdsOnly, returnExtentOnly. Read-only. The key rides as ?token= on the URL, the GeoServices convention. A key sent that way must be a scoped key (counties for the county layer, plus projects for the points layer); an unscoped key is refused with scoped_key_required, because a URL is copied into web-map items, proxy logs and browser history and what leaks must be bounded to what the key names. Sandbox keys and organization keys are not accepted on the URL. Step-by-step for each client: clients/loaders/arcgis_feature_service.md.

https://www.sitepathintel.com/api/v1/arcgis/rest/services/SitePath/FeatureServer/0/query?where=stateCode%3D%27TX%27%20AND%20hasMoratorium%3D1&outFields=fips,county,score,grade&f=geojson&token=$SITEPATH_API_KEY

Sync kit — keep your copy current, automatically

Three ways to have the data update itself in your systems, in order of least work:

  • Feature service. Add the layer once; the GIS client refreshes it. Nothing to run.
  • Webhooks (push). Register an endpoint from your account and SitePath POSTs signed events after each nightly: change.created, county.updated, moratorium.changed, project.status_changed. Verify SitePath-Signature (HMAC-SHA256 over t.body, five-minute tolerance), then fetch the county for the licensed detail. Delivery is idempotent per nightly run and endpoints that fail repeatedly are paused, never silently dropped.
  • Scheduled pull. python -m sitepath.sync reads the bulk manifest, skips a snapshot whose content hash you already hold, downloads NDJSON parts into a dated folder with a latest pointer, and loaders for BigQuery, Snowflake and an ArcGIS Notebook take it from there. Run it hourly, daily or monthly; every row carries the snapshot it came from.
pip install sitepath   # or clients/python
python -m sitepath.sync --key $SITEPATH_ORG_KEY --out ./sitepath-data --datasets counties,changes,projects

Webhooks

Instead of polling /changes, register an endpoint and SitePath will POST signed events to it. Manage endpoints from your account; each gets its own signing secret (shown once, revealable later) and a cadence: a monthly digest (the default), a weekly digest, or every nightly build. Send test on the account page delivers a signed sample to an endpoint right away, so you can verify your receiver before any real event.

Limits by API access

  • Standard — three endpoints; monthly or weekly digests; ten endpoint creations and ten test sends a day.
  • Partner — ten endpoints; monthly, weekly or every-build delivery; twenty creations and twenty test sends a day.

Event types

  • changes.digest — one per endpoint per period it asked for (month, week, or build): how many rows changed in that period, by event type and by state, the period it covers, and the since cursor to fetch the rows from /changes. This is the event every integration should handle; the per-row events below are a convenience for the low-volume, high-value types. A test send carries data.test: true.
  • change.created — a new row in the change feed
  • county.updated — a county's score or status changed
  • moratorium.changed — a moratorium was enacted or lifted
  • project.status_changed — a project decision changed status

Event payload

POST https://your-endpoint.example.com/sitepath
SitePath-Signature: t=1893456000,v1=<hmac-sha256>

{
  "object":  "event",
  "id":      "evt_…",
  "type":    "change.created",
  "created": "2026-08-04T00:00:00Z",
  "data":    { … the change/county/project record … }
}

Verify the signature

Compute HMAC-SHA256(secret, "<t>.<raw-body>") and compare (constant-time) to the v1 value. Reject if t is more than 5 minutes old (replay protection). Always verify before trusting a payload.

# Python
import hmac, hashlib, time
def verify(secret, header, raw_body, tolerance=300):
    parts = dict(kv.split("=") for kv in header.split(","))
    if abs(time.time() - int(parts["t"])) > tolerance: return False
    expected = hmac.new(secret.encode(), f'{parts["t"]}.{raw_body}'.encode(), hashlib.sha256).hexdigest()
    return hmac.compare_digest(expected, parts["v1"])
Requirements & behavior: endpoints must be public HTTPS (private/loopback/metadata hosts are rejected). Return 2xx within a few seconds to acknowledge. Each event is delivered once; a delivery that fails (non-2xx, or no answer within a few seconds) is not retried, and an endpoint that fails on fifteen consecutive runs is auto-disabled until you re-enable it. Volume is bounded by design: every endpoint receives one changes.digest per period it asked for, plus per-row moratorium.changed and project.status_changed events for that period capped at two hundred (the digest says when the cap was hit); change.created and county.updated arrive only inside the digest. Endpoint and creation limits are per plan, above. Reconcile after any gap with /changes?since=…, which is the record the events are cut from. Because the dataset updates on a batch cadence, expect events in bursts after each nightly build, not in real time.

Health check

GET/healthNo auth

Returns 200 for liveness checks. No auth, no rate limit, instant response.

{ "object": "health", "status": "ok", "api_version": "2026-08-01", "ts": "2026-08-01T13:00:00.000Z" }

Counties — list

GET/countiesAPI key required

Query it like a database. Filter by state, grade, fips (comma list), moratorium=true|false (today: the county's score is capped by a solar moratorium we hold on record, verified or not; with the next county-data refresh it narrows to a moratorium in force and verified, and one on record but not verified moves to moratoriumUnverified), ordinanceStatus, minScore/maxScore, q (county name) and, for data-center keys, dcStatus. A score bound that isn't a number, or a grade or moratorium outside its values, answers 400 invalid_parameter rather than an unfiltered list. Sort with sort=rank|score|county|population|trajectory and order=asc|desc. Keep only the keys you need with fields=fips,grade,score. Every row carries links to its profile, report, permits and the county on the website, for every caller.

Returns the public summary record for every U.S. county. Pageable.

Query parameters

NameTypeDescription
statestringFilter by 2-letter state code (case-insensitive), e.g. TX.
gradestringFilter by letter grade A, B, C, D, or F, in any case. Any other value answers 400.
limitintegerPage size. Default 250, max 1000.
cursorstringOpaque cursor from the previous response's next_cursor. See Pagination.

Example

curl "https://www.sitepathintel.com/api/v1/counties?state=TX&grade=A&limit=5" \
  -H "Authorization: Bearer $SITEPATH_API_KEY"

Response

{
  "object": "list",
  "has_more": true,
  "next_cursor": "eyJvIjo1fQ",
  "total": 14,
  "data": [
    {
      "object": "county",
      "fips": "48001",
      "state": "Texas",
      "stateCode": "TX",
      "county": "Anderson",
      "score": 36.9,
      "grade": "B",
      "rank": 12,
      "population": "57735",
      "lat": 31.8133,
      "lng": -95.6526,
      "ordinanceStatus": "Y",
      "hasMoratorium": false,
      "moratoriumUnverified": false,
      "trajectory": "stable",
      "trajectoryLabel": "Stable",
      "trajectoryDelta12mo": 0,
      "dcStatus": "no-activity",
      "dcStatusLabel": "No Specific Activity"
    }
  ]
}

Lookup — which county is this coordinate in

GET/lookup?lat=&lng=API key required

Hand it a parcel centroid or a substation coordinate and get the county that contains it: the county row, links to its profile, report, permits and website pages, and the boundary's geometry URL. Point-in-polygon over the same Census TIGER boundaries the geometry endpoints serve. Open water or a point outside the US returns 404. Send each coordinate as a plain decimal number in degrees, west and south negative (33.0126, -94.3655, 3.30126e1); one that is missing, out of range or written any other way (33.0126N, 94.3655W, 33.0126°) returns 400 invalid_parameter.

curl "https://www.sitepathintel.com/api/v1/lookup?lat=33.0126&lng=-94.3655" \
  -H "Authorization: Bearer $SITEPATH_API_KEY"

Ordinances — the rules search, nationwide

GET/ordinancesAPI key required

Every ordinance row on file, as one flat list with the county's identity on each row — the same rows the county popup and report show, rebuilt nightly from the per-county payloads. Ask the question you actually have: which Texas counties have a solar setback under 300 ft and no moratorium, adopted since 2024?

  • state, fips, technology (solar, bess, data-center, wind, community-solar…)
  • jurisdictionLevel=county|state or countyOwn=true — the county's own rule versus a state framework standing in for one
  • moratorium=true|false, minSetbackPropertyFt, maxSetbackPropertyFt, adoptedSince=YYYY-MM-DD (rows with no value recorded never match a numeric or date filter — missing data is never treated as passing). A setback that isn't a number, or a moratorium or countyOwn other than true/false, answers 400 invalid_parameter.
  • q against the summary, county or jurisdiction name; sort=adopted|setback|county|state; fields; cursor paging

BESS rows travel only with the advanced-technology licence, data-center rows only with the data-centers licence, full-text and moratorium document links only with source_urls. sharedSummaryCount above 1 flags boilerplate shared with that many counties. last_updated is the index's build date.

curl "https://www.sitepathintel.com/api/v1/ordinances?state=TX&technology=solar&countyOwn=true&maxSetbackPropertyFt=300&moratorium=false&sort=adopted" \
  -H "Authorization: Bearer $SITEPATH_API_KEY"

Meetings — across counties

GET/meetingsAPI key + storage, data-center & meetings add-on

The report's meeting record across every county, newest first: date, deciding body, title, outcome, vote, summary, and the agenda, minutes and recording links (with source_urls). Filter by state, fips, since/until (YYYY-MM-DD, both inclusive), outcome (approved, denied, tabled…), body (planning, commission, council…) and q. Each row links to its county's profile and page. Hearings on data centers — including ones that also cover solar, storage or wind — appear only for keys licensed for data centers.

curl "https://www.sitepathintel.com/api/v1/meetings?state=OH&since=2026-06-01&outcome=denied" \
  -H "Authorization: Bearer $SITEPATH_API_KEY"

News — local headlines across counties

GET/newsAPI key required

The energy-siting headlines the report indexes, across every county, newest first, with publisher, sentiment and the county's identity on each row. Filter by state, fips, since (YYYY-MM-DD, inclusive), sentiment and q. Data-center-only headlines travel only with the data-centers licence; article links only with source_urls.

curl "https://www.sitepathintel.com/api/v1/news?state=VA&sentiment=negative&since=2026-08-01" \
  -H "Authorization: Bearer $SITEPATH_API_KEY"

Counties — detail

GET/counties/:fipsAPI key required

Returns the single-county record for a given 5-digit FIPS code. Same field shape as the list endpoint plus a couple of extra trajectory fields.

curl https://www.sitepathintel.com/api/v1/counties/51117 \
  -H "Authorization: Bearer $SITEPATH_API_KEY"

Counties — intelligence report

GET/counties/:fips/reportAPI key required

The depth behind the county row. One call returns everything the SitePath county report is built from:

  • ordinance — every ordinance row on file, per technology, with jurisdictionLevel (the county's own rule, or a state framework standing in for one), section, adoption and amendment dates, setbacks from property lines and dwellings, acreage and density caps, moratorium status and expiry, and a summary. sharedSummaryCount above 1 means the summary is boilerplate shared with that many counties, not a county-specific rule.
  • zoning — mechanism, setbacks, acreage, density, spacing and size restrictions.
  • opposition — score, sentiment and basis, plus each observed opposition group with first and last observed dates.
  • board, meetings, news — who decides, what they recently decided, and what local press reported.
  • coverage — how many rows each section was built from, and the county's data-confidence tier. An empty section is a statement about the county, never a silent load failure: a store that cannot answer returns 503.
  • moratoriumConflict — true when the county's status flag and the published-moratorium gate disagree. Both values are returned as stored; nothing is silently reconciled.
  • adminCorrected — the values in this response that SitePath corrected by hand, as the county report shows them: fields lists their paths (ordinanceStatus, zoning.setbacks, and a value worked out from one, such as moratoriumConflict) and updatedAt says when the corrections were saved. A correction has no source document of its own; every other value comes from the sourced dataset. null when nothing in the response is a correction.

Data-center ordinance rows, board meetings on data centers, opposition groups and headlines are included only when the key is licensed and scoped for data centers; meetings on battery storage only with the advanced-technology licence. Source URLs (ordinance full text, moratorium document, meeting minutes, articles) travel only with the source_urls licence.

curl https://www.sitepathintel.com/api/v1/counties/51117/report \
  -H "Authorization: Bearer $SITEPATH_API_KEY"

Counties — full profile (the popup, as JSON)

GET/counties/:fips/profileAPI key required

The API as a second interface to SitePath. One call returns the same payload the map popup and the county report are rendered from — nothing flattened, nothing summarised on the way out:

  • data — scores and grade, every ordinance row (ordinanceDetail, with full-text links under the source_urls licence), zoning and siting parameters, permit records (permitDetail) and permit research (permitsFull), the governing board, recent meetings with minutes links, local news, the evidence documents on file, opposition, the projects pipeline, state context and the source list. Field names are the popup's own, so what you see on the site is what you get on the wire.
  • links — deep links to every related endpoint (report, permits, geometry, changes, projects) and to the website pages that render this county (web.popup, web.report, web.county_page, web.intel_page, web.state_page). Hand a user straight from your product to ours.
  • sections and unavailable — how many rows each section was built from, and which sections' store could not answer on this call. An empty section is a statement about the county; a section named in unavailable is a retry, never an absence.
  • entitlements — what your key's licence let through, so a client can explain a missing BESS or data-center block rather than guess.
  • adminCorrected — as on /report: which data values (data.ordinanceStatus, data.keyReason, …) SitePath corrected by hand rather than took from a source, and when; null when none are.

BESS rows and scores travel only with the advanced-technology licence; data-center rows, scores and news only with the data-centers licence. Source URLs travel only with the source_urls licence.

curl https://www.sitepathintel.com/api/v1/counties/36093/profile \
  -H "Authorization: Bearer $SITEPATH_API_KEY"

Counties — data-center intelligence

GET/counties/:fips/data-centerAPI key + data-centers licence

What the store holds on data-center development in the county, sourced only: the data-center score, grade, top drivers and model version; the status with its own source document and verification date (statusSourceUrl, statusVerifiedAt), shown only when a document backs it; reported moratoria with their verification state and expiry; the county's data-center ordinance summary and rows; grid region, primary and secondary utility and IRP; state incentives and context; environmental constraints (floodplain, protected land…) with a source on record; opposition groups; the project record (projectsLimit, default 100, max 500); and the local publishers behind it.

A facility, moratorium report, opposition group or ordinance row with no source document behind it is not served; sourcing counts what was left out, so an empty list never reads as nothing on file. Text-mined signals (sentiment, trajectory, concern flags, capacity mentions) are not sources and are null. A field the store lacks is null. Nothing here is estimated. Source URLs travel with the source_urls licence.

curl https://www.sitepathintel.com/api/v1/counties/51107/data-center \
  -H "Authorization: Bearer $SITEPATH_API_KEY"

Counties — provenance

GET/counties/:fips/sourcesAPI key required

Where the county's record comes from. Every source in the ledger with its type (ordinance, statute, permit portal, agency page, dataset, project record, news, opposition, utility IRP, SEC filing…), title, verification status and last check, whether it is a generic landing page rather than the document, and whether it was auto-repaired; plus the county's lastVerified, dataAsOf, dataConfidence, the scoring model version, a by-type summary, and the verification dates each section carries. Filter with type and verified=true.

curl "https://www.sitepathintel.com/api/v1/counties/51107/sources?type=ordinance&verified=true" \
  -H "Authorization: Bearer $SITEPATH_API_KEY"

Counties — score history

GET/counties/:fips/historyAPI key required

The county's monthly composite score with the grade each point banded to, the current score, rank and national percentile, the trajectory (direction, 12-month delta, streak) and the scoring model version. A grade change becomes traceable, not just observed.

curl https://www.sitepathintel.com/api/v1/counties/51107/history \
  -H "Authorization: Bearer $SITEPATH_API_KEY"

Status — freshness, publicly

GET/statusNo key

What /health cannot say. For every dataset the API serves: when it was last rebuilt and published, a snapshot id, and whether it is inside the freshness SLA. status is degraded the moment any dataset is stale or missing. A human view lives at /status.html. Poll it before a nightly load; alert on degraded.

curl https://www.sitepathintel.com/api/v1/status

Counties — permit checklist

GET/counties/:fips/permitsAPI key required

One permit record per approval a project in this county needs, across local, state and federal levels: the filing and regulatory authority, application type, whether a hearing, board review, public comment period or pre-application meeting is required, prerequisite and parallel state permits, typical timeline, permit lifespan and fee where researched, the appeal process, and practitioner notes. The envelope carries the county's permit decision track record (permitApprovalRate, permitDecisionCount, permitConfidence) and the state permitting context.

Application forms, portals, fee schedules, instructions and agency pages travel only with the source_urls licence; a link the pipeline has found dead is listed in deadLinks rather than silently dropped. Data-center permits are included only for keys licensed and scoped for data centers. A county store or permit catalog that cannot answer returns 503 — an empty checklist is never served in place of "could not be loaded".

curl https://www.sitepathintel.com/api/v1/counties/20209/permits \
  -H "Authorization: Bearer $SITEPATH_API_KEY"

Counties — geometry (GeoJSON)

GET/counties/:fips/geometryAPI key required

Returns a single county boundary as a GeoJSON Feature (Polygon or MultiPolygon), sourced from US Census TIGER (public domain). properties.fips is the 5-digit join key. Drops straight into Leaflet, Mapbox, ArcGIS, or any GeoJSON-aware tool.

curl https://www.sitepathintel.com/api/v1/counties/51117/geometry \
  -H "Authorization: Bearer $SITEPATH_API_KEY"
GET/counties/geometryAPI key required

Paged list of county boundaries (one GeoJSON Feature per record in data), for loading a nationwide layer. Join to /counties on fips to colour a choropleth by score, grade, trajectory, or moratorium status.

Query parameters

NameTypeDescription
fipsstringComma-separated FIPS codes to return (e.g. 01001,01003). Omit to page all every county.
limitintegerPage size. Default 250, max 1000.
cursorstringOpaque cursor from the previous page's nextCursor.

Changes feed

GET/changesAPI key required

The live change feed — every ordinance update, moratorium event, board action, and score adjustment SitePath has detected. Each entry carries its type, severity, location, a title and summary, its source signal, and two dates: date, the day the event itself happened, and added, the day SitePath added it to the feed.

Query parameters

NameTypeDescription
statestringFilter by 2-letter state code.
sincestringOnly return entries SitePath added to the feed on or after this date: added >= since (an entry without added is compared on its date). Format YYYY-MM-DD; a value that doesn't start with a real date answers 400 invalid_parameter. Inclusive, so re-querying from the last added you saw returns that day again; de-duplicate by id. This is the cursor a webhook digest hands you in fetch.since.
limitintegerPage size. Default 100, max 1000.

BESS dataset

GET/bessAPI key required

Full Energy Storage Intel dataset — every county where SitePath has identified battery-storage (BESS) regulatory activity. Includes per-county ordinance flags (fire-marshal review, NFPA 855, decommissioning bonds, setbacks), status, sentiment, trajectory, and the project pipeline aggregates.

Data Centers dataset

GET/data-centersAPI key required

Full Data Center Intel dataset — county-level moratoria, bans, restrictions, emerging markets, water / power / opposition flags, top developers, and news-source attribution.

Project pipeline

GET/projectsAPI key required

Every solar, BESS, and data-center project decision SitePath has indexed, nationwide. Every record is sourced: each one resolves to a verified primary source via one of four explicit verification levels, returned as verificationLevel. The API returns the source signal — publisher, reliability, verified flag, and last-checked date — but not the underlying source URL. The generated field in the response tells you when the underlying index was last built.

Verification levels

LevelWhat it means
per-projectSitePath holds a direct primary-source document for this exact filing / docket / record.
categoryBacked by a verified state-level or category-level source (e.g. a state PUC docket index, EIA Form 860) — verified, but not a per-row document.
narrativeA research note in the summary field documenting how SitePath knows about this record. Used for BESS / data-center projects sourced from news coverage or industry filings without a per-project URL.
noneReserved for any record that loses its source attribution.

Query parameters

NameTypeDescription
statestringFilter by 2-letter state code.
statusstringapproved, denied, under-construction, announced, operating.
technologystringsolar, bess, or data-center.
verificationLevelstringper-project, category, narrative, or none. Filter to a specific evidence tier.
minMwnumberCapacity floor. Records without a recorded capacity are excluded — they aren't inferred as zero. A value that isn't a number answers 400.
sourced1 / trueOnly return records that carry source attribution; records with no source on file are excluded. Leave it out for every record; any other value answers 400.
limitintegerDefault 100, max 500.
cursorstringOpaque cursor from the previous response's next_cursor. See Pagination.

Example

curl "https://www.sitepathintel.com/api/v1/projects?state=TX&status=denied&minMw=50&verificationLevel=per-project" \
  -H "Authorization: Bearer $SITEPATH_API_KEY"

Field shape (one project, inside data[])

{
  "object":            "project",
  "projectId":         "48201-...",
  "name":              "…",
  "developer":         "",            // empty = not recorded (NEVER inferred)
  "technology":        "solar",       // solar | bess | data-center
  "capacityMw":        "260",         // string; empty = not recorded
  "capacityMwh":       "",            // BESS only
  "acres":             "",
  "status":            "approved",
  "filedDate":         "",
  "decisionDate":      "2024-12-02",
  "lastStatusCheck":   "2026-04-15",  // solar only; when the status was last reverified
  "lastStatusChange":  "2024-12-02",  // solar only; when the status last changed
  "docketNumber":      "",
  "fips":              "48201",
  "stateCode":         "TX",
  "state":             "Texas",
  "county":            "Harris",
  "summary":           "",            // narrative source text (BESS / DC) when applicable
  "verificationLevel": "category",    // see table above
  "sourced":           true,          // is this record backed by a verified source?
  "source": {                         // source SIGNAL only — the URL is intentionally not exposed
    "publisher":   "EIA / state PUC",
    "reliability": 0.5,               // 0–1 confidence in the source
    "tier":        "category",
    "verified":    true,
    "lastChecked": "2026-08-01 10:57:00+00:00"
  }
}

Data integrity

The API serves the same datasets the rest of SitePath uses — there's no separate "API copy" that could drift. Every field traces back to a primary government document; if a value can't be verified, it isn't published.

The county dataset (data-public.js) is regenerated on the same cadence as the website. BESS and Data Center datasets refresh on the same automated sync cadence. The changes feed reflects whatever the last sync produced.

If you spot incorrect data, email support@sitepathintel.com with the FIPS code and the field — we verify and correct within 48 hours, and the fix shows up in the next API response.

Service levels & deprecation

Freshness. Datasets are rebuilt nightly. A dataset older than two days is reported stale on /status and alarms internally; it is never silently served as current. Every list response carries last_updated; every bulk response carries its snapshot identity.

Availability. The API is served from a global edge with the read models in a managed database. Uptime is probed externally on /health, and freshness on the data itself. We do not yet publish a contractual uptime percentage; enterprise agreements state one.

Versioning and deprecation. The wire format is dated (X-API-Version). Changes are additive: new endpoints, new fields, new filters. A field or endpoint is removed only after a dated deprecation notice in this changelog and in the Deprecation and Sunset headers on the affected responses, with at least 90 days between notice and removal. Scoring models are versioned (scoringModelVersion); historical scores stay reproducible.

Limits. Per-minute burst limits and a monthly ceiling per tier, both surfaced in response headers (RateLimit-*, X-RateLimit-Reset) and on /usage. Bulk delivery exists so a warehouse load never has to page.

Support and corrections. Report a wrong value with the FIPS and field to support@sitepathintel.com; corrections land in the next nightly and appear in the change feed.

Security

  • Transport. TLS only; HSTS; plain-HTTP requests are upgraded.
  • Keys. API keys are shown once and stored hashed; a leaked key is revoked from the account page or, for organizations, by an org owner, and stops working immediately. Individual keys can be narrowed to dataset scopes and a monthly ceiling. Organization keys carry scopes, add-ons, per-org ceilings and a sandbox mode with truncated data.
  • Browser embeds. Publishable tokens (sp_pub_…) serve the map layer only, never the Bearer API and never source URLs, and only to pages on the origins registered for the organization (the browser's Origin, the Referer, or the parent-page origin the SDK reports from an embed). A request with no origin at all is refused. This is a browser-grade control: it stops a copied token being used from another site, and what the token can reach is bounded on the server so a scripted misuse is rate-limited, counted against the organization's ceiling, and cannot reach licensed material. Bearer keys are refused CORS by design so a key cannot be built into a browser app by accident.
  • Key store. Individual API keys are authenticated from a server-owned table that only the service role can read or write; scopes, ceilings, expiry and usage are read from that table and the usage ledger. The account record confirms the account still holds an API tier, and a key the table has not yet recorded (minted before 2026-09-28 and unused since) is accepted from the account record once and written into the table on first use. A key whose entitlement the account record has not confirmed within three days is refused until it can be.
  • Audit trail. Every key mint, limit change, revoke, and every webhook create, secret reveal, rotate and delete writes an append-only audit row (time, address, what changed) before the response is returned; the table refuses updates and deletes even to the service role, and the owner can read their own entries.
  • Revocation. A revoked key is recorded server-side and refused on every lookup within a minute, independent of the account store; the map layer's edge cache expires within ten minutes of a token being revoked.
  • Abuse controls. Failed authentication is throttled per address before any key lookup; per-key burst limits and monthly ceilings apply to Bearer and layer traffic alike.
  • Egress control. Source URLs leave the API only under the source_urls licence; a value-based backstop sweeps every data response so a projection cannot leak one by omission. Internal fields never leave the store.
  • Data at rest. Read models live in a managed Postgres with row-level security; the canonical dataset is append-only with full version history.
  • Webhooks. Each endpoint has its own signing secret; deliveries are signed so a consumer can verify them.
  • What we do not yet claim. No third-party certification (SOC 2 or similar) is held today. Enterprise agreements can include a security questionnaire and a penetration-test summary on request.

Bulk delivery & snapshots

Organization keys with the bulk scope pull whole datasets through GET /bulk/:dataset (counties, changes, projects, bess, data-centers, county-geometry), in fixed parts. Add ?format=ndjson for one record per line (application/x-ndjson), the shape BigQuery, Snowflake and S3 loaders ingest without a transform; each line carries _bulk (dataset, part, version, hash) and _license_ref.

GET /bulk/:dataset/manifest returns the snapshot identity — build date, content_sha256, byte size, publish time, part size and every part URL — and every bulk response repeats it in X-Dataset-Version, X-Dataset-Sha256 and X-Dataset-Published. Pin a version, skip an unchanged snapshot by hash, and record which build each row came from.

curl https://www.sitepathintel.com/api/v1/bulk/counties/manifest -H "Authorization: Bearer $ORG_KEY"
curl "https://www.sitepathintel.com/api/v1/bulk/counties?part=0&format=ndjson" -H "Authorization: Bearer $ORG_KEY" \
  | gsutil cp - gs://your-bucket/sitepath/counties/$(date +%F)/part-0.ndjson

Changelog

2026-10-07 — county endpoints show SitePath's hand corrections, and name them

Change. When SitePath corrects a county by hand — its ordinance status, setbacks, board or the reason behind its score — the county report shows the correction, but /counties/:fips/report, /counties/:fips/profile and the map layer's county report went on returning the value it replaced. They now return the corrected value, the same one the report shows, and a new field, adminCorrected, lists the path of every corrected value in the response and when the corrections were saved, so you can tell a correction, which has no source document of its own, from a sourced value. /permits, /sources, /history and /data-center carry the field too; nothing they return can be corrected by hand today, so it is null there. /counties and /counties/:fips return the published dataset and carry no corrections, so for a corrected county their ordinanceStatus can differ from the report's. Licences and source-URL rules apply to corrected values exactly as to any other. A corrected sub-score (such as complianceScore) does not recompute score or grade, which stay as the dataset computed them. The map layer's county report is cached for up to about an hour, so a new or removed correction can take that long to appear there.

2026-10-04 — SitePathLayer.addTo() tells you when it retries

Addition. SitePathLayer.addTo(map, { onRetry }) is called before each automatic retry with { status, code, requestId, retryAfter, attempt, delayMs } (attempt 1 is the first retry); return false to stop retrying, and addTo() rejects with that refusal at once. The return value is read synchronously, so a promise cannot cancel. A county report that will not load now offers Try again in its panel, held until any short wait the server named has passed.

2026-10-02 — the map layer's 500s are readable from your page too

Fix. An unexpected 500 internal_error on a map-layer request from a registered origin now carries the same CORS headers as the layer's 429s and 503s, so browser code can read the request_id it asks you to send support; a failure before the origin is matched stays CORS-closed. The Map SDK shows it in plain words with the request id and a Try again button.

2026-10-02 — your page can read the map layer's 429s and 503s

Fix. On a registered origin, map-layer 429 and 503 responses now carry CORS and expose Request-Id and Retry-After, so browser code sees the refusal instead of a network error; origin refusals (403) stay CORS-closed. Layer 200s and 404s expose Request-Id too (on an edge-cached answer, the id of the request that filled the cache). The Map SDK shows a refusal with its request id and retries by itself only when the refusal names a Retry-After of two minutes or less (today, the burst limit's 429); for a refused load, on('error', (message, detail)) passes { status, code, requestId, retryAfter, retrying }. SitePathLayer.addTo() rejects with an Error carrying status, code, requestId and retryAfter, and SitePathLayer.fetchReport() rejects on a refusal instead of resolving null (still null for an unknown county). The OpenAPI layer token parameter now admits publishable tokens only, as the layer has enforced since 2026-09-28.

2026-10-02 — a filter that can't be read is a 400

Behaviour change. minScore and maxScore on /counties, minSetbackPropertyFt and maxSetbackPropertyFt on /ordinances and minMw on /projects now answer 400 invalid_parameter when the value isn't a plain decimal number. They used to answer 200 with a wrong result and no signal: abc skipped the filter and returned the whole list as if every row matched, and values such as 1,000, 50ft or abc5 filtered at a number other than the one written (on the score and setback filters 1e3 read as 13). 50, -3, 12.5, .5, 5., 1e3 and 1e-3 are all accepted, at their true value; send a leading + as %2B, since an unencoded + in a query string means a space. The fixed-choice filters do the same: grade other than A, B, C, D or F, moratorium or countyOwn other than true/false, and sourced other than 1/true answer 400. moratorium=yes used to return every row, and grade=Z an empty list. true and false now match in any case, so the Python client's moratorium=True, countyOwn=True and sourced=True, which were ignored, filter as written; its sourced=False is now a 400, so leave sourced out for every project. An empty value still means no filter.

2026-10-02 — a /lookup coordinate that isn't a plain number is a 400

Behaviour change. lat and lng (or lon) on /lookup now answer 400 invalid_parameter unless the value is a plain decimal number: 33.0126, -94.3655, .5 and 3.30126e1 are read as written. They used to be read by dropping every character that wasn't a digit, a point or a minus sign, so a value in another form got an answer for a point you never sent: 33.0126abc, 33.0126N and 3,3.0126 all became 33.0126; 94.3655W became +94.3655, the wrong hemisphere; and 1e1 became 11, not 10. A value with a space around it is refused too, so trim before you send; an unencoded + in a query string can arrive as a space, so send it as %2B or leave it off. The message now names the parameter that is wrong, lat first when both are. A request with plain numbers in range gets the same answer as before, except a number written with an exponent, which is now read as written: 3.30126e1 is 33.0126 (Cass County, Texas), where it used to be read as 3.301261.

2026-10-02 — request ids on every error, map layer included

Fix. Map-layer refusals (403, 429, 503) now carry a Request-Id header and the same id as request_id, the layer's monthly_ceiling 429 sends Retry-After, and an unexpected failure answers a typed 500 internal_error (api_error) with its request id instead of an untyped body.

2026-10-02 — board meetings follow the technology licences

Change. A board meeting is now licensed by the technologies it concerns, as ordinance rows always were. On /meetings, /counties/:fips/report and /counties/:fips/profile, a meeting tagged with data centers needs the data-centers licence even when it also covers solar, storage or wind — before, such a mixed meeting reached every key because its text mentioned solar — and a meeting on battery storage needs the advanced-technology licence. The old rule still applies on top: a meeting whose text concerns data centers alone needs the data-centers licence whatever its tags. No key receives a meeting it did not receive before.

2026-10-02 — every 4xx carries the error.type its status documents

Contract change to error.type; status, code and message are unchanged. These 4xx codes were typed api_error, the server-fault class, and now carry their documented class: scoped_key_required → authentication_error; addon_required, scope_required, org_suspended, org_key_required, sandbox_not_allowed, publishable_key_layer_only, publishable_token_required, layer_forbidden, origin_required, origin_not_allowed → permission_error; geometry_not_found → invalid_request_error; monthly_ceiling → rate_limit_error. A client that retried on api_error was retrying these refusals and now stops; one that branches on type for them should be checked.

2026-10-02 — a date filter that isn't a date is a 400

Behaviour change. since on /changes, /meetings and /news, until on /meetings and adoptedSince on /ordinances now answer 400 invalid_parameter when the value doesn't start with a real date as YYYY-MM-DD. They used to answer 200 with a wrong result and no signal: on since and adoptedSince, 20261001, 2026-9-1, 2026/10/01 or last week matched nothing, which reads as a quiet period, and 0 or a value with a leading space matched everything; on until it was the other way round. A day no calendar has, such as 2026-02-30, is refused too. Anything after the date is ignored, as before, so a full timestamp (2026-10-01T12:00:00Z) still filters on its day; an empty value still means no filter; and the cursor a webhook digest hands you is already in this form. invalid_parameter is now typed invalid_request_error, including on /lookup, where it was typed api_error.

2026-10-01 — /changes?since= returns entries again

Fix. since on /changes answered every date with an empty list: it compared against a field the feed does not carry. It now returns the entries SitePath added to the feed on or after the date, read from each entry's new added field (or its date where it has none), which is the cursor a changes.digest webhook hands you in fetch.since. Every entry now carries added; nothing was removed. An integration that synced with since received nothing, so re-pull once from the date it started.

2026-09-28 — integrations and the server-owned key store

Additive. An ArcGIS-compatible feature service so ArcGIS Pro, ArcGIS Online, ArcGIS Enterprise and QGIS add SitePath as a native layer. Webhooks leave preview: endpoints live server-side, events go out after every nightly, delivery is idempotent per run. A sync kit for BigQuery, Snowflake and hosted ArcGIS layers with snapshot pinning. Individual API keys now authenticate from a server-owned store, with an audit trail readable from the account. The audit action on the account keys endpoint returns it.

2026-09-28 — the enterprise layer

Additive. GET /status publishes dataset freshness against the SLA, no key needed (human view at /status.html). GET /counties/:fips/data-center is the data-center intelligence object; /sources the provenance ledger; /history the score history. Bulk pulls gain ?format=ndjson, a snapshot manifest and X-Dataset-* identity headers. New reference sections: service levels & deprecation and security.

2026-09-28 — the Map SDK

sitepath-map.js turns the data layer into the SitePath map itself, embeddable: layer toggles, filters that grey out what they exclude, the fixed SitePath grade ramp with transparency and border controls, a live legend, search by name, FIPS or coordinate, the county report behind every popup (ordinance, permitting, zoning, opposition, news, board, meetings), GeoJSON/CSV export of what is visible, and share links that restore the whole state. New layer resource points.geojson; counties.geojson carries the fields the SDK filters by. /embed.html?token=… for iframes.

2026-09-28 — the discovery layer

Additive. The API now answers the questions a developer starts with, not only "tell me about FIPS X". GET /counties filters by moratorium, ordinance status, score range and name, sorts, and projects fields. GET /lookup turns a coordinate into its county. GET /ordinances searches every ordinance row nationwide by technology, whose rule it is, setbacks, moratorium and adoption date. GET /meetings and GET /news are the report's meetings and headlines across counties. Every county row and every feed row now carries links to its profile, report, permits and website pages, for every caller. Three new key scopes: ordinances, meetings, news. The wire version is unchanged.

2026-09-26 — the popup as JSON

Additive. GET /counties/:fips/profile returns the full county payload the map popup and county report are rendered from — ordinance rows with full-text links, zoning, permits and permit research, board, meetings with minutes, news, evidence documents, opposition, projects, state context and sources — plus links to every related endpoint and website page, sections counts and an unavailable list. Sits under the counties key scope; the wire version is unchanged.

2026-09-24 — ordinance and permit depth

Two additive endpoints. GET /counties/:fips/report returns the full county intelligence report (per-technology ordinance rows with jurisdiction, siting parameters, opposition, board, meetings, news, coverage counts and the moratorium-conflict flag). GET /counties/:fips/permits returns the county's permit checklist and decision track record. Both sit under the counties key scope; the wire version is unchanged. The "Official libraries" note now says how the clients are actually delivered.

2026-08-01 — developer-experience upgrade

The wire API is now dated (X-API-Version: 2026-08-01). New: a Request-Id on every response (and in every error); typed error objects (error.type / code / doc_url + request_id); a consistent list envelope (object, data, has_more, opaque next_cursor, total) with an object type on every record; X-RateLimit-Reset and IETF RateLimit headers; and a public OpenAPI 3.1 spec. List responses moved from { counties: […] }/{ projects: […] } to { data: […] }, and cursors are now opaque tokens.

v1.2 — source signal, not source links

API responses now expose the source signal — publisher, reliability, verified, lastChecked — in place of raw source URLs, across /projects, /bess, /data-centers, and /changes. Each project also carries a sourced boolean. No data fields were removed. This keeps every record independently gradable for trust while the curated source-link corpus stays out of bulk export.

v1.1 — project pipeline enrichment

The /projects endpoint now returns BESS and data-center decisions in addition to solar. New fields: verificationLevel, summary (narrative source text), lastStatusCheck, lastStatusChange, capacityMwh (BESS). New query parameter: verificationLevel for filtering by evidence tier.

v1.0 — initial release

Endpoints: /health, /counties, /counties/:fips, /counties/geometry, /changes, /bess, /data-centers, /projects. Bearer-token auth via sp_live_* keys. Per-key rate limits.