GetSitePlan.ai API
Generate professional site plans programmatically. Submit an address, get back a PDF and GeoJSON features with detected structures, trees, driveways, and more.
Base URL: https://getsiteplan.ai/api/v1
Authentication
All requests require a Bearer token in the Authorization header. Create tokens from the API Tokens page in your dashboard.
Authorization: Bearer spf_<your-token>
- Tokens start with
spf_and are 68 characters long - The full token is shown once at creation — store it securely
- Up to 10 active tokens per account
- Revoke compromised tokens instantly from the dashboard
Quick Start
Generate a site plan in four steps. Set your token as an environment variable first:
export TOKEN="spf_your-token-here"
1. Check your balance
curl -H "Authorization: Bearer $TOKEN" \
https://getsiteplan.ai/api/v1/balance
# {"credits": 290}
2. Submit an address (costs 29 credits)
curl -X POST -H "Authorization: Bearer $TOKEN" \
-H "Content-Type: application/json" \
-d '{"address": "5811 Rose Ct, Countryside, IL"}' \
https://getsiteplan.ai/api/v1/site-plan
# {"job_id": "abc-123", "status": "processing"}
3. Poll for completion (60–90 seconds)
curl -H "Authorization: Bearer $TOKEN" \
https://getsiteplan.ai/api/v1/site-plan/JOB_ID
# {"job_id": "abc-123", "status": "ready", "pdf_url": "https://..."}
4. Get detected features as GeoJSON
curl -H "Authorization: Bearer $TOKEN" \
https://getsiteplan.ai/api/v1/site-plan/JOB_ID/features
# {"type": "FeatureCollection", "features": [...]}
Endpoints
/api/v1/balance
Returns your current credit balance.
// Response 200
{
"credits": 290
}
/api/v1/site-plan
Submit an address for site plan generation. Deducts 29 credits and starts async processing.
Request Body
| Field | Type | Required | Description |
|---|---|---|---|
address | string | Yes | Full street address |
curl -X POST -H "Authorization: Bearer $TOKEN" \
-H "Content-Type: application/json" \
-d '{"address": "5811 Rose Ct, Countryside, IL"}' \
https://getsiteplan.ai/api/v1/site-plan
// Response 202
{
"job_id": "a1b2c3d4-e5f6-7890-abcd-ef1234567890",
"status": "processing"
}
Errors
| Status | Error | When |
|---|---|---|
| 400 | invalid_request | Missing or invalid address |
| 402 | insufficient_credits | Not enough credits |
| 422 | validation_error | Address could not be resolved |
/api/v1/site-plan/:jobId
Poll this endpoint to check whether your site plan is ready.
Status Values
| Status | Meaning |
|---|---|
pending | Job created, waiting to start |
processing | Detection and rendering in progress |
ready | PDF available for download |
failed | Generation failed (credits refunded) |
// Response 200 — ready
{
"job_id": "a1b2c3d4-e5f6-7890-abcd-ef1234567890",
"status": "ready",
"pdf_url": "https://storage.googleapis.com/...",
"dxf_url": "https://storage.googleapis.com/..."
}
// Response 200 — failed (credits refunded)
{
"job_id": "a1b2c3d4-e5f6-7890-abcd-ef1234567890",
"status": "failed",
"error": "Pipeline timeout"
}
- Both
pdf_urlanddxf_urlare signed URLs that expire after 7 days - The
dxf_urlprovides a CAD-compatible DXF file (R2007 format) - Typical processing time is 60–90 seconds
Errors
| Status | Error | When |
|---|---|---|
| 400 | invalid_request | Missing jobId |
| 404 | not_found | Job does not exist |
/api/v1/site-plan/:jobId/features
Returns AI-detected property features as a GeoJSON FeatureCollection. Only available for completed jobs.
Query Parameters
| Param | Type | Description |
|---|---|---|
types | string | Comma-separated category filter. Omit for all. |
include | string | Add parcel and/or building boundary polygons. These use a different property shape (see below). |
GET /api/v1/site-plan/:jobId/features GET /api/v1/site-plan/:jobId/features?types=trees,structures GET /api/v1/site-plan/:jobId/features?types=pools&include=parcel,building
Feature Categories
Each emitted Feature carries a catalogType identity slug under properties. The ?types=… filter param uses the plural URL form on the left; it matches the catalogType value(s) on the right.
| Category (URL filter) | Matching catalogType | Sub-kind discriminator |
|---|---|---|
trees | tree | — |
hedges | hedge | — |
fences | fence | — |
pools | pool | — |
vehicles | vehicle | attrs.type — sedan, suv, pickup, truck, unknown |
structures | structure | attrs.kind — shed, garage, gazebo, pergola, outbuilding, unknown |
amenities | amenity | attrs.kind — tennis_court, basketball_court, amenity |
surfaces | driveway or surface | kind on surface variants — patio, deck, walkway, hot_tub |
catalogType values are canonical: aliases such as swimming pool, shrub_row, and privacy_hedge are normalised to pool and hedge. Filter by category (?types=hedges) for stable behaviour across detection updates.
// Response 200 — ?include=parcel,building
{
"type": "FeatureCollection",
"features": [
{
"type": "Feature",
"geometry": {
"type": "Polygon",
"coordinates": [[[-87.878, 41.748], [-87.878, 41.749], [-87.877, 41.749], [-87.878, 41.748]]]
},
"properties": {
"catalogType": "tree",
"attrs": {
"heightM": 8.4,
"species": "deciduous",
"canopyRadiusM": 2.8
},
"confidence": 0.92,
"source": "sam",
"areaSqm": 24.5
}
},
{
"type": "Feature",
"geometry": { "type": "Polygon", "coordinates": [[[...]]] },
"properties": { "catalogType": "parcel" }
},
{
"type": "Feature",
"geometry": { "type": "Polygon", "coordinates": [[[...]]] },
"properties": {
"catalogType": "building",
"attrs": {
"heightM": 6.1,
"stories": 2,
"roofType": "hip",
"roofAspect": "south",
"garageFaceEdgeIndex": 10,
"garageDoorCount": 2,
"architecturalStyle": "contemporary",
"hasSolarPanels": false
},
"confidence": 0.95,
"source": "vision-qa"
}
}
],
"parcel": {
"classification": {"type": "irregular", "confidence": 0.85, "source": "vision-qa"},
"sides": [
{"id": "side-0", "index": 0, "role": "interior", "relation": "left", "measuredLengthMeters": 38.2}
]
},
"yards": [
{"id": "yard-front", "kind": "front", "boundingSideIds": ["side-2"]},
{"id": "yard-back", "kind": "back", "boundingSideIds": ["side-0"]}
],
"gates": [
{"coordinates": [[-87.878, 41.748], [-87.878, 41.7485]]}
],
"analysis": {
"setbackReport": {
"bySide": [{"sideId": "side-0", "actualFeet": 18.3}],
"totalSides": 4,
"compliantSides": 4
},
"shadeReport": {
"byYard": [{"kind": "front", "sunHoursPerDay": 12.0, "band": "full_sun"}]
},
"lotGrade": {
"meanSlopeDeg": 1.4,
"dominantAspect": "south",
"reliefMeters": 0.6,
"isFlat": true
}
}
}
Detected Feature Properties
Each AI-detected Feature (every catalogType in the Feature Categories table above — tree, hedge, fence, pool, vehicle, structure, amenity, driveway, surface) carries the following properties. Features added via ?include=parcel,building use a different, narrower shape — see Included Geometry: parcel + building below.
| Property | Type | Always Present | Description |
|---|---|---|---|
catalogType | string | Yes | Canonical entity identity slug (see Feature Categories above). |
attrs | object | Yes | Per-instance attribute bag, typed by catalogType (see Per-Category Attribute Schemas below). Empty object when no attributes were inferred. |
confidence | number | Yes | Detection confidence (0–1) |
source | string | Yes | Detection provenance: sam, sam3, vision, vision-qa, corrected, fused, roadmap, osm, mask, image2, or manual. Treat unfamiliar values as opaque strings. |
areaSqm | number | No | Area in square meters, computed from the polygon. Omitted for degenerate (<4-point) rings. |
sideId | string | No | Parcel side identifier this feature aligns with. Present on hedge and fence when classifiable. |
roadId | string | No | Connecting road identifier. Present on driveway when known. |
kind | string | No | Surface sub-kind: patio, deck, walkway, hot_tub. Present on surface only. |
headingDeg | number | No | Vehicle heading in degrees clockwise from north (0–360). Present on vehicle only. |
parkedOn | string | No | Identifier of the surface (typically a driveway) the vehicle is parked on. Present on vehicle only. |
Per-Category Attribute Schemas
Each catalogType's properties.attrs bag carries category-specific per-instance attributes when the engine could populate them. All fields are optional; clients should treat absence as “not detected / unknown”. The bag is empty ({}) when no attributes were inferred.
catalogType | attrs field | Type | Description |
|---|---|---|---|
tree | heightM | number | Tree height in meters above ground (DSM-derived). |
species | string | One of deciduous, evergreen, palm, unknown (vision QA). | |
canopyRadiusM | number | Canopy radius in meters. | |
hedge | lengthFt | number | Hedge run length in feet. |
heightFt | number | Mean hedge height in feet. | |
fence | material | string | Fence material: wood, vinyl, chain_link, wrought_iron, masonry, composite, unknown. |
heightFt | number | Fence height in feet. | |
pool | kind | string | Installation type: in_ground, above_ground, spa, unknown. |
shape | string | Plan-view shape: rectangular, kidney, freeform, lap, round, oval, unknown. | |
areaSqFt | number | Surface area in square feet. | |
fenced | boolean | True if a perimeter fence around the pool was detected. | |
vehicle | type | string | Vehicle body style: sedan, suv, pickup, truck, unknown. |
structure | kind | string | Sub-kind: shed, garage, gazebo, pergola, outbuilding, unknown. |
heightM | number | Structure height in meters above ground (DSM). | |
amenity | kind | string | Sub-kind: tennis_court, basketball_court, amenity. |
driveway / surface | material | string | Surface material: concrete, asphalt, pavers, gravel, dirt, unknown. |
Included Geometry: parcel + building
?include=parcel,building Features carry a narrower property shape than detected features — attrs, confidence, and source are not guaranteed. Three concrete shapes:
| Source | properties shape |
|---|---|
| Parcel (always) | {catalogType: 'parcel'} — no attrs (the parcel's classification, sides, and entrance live on the top-level parcel response field). |
| Building, rich variant (engine analysis available) | {catalogType: 'building', attrs: BuildingAttrs, confidence, source} |
| Building, geometry-only fallback (older order, no engine analysis pass) | {catalogType: 'building'} |
Building attrs fields, when present:
attrs field | Type | Description |
|---|---|---|
heightM | number | Building height in meters (DSM-derived). |
stories | number | Story count, if known. |
roofType | string | Roof topology: gable, hip, flat, shed, unknown (vision QA). |
roofSlope | number | Mean roof pitch in degrees. |
roofAspect | string | Cardinal aspect of the roof's downslope direction. |
garageFaceEdgeIndex | number | Index of the footprint edge that hosts the garage door. |
garageDoorCount | number | Count of visible garage doors (0–4, vision QA). |
architecturalStyle | string | One of modern, contemporary, traditional, colonial, craftsman, ranch, mediterranean, unknown (vision QA). |
hasSolarPanels | boolean | Whether visible solar panels were detected on the roof (vision QA). |
Top-Level Site Analysis
Alongside the standard features array, the response carries derived analysis layers as additional top-level fields. These are populated when an engine run produced them; otherwise the field is absent.
| Field | Type | Description |
|---|---|---|
parcel | object | {classification, sides[]} — parcel shape classification (rectangular, corner, culdesac, flag, irregular) plus per-side metadata (id, index, role, relation, measuredLengthMeters). |
yards | array | Front- and back-yard regions: {id, kind, boundingSideIds}. |
gates | array | Proposed-fence gate edges along the property boundary: [{coordinates: [[lng, lat], ...]}]. Each entry is an open polyline marking where a gate would sit on the recommended fence layout. Empty when no fence-layout stage ran. |
analysis.setbackReport | object | Per-side setback report: distance from the primary building footprint to each parcel side, optionally compared against zoning minimums. |
analysis.shadeReport | object | Per-yard shade analysis: estimated daily sun hours and a categorical band (full-sun, partial-sun, shaded) on a representative summer day. |
analysis.lotGrade | object | Lot grade summary from the DSM: {meanSlopeDeg, dominantAspect, reliefMeters, isFlat}. |
analysis.fenceLayout | object | Proposed fence linework recommendation, when an automatic fence layout was generated. |
Errors
| Status | Error | When |
|---|---|---|
| 400 | invalid_request | Invalid types or include value |
| 404 | not_found | Job does not exist |
| 422 | not_ready | Job is still processing or has failed |
/api/v1/usage
Returns API call counts per token over a given period.
Query Parameters
| Param | Type | Default | Description |
|---|---|---|---|
days | integer | 30 | Lookback period in days |
// Response 200
{
"period_days": 30,
"tokens": [
{
"token_id": "tok-uuid",
"name": "Production",
"total_calls": 142,
"endpoints": {
"/api/v1/balance": 10,
"/api/v1/site-plan": 25,
"/api/v1/site-plan/abc-123": 82,
"/api/v1/site-plan/abc-123/features": 25
}
}
]
}
Code Examples
Complete examples for generating a site plan and polling for the result.
#!/bin/bash
TOKEN="spf_your-token-here"
BASE="https://getsiteplan.ai/api/v1"
# Submit address
RESPONSE=$(curl -s -X POST -H "Authorization: Bearer $TOKEN" \
-H "Content-Type: application/json" \
-d '{"address": "5811 Rose Ct, Countryside, IL"}' \
"$BASE/site-plan")
JOB_ID=$(echo "$RESPONSE" | grep -o '"job_id":"[^"]*"' | cut -d'"' -f4)
echo "Job submitted: $JOB_ID"
# Poll until ready
while true; do
STATUS_RESP=$(curl -s -H "Authorization: Bearer $TOKEN" \
"$BASE/site-plan/$JOB_ID")
STATUS=$(echo "$STATUS_RESP" | grep -o '"status":"[^"]*"' | cut -d'"' -f4)
echo "Status: $STATUS"
if [ "$STATUS" = "ready" ] || [ "$STATUS" = "failed" ]; then
break
fi
sleep 5
done
# Download PDF
PDF_URL=$(echo "$STATUS_RESP" | grep -o '"pdf_url":"[^"]*"' | cut -d'"' -f4)
if [ -n "$PDF_URL" ]; then
curl -o site-plan.pdf "$PDF_URL"
echo "Downloaded: site-plan.pdf"
fi
import time
import requests
TOKEN = "spf_your-token-here"
BASE = "https://getsiteplan.ai/api/v1"
HEADERS = {"Authorization": f"Bearer {TOKEN}"}
# Submit address
resp = requests.post(
f"{BASE}/site-plan",
headers=HEADERS,
json={"address": "5811 Rose Ct, Countryside, IL"},
)
resp.raise_for_status()
job_id = resp.json()["job_id"]
print(f"Job submitted: {job_id}")
# Poll until ready
while True:
resp = requests.get(f"{BASE}/site-plan/{job_id}", headers=HEADERS)
data = resp.json()
print(f"Status: {data['status']}")
if data["status"] in ("ready", "failed"):
break
time.sleep(5)
# Download PDF
if data["status"] == "ready":
pdf = requests.get(data["pdf_url"])
with open("site-plan.pdf", "wb") as f:
f.write(pdf.content)
print("Downloaded: site-plan.pdf")
# Get GeoJSON features
features = requests.get(
f"{BASE}/site-plan/{job_id}/features",
headers=HEADERS,
).json()
print(f"Detected {len(features['features'])} features")
const TOKEN = "spf_your-token-here";
const BASE = "https://getsiteplan.ai/api/v1";
const headers = {
Authorization: `Bearer ${TOKEN}`,
"Content-Type": "application/json",
};
async function generateSitePlan(address) {
// Submit address
const submitResp = await fetch(`${BASE}/site-plan`, {
method: "POST",
headers,
body: JSON.stringify({ address }),
});
const { job_id } = await submitResp.json();
console.log(`Job submitted: ${job_id}`);
// Poll until ready
let data;
while (true) {
const statusResp = await fetch(`${BASE}/site-plan/${job_id}`, { headers });
data = await statusResp.json();
console.log(`Status: ${data.status}`);
if (data.status === "ready" || data.status === "failed") break;
await new Promise((r) => setTimeout(r, 5000));
}
// Download PDF
if (data.status === "ready") {
const pdf = await fetch(data.pdf_url);
const blob = await pdf.blob();
console.log(`PDF size: ${blob.size} bytes`);
}
// Get GeoJSON features
const featResp = await fetch(`${BASE}/site-plan/${job_id}/features`, { headers });
const features = await featResp.json();
console.log(`Detected ${features.features.length} features`);
return { data, features };
}
generateSitePlan("5811 Rose Ct, Countryside, IL");
const TOKEN = "spf_your-token-here";
const BASE = "https://getsiteplan.ai/api/v1";
const headers = {
Authorization: `Bearer ${TOKEN}`,
"Content-Type": "application/json",
};
// --- Response types ---
interface BalanceResponse {
credits: number;
}
interface JobSubmitResponse {
job_id: string;
status: "processing";
}
interface JobStatusResponse {
job_id: string;
status: "pending" | "processing" | "ready" | "failed";
pdf_url?: string;
dxf_url?: string;
error?: string;
}
type VehicleType = "sedan" | "suv" | "pickup" | "truck" | "unknown";
type DetectedCatalogType =
| "tree" | "hedge" | "fence" | "pool" | "vehicle"
| "structure" | "amenity" | "driveway" | "surface";
// AI-detected entities — always carry attrs + confidence + source.
interface DetectedFeatureProps {
catalogType: DetectedCatalogType;
attrs: Record<string, unknown>; // narrow on catalogType for typed access
confidence: number;
source: string;
areaSqm?: number;
sideId?: string; // hedge, fence
roadId?: string; // driveway
kind?: string; // surface (patio | deck | walkway | hot_tub)
headingDeg?: number; // vehicle
parkedOn?: string; // vehicle
}
// ?include=parcel — narrower shape, no attrs.
interface ParcelFeatureProps {
catalogType: "parcel";
}
// ?include=building — rich variant when engine analysis is available;
// geometry-only fallback otherwise. attrs/confidence/source are all
// optional — narrow on their presence before reading.
interface BuildingFeatureProps {
catalogType: "building";
attrs?: Record<string, unknown>; // BuildingAttrs when present
confidence?: number;
source?: string;
}
type FeatureProps =
| DetectedFeatureProps
| ParcelFeatureProps
| BuildingFeatureProps;
interface DetectedFeature {
type: "Feature";
geometry: { type: "Polygon"; coordinates: number[][][] };
properties: FeatureProps;
}
interface FeatureCollection {
type: "FeatureCollection";
features: DetectedFeature[];
}
// --- API helpers ---
async function api<T>(path: string, init?: RequestInit): Promise<T> {
const resp = await fetch(`${BASE}${path}`, { ...init, headers });
if (!resp.ok) throw new Error(`API ${resp.status}: ${await resp.text()}`);
return resp.json() as Promise<T>;
}
function sleep(ms: number): Promise<void> {
return new Promise((r) => setTimeout(r, ms));
}
// --- Main workflow ---
async function generateSitePlan(address: string) {
// Check balance
const { credits } = await api<BalanceResponse>("/balance");
console.log(`Credits: ${credits}`);
// Submit address
const { job_id } = await api<JobSubmitResponse>("/site-plan", {
method: "POST",
body: JSON.stringify({ address }),
});
console.log(`Job submitted: ${job_id}`);
// Poll until ready
let job: JobStatusResponse;
do {
await sleep(5000);
job = await api<JobStatusResponse>(`/site-plan/${job_id}`);
console.log(`Status: ${job.status}`);
} while (job.status !== "ready" && job.status !== "failed");
if (job.status === "failed") {
throw new Error(`Job failed: ${job.error}`);
}
// Download PDF
const pdf = await fetch(job.pdf_url!);
const blob = await pdf.blob();
console.log(`PDF: ${blob.size} bytes`);
// Get GeoJSON features
const fc = await api<FeatureCollection>(`/site-plan/${job_id}/features`);
console.log(`Detected ${fc.features.length} features`);
// Filter vehicles with body style
const vehicles = fc.features.filter((f) => f.properties.catalogType === "vehicle");
for (const v of vehicles) {
const type = (v.properties.attrs as { type?: VehicleType }).type ?? "unknown";
console.log(` ${type} (${v.properties.confidence})`);
}
return { job, features: fc };
}
generateSitePlan("5811 Rose Ct, Countryside, IL");
Credits & Pricing
Each site plan costs 29 credits. New accounts receive 29 free credits (one plan). If a job fails during generation, credits are automatically refunded.
| Package | Credits | Price | Per Plan |
|---|---|---|---|
| 10 plans | 290 | $199 | $19.90 |
| 25 plans | 725 | $399 | $15.96 |
| 50 plans | 1,450 | $649 | $12.98 |
Purchase credits from the Credits page in your dashboard.
Rate Limits
API requests are limited to 60 requests per minute per token. When exceeded, the API returns 429 with a Retry-After header.
// 429 Too Many Requests
{
"error": "rate_limit_exceeded",
"message": "Rate limit exceeded. Retry after 60 seconds",
"retry_after_seconds": 60
}
Error Reference
All errors return a JSON body with error and message fields:
{
"error": "error_code",
"message": "Human-readable description"
}
| HTTP Status | Error Code | Description |
|---|---|---|
| 400 | invalid_request | Missing or malformed request parameters |
| 401 | unauthorized | Missing, malformed, or revoked API token |
| 402 | insufficient_credits | Not enough credits to create a site plan |
| 404 | not_found | Resource does not exist |
| 405 | method_not_allowed | Wrong HTTP method for this endpoint |
| 422 | not_ready | Job has not completed yet |
| 422 | validation_error | Address validation failed |
| 429 | rate_limit_exceeded | Too many requests (see Retry-After header) |
| 500 | internal | Unexpected server error |
CORS
The API allows cross-origin requests from any origin:
Access-Control-Allow-Origin: * Access-Control-Allow-Methods: GET, POST, OPTIONS Access-Control-Allow-Headers: Authorization, Content-Type