{"openapi":"3.1.0","info":{"title":"Strand AI Platform API","version":"v1","description":"REST surface for uploading WSIs, submitting POSTMAN inference jobs, and streaming results. All endpoints require an `Authorization: Bearer sk-strand-...` header. Generate keys at `/settings/api-keys`."},"servers":[{"url":"/api/v1","description":"Current host"}],"security":[{"ApiKey":[]}],"components":{"securitySchemes":{"ApiKey":{"type":"http","scheme":"bearer","bearerFormat":"sk-strand-XXXXXXXXXXXXXXXXXXXXXXXX"}},"schemas":{"Error":{"type":"object","required":["error","message"],"properties":{"error":{"type":"string"},"message":{"type":"string"},"required":{"type":"integer","nullable":true}}},"UploadCreated":{"type":"object","required":["uploadId","uploadUrl","gcsPath"],"properties":{"uploadId":{"type":"string","format":"uuid"},"uploadUrl":{"type":"string","description":"Resumable upload URL — PUT slide bytes here"},"gcsPath":{"type":"string"}}},"UploadComplete":{"type":"object","required":["uploadId","status","widthPx","heightPx"],"properties":{"uploadId":{"type":"string","format":"uuid"},"status":{"type":"string","enum":["ready"]},"widthPx":{"type":"integer"},"heightPx":{"type":"integer"},"dimensionsSource":{"type":"string","enum":["sharp","stub"]}}},"Estimate":{"type":"object","required":["patchCount","markerCount","estimatedCredits","orgBalance"],"properties":{"patchCount":{"type":"integer"},"markerCount":{"type":"integer"},"estimatedCredits":{"type":"integer"},"orgBalance":{"type":"integer"},"orgPending":{"type":"integer"}}},"Submission":{"type":"object","required":["jobId","reservedCredits","status"],"properties":{"jobId":{"type":"string","format":"uuid"},"reservedCredits":{"type":"integer"},"status":{"type":"string","enum":["queued"]}}},"Job":{"type":"object","required":["id","status","markers"],"properties":{"id":{"type":"string","format":"uuid"},"status":{"type":"string"},"progress":{"type":"number","nullable":true},"reservedCredits":{"type":"integer","nullable":true},"markers":{"type":"array","items":{"type":"string"}},"createdAt":{"type":"string","format":"date-time","nullable":true},"startedAt":{"type":"string","format":"date-time","nullable":true},"completedAt":{"type":"string","format":"date-time","nullable":true},"errorMessage":{"type":"string","nullable":true},"resultsAvailable":{"type":"boolean"}}},"Results":{"type":"object","required":["resultUrl","expiresAt"],"properties":{"resultUrl":{"type":"string"},"resultBasePath":{"type":"string"},"expiresAt":{"type":"string","format":"date-time"}}}}},"paths":{"/uploads":{"post":{"summary":"Initiate a resumable upload","requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","required":["filename","fileSize","contentType"],"properties":{"filename":{"type":"string"},"fileSize":{"type":"integer"},"contentType":{"type":"string"}}}}}},"responses":{"200":{"description":"Resumable upload URL","content":{"application/json":{"schema":{"$ref":"#/components/schemas/UploadCreated"}}}},"401":{"description":"Missing or invalid API key","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}}},"/uploads/{id}/complete":{"post":{"summary":"Finalize a resumable upload","description":"Marks the upload `ready` and writes width/height into metadata (used by credit estimates).","parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string","format":"uuid"}}],"responses":{"200":{"description":"Upload finalized","content":{"application/json":{"schema":{"$ref":"#/components/schemas/UploadComplete"}}}},"404":{"description":"Upload not found","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}}},"/predict/estimate":{"post":{"summary":"Estimate credit cost for a prediction","requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","required":["uploadId","markers"],"properties":{"uploadId":{"type":"string","format":"uuid"},"markers":{"type":"array","items":{"type":"string"},"minItems":1}}}}}},"responses":{"200":{"description":"Estimate","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Estimate"}}}},"409":{"description":"Upload missing dimensions","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}}},"/predict":{"post":{"summary":"Submit a prediction","description":"Reserves credits atomically and enqueues preprocessing. Returns 202 + `jobId`. On insufficient balance returns 402 with rollback. On per-org concurrency cap returns 429 with `Retry-After`.","requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","required":["uploadId","markers"],"properties":{"uploadId":{"type":"string","format":"uuid"},"markers":{"type":"array","items":{"type":"string"},"minItems":1}}}}}},"responses":{"202":{"description":"Job accepted and credits reserved","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Submission"}}}},"402":{"description":"Insufficient credits","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"409":{"description":"Upload not ready","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"429":{"description":"Per-org concurrent job cap exceeded","headers":{"Retry-After":{"schema":{"type":"integer"},"description":"Seconds to wait before retrying"}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}}},"/jobs/{id}":{"get":{"summary":"Get job status","parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string","format":"uuid"}}],"responses":{"200":{"description":"Job","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Job"}}}},"404":{"description":"Not found","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}}},"/jobs/{id}/stream":{"get":{"summary":"Stream job status (SSE)","description":"Server-Sent Events stream emitting JSON snapshots of `{status, progress, resultGcsPath}` on each pg_notify. Keep-alive heartbeats every 15s. Closes on terminal status.","parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string","format":"uuid"}}],"responses":{"200":{"description":"text/event-stream","content":{"text/event-stream":{"schema":{"type":"string"}}}}}}},"/jobs/{id}/results":{"get":{"summary":"Get signed download URL for results","description":"Returns a signed GCS URL for the OME-Zarr root metadata (`zarr.json`) plus the `resultBasePath` of the zarr store. Useful for clients that already have GCS credentials. SDK clients should generally prefer `GET /jobs/{id}/results/files/{path}` (API-key authenticated) for walking the tree.","parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string","format":"uuid"}}],"responses":{"200":{"description":"Signed URL","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Results"}}}},"409":{"description":"Job has not completed","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}}},"/jobs/{id}/results/files/{path}":{"get":{"summary":"Stream a single result file (API-key authenticated)","description":"Proxies a single file from the OME-Zarr result store. `path` is the path within the zarr store (e.g. `zarr.json`, `CD3/zarr.json`, `CD3/c/0/0/0`). Authenticated via the same bearer API key as the rest of `/api/v1/`; org-scoped to the job. Use this when walking the zarr tree from a client that does not have direct GCS credentials.","parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string","format":"uuid"}},{"name":"path","in":"path","required":true,"description":"Path within the zarr store; slashes are not URL-encoded.","schema":{"type":"string"}}],"responses":{"200":{"description":"File bytes (binary for chunks, JSON for metadata).","content":{"application/octet-stream":{"schema":{"type":"string","format":"binary"}},"application/json":{"schema":{"type":"object"}}}},"404":{"description":"Job or file not found","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"409":{"description":"Job has not completed","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}}}}}