REST API v1

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>

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

GET /api/v1/balance

Returns your current credit balance.

// Response 200
{
  "credits": 290
}

POST /api/v1/site-plan

Submit an address for site plan generation. Deducts 29 credits and starts async processing.

Request Body

FieldTypeRequiredDescription
addressstringYesFull 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

StatusErrorWhen
400invalid_requestMissing or invalid address
402insufficient_creditsNot enough credits
422validation_errorAddress could not be resolved

GET /api/v1/site-plan/:jobId

Poll this endpoint to check whether your site plan is ready.

Status Values

StatusMeaning
pendingJob created, waiting to start
processingDetection and rendering in progress
readyPDF available for download
failedGeneration 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_url and dxf_url are signed URLs that expire after 7 days
  • The dxf_url provides a CAD-compatible DXF file (R2007 format)
  • Typical processing time is 60–90 seconds

Errors

StatusErrorWhen
400invalid_requestMissing jobId
404not_foundJob does not exist

GET /api/v1/site-plan/:jobId/features

Returns AI-detected property features as a GeoJSON FeatureCollection. Only available for completed jobs.

Query Parameters

ParamTypeDescription
typesstringComma-separated category filter. Omit for all.
includestringAdd 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 catalogTypeSub-kind discriminator
treestree
hedgeshedge
fencesfence
poolspool
vehiclesvehicleattrs.type — sedan, suv, pickup, truck, unknown
structuresstructureattrs.kind — shed, garage, gazebo, pergola, outbuilding, unknown
amenitiesamenityattrs.kind — tennis_court, basketball_court, amenity
surfacesdriveway or surfacekind 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.

PropertyTypeAlways PresentDescription
catalogTypestringYesCanonical entity identity slug (see Feature Categories above).
attrsobjectYesPer-instance attribute bag, typed by catalogType (see Per-Category Attribute Schemas below). Empty object when no attributes were inferred.
confidencenumberYesDetection confidence (0–1)
sourcestringYesDetection provenance: sam, sam3, vision, vision-qa, corrected, fused, roadmap, osm, mask, image2, or manual. Treat unfamiliar values as opaque strings.
areaSqmnumberNoArea in square meters, computed from the polygon. Omitted for degenerate (<4-point) rings.
sideIdstringNoParcel side identifier this feature aligns with. Present on hedge and fence when classifiable.
roadIdstringNoConnecting road identifier. Present on driveway when known.
kindstringNoSurface sub-kind: patio, deck, walkway, hot_tub. Present on surface only.
headingDegnumberNoVehicle heading in degrees clockwise from north (0–360). Present on vehicle only.
parkedOnstringNoIdentifier 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.

catalogTypeattrs fieldTypeDescription
treeheightMnumberTree height in meters above ground (DSM-derived).
speciesstringOne of deciduous, evergreen, palm, unknown (vision QA).
canopyRadiusMnumberCanopy radius in meters.
hedgelengthFtnumberHedge run length in feet.
heightFtnumberMean hedge height in feet.
fencematerialstringFence material: wood, vinyl, chain_link, wrought_iron, masonry, composite, unknown.
heightFtnumberFence height in feet.
poolkindstringInstallation type: in_ground, above_ground, spa, unknown.
shapestringPlan-view shape: rectangular, kidney, freeform, lap, round, oval, unknown.
areaSqFtnumberSurface area in square feet.
fencedbooleanTrue if a perimeter fence around the pool was detected.
vehicletypestringVehicle body style: sedan, suv, pickup, truck, unknown.
structurekindstringSub-kind: shed, garage, gazebo, pergola, outbuilding, unknown.
heightMnumberStructure height in meters above ground (DSM).
amenitykindstringSub-kind: tennis_court, basketball_court, amenity.
driveway / surfacematerialstringSurface 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:

Sourceproperties 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 fieldTypeDescription
heightMnumberBuilding height in meters (DSM-derived).
storiesnumberStory count, if known.
roofTypestringRoof topology: gable, hip, flat, shed, unknown (vision QA).
roofSlopenumberMean roof pitch in degrees.
roofAspectstringCardinal aspect of the roof's downslope direction.
garageFaceEdgeIndexnumberIndex of the footprint edge that hosts the garage door.
garageDoorCountnumberCount of visible garage doors (0–4, vision QA).
architecturalStylestringOne of modern, contemporary, traditional, colonial, craftsman, ranch, mediterranean, unknown (vision QA).
hasSolarPanelsbooleanWhether 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.

FieldTypeDescription
parcelobject{classification, sides[]} — parcel shape classification (rectangular, corner, culdesac, flag, irregular) plus per-side metadata (id, index, role, relation, measuredLengthMeters).
yardsarrayFront- and back-yard regions: {id, kind, boundingSideIds}.
gatesarrayProposed-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.setbackReportobjectPer-side setback report: distance from the primary building footprint to each parcel side, optionally compared against zoning minimums.
analysis.shadeReportobjectPer-yard shade analysis: estimated daily sun hours and a categorical band (full-sun, partial-sun, shaded) on a representative summer day.
analysis.lotGradeobjectLot grade summary from the DSM: {meanSlopeDeg, dominantAspect, reliefMeters, isFlat}.
analysis.fenceLayoutobjectProposed fence linework recommendation, when an automatic fence layout was generated.

Errors

StatusErrorWhen
400invalid_requestInvalid types or include value
404not_foundJob does not exist
422not_readyJob is still processing or has failed

GET /api/v1/usage

Returns API call counts per token over a given period.

Query Parameters

ParamTypeDefaultDescription
daysinteger30Lookback 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.

PackageCreditsPricePer Plan
10 plans290$199$19.90
25 plans725$399$15.96
50 plans1,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 StatusError CodeDescription
400invalid_requestMissing or malformed request parameters
401unauthorizedMissing, malformed, or revoked API token
402insufficient_creditsNot enough credits to create a site plan
404not_foundResource does not exist
405method_not_allowedWrong HTTP method for this endpoint
422not_readyJob has not completed yet
422validation_errorAddress validation failed
429rate_limit_exceededToo many requests (see Retry-After header)
500internalUnexpected 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