{"openapi":"3.2.0","info":{"title":"OwnClimb Detection API","version":"2.0.0","description":"Climbing-hold detection. Submit a wall photo and take the result the way that suits you: wait for it, poll for it, or stream it as it is found."},"servers":[{"url":"/api/v1"}],"security":[{"ApiKeyAuth":[]}],"paths":{"/detections/blocking":{"post":{"operationId":"detectBlocking","summary":"Detect holds and wait for the result","description":"Submits an image and holds the connection until the detection finishes. If it has not finished within 120 seconds the response is a 202 carrying the detection's id instead, so no work is lost — follow it with GET /detections/{id}.","requestBody":{"required":true,"content":{"multipart/form-data":{"schema":{"$ref":"#/components/schemas/DetectionRequest"}}}},"responses":{"200":{"description":"The finished detection.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Detection"}}}},"202":{"description":"Accepted. The detection is running; poll or stream it by id.","headers":{"Location":{"description":"The detection's URL.","schema":{"type":"string"}}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Detection"}}}},"400":{"$ref":"#/components/responses/Error"},"401":{"$ref":"#/components/responses/Error"},"402":{"$ref":"#/components/responses/Error"},"413":{"$ref":"#/components/responses/Error"},"429":{"$ref":"#/components/responses/Error"},"500":{"$ref":"#/components/responses/Error"},"503":{"$ref":"#/components/responses/Error"}}}},"/detections/polling":{"post":{"operationId":"detectPolling","summary":"Detect holds and return immediately","description":"Submits an image and answers at once with the detection's id and current state. Read GET /detections/{id} until `status` is `done` or `failed`.","requestBody":{"required":true,"content":{"multipart/form-data":{"schema":{"$ref":"#/components/schemas/DetectionRequest"}}}},"responses":{"202":{"description":"Accepted. The detection is running; poll or stream it by id.","headers":{"Location":{"description":"The detection's URL.","schema":{"type":"string"}}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Detection"}}}},"400":{"$ref":"#/components/responses/Error"},"401":{"$ref":"#/components/responses/Error"},"402":{"$ref":"#/components/responses/Error"},"413":{"$ref":"#/components/responses/Error"},"429":{"$ref":"#/components/responses/Error"},"500":{"$ref":"#/components/responses/Error"},"503":{"$ref":"#/components/responses/Error"}}}},"/detections/streaming":{"post":{"operationId":"detectStreaming","summary":"Detect holds and stream the result as it is found","description":"Submits an image and streams every state the detection passes through. Each item is a complete Detection: `holds` grows as stages land, and the last item is the one whose `status` is `done` or `failed`. Defaults to newline-delimited JSON; send `Accept: text/event-stream` for server-sent events, where each item arrives as a `state` event.\n\nWhich stages appear depends on the request: `refining` only for `detail=refined`, and `coloring` only when `color` is true. Do not wait for a stage a run will never report.","requestBody":{"required":true,"content":{"multipart/form-data":{"schema":{"$ref":"#/components/schemas/DetectionRequest"}}}},"responses":{"200":{"description":"A stream of detection states, ending at a terminal one.","content":{"application/x-ndjson":{"itemSchema":{"$ref":"#/components/schemas/Detection"}},"text/event-stream":{"itemSchema":{"$ref":"#/components/schemas/Detection"}}}},"400":{"$ref":"#/components/responses/Error"},"401":{"$ref":"#/components/responses/Error"},"402":{"$ref":"#/components/responses/Error"},"413":{"$ref":"#/components/responses/Error"},"429":{"$ref":"#/components/responses/Error"},"500":{"$ref":"#/components/responses/Error"},"503":{"$ref":"#/components/responses/Error"}}}},"/detections/{id}":{"get":{"operationId":"getDetection","summary":"Get a detection","parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string","format":"uuid"}}],"responses":{"200":{"description":"The detection's current state.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Detection"}}}},"400":{"$ref":"#/components/responses/Error"},"401":{"$ref":"#/components/responses/Error"},"404":{"$ref":"#/components/responses/Error"},"500":{"$ref":"#/components/responses/Error"},"503":{"$ref":"#/components/responses/Error"}}}}},"components":{"securitySchemes":{"ApiKeyAuth":{"type":"http","scheme":"bearer","description":"Send your `oc_` API key as `Authorization: Bearer <key>` (or the `X-API-Key` header)."}},"responses":{"Error":{"description":"Structured error.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}},"schemas":{"Detection":{"$schema":"https://json-schema.org/draft/2020-12/schema","type":"object","properties":{"id":{"type":"string","format":"uuid","pattern":"^([0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[1-8][0-9a-fA-F]{3}-[89abAB][0-9a-fA-F]{3}-[0-9a-fA-F]{12}|00000000-0000-0000-0000-000000000000|ffffffff-ffff-ffff-ffff-ffffffffffff)$"},"status":{"type":"string","enum":["queued","running","done","failed"]},"detail":{"type":"string","enum":["raw","refined"]},"color":{"type":"boolean","description":"Whether colours were sampled for this run."},"stage":{"anyOf":[{"type":"string","enum":["detecting","refining","coloring"]},{"type":"null"}]},"image":{"type":"object","properties":{"width":{"type":"integer","minimum":-9007199254740991,"maximum":9007199254740991},"height":{"type":"integer","minimum":-9007199254740991,"maximum":9007199254740991}},"required":["width","height"],"additionalProperties":false,"description":"The pixel space `holds` are expressed in. The upload is normalised before inference, so this is not the size of the file you sent."},"holds":{"type":"array","items":{"type":"object","properties":{"polygon":{"type":"array","items":{"type":"number"},"description":"Flat polygon coordinates [x0,y0,x1,y1,…] in the space of `image`."},"confidence":{"type":"number"},"color":{"description":"Dominant colour inside the polygon, as #rrggbb.","type":"string"}},"required":["polygon","confidence"],"additionalProperties":false}},"created_at":{"type":"string","format":"date-time","pattern":"^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d(?::[0-5]\\d(?:\\.\\d+)?)?(?:Z))$"},"finished_at":{"anyOf":[{"type":"string","format":"date-time","pattern":"^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d(?::[0-5]\\d(?:\\.\\d+)?)?(?:Z))$"},{"type":"null"}]},"model_time_ms":{"anyOf":[{"type":"number"},{"type":"null"}]},"cached":{"type":"boolean"},"error":{"anyOf":[{"type":"object","properties":{"code":{"type":"string"},"message":{"type":"string"}},"required":["code","message"],"additionalProperties":false},{"type":"null"}]}},"required":["id","status","detail","color","stage","image","holds","created_at","finished_at","model_time_ms","cached","error"],"additionalProperties":false},"Hold":{"$schema":"https://json-schema.org/draft/2020-12/schema","type":"object","properties":{"polygon":{"type":"array","items":{"type":"number"},"description":"Flat polygon coordinates [x0,y0,x1,y1,…] in the space of `image`."},"confidence":{"type":"number"},"color":{"description":"Dominant colour inside the polygon, as #rrggbb.","type":"string"}},"required":["polygon","confidence"],"additionalProperties":false},"Error":{"$schema":"https://json-schema.org/draft/2020-12/schema","type":"object","properties":{"error":{"type":"object","properties":{"code":{"type":"string","enum":["invalid_request","invalid_image","image_too_large","unauthorized","not_found","quota_exceeded","account_locked","rate_limited","inference_failed","storage_unavailable"]},"message":{"type":"string"},"details":{}},"required":["code","message"],"additionalProperties":false}},"required":["error"],"additionalProperties":false},"DetectionRequest":{"type":"object","required":["image"],"properties":{"image":{"type":"string","format":"binary","contentMediaType":"image/*","description":"Wall photo (JPEG/PNG/WebP/HEIC, max 40 MB)."},"detail":{"type":"string","enum":["raw","refined"],"default":"refined","description":"`refined` returns mask-accurate outlines. `raw` stops at the detector's own polygons: roughly twice as fast, looser geometry."},"color":{"type":"string","enum":["true","false"],"default":"true","description":"Sample the dominant colour inside each polygon."},"cache":{"type":"string","enum":["true","false"],"default":"true","description":"Whether this run may serve as the cached result for future identical submissions. `false` still answers this request — including from an earlier cached run — it only marks the new run as never usable by anyone else."},"metadata":{"type":"string","description":"Optional JSON object echoed back on the detection."}}}}}}