TURF Insight

API Reference

REST API documentation for the TURF Insight SAAS platform v1.0.3

Base URL:
SECTIONS: System Courses Preferences Flights Annotations Annotation Groups Design Overlays Import / Export Observations API Keys Partner Tiles Jobs Admin

System

GET /api/health Service health check

Response

{ "status": "ok", "version": "0.1.0", "courses": 1 }

Courses

GET /api/courses List all courses

Response

{ "courses": [{ "slug": "castle_pines", "name": "Castle Pines Golf Club", ... }] }
GET /api/courses/{slug} Get course by slug

Response

{ "course": { "slug": "castle_pines", "name": "...", "center_lat": 39.46, "center_lng": -104.87, "projection_epsg": 6428, ... } }
POST /api/courses Create a new course

Request Body (JSON)

{
  "slug": "my_course",       // required, unique
  "name": "My Course",       // required
  "bounds_xmin": -105.0,     // optional
  "bounds_ymin": 39.5,
  "bounds_xmax": -104.9,
  "bounds_ymax": 39.6,
  "projection_epsg": 6428,   // optional
  "center_lat": 39.55,       // optional
  "center_lng": -104.95,
  "tier": "pro",             // optional
  "address": "123 Main St",  // optional
  "timezone": "US/Mountain"  // optional
}
PATCH /api/courses/{slug} Update course details

Request Body (JSON) — all fields optional

{ "name": "New Name", "center_lat": 39.5, "center_lng": -104.9, "tier": "enterprise", "address": "...", "timezone": "US/Eastern", "projection_epsg": 6428 }
DELETE /api/courses/{slug} Delete course and all associated data

⚠️ Permanently deletes the course, all flights, annotations, observations, and files.

POST /api/courses/{slug}/archive Archive a course (soft delete)

Sets archived_at timestamp. Course is hidden from listings but data is preserved.

POST /api/courses/{slug}/restore Restore an archived course

Clears archived_at, making the course visible again.

Course Preferences

GET /api/courses/{slug}/prefs Get saved UI preferences for a course

Response

{
  "layers": { "ortho": true, "dsm": false, "contours": true },
  "hiddenGroups": ["group-uuid-1"],
  "hiddenOverlays": ["overlay-uuid-1"],
  "annotations": { "pointsLines": true, "polygonsVolumes": true }
}

💡 Returns {} if no prefs saved yet.

PUT /api/courses/{slug}/prefs Save UI preferences (replaces entire prefs object)

Request Body (JSON) — arbitrary prefs object

{ "layers": { "ortho": true, "dsm": false }, "hiddenGroups": [] }

Flights

GET /api/courses/{slug}/flights List flights for a course
POST /api/courses/{slug}/flights Create a new flight

Request Body (JSON)

{
  "flight_date": "2026-03-18",
  "label": "Spring Survey",           // optional
  "ortho_container": "ortho.rat",      // optional
  "dsm_container": "dsm.rat",         // optional
  "pointcloud_container": "pc.rat",   // optional
  "contour_container": "contours.rat" // optional
}
PATCH /api/courses/{slug}/flights/{flight_id} Update flight details

Request Body (JSON) — all fields optional

{ "flight_date": "2026-03-19", "label": "Updated Label", "ortho_container": "new_ortho.rat", "processing_status": "complete" }
DELETE /api/courses/{slug}/flights/{flight_id} Delete a flight

Deletes the flight record and associated data.

POST /api/courses/{slug}/flights/{flight_id}/archive Archive a flight

Sets archived_at on the flight.

POST /api/courses/{slug}/flights/{flight_id}/restore Restore an archived flight

Clears archived_at on the flight.

Annotations

GET /api/courses/{slug}/annotations List annotations (optional filters)
Query ParamTypeInfo
group_idstringoptional Filter by group
typestringoptional point | linestring | polygon | circle
POST /api/courses/{slug}/annotations Create annotation (GeoJSON geometry)

Request Body (JSON)

