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.
1Get access
API access is provisioned by SitePath to enterprise customers. If your organization has API access, the account owner can mint keys:
- Sign in and open Account.
- Find the API keys card, click Create key, and give it a name (e.g.
warehouse-sync-prod). - Copy the key — it starts with
sp_live_and is shown once. Store it in your secret manager immediately.
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"
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.
| Endpoint | Returns |
|---|---|
GET /health | Liveness check (no auth). |
GET /counties | County list — scores, grades, ordinance/moratorium status, trajectory, DC status. Filter ?state=, ?grade=. |
GET /counties/:fips | One county in full detail. |
GET /changes | Recent change feed. Filter ?state=, ?since=YYYY-MM-DD. |
GET /bess | Battery-storage dataset — rollups + project points. |
GET /data-centers | Data-center dataset — rollups + project points. |
GET /projects | Solar/BESS/DC project pipeline. Filter ?state=, ?status=, ?technology=, ?minMw=, ?verificationLevel=. |
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:
| Header | Meaning |
|---|---|
X-RateLimit-Limit | Requests allowed per rolling window (typically 300). |
X-RateLimit-Remaining | Requests left in the current window. |
X-RateLimit-Window | Window length in seconds (60). |
X-Monthly-Limit | Monthly request ceiling (typically 200,000). |
X-Monthly-Remaining | Approximate requests left this month (a hint, ≤60s stale). |
X-Monthly-Reset | Period 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
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.
| Status | code | Do |
|---|---|---|
| 401 | missing_key / invalid_key | Fix the Authorization header. Don't retry blindly. |
| 403 | plan_required | Key's plan lacks API access. Contact your account owner. |
| 429 | rate_limited | Sleep Retry-After seconds, then retry. |
| 429 | monthly_quota_exceeded | Monthly ceiling hit. Back off until next month or request a higher limit. |
| 404 | county_not_found / not_found | Bad FIPS or unknown path. Fix the request. |
| 503 | data_unavailable | Transient — 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-Matchcaching on the large endpoints (/counties,/projects,/bess,/data-centers). - ☐ Backoff on
429(honorRetry-After) and503(exponential). - ☐ Sync on a sensible cadence (daily is plenty — the data refreshes ~daily).
- ☐ Branch on the
codefield, not message text. - ☐ Alert if
X-Monthly-Remainingtrends 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.