v1 Enterprise

API Integration Guide

A start-to-finish path for engineering teams wiring SitePath into their own due-diligence, GIS, or BI systems. Follow it top to bottom the first time; keep the full API reference open for exhaustive field and parameter lists.

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

Overview

The SitePath API is a server-to-server REST API. It returns the same nationwide dataset that powers the SitePath platform — county siting scores and grades, ordinance and moratorium status, the solar / BESS / data-center project pipeline, and a live change feed — as clean JSON your systems can ingest on a schedule.

It's built for a few well-understood integration patterns:

  • Due-diligence enrichment — look up a county or FIPS at deal time and pull scores, ordinance status, and nearby projects into your underwriting tool.
  • Warehouse / BI sync — a nightly job that mirrors counties, projects, and the change feed into your data warehouse or BI layer.
  • GIS overlay — join county scores and project points (lat/lng included) onto your own map layers.
What it is not: a browser/client-side API. CORS is intentionally closed, so calls must originate from your backend — never ship an API key to a browser or mobile app, where it would be exposed.

1Get access

API access is provisioned by SitePath to enterprise customers. If your organization has API access, the account owner can mint keys:

  1. Sign in and open Account.
  2. Find the API keys card, click Create key, and give it a name (e.g. warehouse-sync-prod).
  3. Copy the key — it starts with sp_live_ and is shown once. Store it in your secret manager immediately.
Key hygiene: one key per system/environment (so you can revoke a single integration without breaking others). Rotate on staff changes. If a key leaks, revoke it from the same card — revocation takes effect within ~60 seconds. Only the account owner can create keys; team members ask the owner.

Don't have Enterprise yet? Request a demo to talk through API access.

2Authenticate

Every request (except /health) carries your key in the Authorization header as a bearer token. The key is never accepted in the URL.

Authorization: Bearer sp_live_XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX

Store the key in an environment variable or secret manager, never in source control:

export SITEPATH_API_KEY="sp_live_XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX"

3First call

Confirm connectivity with the unauthenticated health check, then make your first real call:

# Health — no key needed, always 200
curl -s https://www.sitepathintel.com/api/v1/health

# First authenticated call — one county by FIPS
curl -s "https://www.sitepathintel.com/api/v1/counties/48201" \
  -H "Authorization: Bearer $SITEPATH_API_KEY"
A 200 with a JSON county object means you're wired up. A 401 means the key is missing/invalid; a 403 means the key's plan doesn't include API access.

4Pull the data

Seven read-only endpoints. See the full reference for every field and filter.

EndpointReturns
GET /healthLiveness check (no auth).
GET /countiesCounty list — scores, grades, ordinance/moratorium status, trajectory, DC status. Filter ?state=, ?grade=.
GET /counties/:fipsOne county in full detail.
GET /changesRecent change feed. Filter ?state=, ?since=YYYY-MM-DD.
GET /bessBattery-storage dataset — rollups + project points.
GET /data-centersData-center dataset — rollups + project points.
GET /projectsSolar/BESS/DC project pipeline. Filter ?state=, ?status=, ?technology=, ?minMw=, ?verificationLevel=.
Prefer to generate a client? Grab the machine-readable OpenAPI 3.1 spec at GET /api/v1/openapi.json (no key required) and import it into Postman/Insomnia or run openapi-generator.

5Paging

List endpoints return a consistent envelope: records in data, plus has_more and an opaque next_cursor. Pass limit and the previous next_cursor as ?cursor=. Loop until has_more is false. Treat the cursor as opaque — don't build or parse it.

cursor=null
while true:
  GET /projects?state=TX&limit=500&cursor=$cursor
  → process response.data
  → if not response.has_more: stop
  → else: cursor = response.next_cursor

Defaults and caps: /counties limit default 250 (max 1000); /projects default 100 (max 500); /changes default 100 (max 1000).

6Rate limits & quota

Two independent limits protect the service. Every 2xx response tells you exactly where you stand — read these headers and self-throttle rather than discovering limits through errors:

HeaderMeaning
X-RateLimit-LimitRequests allowed per rolling window (typically 300).
X-RateLimit-RemainingRequests left in the current window.
X-RateLimit-WindowWindow length in seconds (60).
X-Monthly-LimitMonthly request ceiling (typically 200,000).
X-Monthly-RemainingApproximate requests left this month (a hint, ≤60s stale).
X-Monthly-ResetPeriod that resets (e.g. 2026-08); the counter rolls over at month start (UTC).

If you exceed a limit you get 429 with a Retry-After header (seconds). Honor it — sleep that long, then retry. The monthly ceiling is a cost tripwire, not per-call billing; realistic sync workloads use a few thousand calls a month, far under it. Need more headroom for a large backfill? Ask us to raise it.

7Cache with ETag

The datasets refresh on a sync cadence (roughly daily), not on every request. So don't re-download unchanged data. Every 200 includes an ETag. Store it, and send it back as If-None-Match on your next pull:

# First pull — save the ETag from the response headers
curl -sD - "https://www.sitepathintel.com/api/v1/counties" \
  -H "Authorization: Bearer $SITEPATH_API_KEY" -o counties.json
# → ETag: "3f9c...a1"

# Next pull — send it back
curl -s "https://www.sitepathintel.com/api/v1/counties" \
  -H "Authorization: Bearer $SITEPATH_API_KEY" \
  -H 'If-None-Match: "3f9c...a1"' -o /dev/null -w "%{http_code}\n"