{
  "group_id": "group-uuid",         // required
  "geometry_type": "polygon",       // required: point|linestring|polygon|circle
  "geometry_json": "{\"type\":\"Polygon\",\"coordinates\":...}",  // required
  "name": "Green 5",               // optional
  "description": "Main green",     // optional
  "area_sqmeters": 502.3,          // optional (auto-calculated client-side)
  "perimeter_meters": 89.1,        // optional
  "length_meters": 45.2,           // optional (for lines)
  "radius_meters": 12.0,           // optional (for circles)
  "flight_id": "flight-uuid"       // optional (scopes to flight)
}
PATCH /api/courses/{slug}/annotations/{id} Update annotation (name, description, measurements, group)

Request Body (JSON) — all fields optional

{ "name": "Green 5A", "description": "...", "area_sqmeters": 510.0, "group_id": "new-group-id" }
DELETE /api/courses/{slug}/annotations/{id} Delete an annotation

Permanently removes the annotation.

Annotation Groups

GET /api/courses/{slug}/annotation-groups List annotation groups
POST /api/courses/{slug}/annotation-groups Create annotation group
{ "name": "Bunkers", "category": "course", "color": "#f5a623", "parent_id": null }
PATCH /api/courses/{slug}/annotation-groups/{id} Update group (name, color, category, parent)
{ "name": "New Name", "color": "#00ff00", "category": "flight", "parent_id": "parent-uuid" }
DELETE /api/courses/{slug}/annotation-groups/{id} Delete group and all its annotations

⚠️ Also deletes all annotations within this group.

Design Overlays

GET /api/courses/{slug}/overlays List all design overlays
GET /api/courses/{slug}/overlays/{id} Get a single overlay

Returns full overlay metadata including transform parameters.

POST /api/courses/{slug}/overlays Create overlay record
{ "name": "Sheet 1 As-built", "overlay_type": "image", "source_format": "pdf", "color": "#ff0000" }
PATCH /api/courses/{slug}/overlays/{id} Update overlay transform/metadata

Request Body (JSON) — all fields optional

{
  "name": "...", "opacity": 0.7, "rotation_deg": 15.0, "scale": 1.2,
  "center_lat": 39.46, "center_lng": -104.87,
  "bounds_north": 39.47, "bounds_south": 39.45, "bounds_east": -104.86, "bounds_west": -104.88
}
DELETE /api/courses/{slug}/overlays/{id} Delete overlay and its files

Removes overlay record and associated image files.

POST /api/courses/{slug}/overlays/{id}/upload Upload source file (PDF, PNG, JPG)

Multipart file upload. Field name: file. Accepts PDF, PNG, JPG.

POST /api/courses/{slug}/overlays/{id}/upload-rendered Upload rendered PNG (from browser PDF rendering)

Multipart upload of browser-rendered PNG for PDF overlays.

GET /api/courses/{slug}/overlays/{id}/image Serve overlay image

Returns the rendered overlay image as image/png or image/jpeg.

Import / Export

POST /api/courses/{slug}/import Import GeoJSON FeatureCollection as annotations

Send a GeoJSON FeatureCollection in the request body. Creates annotation groups and features.

POST /api/courses/{slug}/import-dxf Import DXF file as annotation groups/features

Multipart file upload. Field: file. DXF layers become annotation groups, entities become features. Query param ?epsg=6428 for coordinate reprojection.

POST /api/courses/{slug}/import-csv Import CSV file as point annotations

Multipart file upload. Expects columns: name, lat/latitude, lng/longitude. Creates point annotations.

GET /api/courses/{slug}/export Export annotations as GeoJSON FeatureCollection
GET /api/courses/{slug}/export-csv Export annotations as CSV

Returns a CSV download with all annotations and their measurements.

POST /api/validate-file Validate an uploaded file (DXF, GeoJSON, CSV)

Multipart upload. Returns validation results without importing.

Observations (Living Ledger)

GET /api/courses/{slug}/observations List observations (with filters)
Query ParamTypeInfo
severitystringhigh | medium | low | info
statusstringopen | resolved
categorystringDisease | Pest | Weed | etc.
from_datestringISO date (YYYY-MM-DD)
to_datestringISO date
qstringFull-text search
limitintMax results
offsetintPagination offset
POST /api/courses/{slug}/observations Create observation (multipart: photo + metadata)

Multipart Fields

