{"openapi":"3.1.0","info":{"title":"ForestSens API","version":"0.1.0"},"paths":{"/v1/uploads":{"post":{"tags":["uploads"],"summary":"Create Upload","operationId":"create_upload_v1_uploads_post","parameters":[{"name":"X-Forwarded-Sub","in":"header","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"X-Forwarded-Sub"}},{"name":"X-Api-Key","in":"header","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"X-Api-Key"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/UploadCreateRequest"}}}},"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}},"get":{"tags":["uploads"],"summary":"List Uploads","description":"Past uploads for the caller's tenant, newest first -- lets a client\n(the frontend's upload-browsing page) pick an existing upload to\nsubmit a new batch against instead of re-uploading the same files.\nSame cursor-pagination shape as GET /v1/batches (services/batches.py's\nsibling endpoint), for one consistent pattern across the API.","operationId":"list_uploads_v1_uploads_get","parameters":[{"name":"status","in":"query","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Status"}},{"name":"limit","in":"query","required":false,"schema":{"type":"integer","maximum":100,"minimum":1,"default":20,"title":"Limit"}},{"name":"cursor","in":"query","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Cursor"}},{"name":"X-Forwarded-Sub","in":"header","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"X-Forwarded-Sub"}},{"name":"X-Api-Key","in":"header","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"X-Api-Key"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/v1/uploads/{upload_id}/complete":{"post":{"tags":["uploads"],"summary":"Complete Upload","operationId":"complete_upload_v1_uploads__upload_id__complete_post","parameters":[{"name":"upload_id","in":"path","required":true,"schema":{"type":"string","format":"uuid","title":"Upload Id"}},{"name":"X-Forwarded-Sub","in":"header","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"X-Forwarded-Sub"}},{"name":"X-Api-Key","in":"header","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"X-Api-Key"}}],"requestBody":{"content":{"application/json":{"schema":{"anyOf":[{"$ref":"#/components/schemas/UploadCompleteRequest"},{"type":"null"}],"title":"Payload"}}}},"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/v1/uploads/{upload_id}":{"get":{"tags":["uploads"],"summary":"Get Upload","operationId":"get_upload_v1_uploads__upload_id__get","parameters":[{"name":"upload_id","in":"path","required":true,"schema":{"type":"string","format":"uuid","title":"Upload Id"}},{"name":"X-Forwarded-Sub","in":"header","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"X-Forwarded-Sub"}},{"name":"X-Api-Key","in":"header","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"X-Api-Key"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}},"patch":{"tags":["uploads"],"summary":"Rename Upload","description":"The first update-in-place endpoint this API has ever had -- every\nother resource is create/read-only. Deliberately narrow: only `name`,\nnothing else about an upload is user-editable. RLS (via\nget_current_principal's set_current_tenant) is what actually stops\nrenaming another tenant's upload -- db.get returns None for a row\noutside the caller's tenant, same as every other by-id lookup here.","operationId":"rename_upload_v1_uploads__upload_id__patch","parameters":[{"name":"upload_id","in":"path","required":true,"schema":{"type":"string","format":"uuid","title":"Upload Id"}},{"name":"X-Forwarded-Sub","in":"header","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"X-Forwarded-Sub"}},{"name":"X-Api-Key","in":"header","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"X-Api-Key"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/UploadRenameRequest"}}}},"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}},"delete":{"tags":["uploads"],"summary":"Delete Upload Route","description":"Roadmap: resource-delete. Admin-gated (not just any tenant member,\nunlike every other route here) -- see require_admin's own docstring\nnaming this as one of its intended consumers. Never blocked by any\nbatch derived from this upload (2026-08-27, explicit ask) -- see\nservices/deletion.py's delete_upload for exactly what happens to\nthose instead (decoupled, never destroyed).","operationId":"delete_upload_route_v1_uploads__upload_id__delete","parameters":[{"name":"upload_id","in":"path","required":true,"schema":{"type":"string","format":"uuid","title":"Upload Id"}},{"name":"confirm_name","in":"query","required":true,"schema":{"type":"string","description":"The upload's own currently-displayed name (or its filenames-based fallback if unnamed), typed by the caller -- must match exactly. Same type-to-confirm step-up already shipped for operator principal-removal (confirm_email): this delete is real and irreversible (real Object Storage bytes + the DB row itself, unlike every other mutation on this router).","title":"Confirm Name"},"description":"The upload's own currently-displayed name (or its filenames-based fallback if unnamed), typed by the caller -- must match exactly. Same type-to-confirm step-up already shipped for operator principal-removal (confirm_email): this delete is real and irreversible (real Object Storage bytes + the DB row itself, unlike every other mutation on this router)."},{"name":"X-Forwarded-Sub","in":"header","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"X-Forwarded-Sub"}},{"name":"X-Api-Key","in":"header","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"X-Api-Key"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/v1/uploads/{upload_id}/files/{file_id}/multipart/start":{"post":{"tags":["uploads"],"summary":"Start Multipart Upload","description":"Idempotent: a fresh call starts a new multipart upload; a repeat\ncall (the resume path, after e.g. a closed tab on the same page) sees\nmultipart_upload_id already set and just reports the real, live parts\nalready landed instead of starting over.","operationId":"start_multipart_upload_v1_uploads__upload_id__files__file_id__multipart_start_post","parameters":[{"name":"upload_id","in":"path","required":true,"schema":{"type":"string","format":"uuid","title":"Upload Id"}},{"name":"file_id","in":"path","required":true,"schema":{"type":"string","format":"uuid","title":"File Id"}},{"name":"X-Forwarded-Sub","in":"header","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"X-Forwarded-Sub"}},{"name":"X-Api-Key","in":"header","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"X-Api-Key"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/v1/uploads/{upload_id}/files/{file_id}/multipart/parts/{part_number}":{"put":{"tags":["uploads"],"summary":"Upload Multipart Part","operationId":"upload_multipart_part_v1_uploads__upload_id__files__file_id__multipart_parts__part_number__put","parameters":[{"name":"upload_id","in":"path","required":true,"schema":{"type":"string","format":"uuid","title":"Upload Id"}},{"name":"file_id","in":"path","required":true,"schema":{"type":"string","format":"uuid","title":"File Id"}},{"name":"part_number","in":"path","required":true,"schema":{"type":"integer","title":"Part Number"}},{"name":"X-Forwarded-Sub","in":"header","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"X-Forwarded-Sub"}},{"name":"X-Api-Key","in":"header","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"X-Api-Key"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/v1/uploads/{upload_id}/files/{file_id}/multipart/commit":{"post":{"tags":["uploads"],"summary":"Commit Multipart Upload Route","description":"Never trusts a client-submitted parts list -- asks storage itself\n(list_multipart_parts) for the real, authoritative set of parts right\nbefore committing, so a client can't commit with a forged etag or a\npart it never actually finished uploading.","operationId":"commit_multipart_upload_route_v1_uploads__upload_id__files__file_id__multipart_commit_post","parameters":[{"name":"upload_id","in":"path","required":true,"schema":{"type":"string","format":"uuid","title":"Upload Id"}},{"name":"file_id","in":"path","required":true,"schema":{"type":"string","format":"uuid","title":"File Id"}},{"name":"X-Forwarded-Sub","in":"header","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"X-Forwarded-Sub"}},{"name":"X-Api-Key","in":"header","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"X-Api-Key"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/v1/uploads/{upload_id}/files/{file_id}/multipart/abort":{"post":{"tags":["uploads"],"summary":"Abort Multipart Upload Route","operationId":"abort_multipart_upload_route_v1_uploads__upload_id__files__file_id__multipart_abort_post","parameters":[{"name":"upload_id","in":"path","required":true,"schema":{"type":"string","format":"uuid","title":"Upload Id"}},{"name":"file_id","in":"path","required":true,"schema":{"type":"string","format":"uuid","title":"File Id"}},{"name":"X-Forwarded-Sub","in":"header","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"X-Forwarded-Sub"}},{"name":"X-Api-Key","in":"header","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"X-Api-Key"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/_dev/storage/{key}":{"put":{"tags":["dev-storage"],"summary":"Put Object","operationId":"put_object__dev_storage__key__put","parameters":[{"name":"key","in":"path","required":true,"schema":{"type":"string","title":"Key"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}},"get":{"tags":["dev-storage"],"summary":"Get Object","operationId":"get_object__dev_storage__key__get","parameters":[{"name":"key","in":"path","required":true,"schema":{"type":"string","title":"Key"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/v1/nodes":{"get":{"tags":["nodes"],"summary":"List Nodes","operationId":"list_nodes_v1_nodes_get","parameters":[{"name":"sense","in":"query","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Sense"}},{"name":"kind","in":"query","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Kind"}},{"name":"X-Forwarded-Sub","in":"header","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"X-Forwarded-Sub"}},{"name":"X-Api-Key","in":"header","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"X-Api-Key"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/v1/pipelines":{"get":{"tags":["pipelines"],"summary":"List Pipelines","operationId":"list_pipelines_v1_pipelines_get","parameters":[{"name":"sense","in":"query","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Sense"}},{"name":"X-Forwarded-Sub","in":"header","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"X-Forwarded-Sub"}},{"name":"X-Api-Key","in":"header","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"X-Api-Key"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/v1/senses":{"get":{"tags":["senses"],"summary":"List Senses","description":"The full catalog of Senses (DroneSens, PointSens, PlantSens, ...) --\na small, real lookup table, not hardcoded client-side, so a new Sens\nis visible to every client the moment it's seeded, not on the next\nfrontend deploy. Read-only, no pagination -- there are a handful of\nthese, not pages of them.","operationId":"list_senses_v1_senses_get","parameters":[{"name":"X-Forwarded-Sub","in":"header","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"X-Forwarded-Sub"}},{"name":"X-Api-Key","in":"header","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"X-Api-Key"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/v1/batches":{"post":{"tags":["batches"],"summary":"Create Batch","operationId":"create_batch_v1_batches_post","parameters":[{"name":"X-Forwarded-Sub","in":"header","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"X-Forwarded-Sub"}},{"name":"X-Api-Key","in":"header","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"X-Api-Key"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/BatchCreateRequest"}}}},"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}},"get":{"tags":["batches"],"summary":"List Batches","operationId":"list_batches_v1_batches_get","parameters":[{"name":"status","in":"query","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Status"}},{"name":"upload_id","in":"query","required":false,"schema":{"anyOf":[{"type":"string","format":"uuid"},{"type":"null"}],"title":"Upload Id"}},{"name":"limit","in":"query","required":false,"schema":{"type":"integer","maximum":100,"minimum":1,"default":20,"title":"Limit"}},{"name":"cursor","in":"query","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Cursor"}},{"name":"X-Forwarded-Sub","in":"header","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"X-Forwarded-Sub"}},{"name":"X-Api-Key","in":"header","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"X-Api-Key"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/v1/batches/{batch_id}/retry":{"post":{"tags":["batches"],"summary":"Retry Batch Route","description":"Manual, human-triggered retry -- see services/batches.py's\nretry_batch for the real reasoning (no automatic failure\nclassification; in-place resume skipping already-complete steps).","operationId":"retry_batch_route_v1_batches__batch_id__retry_post","parameters":[{"name":"batch_id","in":"path","required":true,"schema":{"type":"string","format":"uuid","title":"Batch Id"}},{"name":"X-Forwarded-Sub","in":"header","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"X-Forwarded-Sub"}},{"name":"X-Api-Key","in":"header","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"X-Api-Key"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/v1/batches/{batch_id}":{"get":{"tags":["batches"],"summary":"Get Batch","operationId":"get_batch_v1_batches__batch_id__get","parameters":[{"name":"batch_id","in":"path","required":true,"schema":{"type":"string","format":"uuid","title":"Batch Id"}},{"name":"X-Forwarded-Sub","in":"header","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"X-Forwarded-Sub"}},{"name":"X-Api-Key","in":"header","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"X-Api-Key"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}},"delete":{"tags":["batches"],"summary":"Delete Batch Route","description":"Roadmap: resource-delete. Admin-gated (not just any tenant member,\nunlike every other route here) -- see require_admin's own docstring\nnaming this as one of its intended consumers. Blocks (409) rather\nthan cascading if any of this batch's own output artifacts were\nconsumed as another *different* batch's input -- see\nservices/deletion.py's delete_batch for the real FK facts behind\nthat.","operationId":"delete_batch_route_v1_batches__batch_id__delete","parameters":[{"name":"batch_id","in":"path","required":true,"schema":{"type":"string","format":"uuid","title":"Batch Id"}},{"name":"confirm_name","in":"query","required":true,"schema":{"type":"string","description":"The batch's own currently-displayed name (its custom name, or its pipeline name if unnamed), typed by the caller -- must match exactly. Same type-to-confirm step-up already shipped for operator principal-removal (confirm_email): this delete is real and irreversible (real Object Storage bytes + the DB row itself, unlike every other mutation on this router).","title":"Confirm Name"},"description":"The batch's own currently-displayed name (its custom name, or its pipeline name if unnamed), typed by the caller -- must match exactly. Same type-to-confirm step-up already shipped for operator principal-removal (confirm_email): this delete is real and irreversible (real Object Storage bytes + the DB row itself, unlike every other mutation on this router)."},{"name":"X-Forwarded-Sub","in":"header","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"X-Forwarded-Sub"}},{"name":"X-Api-Key","in":"header","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"X-Api-Key"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}},"patch":{"tags":["batches"],"summary":"Rename Batch","description":"Roadmap: batch-names. Same shape as uploads.py's rename_upload --\nRLS (via get_current_principal's set_current_tenant) is what actually\nstops renaming another tenant's batch; db.get returns None for a row\noutside the caller's tenant, same as every other by-id lookup here.","operationId":"rename_batch_v1_batches__batch_id__patch","parameters":[{"name":"batch_id","in":"path","required":true,"schema":{"type":"string","format":"uuid","title":"Batch Id"}},{"name":"X-Forwarded-Sub","in":"header","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"X-Forwarded-Sub"}},{"name":"X-Api-Key","in":"header","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"X-Api-Key"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/BatchRenameRequest"}}}},"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/v1/batches/{batch_id}/artifacts":{"get":{"tags":["results"],"summary":"List Artifacts","operationId":"list_artifacts_v1_batches__batch_id__artifacts_get","parameters":[{"name":"batch_id","in":"path","required":true,"schema":{"type":"string","format":"uuid","title":"Batch Id"}},{"name":"step_id","in":"query","required":false,"schema":{"anyOf":[{"type":"string","format":"uuid"},{"type":"null"}],"title":"Step Id"}},{"name":"output_type","in":"query","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Output Type"}},{"name":"limit","in":"query","required":false,"schema":{"type":"integer","maximum":100,"minimum":1,"default":20,"title":"Limit"}},{"name":"cursor","in":"query","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Cursor"}},{"name":"X-Forwarded-Sub","in":"header","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"X-Forwarded-Sub"}},{"name":"X-Api-Key","in":"header","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"X-Api-Key"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/v1/batches/{batch_id}/detections":{"get":{"tags":["results"],"summary":"List Detections","operationId":"list_detections_v1_batches__batch_id__detections_get","parameters":[{"name":"batch_id","in":"path","required":true,"schema":{"type":"string","format":"uuid","title":"Batch Id"}},{"name":"step_id","in":"query","required":false,"schema":{"anyOf":[{"type":"string","format":"uuid"},{"type":"null"}],"title":"Step Id"}},{"name":"class","in":"query","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Class"}},{"name":"confidence_min","in":"query","required":false,"schema":{"anyOf":[{"type":"number","maximum":1,"minimum":0},{"type":"null"}],"title":"Confidence Min"}},{"name":"bbox","in":"query","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Bbox"}},{"name":"limit","in":"query","required":false,"schema":{"type":"integer","maximum":100,"minimum":1,"default":20,"title":"Limit"}},{"name":"cursor","in":"query","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Cursor"}},{"name":"X-Forwarded-Sub","in":"header","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"X-Forwarded-Sub"}},{"name":"X-Api-Key","in":"header","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"X-Api-Key"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/v1/batches/{batch_id}/segments":{"get":{"tags":["results"],"summary":"List Segments","operationId":"list_segments_v1_batches__batch_id__segments_get","parameters":[{"name":"batch_id","in":"path","required":true,"schema":{"type":"string","format":"uuid","title":"Batch Id"}},{"name":"step_id","in":"query","required":false,"schema":{"anyOf":[{"type":"string","format":"uuid"},{"type":"null"}],"title":"Step Id"}},{"name":"class","in":"query","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Class"}},{"name":"confidence_min","in":"query","required":false,"schema":{"anyOf":[{"type":"number","maximum":1,"minimum":0},{"type":"null"}],"title":"Confidence Min"}},{"name":"bbox","in":"query","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Bbox"}},{"name":"limit","in":"query","required":false,"schema":{"type":"integer","maximum":100,"minimum":1,"default":20,"title":"Limit"}},{"name":"cursor","in":"query","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Cursor"}},{"name":"X-Forwarded-Sub","in":"header","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"X-Forwarded-Sub"}},{"name":"X-Api-Key","in":"header","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"X-Api-Key"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/v1/batches/{batch_id}/linear-features":{"get":{"tags":["results"],"summary":"List Linear Features","operationId":"list_linear_features_v1_batches__batch_id__linear_features_get","parameters":[{"name":"batch_id","in":"path","required":true,"schema":{"type":"string","format":"uuid","title":"Batch Id"}},{"name":"step_id","in":"query","required":false,"schema":{"anyOf":[{"type":"string","format":"uuid"},{"type":"null"}],"title":"Step Id"}},{"name":"class","in":"query","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Class"}},{"name":"confidence_min","in":"query","required":false,"schema":{"anyOf":[{"type":"number","maximum":1,"minimum":0},{"type":"null"}],"title":"Confidence Min"}},{"name":"bbox","in":"query","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Bbox"}},{"name":"limit","in":"query","required":false,"schema":{"type":"integer","maximum":100,"minimum":1,"default":20,"title":"Limit"}},{"name":"cursor","in":"query","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Cursor"}},{"name":"X-Forwarded-Sub","in":"header","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"X-Forwarded-Sub"}},{"name":"X-Api-Key","in":"header","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"X-Api-Key"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/v1/batches/{batch_id}/tree-inventory":{"get":{"tags":["results"],"summary":"List Tree Inventory","operationId":"list_tree_inventory_v1_batches__batch_id__tree_inventory_get","parameters":[{"name":"batch_id","in":"path","required":true,"schema":{"type":"string","format":"uuid","title":"Batch Id"}},{"name":"step_id","in":"query","required":false,"schema":{"anyOf":[{"type":"string","format":"uuid"},{"type":"null"}],"title":"Step Id"}},{"name":"species","in":"query","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Species"}},{"name":"dbh_min","in":"query","required":false,"schema":{"anyOf":[{"type":"number"},{"type":"null"}],"title":"Dbh Min"}},{"name":"dbh_max","in":"query","required":false,"schema":{"anyOf":[{"type":"number"},{"type":"null"}],"title":"Dbh Max"}},{"name":"limit","in":"query","required":false,"schema":{"type":"integer","maximum":100,"minimum":1,"default":20,"title":"Limit"}},{"name":"cursor","in":"query","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Cursor"}},{"name":"X-Forwarded-Sub","in":"header","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"X-Forwarded-Sub"}},{"name":"X-Api-Key","in":"header","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"X-Api-Key"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/v1/principals/me":{"get":{"tags":["principals"],"summary":"Get My Principal","description":"The one place is_platform_operator is surfaced (see\nMyPrincipalOut's own docstring for why not on the base PrincipalOut).\nplatform_operators carries no RLS -- same reasoning\nrequire_platform_operator's own lookup in deps.py already relies on\n-- so this works fine under the normal get_db session, no\nget_operator_db needed for a single-row existence check.","operationId":"get_my_principal_v1_principals_me_get","parameters":[{"name":"X-Forwarded-Sub","in":"header","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"X-Forwarded-Sub"}},{"name":"X-Api-Key","in":"header","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"X-Api-Key"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}},"patch":{"tags":["principals"],"summary":"Rename My Principal","description":"Self-service only -- a principal can rename themselves, nothing\nelse about their own row. Same shape as uploads.py's rename_upload.","operationId":"rename_my_principal_v1_principals_me_patch","parameters":[{"name":"X-Forwarded-Sub","in":"header","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"X-Forwarded-Sub"}},{"name":"X-Api-Key","in":"header","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"X-Api-Key"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/PrincipalRenameRequest"}}}},"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/v1/principals":{"get":{"tags":["principals"],"summary":"List Principals","description":"Every principal in the caller's own tenant -- admin-only. RLS\nscopes this to the caller's tenant \"for free\", same reasoning\nalready established for every other by-tenant list in this API.","operationId":"list_principals_v1_principals_get","parameters":[{"name":"X-Forwarded-Sub","in":"header","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"X-Forwarded-Sub"}},{"name":"X-Api-Key","in":"header","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"X-Api-Key"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}},"post":{"tags":["principals"],"summary":"Create Principal","description":"Admin-driven account creation -- the real replacement for what's\nbeen a fully manual process (create the domain user by hand, then\nINSERT a principals row by hand over a bastion tunnel) up to this\npoint. See services/registration.py for the reusable core -- this\nroute is deliberately a thin wrapper around it, not where the real\nlogic lives, so a future self-service flow can call the exact same\nfunction.","operationId":"create_principal_v1_principals_post","parameters":[{"name":"X-Forwarded-Sub","in":"header","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"X-Forwarded-Sub"}},{"name":"X-Api-Key","in":"header","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"X-Api-Key"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/PrincipalCreateRequest"}}}},"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/v1/principals/{principal_id}/role":{"patch":{"tags":["principals"],"summary":"Update Principal Role","description":"Admin-only. Real guard, not hypothetical: refuses to demote the\ntenant's last remaining admin -- otherwise an admin could lock\neveryone (including themselves) out of admin functionality forever,\nwith no self-service way back in (see README.md's own note that\nonboarding/role changes are otherwise fully manual).","operationId":"update_principal_role_v1_principals__principal_id__role_patch","parameters":[{"name":"principal_id","in":"path","required":true,"schema":{"type":"string","format":"uuid","title":"Principal Id"}},{"name":"X-Forwarded-Sub","in":"header","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"X-Forwarded-Sub"}},{"name":"X-Api-Key","in":"header","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"X-Api-Key"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/PrincipalRoleUpdateRequest"}}}},"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/v1/principals/{principal_id}/api-keys":{"post":{"tags":["principals"],"summary":"Issue Api Key For Principal","description":"Admin-issued -- the credential path for a member who isn't\nself-serving one (this is what feeds python-client: a real external\nclient needs a key an admin hands them, not one they minted for\nthemselves via account settings). Same generate_api_key()/reveal-once\ncontract as api_keys.py's self-service POST /v1/api-keys; the only\ndifference is whose principal_id the key is stamped with -- see\nApiKey's own model docstring, no schema change needed.\n\nDeliberately reachable via X-Api-Key auth, not just a browser JWT\nsession (unlike require_platform_operator) -- a tenant admin needs to\nissue keys programmatically (their own customer-facing system handing\nnew keys to real customers), same as every other require_admin\nendpoint already allows. check_admin_issuance_rate_limit is the real\nmitigation for a leaked admin credential being used to mass-generate\nkeys, not an auth-method restriction.\n\nAn admin issuing a key for their own principal_id through this route\n(rather than the self-service one) is allowed, not special-cased or\nredirected -- update_principal_role has no self-exclusion either\n(beyond the last-admin guard, which doesn't apply here), so refusing\nit here would be an inconsistent extra rule for no real safety gain.","operationId":"issue_api_key_for_principal_v1_principals__principal_id__api_keys_post","parameters":[{"name":"principal_id","in":"path","required":true,"schema":{"type":"string","format":"uuid","title":"Principal Id"}},{"name":"X-Forwarded-Sub","in":"header","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"X-Forwarded-Sub"}},{"name":"X-Api-Key","in":"header","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"X-Api-Key"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiKeyCreateRequest"}}}},"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}},"get":{"tags":["principals"],"summary":"List Api Keys For Principal","description":"The admin roster view api_keys.py's list_api_keys docstring points\nto as \"the deferred round\" -- every key belonging to one member, not\njust the caller's own. RLS scopes principal_id resolution to the\ncaller's tenant (a cross-tenant id 404s here, same as /role above);\nApiKey rows are then narrowed to that one principal_id, the same\ntwo-layer scoping the self-service list already uses. Unfiltered on\nrevoked_at, matching that same self-service list's existing behavior.","operationId":"list_api_keys_for_principal_v1_principals__principal_id__api_keys_get","parameters":[{"name":"principal_id","in":"path","required":true,"schema":{"type":"string","format":"uuid","title":"Principal Id"}},{"name":"X-Forwarded-Sub","in":"header","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"X-Forwarded-Sub"}},{"name":"X-Api-Key","in":"header","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"X-Api-Key"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/v1/api-keys":{"post":{"tags":["api-keys"],"summary":"Create Api Key","description":"Self-service -- a principal can only ever create a key for\nthemselves (see migration 0007's docstring for why this is\nForestSens-native rather than IDCS-issued). The raw key is returned\nexactly once, right here; only its hash is persisted, so this response\nis the one and only chance to see it.","operationId":"create_api_key_v1_api_keys_post","parameters":[{"name":"X-Forwarded-Sub","in":"header","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"X-Forwarded-Sub"}},{"name":"X-Api-Key","in":"header","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"X-Api-Key"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiKeyCreateRequest"}}}},"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}},"get":{"tags":["api-keys"],"summary":"List Api Keys","description":"The caller's own keys only -- self-service, not an admin roster\n(that's the deferred admin-issued-keys round). RLS already scopes to\nthe caller's tenant; principal_id narrows further to \"mine\", the same\ntwo-layer scoping create/revoke both use.","operationId":"list_api_keys_v1_api_keys_get","parameters":[{"name":"X-Forwarded-Sub","in":"header","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"X-Forwarded-Sub"}},{"name":"X-Api-Key","in":"header","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"X-Api-Key"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/v1/api-keys/{key_id}":{"delete":{"tags":["api-keys"],"summary":"Revoke Api Key","description":"Soft revoke -- see migration 0007 for why (audit trail). Your own\nkey: self-service, unaudited, unchanged from before. Someone else's\nkey (even within the same tenant): only reachable at all if the\ncaller is an admin -- RLS on the db.get lookup below already makes a\ncross-tenant key_id invisible before this ownership check ever runs,\nso the tenant boundary needs no extra check here, same reasoning\nalready established for principals.py's /role. An admin's override\nrevoke IS audited (api_key.admin_revoked); a normal self-revoke still\nisn't, matching today's behavior exactly.","operationId":"revoke_api_key_v1_api_keys__key_id__delete","parameters":[{"name":"key_id","in":"path","required":true,"schema":{"type":"string","format":"uuid","title":"Key Id"}},{"name":"X-Forwarded-Sub","in":"header","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"X-Forwarded-Sub"}},{"name":"X-Api-Key","in":"header","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"X-Api-Key"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/v1/usage/summary":{"get":{"tags":["usage"],"summary":"Get Usage Summary","description":"Admin-gated, same precedent as GET /v1/principals -- RLS scopes the\nunderlying queries to the caller's own tenant \"for free\", so no\nexplicit tenant filter is needed beyond fetching the Tenant row\nitself.","operationId":"get_usage_summary_v1_usage_summary_get","parameters":[{"name":"X-Forwarded-Sub","in":"header","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"X-Forwarded-Sub"}},{"name":"X-Api-Key","in":"header","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"X-Api-Key"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/v1/operator/batches":{"get":{"tags":["operator"],"summary":"List Operator Batches","operationId":"list_operator_batches_v1_operator_batches_get","parameters":[{"name":"status","in":"query","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Status"}},{"name":"sense","in":"query","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Sense"}},{"name":"upload_id","in":"query","required":false,"schema":{"anyOf":[{"type":"string","format":"uuid"},{"type":"null"}],"title":"Upload Id"}},{"name":"limit","in":"query","required":false,"schema":{"type":"integer","maximum":100,"minimum":1,"default":20,"title":"Limit"}},{"name":"cursor","in":"query","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Cursor"}},{"name":"X-Forwarded-Sub","in":"header","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"X-Forwarded-Sub"}},{"name":"X-Api-Key","in":"header","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"X-Api-Key"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/v1/operator/batches/{batch_id}":{"get":{"tags":["operator"],"summary":"Get Operator Batch","operationId":"get_operator_batch_v1_operator_batches__batch_id__get","parameters":[{"name":"batch_id","in":"path","required":true,"schema":{"type":"string","format":"uuid","title":"Batch Id"}},{"name":"X-Forwarded-Sub","in":"header","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"X-Forwarded-Sub"}},{"name":"X-Api-Key","in":"header","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"X-Api-Key"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/v1/operator/batches/{batch_id}/detections":{"get":{"tags":["operator"],"summary":"Get Operator Batch Detections","operationId":"get_operator_batch_detections_v1_operator_batches__batch_id__detections_get","parameters":[{"name":"batch_id","in":"path","required":true,"schema":{"type":"string","format":"uuid","title":"Batch Id"}},{"name":"step_id","in":"query","required":false,"schema":{"anyOf":[{"type":"string","format":"uuid"},{"type":"null"}],"title":"Step Id"}},{"name":"class","in":"query","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Class"}},{"name":"confidence_min","in":"query","required":false,"schema":{"anyOf":[{"type":"number","maximum":1,"minimum":0},{"type":"null"}],"title":"Confidence Min"}},{"name":"bbox","in":"query","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Bbox"}},{"name":"limit","in":"query","required":false,"schema":{"type":"integer","maximum":100,"minimum":1,"default":20,"title":"Limit"}},{"name":"cursor","in":"query","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Cursor"}},{"name":"X-Forwarded-Sub","in":"header","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"X-Forwarded-Sub"}},{"name":"X-Api-Key","in":"header","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"X-Api-Key"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/v1/operator/batches/{batch_id}/segments":{"get":{"tags":["operator"],"summary":"Get Operator Batch Segments","operationId":"get_operator_batch_segments_v1_operator_batches__batch_id__segments_get","parameters":[{"name":"batch_id","in":"path","required":true,"schema":{"type":"string","format":"uuid","title":"Batch Id"}},{"name":"step_id","in":"query","required":false,"schema":{"anyOf":[{"type":"string","format":"uuid"},{"type":"null"}],"title":"Step Id"}},{"name":"class","in":"query","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Class"}},{"name":"confidence_min","in":"query","required":false,"schema":{"anyOf":[{"type":"number","maximum":1,"minimum":0},{"type":"null"}],"title":"Confidence Min"}},{"name":"bbox","in":"query","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Bbox"}},{"name":"limit","in":"query","required":false,"schema":{"type":"integer","maximum":100,"minimum":1,"default":20,"title":"Limit"}},{"name":"cursor","in":"query","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Cursor"}},{"name":"X-Forwarded-Sub","in":"header","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"X-Forwarded-Sub"}},{"name":"X-Api-Key","in":"header","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"X-Api-Key"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/v1/operator/batches/{batch_id}/linear-features":{"get":{"tags":["operator"],"summary":"Get Operator Batch Linear Features","operationId":"get_operator_batch_linear_features_v1_operator_batches__batch_id__linear_features_get","parameters":[{"name":"batch_id","in":"path","required":true,"schema":{"type":"string","format":"uuid","title":"Batch Id"}},{"name":"step_id","in":"query","required":false,"schema":{"anyOf":[{"type":"string","format":"uuid"},{"type":"null"}],"title":"Step Id"}},{"name":"class","in":"query","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Class"}},{"name":"confidence_min","in":"query","required":false,"schema":{"anyOf":[{"type":"number","maximum":1,"minimum":0},{"type":"null"}],"title":"Confidence Min"}},{"name":"bbox","in":"query","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Bbox"}},{"name":"limit","in":"query","required":false,"schema":{"type":"integer","maximum":100,"minimum":1,"default":20,"title":"Limit"}},{"name":"cursor","in":"query","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Cursor"}},{"name":"X-Forwarded-Sub","in":"header","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"X-Forwarded-Sub"}},{"name":"X-Api-Key","in":"header","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"X-Api-Key"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/v1/operator/batches/{batch_id}/tree-inventory":{"get":{"tags":["operator"],"summary":"Get Operator Batch Tree Inventory","operationId":"get_operator_batch_tree_inventory_v1_operator_batches__batch_id__tree_inventory_get","parameters":[{"name":"batch_id","in":"path","required":true,"schema":{"type":"string","format":"uuid","title":"Batch Id"}},{"name":"step_id","in":"query","required":false,"schema":{"anyOf":[{"type":"string","format":"uuid"},{"type":"null"}],"title":"Step Id"}},{"name":"species","in":"query","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Species"}},{"name":"dbh_min","in":"query","required":false,"schema":{"anyOf":[{"type":"number"},{"type":"null"}],"title":"Dbh Min"}},{"name":"dbh_max","in":"query","required":false,"schema":{"anyOf":[{"type":"number"},{"type":"null"}],"title":"Dbh Max"}},{"name":"limit","in":"query","required":false,"schema":{"type":"integer","maximum":100,"minimum":1,"default":20,"title":"Limit"}},{"name":"cursor","in":"query","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Cursor"}},{"name":"X-Forwarded-Sub","in":"header","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"X-Forwarded-Sub"}},{"name":"X-Api-Key","in":"header","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"X-Api-Key"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/v1/operator/uploads":{"get":{"tags":["operator"],"summary":"List Operator Uploads","operationId":"list_operator_uploads_v1_operator_uploads_get","parameters":[{"name":"status","in":"query","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Status"}},{"name":"limit","in":"query","required":false,"schema":{"type":"integer","maximum":100,"minimum":1,"default":20,"title":"Limit"}},{"name":"cursor","in":"query","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Cursor"}},{"name":"X-Forwarded-Sub","in":"header","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"X-Forwarded-Sub"}},{"name":"X-Api-Key","in":"header","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"X-Api-Key"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/v1/operator/uploads/{upload_id}":{"get":{"tags":["operator"],"summary":"Get Operator Upload","operationId":"get_operator_upload_v1_operator_uploads__upload_id__get","parameters":[{"name":"upload_id","in":"path","required":true,"schema":{"type":"string","format":"uuid","title":"Upload Id"}},{"name":"X-Forwarded-Sub","in":"header","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"X-Forwarded-Sub"}},{"name":"X-Api-Key","in":"header","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"X-Api-Key"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/v1/operator/principals":{"get":{"tags":["operator"],"summary":"List Operator Principals","operationId":"list_operator_principals_v1_operator_principals_get","parameters":[{"name":"limit","in":"query","required":false,"schema":{"type":"integer","maximum":100,"minimum":1,"default":20,"title":"Limit"}},{"name":"cursor","in":"query","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Cursor"}},{"name":"X-Forwarded-Sub","in":"header","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"X-Forwarded-Sub"}},{"name":"X-Api-Key","in":"header","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"X-Api-Key"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/v1/operator/principals/{principal_id}":{"delete":{"tags":["operator"],"summary":"Remove Operator Principal Access","description":"Real dual removal: deactivates the actual Identity Domain SCIM\nuser (login genuinely stops working) AND soft-flags this principals\nrow (access_removed_at) -- same \"soft, never a hard delete\" idiom\nrevoke_api_key already established, extended here to a genuinely\ncross-tenant action gated by require_platform_operator rather than\nper-principal ownership. Idempotent -- removing an already-removed\nprincipal just returns its current (already-removed) state. Every\ncall (idempotent or not) is recorded in admin_audit_log -- see\nservices/audit.py.","operationId":"remove_operator_principal_access_v1_operator_principals__principal_id__delete","parameters":[{"name":"principal_id","in":"path","required":true,"schema":{"type":"string","format":"uuid","title":"Principal Id"}},{"name":"confirm_email","in":"query","required":true,"schema":{"type":"string","description":"The target principal's own idp_subject (email), typed by the caller -- must match exactly. A lightweight step-up: cheap to add, and stops a scripted/blind DELETE-by-id from silently removing the wrong principal (this router is cross-tenant, so an id alone is easy to get wrong or paste from the wrong tab).","title":"Confirm Email"},"description":"The target principal's own idp_subject (email), typed by the caller -- must match exactly. A lightweight step-up: cheap to add, and stops a scripted/blind DELETE-by-id from silently removing the wrong principal (this router is cross-tenant, so an id alone is easy to get wrong or paste from the wrong tab)."},{"name":"force","in":"query","required":false,"schema":{"type":"boolean","default":false,"title":"Force"}},{"name":"X-Forwarded-Sub","in":"header","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"X-Forwarded-Sub"}},{"name":"X-Api-Key","in":"header","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"X-Api-Key"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/v1/operator/audit-log":{"get":{"tags":["operator"],"summary":"List Admin Audit Log","description":"The read half of services/audit.py's log_admin_action -- every\nadmin/operator mutation recorded so far, newest first. Same cursor-\npagination shape as list_operator_principals above. Operator-only for\nnow (no per-tenant view yet -- see AdminAuditLog's own docstring).","operationId":"list_admin_audit_log_v1_operator_audit_log_get","parameters":[{"name":"limit","in":"query","required":false,"schema":{"type":"integer","maximum":100,"minimum":1,"default":20,"title":"Limit"}},{"name":"cursor","in":"query","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Cursor"}},{"name":"X-Forwarded-Sub","in":"header","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"X-Forwarded-Sub"}},{"name":"X-Api-Key","in":"header","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"X-Api-Key"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/v1/operator/usage":{"get":{"tags":["operator"],"summary":"List Operator Usage","description":"Every tenant's own usage/quota headroom in one cross-tenant list --\nthe system-monitoring dashboard's per-tenant GPU/CPU drill-down\n(ROADMAP item 8, explicit ask). Reuses services/usage.py's\nbuild_usage_summary verbatim, once per tenant, against get_operator_db\n(RLS bypassed, so each call genuinely sees that tenant's own batches,\nnot just the caller's) -- the exact same computation\nGET /v1/usage/summary already gives a tenant admin for their own\ntenant, not a second calculation, so this view and real per-tenant\nquota enforcement can never disagree. Same cursor-pagination shape as\nthe other operator list endpoints, paginating over tenants rather\nthan the usual batch/principal/log rows.","operationId":"list_operator_usage_v1_operator_usage_get","parameters":[{"name":"limit","in":"query","required":false,"schema":{"type":"integer","maximum":100,"minimum":1,"default":20,"title":"Limit"}},{"name":"cursor","in":"query","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Cursor"}},{"name":"X-Forwarded-Sub","in":"header","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"X-Forwarded-Sub"}},{"name":"X-Api-Key","in":"header","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"X-Api-Key"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/{full_path}":{"get":{"summary":"Serve Spa","operationId":"serve_spa__full_path__get","parameters":[{"name":"full_path","in":"path","required":true,"schema":{"type":"string","title":"Full Path"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}}},"components":{"schemas":{"ApiKeyCreateRequest":{"properties":{"name":{"type":"string","title":"Name"}},"type":"object","required":["name"],"title":"ApiKeyCreateRequest"},"BatchCreateRequest":{"properties":{"pipeline_id":{"anyOf":[{"type":"string","format":"uuid"},{"type":"null"}],"title":"Pipeline Id"},"pipeline":{"anyOf":[{"$ref":"#/components/schemas/InlinePipeline"},{"type":"null"}]},"inputs":{"items":{"$ref":"#/components/schemas/BatchInputItem"},"type":"array","title":"Inputs"},"name":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Name"}},"type":"object","required":["inputs"],"title":"BatchCreateRequest"},"BatchInputItem":{"properties":{"slot":{"type":"string","title":"Slot"},"upload_id":{"anyOf":[{"type":"string","format":"uuid"},{"type":"null"}],"title":"Upload Id"},"artifact_id":{"anyOf":[{"type":"string","format":"uuid"},{"type":"null"}],"title":"Artifact Id"}},"type":"object","required":["slot"],"title":"BatchInputItem"},"BatchRenameRequest":{"properties":{"name":{"type":"string","title":"Name"}},"type":"object","required":["name"],"title":"BatchRenameRequest"},"HTTPValidationError":{"properties":{"detail":{"items":{"$ref":"#/components/schemas/ValidationError"},"type":"array","title":"Detail"}},"type":"object","title":"HTTPValidationError"},"InlinePipeline":{"properties":{"graph":{"items":{"additionalProperties":true,"type":"object"},"type":"array","title":"Graph"},"name":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Name"},"description":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Description"}},"type":"object","required":["graph"],"title":"InlinePipeline"},"PrincipalCreateRequest":{"properties":{"email":{"type":"string","title":"Email"},"display_name":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Display Name"},"role":{"type":"string","enum":["member","admin"],"title":"Role","default":"member"}},"type":"object","required":["email"],"title":"PrincipalCreateRequest"},"PrincipalRenameRequest":{"properties":{"display_name":{"type":"string","title":"Display Name"}},"type":"object","required":["display_name"],"title":"PrincipalRenameRequest"},"PrincipalRoleUpdateRequest":{"properties":{"role":{"type":"string","enum":["member","admin"],"title":"Role"}},"type":"object","required":["role"],"title":"PrincipalRoleUpdateRequest"},"UploadCompleteRequest":{"properties":{"output_type":{"type":"string","title":"Output Type","default":"raw_upload"},"format":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Format"}},"type":"object","title":"UploadCompleteRequest"},"UploadCreateRequest":{"properties":{"files":{"items":{"$ref":"#/components/schemas/UploadFileCreate"},"type":"array","title":"Files"},"name":{"type":"string","title":"Name"}},"type":"object","required":["files","name"],"title":"UploadCreateRequest"},"UploadFileCreate":{"properties":{"filename":{"type":"string","title":"Filename"},"mime_type":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Mime Type"}},"type":"object","required":["filename"],"title":"UploadFileCreate"},"UploadRenameRequest":{"properties":{"name":{"type":"string","title":"Name"}},"type":"object","required":["name"],"title":"UploadRenameRequest"},"ValidationError":{"properties":{"loc":{"items":{"anyOf":[{"type":"string"},{"type":"integer"}]},"type":"array","title":"Location"},"msg":{"type":"string","title":"Message"},"type":{"type":"string","title":"Error Type"},"input":{"title":"Input"},"ctx":{"type":"object","title":"Context"}},"type":"object","required":["loc","msg","type"],"title":"ValidationError"}}}}