# → 304 Not Modified (empty body) when nothing changed — reuse your cached copy
A 304 still counts as one request but transfers no payload — it keeps both sides fast and cheap. Poll daily with If-None-Match and only reprocess when you get a 200.

8Understand the source policy

Every SitePath record is backed by a verified primary source. The API exposes the source signal — who published it, a reliability score, a verified flag, and when it was last checked — but not the raw source URL. So a record looks like:

"source": {
  "publisher":   "U.S. Energy Information Administration",
  "reliability": 0.5,
  "tier":        "category",
  "verified":    true,
  "lastChecked": "2026-08-01 10:57:00+00:00"
}

For due diligence this lets you filter and weight records by how well-sourced they are (e.g. only ingest verified: true with reliability >= 0.6), and verificationLevel on projects tells you the evidence tier. The underlying deep links stay with SitePath; if you need the source document for a specific record, contact support and we'll retrieve it.

9Errors & retries

Errors are typed JSON: { "error": { "type", "code", "message", "doc_url" }, "request_id" }. Branch on error.type or error.code, not the message text. Log the request_id (also returned as the Request-Id header) — quote it to support for a specific failed call.

StatuscodeDo
401missing_key / invalid_keyFix the Authorization header. Don't retry blindly.
403plan_requiredKey's plan lacks API access. Contact your account owner.
429rate_limitedSleep Retry-After seconds, then retry.
429monthly_quota_exceededMonthly ceiling hit. Back off until next month or request a higher limit.
404county_not_found / not_foundBad FIPS or unknown path. Fix the request.
503data_unavailableTransient — retry with exponential backoff.

Recommended policy: retry 429 (after Retry-After) and 503 (exponential backoff, a few attempts); never auto-retry 4xx auth errors.

10Sample clients

Python

import os, time, requests

BASE = "https://www.sitepathintel.com/api/v1"
KEY  = os.environ["SITEPATH_API_KEY"]
S    = requests.Session()
S.headers["Authorization"] = f"Bearer {KEY}"

def get(path, params=None, etag=None):
    headers = {"If-None-Match": etag} if etag else {}
    for attempt in range(5):
        r = S.get(f"{BASE}{path}", params=params, headers=headers, timeout=30)
        if r.status_code == 304:
            return None, etag                      # unchanged — reuse your cache
        if r.status_code == 429:
            time.sleep(int(r.headers.get("Retry-After", 60)))
            continue
        if r.status_code == 503:
            time.sleep(2 ** attempt)
            continue
        r.raise_for_status()
        return r.json(), r.headers.get("ETag")
    raise RuntimeError(f"gave up on {path}")

def all_projects(state):
    cursor, out = 0, []
    while True:
        data, _ = get("/projects", {"state": state, "limit": 500, "cursor": cursor})
        out += data["data"]
        if not data["has_more"]:
            return out
        cursor = data["next_cursor"]

if __name__ == "__main__":
    print("TX projects:", len(all_projects("TX")))

Node.js

const BASE = "https://www.sitepathintel.com/api/v1";
const KEY  = process.env.SITEPATH_API_KEY;
const sleep = ms => new Promise(r => setTimeout(r, ms));

async function get(path, params = {}, etag = null) {
  const url = new URL(BASE + path);
  Object.entries(params).forEach(([k, v]) => url.searchParams.set(k, v));
  const headers = { Authorization: `Bearer ${KEY}` };
  if (etag) headers["If-None-Match"] = etag;
  for (let attempt = 0; attempt < 5; attempt++) {
    const r = await fetch(url, { headers });
    if (r.status === 304) return { data: null, etag };            // unchanged
    if (r.status === 429) { await sleep(1000 * (+r.headers.get("Retry-After") || 60)); continue; }
    if (r.status === 503) { await sleep(2 ** attempt * 1000); continue; }
    if (!r.ok) { const e = await r.json(); throw new Error(`${r.status} ${e.error?.code} (request ${e.request_id})`); }
    return { data: await r.json(), etag: r.headers.get("ETag") };
  }
  throw new Error(`gave up on ${path}`);
}

async function allProjects(state) {
  let cursor = 0; const out = [];
  for (;;) {
    const { data } = await get("/projects", { state, limit: 500, cursor });
    out.push(...data.data);
    if (!data.has_more) return out;
    cursor = data.next_cursor;
  }
}

allProjects("TX").then(p => console.log("TX projects:", p.length));

Production checklist

  • ☐ Key stored in a secret manager, injected as SITEPATH_API_KEY — never in code or the browser.
  • ☐ Separate keys per environment (dev / staging / prod).
  • ☐ ETag / If-None-Match caching on the large endpoints (/counties, /projects, /bess, /data-centers).
  • ☐ Backoff on 429 (honor Retry-After) and 503 (exponential).
  • ☐ Sync on a sensible cadence (daily is plenty — the data refreshes ~daily).
  • ☐ Branch on the code field, not message text.
  • ☐ Alert if X-Monthly-Remaining trends toward zero.

License & support

API use is governed by the SitePath API Terms of Use, which supplement the Terms of Service. In short: the data is licensed for your organization's internal use — no redistribution, resale, or rebuilding of the dataset or a derivative of it, and the data is informational only and must be independently verified before you rely on it. Every data response carries a Link: <…/api-terms>; rel="license" header as a reminder.

Questions, a broken value, or need a higher monthly limit for a backfill? Email support@sitepathintel.com with your key prefix (the first 8 characters shown on your Account page) — never the full key — and we'll help. We correct verified data issues within 48 hours, and the fix shows up in the next API response.