FieldTypeInfo
photofilerequired JPEG/PNG image
metadataJSONrequired Observation data

Metadata JSON

{
  "latitude": 39.738, "longitude": -104.99,
  "captured_at": "2026-03-08T12:00:00Z",
  "severity": "medium", "category": "Disease",
  "description": "Brown patch on green 5",
  "inspector": "Field Inspector",
  "location_name": "Green 5", "tags": "fungus,patch"
}

💡 EXIF fallback: If lat/lng are 0, GPS coords are extracted from photo EXIF data.

POST /api/courses/{slug}/observations/batch Batch multi-photo upload with auto-EXIF

Multipart Fields

photo1, photo2, … — Multiple JPEG/PNG images
metadata — JSON with shared batch data (severity, category, etc.)

Response

{ "total": 5, "created": 5, "results": [{ "id": "...", "status": "created" }] }
PATCH /api/courses/{slug}/observations/{id} Update observation
{ "status": "resolved", "severity": "low", "description": "Fixed", "resolution_notes": "Applied fungicide" }
POST /api/courses/{slug}/observations/bulk-update Bulk update multiple observations
{ "ids": ["id1", "id2"], "update": { "status": "resolved" } }
DELETE /api/courses/{slug}/observations/{id} Delete observation + cleanup files

Deletes observation record, photo file, and thumbnail.

GET /api/courses/{slug}/observations/stats Get observation statistics

Response

{ "total": 42, "open": 30, "resolved": 12, "high": 5, "medium": 10, "low": 15, "info": 12 }
GET /api/courses/{slug}/observations/export.csv Export observations as CSV

Downloads all observations as a CSV file.

GET /api/courses/{slug}/observations/export.json Export observations as GeoJSON

Returns observations as a GeoJSON FeatureCollection.

GET /api/observations/{id}/photo Serve full-size photo

Returns the original JPEG image as image/jpeg.

GET /api/observations/{id}/thumb Serve 400px thumbnail

Returns a 400px-wide JPEG thumbnail. Auto-generated on upload.

API Keys

All key management endpoints require a superadmin JWT in the Authorization: Bearer … header. Keys support optional scope restrictions: course_slug → flight_id → layer_key. A null scope field is a wildcard — it allows access to everything at that level.

POST /api/keys Create a new scoped API key

🔒 Requires superadmin JWT

Request Body (JSON)

{
  "label": "Beta Partner A",                      // required
  "key_type": "partner",                          // global | partner | capture
  "course_slug": "castle_pines",                  // optional — null = all courses
  "flight_id": "flt_abc123...",                   // optional — null = all flights
  "layer_key": "castle_pines_ortho_5_9_25"        // optional — null = all layers
}

Response

{
  "id": "uuid",
  "key": "ti_a3f8b2c1d4e5...",   // ⚠️ shown ONCE — save immediately
  "label": "Beta Partner A",
  "key_type": "partner",
  "course_slug": "castle_pines",
  "flight_id": null,
  "layer_key": null,
  "message": "Save this key — it will not be shown again."
}
GET /api/keys List all API keys with scope (no raw values returned)

🔒 Requires superadmin JWT

Response

{
  "keys": [
    {
      "id": "uuid",
      "label": "Beta Partner A",
      "is_active": true,
      "created_at": "2026-05-08T09:48:00Z",
      "expires_at": null,
      "key_type": "partner",
      "course_slug": "castle_pines",
      "flight_id": null,
      "layer_key": null
    }
  ]
}
DELETE /api/keys/{id} Revoke a key (immediate, irreversible)

🔒 Requires superadmin JWT

Response

{ "status": "revoked", "id": "uuid" }

⚠️ Revocation is immediate — any active requests using this key will start failing.

POST /api/keys/validate Check if a raw key is valid (open endpoint — no auth)

✅ No auth required — safe for partners to call

Request Body

{ "key": "ti_a3f8b2c1d4e5..." }

Response

{ "valid": true }

Partner Tile Access

