{
  "openapi": "3.1.0",
  "info": {
    "title": "HalfPage API",
    "description": "The **HalfPage API** runs Cellpose-based cell segmentation on your microscopy\nimages programmatically — upload an image, run a prediction, and export the\nresulting cell masks, ROIs, and measurements.\n\n## Base URL\n\n```\nhttps://api.halfpagetechnologies.com/backend/api/v1\n```\n\nEvery path in this reference is relative to that base URL. A staging environment\nis available at `https://staging-api.halfpagetechnologies.com/backend/api/v1`.\n\n## Authentication\n\nAuthenticate every request with an API key in the `Authorization` header as a\nbearer token:\n\n```\nAuthorization: Bearer hp_live_xxxxxxxxxxxxxxxxxxxxxxxx\n```\n\nCreate and manage keys from the **API Keys** section of the HalfPage dashboard —\nkeys cannot be minted through the API. A key is scoped to the organization that\nowns it; every resource you create or read is confined to that organization.\nRequests without a valid key receive `401`; a valid key used against an endpoint\noutside the public product surface receives `403`.\n\n## Core workflow\n\nSegmentation runs asynchronously on GPU workers, so the API is poll-based:\n\n1. **Upload an image** (`POST /upload`) as `multipart/form-data` with a `file`\n   field. One call: the image record is created automatically and the response\n   returns it already `ready`, with the `image_id` for the next step.\n   *(Large files: `POST /image` with `resumable: true` returns presigned part\n   URLs — PUT the chunks, call `POST /upload/{image_id}/complete`, then poll\n   `GET /image/{image_id}` until `upload_status` is `ready`.)*\n2. **Pick a model** (`GET /models`) and **submit a prediction**\n   (`POST /job/prediction`) with the `image_id` and a `model_id`. This returns a\n   `job_id`.\n3. **Poll the job** (`GET /job/{job_id}`) until `status` is `COMPLETED`; the\n   response then carries a `segmentation_id`.\n4. **Export** results for that segmentation as CSV measurements\n   (`GET /export/{segmentation_id}/measurements.csv`), GeoJSON ROIs\n   (`.../rois.geojson`), or an ImageJ ROI archive (`.../rois.zip`). To collect a\n   whole project at once, `GET /export/project/{folder_id}/measurements.csv`\n   returns every analyzed image in it as one CSV.\n\n## Quotas\n\nUploads and analyses are metered against your plan. Exceeding your image\nstorage cap or monthly analysis cap returns `402` with a human-readable\n`detail` explaining the limit — upgrade your plan to raise it.\n",
    "version": "1.0.0"
  },
  "servers": [
    {
      "url": "https://api.halfpagetechnologies.com/backend",
      "description": "Production"
    },
    {
      "url": "https://staging-api.halfpagetechnologies.com/backend",
      "description": "Staging"
    }
  ],
  "paths": {
    "/api/v1/upload": {
      "post": {
        "tags": [
          "upload"
        ],
        "summary": "Upload an image (single request)",
        "description": "Upload a microscopy image in a single request (`multipart/form-data` with a\n`file` field) — the simplest way to get an image into HalfPage.\n\nNo prior setup is needed: the image record is created automatically, named\nafter the uploaded file. (To attach the pixels to a record you already\ncreated with `POST /image`, pass its id in the `Image-Id` header; a row\noutside your organization returns `404`.)\n\nThe file is streamed to storage and converted before the response returns,\nso the returned `image` is already `ready` — its `id` can go straight into\n`POST /job/prediction`. For files larger than a few hundred MB or unreliable\nconnections, prefer the resumable flow (`POST /image` with `resumable: true`,\nthen PUT the parts and call `/upload/{image_id}/complete`).",
        "operationId": "upload_file_api_v1_upload_post",
        "security": [
          {
            "HTTPBearer": []
          }
        ],
        "parameters": [
          {
            "name": "Image-Id",
            "in": "header",
            "required": false,
            "schema": {
              "anyOf": [
                {
                  "type": "string"
                },
                {
                  "type": "null"
                }
              ],
              "description": "Optional id of a pre-created image row to attach this file to. Omit it to have the record created automatically from the uploaded file.",
              "title": "Image-Id"
            },
            "description": "Optional id of a pre-created image row to attach this file to. Omit it to have the record created automatically from the uploaded file."
          }
        ],
        "responses": {
          "200": {
            "description": "Successful Response",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/UploadResult"
                }
              }
            }
          },
          "401": {
            "description": "Missing or invalid credentials. Supply a valid `Authorization: Bearer hp_live_...` API key.",
            "content": {
              "application/json": {
                "example": {
                  "detail": "Requires authentication"
                },
                "schema": {
                  "$ref": "#/components/schemas/ErrorResponse"
                }
              }
            }
          },
          "402": {
            "description": "Image storage quota exceeded for your plan. Upgrade to store more images.",
            "content": {
              "application/json": {
                "example": {
                  "detail": "You've reached your storage limit of 100 images. Upgrade to store more."
                },
                "schema": {
                  "$ref": "#/components/schemas/ErrorResponse"
                }
              }
            }
          },
          "404": {
            "description": "The `Image-Id` does not exist in your organization, or the connection dropped."
          },
          "413": {
            "description": "The uploaded file exceeds the 4 GB size limit."
          },
          "422": {
            "description": "The `file` form field is missing."
          }
        }
      }
    },
    "/api/v1/upload/{image_id}/init": {
      "post": {
        "tags": [
          "upload"
        ],
        "summary": "Start a resumable upload",
        "description": "Open an S3 multipart upload for an existing image row and return presigned\npart URLs. PUT each part directly to its URL, then call `/complete`. Gates\nownership and the org image cap before any bytes are stored (`402` when the\nstorage cap is reached).\n\nShortcut: `POST /image` with `resumable: true` creates the record and returns\nthis same presigned plan in one call — this endpoint is only needed when the\nrecord was created separately (or to re-issue URLs for a retry).",
        "operationId": "init_multipart_upload_api_v1_upload__image_id__init_post",
        "security": [
          {
            "HTTPBearer": []
          }
        ],
        "parameters": [
          {
            "name": "image_id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string",
              "title": "Image Id"
            }
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/InitUploadRequest"
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Successful Response",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/InitUploadResponse"
                }
              }
            }
          },
          "401": {
            "description": "Missing or invalid credentials. Supply a valid `Authorization: Bearer hp_live_...` API key.",
            "content": {
              "application/json": {
                "example": {
                  "detail": "Requires authentication"
                },
                "schema": {
                  "$ref": "#/components/schemas/ErrorResponse"
                }
              }
            }
          },
          "402": {
            "description": "Image storage quota exceeded for your plan. Upgrade to store more images.",
            "content": {
              "application/json": {
                "example": {
                  "detail": "You've reached your storage limit of 100 images. Upgrade to store more."
                },
                "schema": {
                  "$ref": "#/components/schemas/ErrorResponse"
                }
              }
            }
          },
          "403": {
            "description": "The resource belongs to another organization.",
            "content": {
              "application/json": {
                "example": {
                  "detail": "You do not have access to this image"
                },
                "schema": {
                  "$ref": "#/components/schemas/ErrorResponse"
                }
              }
            }
          },
          "404": {
            "description": "The resource does not exist, or is not in your organization.",
            "content": {
              "application/json": {
                "example": {
                  "detail": "Image with id 3fa85f64-5717-4562-b3fc-2c963f66afa6 not found"
                },
                "schema": {
                  "$ref": "#/components/schemas/ErrorResponse"
                }
              }
            }
          },
          "409": {
            "description": "The image is not in a state that can start an upload."
          },
          "422": {
            "description": "Invalid `file_size`."
          },
          "501": {
            "description": "The storage backend does not support multipart uploads (local dev only)."
          }
        }
      }
    },
    "/api/v1/upload/{image_id}/complete": {
      "post": {
        "tags": [
          "upload"
        ],
        "summary": "Finish a resumable upload",
        "description": "Assemble a resumable upload from its uploaded parts, mark the image\n`processing`, and kick off async conversion. Returns `202` — conversion runs\noff the request path, so poll `GET /image/{image_id}` until `ready`.\n\nNo body is needed: the server discovers the uploaded parts (and their ETags)\nfrom storage. Pass `parts` only to assemble an explicit subset.",
        "operationId": "complete_multipart_upload_api_v1_upload__image_id__complete_post",
        "security": [
          {
            "HTTPBearer": []
          }
        ],
        "parameters": [
          {
            "name": "image_id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string",
              "title": "Image Id"
            }
          }
        ],
        "requestBody": {
          "content": {
            "application/json": {
              "schema": {
                "anyOf": [
                  {
                    "$ref": "#/components/schemas/CompleteUploadRequest"
                  },
                  {
                    "type": "null"
                  }
                ],
                "title": "Body"
              }
            }
          }
        },
        "responses": {
          "202": {
            "description": "Parts assembled; image processing runs asynchronously.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/MultipartCompleteResponse"
                }
              }
            }
          },
          "401": {
            "description": "Missing or invalid credentials. Supply a valid `Authorization: Bearer hp_live_...` API key.",
            "content": {
              "application/json": {
                "example": {
                  "detail": "Requires authentication"
                },
                "schema": {
                  "$ref": "#/components/schemas/ErrorResponse"
                }
              }
            }
          },
          "403": {
            "description": "The resource belongs to another organization.",
            "content": {
              "application/json": {
                "example": {
                  "detail": "You do not have access to this image"
                },
                "schema": {
                  "$ref": "#/components/schemas/ErrorResponse"
                }
              }
            }
          },
          "404": {
            "description": "The resource does not exist, or is not in your organization.",
            "content": {
              "application/json": {
                "example": {
                  "detail": "Image with id 3fa85f64-5717-4562-b3fc-2c963f66afa6 not found"
                },
                "schema": {
                  "$ref": "#/components/schemas/ErrorResponse"
                }
              }
            }
          },
          "409": {
            "description": "The upload is not in a completable state (or no parts were uploaded)."
          },
          "422": {
            "description": "Validation Error",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/HTTPValidationError"
                }
              }
            }
          }
        }
      }
    },
    "/api/v1/upload/{image_id}/abort": {
      "post": {
        "tags": [
          "upload"
        ],
        "summary": "Abort a resumable upload",
        "description": "Abort an in-flight resumable upload: discard any uploaded chunks from\nstorage and mark the image `failed`. The image record itself remains and\nstill counts against your storage cap — `DELETE /image/{image_id}` frees\nthe slot.",
        "operationId": "abort_multipart_upload_api_v1_upload__image_id__abort_post",
        "security": [
          {
            "HTTPBearer": []
          }
        ],
        "parameters": [
          {
            "name": "image_id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string",
              "title": "Image Id"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Successful Response",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/MultipartAbortResponse"
                }
              }
            }
          },
          "401": {
            "description": "Missing or invalid credentials. Supply a valid `Authorization: Bearer hp_live_...` API key.",
            "content": {
              "application/json": {
                "example": {
                  "detail": "Requires authentication"
                },
                "schema": {
                  "$ref": "#/components/schemas/ErrorResponse"
                }
              }
            }
          },
          "403": {
            "description": "The resource belongs to another organization.",
            "content": {
              "application/json": {
                "example": {
                  "detail": "You do not have access to this image"
                },
                "schema": {
                  "$ref": "#/components/schemas/ErrorResponse"
                }
              }
            }
          },
          "404": {
            "description": "The resource does not exist, or is not in your organization.",
            "content": {
              "application/json": {
                "example": {
                  "detail": "Image with id 3fa85f64-5717-4562-b3fc-2c963f66afa6 not found"
                },
                "schema": {
                  "$ref": "#/components/schemas/ErrorResponse"
                }
              }
            }
          },
          "422": {
            "description": "Validation Error",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/HTTPValidationError"
                }
              }
            }
          }
        }
      }
    },
    "/api/v1/job/{job_id}": {
      "get": {
        "tags": [
          "job"
        ],
        "summary": "Get job status",
        "description": "Poll a job's status after submitting a prediction or training job.\n\nKeep polling until `status` is `COMPLETED` (or `FAILED`). For a completed\nprediction the response carries the `segmentation_id` of the produced\nsegmentation — feed that into the `/export/*` and `/segmentation/*`\nendpoints. A non-UUID `job_id` is rejected with `422` before it reaches the\ndatabase.",
        "operationId": "get_job_api_v1_job__job_id__get",
        "security": [
          {
            "HTTPBearer": []
          }
        ],
        "parameters": [
          {
            "name": "job_id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string",
              "format": "uuid",
              "title": "Job Id"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Successful Response",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/JobStatusResponse"
                },
                "examples": {
                  "running": {
                    "summary": "Still running (poll again)",
                    "value": {
                      "id": "b1e5c7d2-9a4f-4c3b-8e2d-1f6a7b8c9d0e",
                      "status": "STARTED",
                      "job_type": "prediction",
                      "image_id": "3fa85f64-5717-4562-b3fc-2c963f66afa6",
                      "model_id": "c0ffee00-1234-5678-9abc-def012345678",
                      "pipeline_id": "a2b3c4d5-6e7f-4a5b-8c9d-0e1f2a3b4c5d",
                      "created_at": "2026-07-01T12:00:00Z"
                    }
                  },
                  "completed": {
                    "summary": "Completed — segmentation_id available",
                    "value": {
                      "id": "b1e5c7d2-9a4f-4c3b-8e2d-1f6a7b8c9d0e",
                      "status": "COMPLETED",
                      "job_type": "prediction",
                      "image_id": "3fa85f64-5717-4562-b3fc-2c963f66afa6",
                      "model_id": "c0ffee00-1234-5678-9abc-def012345678",
                      "pipeline_id": "a2b3c4d5-6e7f-4a5b-8c9d-0e1f2a3b4c5d",
                      "created_at": "2026-07-01T12:00:00Z",
                      "segmentation_id": "7d9e2f10-4a5b-4c6d-8e9f-0a1b2c3d4e5f"
                    }
                  }
                }
              }
            }
          },
          "401": {
            "description": "Missing or invalid credentials. Supply a valid `Authorization: Bearer hp_live_...` API key.",
            "content": {
              "application/json": {
                "example": {
                  "detail": "Requires authentication"
                },
                "schema": {
                  "$ref": "#/components/schemas/ErrorResponse"
                }
              }
            }
          },
          "404": {
            "description": "The resource does not exist, or is not in your organization.",
            "content": {
              "application/json": {
                "example": {
                  "detail": "Image with id 3fa85f64-5717-4562-b3fc-2c963f66afa6 not found"
                },
                "schema": {
                  "$ref": "#/components/schemas/ErrorResponse"
                }
              }
            }
          },
          "422": {
            "description": "The path id is not a valid UUID (rejected before it reaches the database).",
            "content": {
              "application/json": {
                "example": {
                  "detail": "Input should be a valid UUID"
                },
                "schema": {
                  "$ref": "#/components/schemas/ErrorResponse"
                }
              }
            }
          }
        }
      }
    },
    "/api/v1/job/prediction": {
      "post": {
        "tags": [
          "job"
        ],
        "summary": "Submit a prediction job",
        "description": "Start an asynchronous segmentation job for a ready image.\n\nReturns immediately with a `job_id`; the segmentation runs on a GPU worker.\nPoll `GET /job/{job_id}` until it is `COMPLETED`, then read the\n`segmentation_id` off that response. Starting a job counts against your\nplan's monthly analysis quota (`402` when exceeded). The `image_id` and\n`model_id` are org-scoped: an id outside your organization returns `404`.",
        "operationId": "submit_prediction_job_api_v1_job_prediction_post",
        "requestBody": {
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/PredictionJobRequest"
              }
            }
          },
          "required": true
        },
        "responses": {
          "200": {
            "description": "Successful Response",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/SubmitJobResult"
                }
              }
            }
          },
          "400": {
            "description": "You already have a running job — wait for it to finish."
          },
          "401": {
            "description": "Missing or invalid credentials. Supply a valid `Authorization: Bearer hp_live_...` API key.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResponse"
                },
                "example": {
                  "detail": "Requires authentication"
                }
              }
            }
          },
          "402": {
            "description": "Monthly analysis quota exceeded for your plan. Upgrade to run more analyses.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResponse"
                },
                "example": {
                  "detail": "You've reached your monthly analysis limit. Upgrade to run more."
                }
              }
            }
          },
          "404": {
            "description": "The resource does not exist, or is not in your organization.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResponse"
                },
                "example": {
                  "detail": "Image with id 3fa85f64-5717-4562-b3fc-2c963f66afa6 not found"
                }
              }
            }
          },
          "410": {
            "description": "The selected model's weights have been cleaned up — retrain to recover."
          },
          "422": {
            "description": "Validation Error",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/HTTPValidationError"
                }
              }
            }
          }
        },
        "security": [
          {
            "HTTPBearer": []
          }
        ]
      }
    },
    "/api/v1/job/training": {
      "post": {
        "tags": [
          "job"
        ],
        "summary": "Submit a training job",
        "description": "Start an asynchronous job that fine-tunes a custom model on segmented\nimages. Returns a `job_id`; poll `GET /job/{job_id}` until `COMPLETED`. The\ntrained model then appears in `GET /models` for use in future predictions.\nThe base `model_id` and every `image_id` are org-scoped: an id outside your\norganization returns `404`.",
        "operationId": "submit_training_job_api_v1_job_training_post",
        "requestBody": {
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/TrainingJobRequest"
              }
            }
          },
          "required": true
        },
        "responses": {
          "200": {
            "description": "Successful Response",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/SubmitJobResult"
                }
              }
            }
          },
          "400": {
            "description": "You already have a running job — wait for it to finish."
          },
          "401": {
            "description": "Missing or invalid credentials. Supply a valid `Authorization: Bearer hp_live_...` API key.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResponse"
                },
                "example": {
                  "detail": "Requires authentication"
                }
              }
            }
          },
          "402": {
            "description": "Monthly analysis quota exceeded for your plan. Upgrade to run more analyses.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResponse"
                },
                "example": {
                  "detail": "You've reached your monthly analysis limit. Upgrade to run more."
                }
              }
            }
          },
          "404": {
            "description": "The resource does not exist, or is not in your organization.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResponse"
                },
                "example": {
                  "detail": "Image with id 3fa85f64-5717-4562-b3fc-2c963f66afa6 not found"
                }
              }
            }
          },
          "422": {
            "description": "Validation Error",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/HTTPValidationError"
                }
              }
            }
          }
        },
        "security": [
          {
            "HTTPBearer": []
          }
        ]
      }
    },
    "/api/v1/image": {
      "post": {
        "tags": [
          "image"
        ],
        "summary": "Create an image record",
        "description": "Create an image record for the resumable upload flow.\n\nPass `resumable: true` to open the upload in the same call: the response then\ncarries `upload` with presigned part URLs — PUT each chunk of your file to\nits URL, call `POST /upload/{image_id}/complete`, and poll\n`GET /image/{image_id}` until `upload_status` is `ready`.\n\n(For files up to a few hundred MB the single-request `POST /upload` is\nsimpler still — it creates the record for you and returns it `ready`.)\n\nThe image counts against your plan's storage cap the moment it is created, so\nthis returns `402` when the cap is reached — even before any bytes are\nuploaded.",
        "operationId": "create_image_api_v1_image_post",
        "requestBody": {
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/CreateImageRequest"
              }
            }
          },
          "required": true
        },
        "responses": {
          "201": {
            "description": "The image record was created; upload the pixels next.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/CreateImageResponse"
                },
                "example": {
                  "id": "3fa85f64",
                  "name": "sample.tif",
                  "upload_status": "uploading",
                  "image_type": "TIFF",
                  "size": 4194304,
                  "created_at": "2026-07-16T12:00:00Z",
                  "upload": {
                    "upload_id": "2~aBcD3fGhIjKlMnOpQrStUvWxYz",
                    "part_size": 33554432,
                    "urls": [
                      {
                        "part_number": 1,
                        "url": "https://halfpage.s3.amazonaws.com/...&partNumber=1"
                      }
                    ]
                  }
                }
              }
            }
          },
          "401": {
            "description": "Missing or invalid credentials. Supply a valid `Authorization: Bearer hp_live_...` API key.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResponse"
                },
                "example": {
                  "detail": "Requires authentication"
                }
              }
            }
          },
          "402": {
            "description": "Image storage quota exceeded for your plan. Upgrade to store more images.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResponse"
                },
                "example": {
                  "detail": "You've reached your storage limit of 100 images. Upgrade to store more."
                }
              }
            }
          },
          "404": {
            "description": "The resource does not exist, or is not in your organization.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResponse"
                },
                "example": {
                  "detail": "Image with id 3fa85f64-5717-4562-b3fc-2c963f66afa6 not found"
                }
              }
            }
          },
          "422": {
            "description": "The request body is invalid (e.g. missing `name`/`size`, or a bad `size` with `resumable`)."
          },
          "501": {
            "description": "`resumable: true` but storage does not support multipart uploads (local dev only)."
          }
        },
        "security": [
          {
            "HTTPBearer": []
          }
        ]
      }
    },
    "/api/v1/image/{image_id}": {
      "get": {
        "tags": [
          "image"
        ],
        "summary": "Get image status",
        "description": "Fetch an image's upload lifecycle and metadata.\n\nAfter an upload, poll this until `upload_status` is `ready` — conversion\nruns off the request path, so a freshly uploaded image is not immediately\nusable. Only a `ready` image can be passed to `POST /job/prediction`. A\n`failed` status carries the reason in `upload_error`.",
        "operationId": "get_image_api_v1_image__image_id__get",
        "security": [
          {
            "HTTPBearer": []
          }
        ],
        "parameters": [
          {
            "name": "image_id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string",
              "title": "Image Id"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Successful Response",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ImageStatusResponse"
                }
              }
            }
          },
          "401": {
            "description": "Missing or invalid credentials. Supply a valid `Authorization: Bearer hp_live_...` API key.",
            "content": {
              "application/json": {
                "example": {
                  "detail": "Requires authentication"
                },
                "schema": {
                  "$ref": "#/components/schemas/ErrorResponse"
                }
              }
            }
          },
          "404": {
            "description": "The resource does not exist, or is not in your organization.",
            "content": {
              "application/json": {
                "example": {
                  "detail": "Image with id 3fa85f64-5717-4562-b3fc-2c963f66afa6 not found"
                },
                "schema": {
                  "$ref": "#/components/schemas/ErrorResponse"
                }
              }
            }
          },
          "422": {
            "description": "Validation Error",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/HTTPValidationError"
                }
              }
            }
          }
        }
      },
      "delete": {
        "tags": [
          "image"
        ],
        "summary": "Delete an image",
        "description": "Permanently delete an image and all of its derived storage (converted\nTIFF/PNG, thumbnail, and any training artifacts). Scoped to your\norganization — an image outside it returns `404`, exactly like the reads.",
        "operationId": "delete_image_api_v1_image__image_id__delete",
        "security": [
          {
            "HTTPBearer": []
          }
        ],
        "parameters": [
          {
            "name": "image_id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string",
              "title": "Image Id"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Successful Response",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/DeleteImageResponse"
                }
              }
            }
          },
          "401": {
            "description": "Missing or invalid credentials. Supply a valid `Authorization: Bearer hp_live_...` API key.",
            "content": {
              "application/json": {
                "example": {
                  "detail": "Requires authentication"
                },
                "schema": {
                  "$ref": "#/components/schemas/ErrorResponse"
                }
              }
            }
          },
          "404": {
            "description": "The resource does not exist, or is not in your organization.",
            "content": {
              "application/json": {
                "example": {
                  "detail": "Image with id 3fa85f64-5717-4562-b3fc-2c963f66afa6 not found"
                },
                "schema": {
                  "$ref": "#/components/schemas/ErrorResponse"
                }
              }
            }
          },
          "422": {
            "description": "Validation Error",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/HTTPValidationError"
                }
              }
            }
          }
        }
      }
    },
    "/api/v1/segmentation/{segmentation_id}": {
      "get": {
        "tags": [
          "segmentation"
        ],
        "summary": "Get segmentation metadata",
        "description": "Fetch a segmentation's metadata: its source, whether its annotations are\n`ready`, and its ROI (`annotation_count`).\n\nYou get a `segmentation_id` from a completed prediction job\n(`GET /job/{job_id}`). A non-UUID `segmentation_id` is rejected with `422`\nbefore it reaches the database.",
        "operationId": "get_segmentation_api_v1_segmentation__segmentation_id__get",
        "security": [
          {
            "HTTPBearer": []
          }
        ],
        "parameters": [
          {
            "name": "segmentation_id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string",
              "format": "uuid",
              "title": "Segmentation Id"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Successful Response",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/SegmentationMetadataResponse"
                }
              }
            }
          },
          "401": {
            "description": "Missing or invalid credentials. Supply a valid `Authorization: Bearer hp_live_...` API key.",
            "content": {
              "application/json": {
                "example": {
                  "detail": "Requires authentication"
                },
                "schema": {
                  "$ref": "#/components/schemas/ErrorResponse"
                }
              }
            }
          },
          "404": {
            "description": "The resource does not exist, or is not in your organization.",
            "content": {
              "application/json": {
                "example": {
                  "detail": "Image with id 3fa85f64-5717-4562-b3fc-2c963f66afa6 not found"
                },
                "schema": {
                  "$ref": "#/components/schemas/ErrorResponse"
                }
              }
            }
          },
          "422": {
            "description": "The path id is not a valid UUID (rejected before it reaches the database).",
            "content": {
              "application/json": {
                "example": {
                  "detail": "Input should be a valid UUID"
                },
                "schema": {
                  "$ref": "#/components/schemas/ErrorResponse"
                }
              }
            }
          }
        }
      }
    },
    "/api/v1/segmentation/{segmentation_id}/generate-masks": {
      "post": {
        "tags": [
          "segmentation"
        ],
        "summary": "Generate masks from annotations",
        "description": "Rasterize a segmentation's ROI annotations into a pixel mask and store it.\n\nRecomputes the mask and confluency from the segmentation's current polygons —\ncall after editing annotations to refresh the exportable mask. Scoped to your\norganization — a segmentation outside it returns `404`.",
        "operationId": "generate_masks_api_v1_segmentation__segmentation_id__generate_masks_post",
        "security": [
          {
            "HTTPBearer": []
          }
        ],
        "parameters": [
          {
            "name": "segmentation_id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string",
              "title": "Segmentation Id"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Successful Response",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/GenerateMasksResponse"
                }
              }
            }
          },
          "401": {
            "description": "Missing or invalid credentials. Supply a valid `Authorization: Bearer hp_live_...` API key.",
            "content": {
              "application/json": {
                "example": {
                  "detail": "Requires authentication"
                },
                "schema": {
                  "$ref": "#/components/schemas/ErrorResponse"
                }
              }
            }
          },
          "404": {
            "description": "The resource does not exist, or is not in your organization.",
            "content": {
              "application/json": {
                "example": {
                  "detail": "Image with id 3fa85f64-5717-4562-b3fc-2c963f66afa6 not found"
                },
                "schema": {
                  "$ref": "#/components/schemas/ErrorResponse"
                }
              }
            }
          },
          "422": {
            "description": "Validation Error",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/HTTPValidationError"
                }
              }
            }
          }
        }
      }
    },
    "/api/v1/segmentation/{parent_segmentation_id}/refine": {
      "post": {
        "tags": [
          "segmentation"
        ],
        "summary": "Create a refinement",
        "description": "Fork a new, editable segmentation from an existing one, seeded with a copy\nof its annotations. Use this to iterate on a model prediction without\nmutating the original — the new segmentation's `parent_segmentation_id`\npoints back to the source. Scoped to your organization — a parent\nsegmentation outside it returns `404`.",
        "operationId": "refine_segmentation_api_v1_segmentation__parent_segmentation_id__refine_post",
        "security": [
          {
            "HTTPBearer": []
          }
        ],
        "parameters": [
          {
            "name": "parent_segmentation_id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string",
              "title": "Parent Segmentation Id"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Successful Response",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/SegmentationMetadataResponse"
                }
              }
            }
          },
          "401": {
            "description": "Missing or invalid credentials. Supply a valid `Authorization: Bearer hp_live_...` API key.",
            "content": {
              "application/json": {
                "example": {
                  "detail": "Requires authentication"
                },
                "schema": {
                  "$ref": "#/components/schemas/ErrorResponse"
                }
              }
            }
          },
          "404": {
            "description": "The resource does not exist, or is not in your organization.",
            "content": {
              "application/json": {
                "example": {
                  "detail": "Image with id 3fa85f64-5717-4562-b3fc-2c963f66afa6 not found"
                },
                "schema": {
                  "$ref": "#/components/schemas/ErrorResponse"
                }
              }
            }
          },
          "422": {
            "description": "Validation Error",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/HTTPValidationError"
                }
              }
            }
          }
        }
      }
    },
    "/api/v1/export/project/{folder_id}/measurements.csv": {
      "get": {
        "tags": [
          "export"
        ],
        "summary": "Export project measurements (CSV)",
        "description": "Download per-cell measurements for a whole project as a single CSV — every\nanalyzed image in the project, one row per detected ROI.\n\nEach image contributes its most recent completed segmentation; images that\nhave never been analyzed contribute no rows. Use the `image_name` and\n`segmentation_id` columns to split the file back out per image. Returned as\na file attachment.",
        "operationId": "export_project_measurements_csv_api_v1_export_project__folder_id__measurements_csv_get",
        "security": [
          {
            "HTTPBearer": []
          }
        ],
        "parameters": [
          {
            "name": "folder_id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string",
              "format": "uuid",
              "title": "Folder Id"
            }
          },
          {
            "name": "columns",
            "in": "query",
            "required": false,
            "schema": {
              "anyOf": [
                {
                  "type": "array",
                  "items": {
                    "type": "string"
                  }
                },
                {
                  "type": "null"
                }
              ],
              "description": "Measurement columns to include, repeated once per column. Omit for all columns. Valid columns: image_name, image_width, image_height, segmentation_id, segmentation_source, model_name, confluency, roi_index, roi_id, cell_type, tags, entity_type, area_px, perimeter_px, centroid_x, centroid_y, bbox_x, bbox_y, bbox_w, bbox_h.",
              "examples": [
                [
                  "image_name",
                  "roi_id",
                  "cell_type",
                  "area_px"
                ]
              ],
              "title": "Columns"
            },
            "description": "Measurement columns to include, repeated once per column. Omit for all columns. Valid columns: image_name, image_width, image_height, segmentation_id, segmentation_source, model_name, confluency, roi_index, roi_id, cell_type, tags, entity_type, area_px, perimeter_px, centroid_x, centroid_y, bbox_x, bbox_y, bbox_w, bbox_h."
          }
        ],
        "responses": {
          "200": {
            "description": "A CSV of per-cell measurements for every analyzed image in the project.",
            "content": {
              "text/csv": {
                "schema": {
                  "type": "string",
                  "format": "binary"
                }
              }
            }
          },
          "400": {
            "description": "The `columns` selection names a column this export does not produce.",
            "content": {
              "application/json": {
                "example": {
                  "detail": "Unknown export column(s): area_um. Valid columns are: image_name, image_width, ..."
                },
                "schema": {
                  "$ref": "#/components/schemas/ErrorResponse"
                }
              }
            }
          },
          "401": {
            "description": "Missing or invalid credentials. Supply a valid `Authorization: Bearer hp_live_...` API key.",
            "content": {
              "application/json": {
                "example": {
                  "detail": "Requires authentication"
                },
                "schema": {
                  "$ref": "#/components/schemas/ErrorResponse"
                }
              }
            }
          },
          "404": {
            "description": "The resource does not exist, or is not in your organization.",
            "content": {
              "application/json": {
                "example": {
                  "detail": "Image with id 3fa85f64-5717-4562-b3fc-2c963f66afa6 not found"
                },
                "schema": {
                  "$ref": "#/components/schemas/ErrorResponse"
                }
              }
            }
          },
          "422": {
            "description": "Validation Error",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/HTTPValidationError"
                }
              }
            }
          }
        }
      }
    },
    "/api/v1/export/{segmentation_id}/measurements.csv": {
      "get": {
        "tags": [
          "export"
        ],
        "summary": "Export measurements (CSV)",
        "description": "Download per-cell measurements for a completed segmentation as CSV — one\nrow per detected ROI, with morphometric columns. Returned as a file\nattachment.",
        "operationId": "export_measurements_csv_api_v1_export__segmentation_id__measurements_csv_get",
        "security": [
          {
            "HTTPBearer": []
          }
        ],
        "parameters": [
          {
            "name": "segmentation_id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string",
              "title": "Segmentation Id"
            }
          },
          {
            "name": "columns",
            "in": "query",
            "required": false,
            "schema": {
              "anyOf": [
                {
                  "type": "array",
                  "items": {
                    "type": "string"
                  }
                },
                {
                  "type": "null"
                }
              ],
              "description": "Measurement columns to include, repeated once per column. Omit for all columns. Valid columns: image_name, image_width, image_height, segmentation_id, segmentation_source, model_name, confluency, roi_index, roi_id, cell_type, tags, entity_type, area_px, perimeter_px, centroid_x, centroid_y, bbox_x, bbox_y, bbox_w, bbox_h.",
              "examples": [
                [
                  "image_name",
                  "roi_id",
                  "cell_type",
                  "area_px"
                ]
              ],
              "title": "Columns"
            },
            "description": "Measurement columns to include, repeated once per column. Omit for all columns. Valid columns: image_name, image_width, image_height, segmentation_id, segmentation_source, model_name, confluency, roi_index, roi_id, cell_type, tags, entity_type, area_px, perimeter_px, centroid_x, centroid_y, bbox_x, bbox_y, bbox_w, bbox_h."
          }
        ],
        "responses": {
          "200": {
            "description": "A CSV of per-cell measurements, one row per ROI.",
            "content": {
              "text/csv": {
                "schema": {
                  "type": "string",
                  "format": "binary"
                }
              }
            }
          },
          "400": {
            "description": "The `columns` selection names a column this export does not produce.",
            "content": {
              "application/json": {
                "example": {
                  "detail": "Unknown export column(s): area_um. Valid columns are: image_name, image_width, ..."
                },
                "schema": {
                  "$ref": "#/components/schemas/ErrorResponse"
                }
              }
            }
          },
          "401": {
            "description": "Missing or invalid credentials. Supply a valid `Authorization: Bearer hp_live_...` API key.",
            "content": {
              "application/json": {
                "example": {
                  "detail": "Requires authentication"
                },
                "schema": {
                  "$ref": "#/components/schemas/ErrorResponse"
                }
              }
            }
          },
          "404": {
            "description": "The resource does not exist, or is not in your organization.",
            "content": {
              "application/json": {
                "example": {
                  "detail": "Image with id 3fa85f64-5717-4562-b3fc-2c963f66afa6 not found"
                },
                "schema": {
                  "$ref": "#/components/schemas/ErrorResponse"
                }
              }
            }
          },
          "422": {
            "description": "Validation Error",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/HTTPValidationError"
                }
              }
            }
          }
        }
      }
    },
    "/api/v1/export/{segmentation_id}/rois.geojson": {
      "get": {
        "tags": [
          "export"
        ],
        "summary": "Export ROIs (GeoJSON)",
        "description": "Download a segmentation's ROIs as a GeoJSON FeatureCollection, one polygon\nfeature per cell in image pixel coordinates. Returned as a file attachment.",
        "operationId": "export_geojson_api_v1_export__segmentation_id__rois_geojson_get",
        "security": [
          {
            "HTTPBearer": []
          }
        ],
        "parameters": [
          {
            "name": "segmentation_id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string",
              "title": "Segmentation Id"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "A GeoJSON FeatureCollection of ROI polygons in image pixel coordinates.",
            "content": {
              "application/geo+json": {
                "schema": {
                  "type": "string",
                  "format": "binary"
                }
              }
            }
          },
          "401": {
            "description": "Missing or invalid credentials. Supply a valid `Authorization: Bearer hp_live_...` API key.",
            "content": {
              "application/json": {
                "example": {
                  "detail": "Requires authentication"
                },
                "schema": {
                  "$ref": "#/components/schemas/ErrorResponse"
                }
              }
            }
          },
          "404": {
            "description": "The resource does not exist, or is not in your organization.",
            "content": {
              "application/json": {
                "example": {
                  "detail": "Image with id 3fa85f64-5717-4562-b3fc-2c963f66afa6 not found"
                },
                "schema": {
                  "$ref": "#/components/schemas/ErrorResponse"
                }
              }
            }
          },
          "422": {
            "description": "Validation Error",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/HTTPValidationError"
                }
              }
            }
          }
        }
      }
    },
    "/api/v1/export/{segmentation_id}/rois.zip": {
      "get": {
        "tags": [
          "export"
        ],
        "summary": "Export ROIs (ImageJ ZIP)",
        "description": "Download a segmentation's ROIs as a ZIP of ImageJ-compatible `.roi` files,\nready to open in Fiji/ImageJ's ROI Manager. Returned as a file attachment.",
        "operationId": "export_imagej_roi_zip_api_v1_export__segmentation_id__rois_zip_get",
        "security": [
          {
            "HTTPBearer": []
          }
        ],
        "parameters": [
          {
            "name": "segmentation_id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string",
              "title": "Segmentation Id"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "A ZIP archive of ImageJ-compatible `.roi` files.",
            "content": {
              "application/zip": {
                "schema": {
                  "type": "string",
                  "format": "binary"
                }
              }
            }
          },
          "401": {
            "description": "Missing or invalid credentials. Supply a valid `Authorization: Bearer hp_live_...` API key.",
            "content": {
              "application/json": {
                "example": {
                  "detail": "Requires authentication"
                },
                "schema": {
                  "$ref": "#/components/schemas/ErrorResponse"
                }
              }
            }
          },
          "404": {
            "description": "The resource does not exist, or is not in your organization.",
            "content": {
              "application/json": {
                "example": {
                  "detail": "Image with id 3fa85f64-5717-4562-b3fc-2c963f66afa6 not found"
                },
                "schema": {
                  "$ref": "#/components/schemas/ErrorResponse"
                }
              }
            }
          },
          "422": {
            "description": "Validation Error",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/HTTPValidationError"
                }
              }
            }
          }
        }
      }
    },
    "/api/v1/models": {
      "get": {
        "tags": [
          "models"
        ],
        "summary": "List available models",
        "description": "List the segmentation models available to your organization.\n\nIncludes every shared public base model (e.g. `cpsam`) plus the custom\nmodels your organization has trained. Use a returned `id` as the `model_id`\nwhen submitting a prediction with `POST /job/prediction`.",
        "operationId": "list_models_api_v1_models_get",
        "responses": {
          "200": {
            "description": "Successful Response",
            "content": {
              "application/json": {
                "schema": {
                  "items": {
                    "$ref": "#/components/schemas/ModelSummary"
                  },
                  "type": "array",
                  "title": "Response List Models Api V1 Models Get"
                }
              }
            }
          },
          "401": {
            "description": "Missing or invalid credentials. Supply a valid `Authorization: Bearer hp_live_...` API key.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResponse"
                },
                "example": {
                  "detail": "Requires authentication"
                }
              }
            }
          },
          "404": {
            "description": "Not found"
          }
        },
        "security": [
          {
            "HTTPBearer": []
          }
        ]
      }
    }
  },
  "components": {
    "schemas": {
      "AnnotationProcessingStatus": {
        "type": "string",
        "enum": [
          "creating",
          "ready"
        ],
        "title": "AnnotationProcessingStatus"
      },
      "CompleteUploadRequest": {
        "properties": {
          "parts": {
            "items": {
              "$ref": "#/components/schemas/CompletedPart"
            },
            "type": "array",
            "title": "Parts",
            "description": "Uploaded parts to assemble. Usually omitted — the server discovers them from storage.",
            "examples": [
              [
                {
                  "etag": "\"9a0364b9e99bb480dd25e1f0284c8555\"",
                  "part_number": 1
                }
              ]
            ]
          }
        },
        "type": "object",
        "title": "CompleteUploadRequest",
        "description": "Optional body: with no body (or an empty `parts`), the server asks storage\nwhich parts were uploaded — clients don't need to track ETags. Send `parts`\nonly to assemble an explicit subset."
      },
      "CompletedPart": {
        "properties": {
          "part_number": {
            "type": "integer",
            "title": "Part Number"
          },
          "etag": {
            "type": "string",
            "title": "Etag"
          }
        },
        "type": "object",
        "required": [
          "part_number",
          "etag"
        ],
        "title": "CompletedPart"
      },
      "CreateImageRequest": {
        "properties": {
          "name": {
            "type": "string",
            "title": "Name",
            "description": "Filename or label for the image.",
            "examples": [
              "sample.tif"
            ]
          },
          "size": {
            "type": "integer",
            "title": "Size",
            "description": "Total size of the image file in bytes.",
            "examples": [
              4194304
            ]
          },
          "image_type": {
            "$ref": "#/components/schemas/ImageType",
            "description": "Image format. Only `TIFF` is currently supported.",
            "default": "TIFF"
          },
          "additional_info": {
            "additionalProperties": true,
            "type": "object",
            "title": "Additional Info",
            "description": "Arbitrary JSON metadata to store alongside the image."
          },
          "folder_id": {
            "anyOf": [
              {
                "type": "string",
                "format": "uuid"
              },
              {
                "type": "null"
              }
            ],
            "title": "Folder Id",
            "description": "Optional folder to place the image in; it must belong to your organization. API clients typically have no folders and omit this."
          },
          "resumable": {
            "type": "boolean",
            "title": "Resumable",
            "description": "Set to `true` to also open a resumable upload in the same call: the response then carries `upload` with presigned part URLs sized for `size` (so `size` must be exact). PUT each chunk to its URL, then call `POST /upload/{image_id}/complete` — no separate `/init` call needed.",
            "default": false
          }
        },
        "type": "object",
        "required": [
          "name",
          "size"
        ],
        "title": "CreateImageRequest",
        "description": "The metadata for a new image record. The pixels are uploaded separately;\n`size` is the total byte size of the file you will upload. Set `resumable`\nto also receive the presigned upload plan in the same response.\nServer-controlled fields (`id`, `created_by`, `organization_id`,\n`upload_status`) are not accepted here.",
        "examples": [
          {
            "name": "sample.tif",
            "size": 4194304
          }
        ]
      },
      "CreateImageResponse": {
        "properties": {
          "id": {
            "type": "string",
            "title": "Id"
          },
          "name": {
            "type": "string",
            "title": "Name"
          },
          "upload_status": {
            "$ref": "#/components/schemas/UploadStatus"
          },
          "image_type": {
            "$ref": "#/components/schemas/ImageType"
          },
          "size": {
            "type": "integer",
            "title": "Size"
          },
          "shape": {
            "anyOf": [
              {
                "items": {
                  "type": "integer"
                },
                "type": "array"
              },
              {
                "type": "null"
              }
            ],
            "title": "Shape"
          },
          "folder_id": {
            "anyOf": [
              {
                "type": "string",
                "format": "uuid"
              },
              {
                "type": "null"
              }
            ],
            "title": "Folder Id"
          },
          "created_at": {
            "type": "string",
            "format": "date-time",
            "title": "Created At"
          },
          "processed_at": {
            "anyOf": [
              {
                "type": "string",
                "format": "date-time"
              },
              {
                "type": "null"
              }
            ],
            "title": "Processed At"
          },
          "upload_error": {
            "anyOf": [
              {
                "type": "string"
              },
              {
                "type": "null"
              }
            ],
            "title": "Upload Error"
          },
          "upload": {
            "anyOf": [
              {
                "$ref": "#/components/schemas/InitUploadResponse"
              },
              {
                "type": "null"
              }
            ]
          }
        },
        "type": "object",
        "required": [
          "id",
          "name",
          "upload_status",
          "image_type",
          "size",
          "created_at"
        ],
        "title": "CreateImageResponse",
        "description": "The created image record, plus — when `resumable: true` was requested —\nthe presigned upload plan under `upload` (otherwise `null`).",
        "examples": [
          {
            "created_at": "2026-07-01T12:00:00Z",
            "id": "3fa85f64-5717-4562-b3fc-2c963f66afa6",
            "image_type": "TIFF",
            "name": "sample.tif",
            "processed_at": "2026-07-01T12:00:05Z",
            "shape": [
              512,
              512,
              3
            ],
            "size": 4194304,
            "upload_status": "ready"
          }
        ]
      },
      "DeleteImageResponse": {
        "properties": {
          "success": {
            "type": "boolean",
            "title": "Success",
            "default": true
          }
        },
        "type": "object",
        "title": "DeleteImageResponse",
        "description": "Acknowledgement that an image and its derived storage were deleted."
      },
      "ErrorResponse": {
        "properties": {
          "detail": {
            "type": "string",
            "title": "Detail",
            "examples": [
              "Job with id 11111111-1111-1111-1111-111111111111 not found"
            ]
          }
        },
        "type": "object",
        "required": [
          "detail"
        ],
        "title": "ErrorResponse",
        "description": "The body returned for a handled error: a single human-readable\n``detail`` string."
      },
      "GenerateMasksResponse": {
        "properties": {
          "segmentation_id": {
            "type": "string",
            "title": "Segmentation Id",
            "examples": [
              "7d9e2f10-4a5b-4c6d-8e9f-0a1b2c3d4e5f"
            ]
          },
          "num_cells": {
            "type": "integer",
            "title": "Num Cells",
            "examples": [
              128
            ]
          },
          "confluency": {
            "type": "number",
            "title": "Confluency",
            "examples": [
              63.4
            ]
          }
        },
        "type": "object",
        "required": [
          "segmentation_id",
          "num_cells",
          "confluency"
        ],
        "title": "GenerateMasksResponse",
        "description": "Result of rasterizing a segmentation's annotations into a pixel mask."
      },
      "HTTPValidationError": {
        "properties": {
          "detail": {
            "items": {
              "$ref": "#/components/schemas/ValidationError"
            },
            "type": "array",
            "title": "Detail"
          }
        },
        "type": "object",
        "title": "HTTPValidationError"
      },
      "ImageStatusResponse": {
        "properties": {
          "id": {
            "type": "string",
            "title": "Id"
          },
          "name": {
            "type": "string",
            "title": "Name"
          },
          "upload_status": {
            "$ref": "#/components/schemas/UploadStatus"
          },
          "image_type": {
            "$ref": "#/components/schemas/ImageType"
          },
          "size": {
            "type": "integer",
            "title": "Size"
          },
          "shape": {
            "anyOf": [
              {
                "items": {
                  "type": "integer"
                },
                "type": "array"
              },
              {
                "type": "null"
              }
            ],
            "title": "Shape"
          },
          "folder_id": {
            "anyOf": [
              {
                "type": "string",
                "format": "uuid"
              },
              {
                "type": "null"
              }
            ],
            "title": "Folder Id"
          },
          "created_at": {
            "type": "string",
            "format": "date-time",
            "title": "Created At"
          },
          "processed_at": {
            "anyOf": [
              {
                "type": "string",
                "format": "date-time"
              },
              {
                "type": "null"
              }
            ],
            "title": "Processed At"
          },
          "upload_error": {
            "anyOf": [
              {
                "type": "string"
              },
              {
                "type": "null"
              }
            ],
            "title": "Upload Error"
          }
        },
        "type": "object",
        "required": [
          "id",
          "name",
          "upload_status",
          "image_type",
          "size",
          "created_at"
        ],
        "title": "ImageStatusResponse",
        "description": "An image's upload lifecycle and metadata — enough to know when a freshly\nuploaded image is `ready` to run a prediction against.",
        "examples": [
          {
            "created_at": "2026-07-01T12:00:00Z",
            "id": "3fa85f64-5717-4562-b3fc-2c963f66afa6",
            "image_type": "TIFF",
            "name": "sample.tif",
            "processed_at": "2026-07-01T12:00:05Z",
            "shape": [
              512,
              512,
              3
            ],
            "size": 4194304,
            "upload_status": "ready"
          }
        ]
      },
      "ImageType": {
        "type": "string",
        "enum": [
          "TIFF"
        ],
        "title": "ImageType"
      },
      "InitUploadRequest": {
        "properties": {
          "file_size": {
            "type": "integer",
            "title": "File Size",
            "description": "Total size of the image in bytes.",
            "examples": [
              104857600
            ]
          }
        },
        "type": "object",
        "required": [
          "file_size"
        ],
        "title": "InitUploadRequest"
      },
      "InitUploadResponse": {
        "properties": {
          "upload_id": {
            "type": "string",
            "title": "Upload Id"
          },
          "part_size": {
            "type": "integer",
            "title": "Part Size"
          },
          "urls": {
            "items": {
              "$ref": "#/components/schemas/PartUrlOut"
            },
            "type": "array",
            "title": "Urls"
          }
        },
        "type": "object",
        "required": [
          "upload_id",
          "part_size",
          "urls"
        ],
        "title": "InitUploadResponse",
        "description": "The presigned multipart plan: PUT each chunk of the file to its `url`\n(in part order), then call `/complete` — no body needed, the server\nverifies the uploaded parts with storage directly.\n\nThe internal S3 object key is deliberately not exposed: the presigned `url`s\nalready encode everything the client needs, and the key layout is a\nserver-side implementation detail we keep off the public contract.",
        "examples": [
          {
            "part_size": 33554432,
            "upload_id": "2~aBcD3fGhIjKlMnOpQrStUvWxYz",
            "urls": [
              {
                "part_number": 1,
                "url": "https://halfpage.s3.amazonaws.com/...&partNumber=1"
              },
              {
                "part_number": 2,
                "url": "https://halfpage.s3.amazonaws.com/...&partNumber=2"
              }
            ]
          }
        ]
      },
      "JobStatus": {
        "type": "string",
        "enum": [
          "SUBMITTED",
          "STARTED",
          "COMPLETED",
          "FAILED"
        ],
        "title": "JobStatus"
      },
      "JobStatusResponse": {
        "properties": {
          "id": {
            "type": "string",
            "title": "Id"
          },
          "status": {
            "$ref": "#/components/schemas/JobStatus"
          },
          "job_type": {
            "$ref": "#/components/schemas/JobType"
          },
          "image_id": {
            "type": "string",
            "title": "Image Id"
          },
          "model_id": {
            "type": "string",
            "title": "Model Id"
          },
          "pipeline_id": {
            "anyOf": [
              {
                "type": "string"
              },
              {
                "type": "null"
              }
            ],
            "title": "Pipeline Id"
          },
          "parent_job_id": {
            "anyOf": [
              {
                "type": "string"
              },
              {
                "type": "null"
              }
            ],
            "title": "Parent Job Id"
          },
          "created_at": {
            "type": "string",
            "format": "date-time",
            "title": "Created At"
          },
          "segmentation_id": {
            "anyOf": [
              {
                "type": "string"
              },
              {
                "type": "null"
              }
            ],
            "title": "Segmentation Id"
          }
        },
        "type": "object",
        "required": [
          "id",
          "status",
          "job_type",
          "image_id",
          "model_id",
          "created_at"
        ],
        "title": "JobStatusResponse",
        "description": "A job's polling snapshot: status plus the identifiers a caller needs to\nfetch its result. `segmentation_id` is populated only once the (prediction)\njob is COMPLETED and its segmentation exists.",
        "examples": [
          {
            "created_at": "2026-07-01T12:00:00Z",
            "id": "b1e5c7d2-9a4f-4c3b-8e2d-1f6a7b8c9d0e",
            "image_id": "3fa85f64-5717-4562-b3fc-2c963f66afa6",
            "job_type": "prediction",
            "model_id": "c0ffee00-1234-5678-9abc-def012345678",
            "pipeline_id": "a2b3c4d5-6e7f-4a5b-8c9d-0e1f2a3b4c5d",
            "segmentation_id": "7d9e2f10-4a5b-4c6d-8e9f-0a1b2c3d4e5f",
            "status": "COMPLETED"
          }
        ]
      },
      "JobType": {
        "type": "string",
        "enum": [
          "prediction",
          "training"
        ],
        "title": "JobType"
      },
      "ModelSummary": {
        "properties": {
          "id": {
            "type": "string",
            "title": "Id"
          },
          "name": {
            "type": "string",
            "title": "Name"
          },
          "description": {
            "anyOf": [
              {
                "type": "string"
              },
              {
                "type": "null"
              }
            ],
            "title": "Description"
          },
          "is_public": {
            "type": "boolean",
            "title": "Is Public"
          },
          "created_at": {
            "type": "string",
            "format": "date-time",
            "title": "Created At"
          }
        },
        "type": "object",
        "required": [
          "id",
          "name",
          "is_public",
          "created_at"
        ],
        "title": "ModelSummary",
        "description": "A model the caller can pick for a prediction job. `is_public` marks a\nshared base model; the rest are the org's own saved custom models.",
        "examples": [
          {
            "created_at": "2026-01-15T09:30:00Z",
            "description": "Cellpose-SAM generalist base model",
            "id": "c0ffee00-1234-5678-9abc-def012345678",
            "is_public": true,
            "name": "cpsam"
          }
        ]
      },
      "MultipartAbortResponse": {
        "properties": {
          "status": {
            "type": "string",
            "title": "Status",
            "examples": [
              "failed"
            ]
          }
        },
        "type": "object",
        "required": [
          "status"
        ],
        "title": "MultipartAbortResponse",
        "description": "Acknowledgement that the in-flight upload was aborted."
      },
      "MultipartCompleteResponse": {
        "properties": {
          "status": {
            "type": "string",
            "title": "Status",
            "examples": [
              "processing"
            ]
          }
        },
        "type": "object",
        "required": [
          "status"
        ],
        "title": "MultipartCompleteResponse",
        "description": "Acknowledgement that the parts were assembled and async processing began."
      },
      "PartUrlOut": {
        "properties": {
          "part_number": {
            "type": "integer",
            "title": "Part Number"
          },
          "url": {
            "type": "string",
            "title": "Url"
          }
        },
        "type": "object",
        "required": [
          "part_number",
          "url"
        ],
        "title": "PartUrlOut"
      },
      "PredictionJobRequest": {
        "properties": {
          "model_id": {
            "type": "string",
            "title": "Model Id",
            "examples": [
              "c0ffee00-1234-5678-9abc-def012345678"
            ]
          },
          "image_id": {
            "type": "string",
            "title": "Image Id",
            "examples": [
              "3fa85f64-5717-4562-b3fc-2c963f66afa6"
            ]
          }
        },
        "type": "object",
        "required": [
          "model_id",
          "image_id"
        ],
        "title": "PredictionJobRequest",
        "description": "Run a model over one uploaded image. `image_id` must be `ready` and\n`model_id` must be one of the ids returned by `GET /models`."
      },
      "SegmentationMetadataResponse": {
        "properties": {
          "id": {
            "type": "string",
            "title": "Id"
          },
          "image_id": {
            "type": "string",
            "title": "Image Id"
          },
          "source_type": {
            "$ref": "#/components/schemas/SegmentationSourceType"
          },
          "annotation_processing_status": {
            "$ref": "#/components/schemas/AnnotationProcessingStatus"
          },
          "annotation_count": {
            "type": "integer",
            "title": "Annotation Count"
          },
          "parent_segmentation_id": {
            "anyOf": [
              {
                "type": "string"
              },
              {
                "type": "null"
              }
            ],
            "title": "Parent Segmentation Id"
          },
          "model_id": {
            "anyOf": [
              {
                "type": "string"
              },
              {
                "type": "null"
              }
            ],
            "title": "Model Id"
          },
          "pipeline_id": {
            "anyOf": [
              {
                "type": "string"
              },
              {
                "type": "null"
              }
            ],
            "title": "Pipeline Id"
          },
          "job_id": {
            "anyOf": [
              {
                "type": "string"
              },
              {
                "type": "null"
              }
            ],
            "title": "Job Id"
          },
          "created_at": {
            "type": "string",
            "format": "date-time",
            "title": "Created At"
          }
        },
        "type": "object",
        "required": [
          "id",
          "image_id",
          "source_type",
          "annotation_processing_status",
          "annotation_count",
          "created_at"
        ],
        "title": "SegmentationMetadataResponse",
        "description": "A segmentation's metadata: where it came from, whether its annotations are\nready, and its ROI count. `annotation_processing_status == ready` is the\nsignal the mask/annotations are usable.\n\nThe mask's storage location is intentionally omitted: it is an internal S3\npath (bucket + key layout) with no client value, and results are downloaded\nthrough the `export/*` endpoints. Keeping it server-side avoids leaking the\nbucket name and key scheme to public API-key callers.",
        "examples": [
          {
            "annotation_count": 128,
            "annotation_processing_status": "ready",
            "created_at": "2026-07-01T12:00:10Z",
            "id": "7d9e2f10-4a5b-4c6d-8e9f-0a1b2c3d4e5f",
            "image_id": "3fa85f64-5717-4562-b3fc-2c963f66afa6",
            "job_id": "b1e5c7d2-9a4f-4c3b-8e2d-1f6a7b8c9d0e",
            "model_id": "c0ffee00-1234-5678-9abc-def012345678",
            "pipeline_id": "a2b3c4d5-6e7f-4a5b-8c9d-0e1f2a3b4c5d",
            "source_type": "model_prediction"
          }
        ]
      },
      "SegmentationSourceType": {
        "type": "string",
        "enum": [
          "model_prediction",
          "refinement"
        ],
        "title": "SegmentationSourceType"
      },
      "SubmitJobResult": {
        "properties": {
          "status": {
            "type": "string",
            "enum": [
              "submitted",
              "skipped"
            ],
            "title": "Status"
          },
          "job_id": {
            "type": "string",
            "title": "Job Id"
          },
          "reason": {
            "anyOf": [
              {
                "type": "string"
              },
              {
                "type": "null"
              }
            ],
            "title": "Reason"
          },
          "task_id": {
            "anyOf": [
              {
                "type": "string"
              },
              {
                "type": "null"
              }
            ],
            "title": "Task Id"
          },
          "runpod_job_id": {
            "anyOf": [
              {
                "type": "string"
              },
              {
                "type": "null"
              }
            ],
            "title": "Runpod Job Id"
          }
        },
        "type": "object",
        "required": [
          "status",
          "job_id"
        ],
        "title": "SubmitJobResult",
        "description": "The acknowledgement returned when a job is accepted onto the queue.\n`status` is `submitted` for a fresh job or `skipped` when an identical job\nis already running. Poll `GET /job/{job_id}` for progress.",
        "examples": [
          {
            "job_id": "b1e5c7d2-9a4f-4c3b-8e2d-1f6a7b8c9d0e",
            "runpod_job_id": "runpod-8f2a1c",
            "status": "submitted"
          }
        ]
      },
      "TrainingJobRequest": {
        "properties": {
          "model_id": {
            "type": "string",
            "title": "Model Id",
            "examples": [
              "c0ffee00-1234-5678-9abc-def012345678"
            ]
          },
          "image_ids": {
            "items": {
              "type": "string"
            },
            "type": "array",
            "title": "Image Ids",
            "examples": [
              [
                "3fa85f64-5717-4562-b3fc-2c963f66afa6",
                "5c1d2e3f-4a5b-6c7d-8e9f-0a1b2c3d4e5f"
              ]
            ]
          }
        },
        "type": "object",
        "required": [
          "model_id",
          "image_ids"
        ],
        "title": "TrainingJobRequest",
        "description": "Fine-tune a custom model on one or more already-segmented images."
      },
      "UploadResult": {
        "properties": {
          "success": {
            "type": "boolean",
            "title": "Success",
            "default": true
          },
          "image": {
            "$ref": "#/components/schemas/ImageStatusResponse"
          }
        },
        "type": "object",
        "required": [
          "image"
        ],
        "title": "UploadResult",
        "description": "Result of a single-request upload: the image record the pixels landed on.\nConversion happens in-request, so `image.upload_status` is already `ready` —\nno polling needed before running a prediction."
      },
      "UploadStatus": {
        "type": "string",
        "enum": [
          "uploading",
          "processing",
          "ready",
          "failed"
        ],
        "title": "UploadStatus",
        "description": "Lifecycle of a resumable S3 multipart image upload.\n\nuploading  -> row created, bytes still being PUT directly to S3\nprocessing -> object landed in S3, backend is converting it off the event loop\nready      -> conversion done, `shape` populated, image is usable\nfailed     -> aborted or conversion failed (orphan-sweep cron will reap it)"
      },
      "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"
      }
    },
    "securitySchemes": {
      "HTTPBearer": {
        "type": "http",
        "scheme": "bearer"
      }
    }
  },
  "tags": [
    {
      "name": "upload",
      "description": "Get microscopy images into HalfPage. The single-request `POST /upload` is the simplest path: it creates the image record for you and returns it `ready`. For large files over flaky connections, use the resumable flow: `POST /image` with `resumable: true` (or `init`), PUT the presigned parts, then `complete` (no ETag bookkeeping needed) and poll the image until `ready`."
    },
    {
      "name": "job",
      "description": "Submit and poll asynchronous GPU jobs. `POST /job/prediction` runs segmentation on a ready image; `POST /job/training` fine-tunes a custom model. Both return a `job_id` you poll via `GET /job/{job_id}` until it is `COMPLETED`."
    },
    {
      "name": "image",
      "description": "Create and manage image records. `POST /image` starts the resumable upload flow (with `resumable: true` it returns presigned part URLs in the same call); poll `GET /image/{image_id}` for the upload lifecycle (`uploading` → `processing` → `ready`) and metadata such as pixel `shape`, and delete images you no longer need."
    },
    {
      "name": "segmentation",
      "description": "Read the segmentations produced by prediction jobs and derive new ones. A segmentation is the set of detected cell ROIs for an image; fetch its metadata, regenerate its mask from annotations, or fork a refinement to iterate on the result."
    },
    {
      "name": "export",
      "description": "Download results in analysis-ready formats: a CSV of per-cell measurements, a GeoJSON of ROI polygons, or a ZIP of ImageJ-compatible ROIs. Export one segmentation at a time, or pull every analyzed image in a project into a single CSV. Both CSV endpoints accept a `columns` parameter to narrow the output to the measurements you care about."
    },
    {
      "name": "models",
      "description": "List the segmentation models available to your organization — the shared public base models (e.g. `cpsam`) plus any custom models you have trained — to choose a `model_id` for a prediction job."
    }
  ]
}