> ## Documentation Index
> Fetch the complete documentation index at: https://docs.halfpagetechnologies.com/llms.txt
> Use this file to discover all available pages before exploring further.

# Quickstart

> Upload a microscopy image, run a segmentation, and download the ROIs — end to end.

This walkthrough takes you through the full workflow with runnable `curl` and
Python: upload an image (one call), run a prediction, wait for the job, and
download the resulting ROIs as GeoJSON — entirely over the API.

## Before you start

<Steps>
  <Step title="Get an API key">
    Create one in the dashboard (**Settings → API Keys**) and export it:

    ```bash theme={null}
    export HALFPAGE_API_KEY="hp_live_xxxxxxxxxxxxxxxxxxxxxxxx"
    export BASE="https://api.halfpagetechnologies.com/backend/api/v1"
    ```

    See [Authentication](/authentication) for details.
  </Step>

  <Step title="Install tooling (for the bash examples)">
    The `curl` snippets use [`jq`](https://jqlang.github.io/jq/) to read JSON
    responses. The Python snippets use
    [`requests`](https://requests.readthedocs.io/) (`pip install requests`).
  </Step>
</Steps>

## 1. Upload your image

One request does everything: `POST /upload` streams the file in, creates the
image record for you (named after the file), converts it, and returns the
record already **`ready`**. No pre-registration, no polling.

<CodeGroup>
  ```bash curl theme={null}
  IMAGE_ID=$(curl -s -X POST "$BASE/upload" \
    -H "Authorization: Bearer $HALFPAGE_API_KEY" \
    -F "file=@image.tif" | jq -r .image.id)
  echo "image: $IMAGE_ID"
  # The returned image is already "ready" — no polling needed.
  ```

  ```python Python theme={null}
  import os
  import requests

  BASE_URL = "https://api.halfpagetechnologies.com/backend/api/v1"
  FILE_PATH = "image.tif"

  session = requests.Session()
  session.headers["Authorization"] = f"Bearer {os.environ['HALFPAGE_API_KEY']}"

  # One call: creates the record, uploads and converts the pixels.
  with open(FILE_PATH, "rb") as f:
      upload = session.post(f"{BASE_URL}/upload", files={"file": f})
  upload.raise_for_status()
  IMAGE_ID = upload.json()["image"]["id"]
  ```
</CodeGroup>

<Note>
  Each image counts against your plan's storage cap; `POST /upload` returns
  `402` once the cap is reached. Files can be up to 4 GB, but for anything
  larger than a few hundred MB — or a connection that might drop — use the
  [resumable upload](#large-files-the-resumable-upload) below.
</Note>

## 2. Pick a model and submit a prediction

List the models available to your organization, then submit a prediction job for
your image. `GET /models` returns shared public base models (such as `cpsam`,
the Cellpose-SAM generalist) alongside any custom models your org has trained.

<CodeGroup>
  ```bash curl theme={null}
  # Pick the public cpsam base model (or the first available model).
  MODEL_ID=$(curl -s "$BASE/models" -H "Authorization: Bearer $HALFPAGE_API_KEY" \
    | jq -r '(map(select(.name == "cpsam")) + .)[0].id')

  # Submit the prediction. Returns a job_id to poll.
  JOB_ID=$(curl -s -X POST "$BASE/job/prediction" \
    -H "Authorization: Bearer $HALFPAGE_API_KEY" \
    -H "Content-Type: application/json" \
    -d "{\"model_id\": \"$MODEL_ID\", \"image_id\": \"$IMAGE_ID\"}" | jq -r .job_id)
  echo "job: $JOB_ID"
  ```

  ```python Python theme={null}
  models = session.get(f"{BASE_URL}/models").json()
  model_id = next((m["id"] for m in models if m["name"] == "cpsam"), models[0]["id"])

  job = session.post(
      f"{BASE_URL}/job/prediction",
      json={"model_id": model_id, "image_id": IMAGE_ID},
  ).json()
  job_id = job["job_id"]
  ```
</CodeGroup>

<Note>
  Each organization runs one job at a time. If you already have a job running,
  `POST /job/prediction` returns `400` — wait for it to finish. If an identical
  job is already queued, the response comes back with `status: "skipped"` and the
  existing `job_id`.
</Note>

## 3. Wait until the job completes

Segmentation runs asynchronously on GPU workers, so poll `GET /job/{job_id}`
until `status` is `COMPLETED`. The completed response carries the
`segmentation_id` you'll export.

<CodeGroup>
  ```bash curl theme={null}
  until [ "$(curl -s "$BASE/job/$JOB_ID" \
    -H "Authorization: Bearer $HALFPAGE_API_KEY" | jq -r .status)" = "COMPLETED" ]; do
    echo "segmenting..." && sleep 5
  done
  SEG_ID=$(curl -s "$BASE/job/$JOB_ID" \
    -H "Authorization: Bearer $HALFPAGE_API_KEY" | jq -r .segmentation_id)
  echo "segmentation: $SEG_ID"
  ```

  ```python Python theme={null}
  import time

  while True:
      job = session.get(f"{BASE_URL}/job/{job_id}").json()
      if job["status"] == "COMPLETED":
          segmentation_id = job["segmentation_id"]
          break
      if job["status"] == "FAILED":
          raise RuntimeError("prediction job failed")
      time.sleep(5)
  ```
</CodeGroup>

`status` moves through `SUBMITTED` → `STARTED` → `COMPLETED` (or `FAILED`).

## 4. Export the results

Download the segmentation in whichever analysis-ready format you need. GeoJSON is
handy for GIS and web tools; the CSV is one row per cell; the ZIP is an ImageJ/FIJI
ROI archive.

<CodeGroup>
  ```bash curl theme={null}
  # GeoJSON ROIs
  curl -s "$BASE/export/$SEG_ID/rois.geojson" \
    -H "Authorization: Bearer $HALFPAGE_API_KEY" -o rois.geojson

  # Per-cell measurements (CSV)
  curl -s "$BASE/export/$SEG_ID/measurements.csv" \
    -H "Authorization: Bearer $HALFPAGE_API_KEY" -o measurements.csv

  # ImageJ ROI archive (ZIP)
  curl -s "$BASE/export/$SEG_ID/rois.zip" \
    -H "Authorization: Bearer $HALFPAGE_API_KEY" -o rois.zip
  ```

  ```python Python theme={null}
  geojson = session.get(f"{BASE_URL}/export/{segmentation_id}/rois.geojson")
  geojson.raise_for_status()
  with open("rois.geojson", "wb") as out:
      out.write(geojson.content)

  # Also available:
  #   GET /export/{segmentation_id}/measurements.csv  -> per-cell measurements
  #   GET /export/{segmentation_id}/rois.zip          -> ImageJ ROI archive
  ```
</CodeGroup>

That's the whole loop: **upload → predict → complete → export.** 🎉

## Large files: the resumable upload

For multi-GB files or unreliable connections, upload the pixels straight to
object storage in chunks instead of through the API. Create the record with
`resumable: true` — the response includes presigned part URLs — then `PUT` each
chunk to its URL and call `/complete`. If a chunk fails, just retry that `PUT`;
nothing else is lost.

<Info>
  The presigned `PUT` requests go straight to object storage — do **not** send
  your `Authorization` header on them. All other calls are authenticated with
  your API key as usual.
</Info>

<CodeGroup>
  ```bash curl theme={null}
  # a) Create the record AND get the presigned upload plan in one call.
  CREATE=$(curl -s -X POST "$BASE/image" \
    -H "Authorization: Bearer $HALFPAGE_API_KEY" \
    -H "Content-Type: application/json" \
    -d "{\"name\": \"image.tif\", \"size\": $(wc -c < image.tif), \"resumable\": true}")
  IMAGE_ID=$(jq -r .id <<< "$CREATE")
  PART_SIZE=$(jq -r .upload.part_size <<< "$CREATE")

  # b) Split the file into part_size chunks and PUT each to its presigned URL.
  split -b "$PART_SIZE" image.tif hp_part_
  i=0
  for chunk in hp_part_*; do
    curl -sf -X PUT --data-binary "@$chunk" \
      "$(jq -r ".upload.urls[$i].url" <<< "$CREATE")" -o /dev/null
    i=$((i+1))
  done

  # c) Finish the upload — no body needed; the server verifies the parts itself.
  curl -s -X POST "$BASE/upload/$IMAGE_ID/complete" \
    -H "Authorization: Bearer $HALFPAGE_API_KEY"
  # -> {"status": "processing"}

  # d) Conversion runs asynchronously: poll until the image is ready.
  until [ "$(curl -s "$BASE/image/$IMAGE_ID" \
    -H "Authorization: Bearer $HALFPAGE_API_KEY" | jq -r .upload_status)" = "ready" ]; do
    echo "converting..." && sleep 3
  done
  echo "image ready"
  ```

  ```python Python theme={null}
  import time

  # a) Create the record AND get the presigned upload plan in one call.
  create = session.post(
      f"{BASE_URL}/image",
      json={
          "name": os.path.basename(FILE_PATH),
          "size": os.path.getsize(FILE_PATH),
          "resumable": True,
      },
  ).json()
  IMAGE_ID = create["id"]

  # b) PUT each chunk directly to storage (no auth header on these).
  with open(FILE_PATH, "rb") as f:
      for part in create["upload"]["urls"]:
          chunk = f.read(create["upload"]["part_size"])
          requests.put(part["url"], data=chunk).raise_for_status()

  # c) Finish the upload — no body needed; the server verifies the parts itself.
  session.post(f"{BASE_URL}/upload/{IMAGE_ID}/complete").raise_for_status()

  # d) Conversion runs asynchronously: poll until the image is ready.
  while True:
      image = session.get(f"{BASE_URL}/image/{IMAGE_ID}").json()
      if image["upload_status"] == "ready":
          break
      if image["upload_status"] == "failed":
          raise RuntimeError(f"upload failed: {image.get('upload_error')}")
      time.sleep(3)
  ```
</CodeGroup>

From here, continue at [step 2](#2-pick-a-model-and-submit-a-prediction) —
everything downstream is identical.

<Note>
  The record counts against your storage cap from the moment it is created, and
  keeps counting until you delete it. If you abandon an upload,
  `DELETE /image/{image_id}` frees the slot (`POST /upload/{image_id}/abort`
  only discards the uploaded chunks and marks the record `failed`).
</Note>

## Full Python script

<Accordion title="segment.py — the complete workflow in one file">
  ```python theme={null}
  import os
  import time
  import requests

  BASE_URL = "https://api.halfpagetechnologies.com/backend/api/v1"
  API_KEY = os.environ["HALFPAGE_API_KEY"]
  FILE_PATH = "image.tif"

  session = requests.Session()
  session.headers["Authorization"] = f"Bearer {API_KEY}"


  def upload_image(file_path: str) -> str:
      """One call: creates the record, uploads and converts the pixels, and
      returns the id of the ready-to-segment image."""
      with open(file_path, "rb") as f:
          upload = session.post(f"{BASE_URL}/upload", files={"file": f})
      upload.raise_for_status()
      return upload.json()["image"]["id"]


  def predict(image_id: str) -> str:
      models = session.get(f"{BASE_URL}/models").json()
      model_id = next((m["id"] for m in models if m["name"] == "cpsam"), models[0]["id"])
      job = session.post(
          f"{BASE_URL}/job/prediction",
          json={"model_id": model_id, "image_id": image_id},
      ).json()
      job_id = job["job_id"]
      while True:
          status = session.get(f"{BASE_URL}/job/{job_id}").json()
          if status["status"] == "COMPLETED":
              return status["segmentation_id"]
          if status["status"] == "FAILED":
              raise RuntimeError("prediction job failed")
          time.sleep(5)


  def export(segmentation_id: str, out_path: str = "rois.geojson") -> None:
      resp = session.get(f"{BASE_URL}/export/{segmentation_id}/rois.geojson")
      resp.raise_for_status()
      with open(out_path, "wb") as out:
          out.write(resp.content)


  if __name__ == "__main__":
      image_id = upload_image(FILE_PATH)
      segmentation_id = predict(image_id)
      export(segmentation_id)
      print(f"done — wrote rois.geojson for segmentation {segmentation_id}")
  ```
</Accordion>

## Next steps

<CardGroup cols={2}>
  <Card title="API reference" icon="code" href="/api-reference">
    Every endpoint, parameter, and response — with a live playground.
  </Card>

  <Card title="Using with AI agents" icon="robot" href="/ai-agents">
    Point an agent at the MCP server and OpenAPI spec to drive this flow for you.
  </Card>
</CardGroup>