XYZ raster tile endpoint for external web viewer integrations. Requires an X-Api-Key header (or ?api_key= query param for clients that can't set custom headers). Key scope is enforced — a key scoped to castle_pines cannot access roaring_fork tiles.

Existing /api/tiles/… routes are completely unchanged — no auth required, internal use only. This endpoint is purely additive.

GET /api/partner/tiles/{course}/{flight}/{layer}/{z}/{x}/{y} Serve scoped XYZ raster tile (PNG)

Path Parameters

ParamTypeInfo
coursestringrequired Course slug (e.g. castle_pines)
flightstringrequired Flight ID (UUID)
layerstringrequired Layer key or product type (ortho or castle_pines_ortho_5_9_25)
z / x / yintrequired Standard XYZ tile coordinates (Web Mercator)

Authentication (one of)

// HTTP header (preferred):
X-Api-Key: ti_a3f8b2c1d4e5...

// Query param (for clients that can't set headers):
GET /api/partner/tiles/castle_pines/flt_abc/ortho/12/1024/512?api_key=ti_a3f8b2c1...

Responses

200  image/png  — 256x256 raster tile (transparent PNG if tile outside bounds)
401  { "error": "X-Api-Key header (or ?api_key= query param) required" }
401  { "error": "Invalid or expired API key" }
403  { "error": "Key not authorized for this course" }
403  { "error": "Key not authorized for this flight" }
403  { "error": "Key not authorized for this layer" }
404  { "error": "No container found", "code": "NOT_FOUND" }

MapLibre GL JS Integration

// Add to map after authentication with your partner key
map.addSource('partner-ortho', {
  type: 'raster',
  tiles: [
    'https://srv1526342.hstgr.cloud/api/partner/tiles/' +
    'castle_pines/FLIGHT_ID/ortho/{z}/{x}/{y}?api_key=ti_YOUR_KEY_HERE'
  ],
  tileSize: 256,
  attribution: '© TURFInsight'
});
map.addLayer({
  id: 'partner-ortho',
  type: 'raster',
  source: 'partner-ortho',
  paint: { 'raster-opacity': 1.0 }
});

Try it — live tile preview

Global WMS Tile Access (QGIS/Web Mercator)

Standard Web Mercator (EPSG:3857) XYZ raster tile endpoint for QGIS, ArcGIS, and other global mapping applications. This microservice automatically stitches and reprojects native local state-plane data on-the-fly. Requires an api_key= query param for authentication.

GET /api/wms/global/{layer}/{z}/{x}/{y} Serve dynamically reprojected EPSG:3857 XYZ raster tile

Path Parameters

ParamTypeInfo
layerstringrequired Layer type (e.g. ortho or dsm)
z / x / yintrequired Standard XYZ tile coordinates (Web Mercator EPSG:3857)

Authentication

GET /api/wms/global/ortho/22/874972/1596405?api_key=ti_a3f8b2c1...

Responses

200  image/png  — 256x256 raster tile
401  — API Key required or invalid
404  — No intersection with any course or layer

QGIS Integration

1. In QGIS Browser, right click 'XYZ Tiles' -> 'New Connection'
2. Name: TURFInsight Global Ortho
3. URL: https://srv1526342.hstgr.cloud/api/wms/global/ortho/{z}/{x}/{y}?api_key=YOUR_API_KEY
4. Max Zoom Level: 23

Background Jobs

GET /api/jobs List all background jobs
GET /api/jobs/{job_id} Poll job status

Response

{
  "job_id": "...", "status": "running", "progress": 45,
  "message": "Processing tiles...", "product_type": "ortho",
  "flight_id": "...", "course_slug": "castle_pines",
  "started_at": "2026-03-19T...", "completed_at": null
}

Admin

POST /api/admin/restart-rat-server Restart the rat-server tile service

Kills and restarts rat-server with all discovered .rat containers.

POST /api/courses/{slug}/upload Upload .rat container file

Multipart upload. Field: file. Accepts .rat container files for tile serving.

POST /api/courses/{slug}/process-flight Start background flight processing

Request Body (JSON)

{ "flight_id": "...", "product_type": "ortho", "container": "ortho.rat" }

Response

{ "job_id": "...", "status": "queued" }
GET /api/courses/{slug}/validate-container Check if a .rat container file exists
Query ParamTypeInfo
filestringrequired Filename to check

Response

{ "exists": true, "file": "ortho.rat", "path": "/data/courses/..." }