v1 Enterprise

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:

PlanBurstMonthly ceiling
Standard120 requests / minute100,000 requests / month
Enterprise300 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.

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 (404/405), rate_limit_error (429), api_error (5xx). Codes you might see:

StatusCodeMeaning
401missing_keyNo Authorization header.
401invalid_keyKey didn't match. Could be revoked or typo'd.
403plan_requiredPlan doesn't include API access.
404not_found / county_not_foundUnknown endpoint or FIPS.
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.
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 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 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. 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 previewask support to enable it for your account.

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

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.
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,
      "trajectory": "stable",
      "trajectoryLabel": "Stable",
      "trajectoryDelta12mo": 0,
      "dcStatus": "no-activity",
      "dcStatusLabel": "No Specific Activity"
    }
  ]
}

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"

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 includes the field name, old value, new value, and run date.

Query parameters

NameTypeDescription
statestringFilter by 2-letter state code.
sincestringOnly return entries with runDate >= since. Format YYYY-MM-DD.
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 (currently 0 records).

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.
sourced1 / trueOnly return records with verified source attribution (today: all records, since 100% are sourced).
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.

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 signalpublisher, 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.