{
  "openapi": "3.1.0",
  "info": {
    "title": "HalfPage API",
    "description": "The **HalfPage API** runs Cellpose-based cell segmentation on your microscopy\nimages programmatically \u2014 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 \u2014\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\nThree steps \u2014 **upload, predict, export** \u2014 plus a model list to choose from.\nSegmentation runs asynchronously on GPU workers, so step 2 is poll-based:\n\n1. **Upload an image** (`POST /upload`) as `multipart/form-data` with a `file`\n   part. One call: the image record is created for you and the response returns\n   it already `ready`, with the `image.id` for the next step.\n   *(Large file or flaky connection? POST the same endpoint as\n   `application/json` with `{\"name\": ..., \"size\": ...}` instead. You get back\n   presigned part URLs \u2014 PUT the chunks, call\n   `POST /upload/{image_id}/complete`, then poll `GET /upload/{image_id}` until\n   `upload_status` is `ready`.)*\n2. **Pick a model** (`GET /models`) and **run a prediction** (`POST /predict`)\n   with the `image_id` and a `model_id`. That returns a `prediction_id`; poll\n   `GET /predict/{prediction_id}` until `status` is `COMPLETED`. The response\n   then carries a `segmentation_id` and the `cell_count` detected.\n3. **Export** that segmentation as CSV measurements\n   (`GET /export/{segmentation_id}/measurements.csv`), GeoJSON ROIs\n   (`.../rois.geojson`), or an ImageJ ROI archive (`.../rois.zip`).\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 \u2014 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",
        "description": "Get a microscopy image into HalfPage. How the request is sent decides\nwhich of the two modes you get:\n\n**One request** \u2014 send `multipart/form-data` with a `file` part. The file is\nstreamed to storage and converted before the response returns, so the\nreturned `image` is already `ready` and its `id` can go straight into\n`POST /predict`. Simplest, and the right choice up to a few hundred MB.\n\n**Resumable** \u2014 send `application/json` with `{\"name\": ..., \"size\": ...}`\ninstead of a file. No bytes are sent on this request; the response carries\n`upload` with presigned part URLs. PUT each chunk directly to its `url` (in\npart order), call `POST /upload/{image_id}/complete`, then poll\n`GET /upload/{image_id}` until `upload_status` is `ready`. Use this for\nlarge files or unreliable connections \u2014 an interrupted part can be retried\non its own instead of restarting the whole transfer.\n\nEither way the image counts against your plan's storage cap, so both modes\nreturn `402` once that cap is reached.",
        "operationId": "upload_api_v1_upload_post",
        "requestBody": {
          "content": {
            "multipart/form-data": {
              "schema": {
                "properties": {
                  "file": {
                    "type": "string",
                    "format": "binary",
                    "description": "The microscopy image to upload (TIFF). Up to 4 GB."
                  }
                },
                "type": "object",
                "required": [
                  "file"
                ],
                "title": "SingleRequestUpload",
                "description": "Send the whole file in one request. The response returns the image already `ready`."
              }
            },
            "application/json": {
              "schema": {
                "properties": {
                  "name": {
                    "type": "string",
                    "title": "Name",
                    "description": "Filename or label for the image.",
                    "examples": [
                      "sample.tif"
                    ]
                  },
                  "size": {
                    "type": "integer",
                    "title": "Size",
                    "description": "Exact total size of the file in bytes.",
                    "examples": [
                      104857600
                    ]
                  },
                  "additional_info": {
                    "additionalProperties": true,
                    "type": "object",
                    "title": "Additional Info",
                    "description": "Arbitrary JSON metadata to store alongside the image."
                  }
                },
                "type": "object",
                "required": [
                  "name",
                  "size"
                ],
                "title": "ResumableUploadRequest",
                "description": "Open a resumable upload for a file you are about to send in parts.\n\n`size` must be the file's exact byte size \u2014 the part plan is computed from\nit. Deliberately free of enums and server-controlled fields so the JSON\nbranch of `POST /upload` stays a flat, self-contained schema."
              },
              "example": {
                "name": "sample.tif",
                "size": 104857600
              }
            }
          },
          "required": true
        },
        "responses": {
          "200": {
            "description": "The file was stored (`upload` is `null`, the image is `ready`), or a resumable upload was opened (`upload` carries the presigned plan).",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/UploadResult"
                }
              }
            }
          },
          "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"
                }
              }
            }
          },
          "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."
                }
              }
            }
          },
          "413": {
            "description": "The uploaded file exceeds the 4 GB size limit."
          },
          "422": {
            "description": "The `file` part is missing, or the JSON body is invalid (bad `name`/`size`)."
          },
          "501": {
            "description": "A resumable upload was requested but storage does not support it (local dev only)."
          }
        },
        "security": [
          {
            "HTTPBearer": []
          }
        ]
      }
    },
    "/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` \u2014 conversion runs\noff the request path, so poll `GET /upload/{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"
                }
              }
            }
          },
          "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"
                }
              }
            }
          },
          "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"
                }
              }
            }
          },
          "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}": {
      "get": {
        "tags": [
          "upload"
        ],
        "summary": "Get upload status",
        "description": "Fetch an image's upload lifecycle and metadata.\n\nPoll this after a resumable upload until `upload_status` is `ready` \u2014\nconversion runs off the request path, so a freshly completed upload is not\nimmediately usable. Only a `ready` image can be passed to `POST /predict`. A\n`failed` status carries the reason in `upload_error`. (After a\nsingle-request upload the image is already `ready`, so there is nothing to\npoll for.)",
        "operationId": "get_upload_api_v1_upload__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"
                }
              }
            }
          },
          "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"
                }
              }
            }
          },
          "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"
                }
              }
            }
          },
          "422": {
            "description": "Validation Error",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/HTTPValidationError"
                }
              }
            }
          }
        }
      },
      "delete": {
        "tags": [
          "upload"
        ],
        "summary": "Delete an image",
        "description": "Permanently delete an image and all of its derived storage (converted\nTIFF/PNG, thumbnail, and any training artifacts), freeing its slot against\nyour storage cap. Any resumable upload still in flight is aborted first, so\nits uploaded chunks are released too. Scoped to your organization \u2014 an image\noutside it returns `404`, exactly like the reads.",
        "operationId": "delete_upload_api_v1_upload__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/DeleteUploadResponse"
                }
              }
            }
          },
          "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"
                }
              }
            }
          },
          "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"
                }
              }
            }
          },
          "422": {
            "description": "Validation Error",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/HTTPValidationError"
                }
              }
            }
          }
        }
      }
    },
    "/api/v1/predict": {
      "post": {
        "tags": [
          "predict"
        ],
        "summary": "Run a prediction",
        "description": "Start an asynchronous segmentation of a ready image.\n\nReturns immediately with a `prediction_id`; the segmentation itself runs on a\nGPU worker. Poll `GET /predict/{prediction_id}` until `status` is\n`COMPLETED`, then read `segmentation_id` off that response and hand it to the\n`/export/*` endpoints. Each run counts against your plan's monthly analysis\nquota (`402` when exceeded). The `image_id` and `model_id` are org-scoped: an\nid outside your organization returns `404`.",
        "operationId": "create_prediction_api_v1_predict_post",
        "requestBody": {
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/PredictRequest"
              }
            }
          },
          "required": true
        },
        "responses": {
          "200": {
            "description": "Successful Response",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/PredictionAccepted"
                }
              }
            }
          },
          "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"
                }
              }
            }
          },
          "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"
                }
              }
            }
          },
          "400": {
            "description": "You already have a running job \u2014 wait for it to finish."
          },
          "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."
                }
              }
            }
          },
          "410": {
            "description": "The selected model's weights have been cleaned up \u2014 retrain to recover."
          },
          "422": {
            "description": "Validation Error",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/HTTPValidationError"
                }
              }
            }
          }
        },
        "security": [
          {
            "HTTPBearer": []
          }
        ]
      }
    },
    "/api/v1/predict/{prediction_id}": {
      "get": {
        "tags": [
          "predict"
        ],
        "summary": "Get prediction status",
        "description": "Poll a prediction until it finishes.\n\nKeep polling until `status` is `COMPLETED` (or `FAILED`). A completed run\ncarries the `segmentation_id` of the segmentation it produced, plus the\n`cell_count` detected \u2014 feed the `segmentation_id` into the `/export/*`\nendpoints. A non-UUID `prediction_id` is rejected with `422` before it\nreaches the database.",
        "operationId": "get_prediction_api_v1_predict__prediction_id__get",
        "security": [
          {
            "HTTPBearer": []
          }
        ],
        "parameters": [
          {
            "name": "prediction_id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string",
              "format": "uuid",
              "title": "Prediction Id"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Successful Response",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/PredictionStatus"
                }
              }
            }
          },
          "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"
                }
              }
            }
          },
          "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"
                }
              }
            }
          },
          "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/export/{segmentation_id}/measurements.csv": {
      "get": {
        "tags": [
          "export"
        ],
        "summary": "Export measurements (CSV)",
        "description": "Download per-cell measurements for a completed segmentation as CSV \u2014 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"
                }
              }
            }
          },
          "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"
                }
              }
            }
          },
          "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"
                }
              }
            }
          },
          "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"
                }
              }
            }
          },
          "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"
                }
              }
            }
          },
          "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"
                }
              }
            }
          },
          "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"
                }
              }
            }
          },
          "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"
                }
              }
            }
          },
          "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"
                }
              }
            }
          },
          "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"
                }
              }
            }
          },
          "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 in the dashboard. Use a returned `id`\nas the `model_id` when running a prediction with `POST /predict`.",
        "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"
                }
              }
            }
          },
          "404": {
            "description": "Not found"
          },
          "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"
                }
              }
            }
          }
        },
        "security": [
          {
            "HTTPBearer": []
          }
        ]
      }
    }
  },
  "components": {
    "schemas": {
      "CompleteUploadRequest": {
        "properties": {
          "parts": {
            "items": {
              "$ref": "#/components/schemas/CompletedPart"
            },
            "type": "array",
            "title": "Parts",
            "description": "Uploaded parts to assemble. Usually omitted \u2014 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 \u2014 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"
      },
      "DeleteUploadResponse": {
        "properties": {
          "success": {
            "type": "boolean",
            "title": "Success",
            "default": true
          }
        },
        "type": "object",
        "title": "DeleteUploadResponse",
        "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."
      },
      "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 \u2014 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"
      },
      "JobStatus": {
        "type": "string",
        "enum": [
          "SUBMITTED",
          "STARTED",
          "COMPLETED",
          "FAILED"
        ],
        "title": "JobStatus"
      },
      "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"
          }
        ]
      },
      "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"
      },
      "PredictRequest": {
        "properties": {
          "image_id": {
            "type": "string",
            "title": "Image Id",
            "examples": [
              "3fa85f64-5717-4562-b3fc-2c963f66afa6"
            ]
          },
          "model_id": {
            "type": "string",
            "title": "Model Id",
            "examples": [
              "c0ffee00-1234-5678-9abc-def012345678"
            ]
          }
        },
        "type": "object",
        "required": [
          "image_id",
          "model_id"
        ],
        "title": "PredictRequest",
        "description": "Run a model over one uploaded image. `image_id` must be `ready` (see\n`GET /upload/{image_id}`) and `model_id` must be one of the ids returned by\n`GET /models`."
      },
      "PredictionAccepted": {
        "properties": {
          "prediction_id": {
            "type": "string",
            "title": "Prediction Id",
            "examples": [
              "b1e5c7d2-9a4f-4c3b-8e2d-1f6a7b8c9d0e"
            ]
          },
          "status": {
            "type": "string",
            "enum": [
              "submitted",
              "skipped"
            ],
            "title": "Status"
          },
          "reason": {
            "anyOf": [
              {
                "type": "string"
              },
              {
                "type": "null"
              }
            ],
            "title": "Reason",
            "description": "Why an identical prediction was skipped, when `status` is `skipped`."
          }
        },
        "type": "object",
        "required": [
          "prediction_id",
          "status"
        ],
        "title": "PredictionAccepted",
        "description": "Acknowledgement that a prediction was queued. `status` is `submitted` for\na fresh run, or `skipped` when an identical prediction is already in\nflight \u2014 in both cases `prediction_id` is the one to poll."
      },
      "PredictionStatus": {
        "properties": {
          "prediction_id": {
            "type": "string",
            "title": "Prediction Id"
          },
          "status": {
            "$ref": "#/components/schemas/JobStatus"
          },
          "image_id": {
            "type": "string",
            "title": "Image Id"
          },
          "model_id": {
            "type": "string",
            "title": "Model Id"
          },
          "created_at": {
            "type": "string",
            "format": "date-time",
            "title": "Created At"
          },
          "segmentation_id": {
            "anyOf": [
              {
                "type": "string"
              },
              {
                "type": "null"
              }
            ],
            "title": "Segmentation Id",
            "description": "The result handle \u2014 pass it to the `/export/*` endpoints."
          },
          "cell_count": {
            "anyOf": [
              {
                "type": "integer"
              },
              {
                "type": "null"
              }
            ],
            "title": "Cell Count",
            "description": "Number of ROIs detected, once the run completed."
          }
        },
        "type": "object",
        "required": [
          "prediction_id",
          "status",
          "image_id",
          "model_id",
          "created_at"
        ],
        "title": "PredictionStatus",
        "description": "A prediction's polling snapshot. `segmentation_id` and `cell_count` are\npopulated only once `status` is `COMPLETED`.",
        "examples": [
          {
            "summary": "Still running (poll again)",
            "value": {
              "created_at": "2026-07-01T12:00:00Z",
              "image_id": "3fa85f64-5717-4562-b3fc-2c963f66afa6",
              "model_id": "c0ffee00-1234-5678-9abc-def012345678",
              "prediction_id": "b1e5c7d2-9a4f-4c3b-8e2d-1f6a7b8c9d0e",
              "status": "STARTED"
            }
          },
          {
            "summary": "Completed \u2014 export the segmentation_id",
            "value": {
              "cell_count": 128,
              "created_at": "2026-07-01T12:00:00Z",
              "image_id": "3fa85f64-5717-4562-b3fc-2c963f66afa6",
              "model_id": "c0ffee00-1234-5678-9abc-def012345678",
              "prediction_id": "b1e5c7d2-9a4f-4c3b-8e2d-1f6a7b8c9d0e",
              "segmentation_id": "7d9e2f10-4a5b-4c6d-8e9f-0a1b2c3d4e5f",
              "status": "COMPLETED"
            }
          }
        ]
      },
      "UploadPlan": {
        "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": "UploadPlan",
        "description": "The presigned multipart plan: PUT each chunk of the file to its `url`\n(in part order), then call `POST /upload/{image_id}/complete` \u2014 no body\nneeded, the server verifies the uploaded parts with storage directly.\n\nThe internal storage object key is deliberately not exposed: the presigned\n`url`s already 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"
              }
            ]
          }
        ]
      },
      "UploadResult": {
        "properties": {
          "success": {
            "type": "boolean",
            "title": "Success",
            "default": true
          },
          "image": {
            "$ref": "#/components/schemas/ImageStatusResponse"
          },
          "upload": {
            "anyOf": [
              {
                "$ref": "#/components/schemas/UploadPlan"
              },
              {
                "type": "null"
              }
            ]
          }
        },
        "type": "object",
        "required": [
          "image"
        ],
        "title": "UploadResult",
        "description": "The image the pixels landed on (or will land on).\n\nAfter a single-request upload `upload` is `null` and `image.upload_status`\nis already `ready` \u2014 the `image.id` can go straight into `POST /predict`.\nAfter a resumable upload was opened, `upload` carries the presigned plan and\n`image.upload_status` is `uploading` until you complete it."
      },
      "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, and manage them once they are there. `POST /upload` is the only way in and covers both modes: send `multipart/form-data` with a `file` part to upload in one request (the image comes back `ready`), or send `application/json` with `{name, size}` to open a resumable upload and get presigned part URLs back. Resumable uploads finish with `complete` (no ETag bookkeeping needed) and are polled until `ready`."
    },
    {
      "name": "predict",
      "description": "Run segmentation on a ready image and poll it to completion. `POST /predict` queues the run on a GPU worker and returns a `prediction_id`; `GET /predict/{prediction_id}` reports progress and, once `COMPLETED`, the `segmentation_id` and cell count of the result."
    },
    {
      "name": "export",
      "description": "Download a completed segmentation in analysis-ready formats: a CSV of per-cell measurements, a GeoJSON of ROI polygons, or a ZIP of ImageJ-compatible ROIs. The CSV endpoint accepts a `columns` parameter to narrow the output to the measurements you care about."
    },
    {
      "name": "models",
      "description": "List the segmentation models available to your organization \u2014 the shared public base models (e.g. `cpsam`) plus any custom models trained in the dashboard \u2014 to choose a `model_id` for a prediction."
    }
  ]
}
