REST API documentation for the TURF Insight SAAS platform v1.0.3
Response
{ "status": "ok", "version": "0.1.0", "courses": 1 }
Response
{ "courses": [{ "slug": "castle_pines", "name": "Castle Pines Golf Club", ... }] }
Response
{ "course": { "slug": "castle_pines", "name": "...", "center_lat": 39.46, "center_lng": -104.87, "projection_epsg": 6428, ... } }
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
}
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 }
⚠️ Permanently deletes the course, all flights, annotations, observations, and files.
Sets archived_at timestamp. Course is hidden from listings but data is preserved.
Clears archived_at, making the course visible again.
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.
Request Body (JSON) — arbitrary prefs object
{ "layers": { "ortho": true, "dsm": false }, "hiddenGroups": [] }
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
}
Request Body (JSON) — all fields optional
{ "flight_date": "2026-03-19", "label": "Updated Label", "ortho_container": "new_ortho.rat", "processing_status": "complete" }
Deletes the flight record and associated data.
Sets archived_at on the flight.
Clears archived_at on the flight.
| Query Param | Type | Info |
|---|---|---|
| group_id | string | optional Filter by group |
| type | string | optional point | linestring | polygon | circle |
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)
}
Request Body (JSON) — all fields optional
{ "name": "Green 5A", "description": "...", "area_sqmeters": 510.0, "group_id": "new-group-id" }
Permanently removes the annotation.
{ "name": "Bunkers", "category": "course", "color": "#f5a623", "parent_id": null }
{ "name": "New Name", "color": "#00ff00", "category": "flight", "parent_id": "parent-uuid" }
⚠️ Also deletes all annotations within this group.
Returns full overlay metadata including transform parameters.
{ "name": "Sheet 1 As-built", "overlay_type": "image", "source_format": "pdf", "color": "#ff0000" }
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
}
Removes overlay record and associated image files.
Multipart file upload. Field name: file. Accepts PDF, PNG, JPG.
Multipart upload of browser-rendered PNG for PDF overlays.
Returns the rendered overlay image as image/png or image/jpeg.
Send a GeoJSON FeatureCollection in the request body. Creates annotation groups and features.
Multipart file upload. Field: file. DXF layers become annotation groups, entities become features. Query param ?epsg=6428 for coordinate reprojection.
Multipart file upload. Expects columns: name, lat/latitude, lng/longitude. Creates point annotations.
Returns a CSV download with all annotations and their measurements.
Multipart upload. Returns validation results without importing.
| Query Param | Type | Info |
|---|---|---|
| severity | string | high | medium | low | info |
| status | string | open | resolved |
| category | string | Disease | Pest | Weed | etc. |
| from_date | string | ISO date (YYYY-MM-DD) |
| to_date | string | ISO date |
| q | string | Full-text search |
| limit | int | Max results |
| offset | int | Pagination offset |
Multipart Fields
| Field | Type | Info |
|---|---|---|
| photo | file | required JPEG/PNG image |
| metadata | JSON | required 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.
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" }] }
{ "status": "resolved", "severity": "low", "description": "Fixed", "resolution_notes": "Applied fungicide" }
{ "ids": ["id1", "id2"], "update": { "status": "resolved" } }
Deletes observation record, photo file, and thumbnail.
Response
{ "total": 42, "open": 30, "resolved": 12, "high": 5, "medium": 10, "low": 15, "info": 12 }
Downloads all observations as a CSV file.
Returns observations as a GeoJSON FeatureCollection.
Returns the original JPEG image as image/jpeg.
Returns a 400px-wide JPEG thumbnail. Auto-generated on upload.
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.
🔒 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."
}
🔒 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
}
]
}
🔒 Requires superadmin JWT
Response
{ "status": "revoked", "id": "uuid" }
⚠️ Revocation is immediate — any active requests using this key will start failing.
✅ No auth required — safe for partners to call
Request Body
{ "key": "ti_a3f8b2c1d4e5..." }
Response
{ "valid": true }
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.
Path Parameters
| Param | Type | Info |
|---|---|---|
| course | string | required Course slug (e.g. castle_pines) |
| flight | string | required Flight ID (UUID) |
| layer | string | required Layer key or product type (ortho or castle_pines_ortho_5_9_25) |
| z / x / y | int | required 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
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.
Path Parameters
| Param | Type | Info |
|---|---|---|
| layer | string | required Layer type (e.g. ortho or dsm) |
| z / x / y | int | required 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
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
}
Kills and restarts rat-server with all discovered .rat containers.
Multipart upload. Field: file. Accepts .rat container files for tile serving.
Request Body (JSON)
{ "flight_id": "...", "product_type": "ortho", "container": "ortho.rat" }
Response
{ "job_id": "...", "status": "queued" }
| Query Param | Type | Info |
|---|---|---|
| file | string | required Filename to check |
Response
{ "exists": true, "file": "ortho.rat", "path": "/data/courses/..." }