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"
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:
| Plan | Burst | Monthly ceiling |
|---|---|---|
| Standard | 120 requests / minute | 100,000 requests / month |
| Enterprise | 300 requests / minute | 200,000 requests / month |
Every 2xx response reports where you stand, so you can self-throttle instead of hitting errors:
| Header | Meaning |
|---|---|
X-RateLimit-Limit / -Remaining / -Window / -Reset | Burst budget, window length (seconds), and the epoch-seconds reset time. |
RateLimit / RateLimit-Policy | The same budget in the IETF standard format. |
X-Monthly-Limit / -Remaining / -Reset | Monthly ceiling, approximate remaining (≤60s stale), and the reset period. |
ETag | Content 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.
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 (404/405), rate_limit_error (429), api_error (5xx). Codes you might see:
| Status | Code | Meaning |
|---|---|---|
| 401 | missing_key | No Authorization header. |
| 401 | invalid_key | Key didn't match. Could be revoked or typo'd. |
| 403 | plan_required | Plan doesn't include API access. |
| 404 | not_found / county_not_found | Unknown endpoint or FIPS. |
| 405 | method_not_allowed | Only GET is supported. |
| 429 | rate_limited | Slow down. See Retry-After. |
| 429 | monthly_quota_exceeded | Monthly ceiling reached. Resets on the 1st (UTC), or contact support to raise it. |
| 503 | data_unavailable | The 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 with auto-pagination, retries, ETag caching, and typed errors:
pip install sitepath # Python 3.8+
npm install @sitepath/api # Node 18+
Webhooks Preview
Instead of polling /changes, register an endpoint and SitePath will POST signed events to it as the dataset updates. Manage endpoints from your account; each gets its own signing secret (shown once, revealable later).
Event types
change.created— a new row in the change feedcounty.updated— a county's score or status changedmoratorium.changed— a moratorium was enacted or liftedproject.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"])
2xx within a few seconds to acknowledge. An endpoint that fails repeatedly is auto-disabled; retries happen on the next sync. Because the dataset updates on a batch cadence, expect events in bursts, not real-time. This feature is in preview — ask support to enable it for your account.Health check
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
Returns the public summary record for every U.S. county. Pageable.
Query parameters
| Name | Type | Description |
|---|---|---|
state | string | Filter by 2-letter state code (case-insensitive), e.g. TX. |
grade | string | Filter by letter grade A, B, C, D, or F. |
limit | integer | Page size. Default 250, max 1000. |
cursor | string | Opaque 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,
"trajectory": "stable",
"trajectoryLabel": "Stable",
"trajectoryDelta12mo": 0,
"dcStatus": "no-activity",
"dcStatusLabel": "No Specific Activity"
}
]
}
Counties — detail
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"
Changes feed
The live change feed — every ordinance update, moratorium event, board action, and score adjustment SitePath has detected. Each entry includes the field name, old value, new value, and run date.
Query parameters
| Name | Type | Description |
|---|---|---|
state | string | Filter by 2-letter state code. |
since | string | Only return entries with runDate >= since. Format YYYY-MM-DD. |
limit | integer | Page size. Default 100, max 1000. |
BESS dataset
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
Full Data Center Intel dataset — county-level moratoria, bans, restrictions, emerging markets, water / power / opposition flags, top developers, and news-source attribution.
Project pipeline
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
| Level | What it means |
|---|---|
per-project | SitePath holds a direct primary-source document for this exact filing / docket / record. |
category | Backed 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. |
narrative | A 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. |
none | Reserved for any record that loses its source attribution (currently 0 records). |
Query parameters
| Name | Type | Description |
|---|---|---|
state | string | Filter by 2-letter state code. |
status | string | approved, denied, under-construction, announced, operating. |
technology | string | solar, bess, or data-center. |
verificationLevel | string | per-project, category, narrative, or none. Filter to a specific evidence tier. |
minMw | number | Capacity floor. Records without a recorded capacity are excluded — they aren't inferred as zero. |
sourced | 1 / true | Only return records with verified source attribution (today: all records, since 100% are sourced). |
limit | integer | Default 100, max 500. |
cursor | string | Opaque 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.
Changelog
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, /changes, /bess, /data-centers, /projects. Bearer-token auth via sp_live_* keys. Per-key rate limits.