OwnClimb

OwnClimb Detection API

View as markdown

Turn a wall photo into scored hold polygons. Submit an image and take the result the way that suits you: wait for it, poll for it, or stream it as it is found. The OpenAPI document is the machine-readable contract for the same reference.

Machine-readable contract: /api/v1/openapi. Raw markdown: /docs.md.

Base URL: https://ownclimb.com/api/v1

Authentication

Send your oc_ API key as Authorization: Bearer <key>, or as the X-API-Key header.

Quickstart

One request, one answer:

bash
curl -X POST https://ownclimb.com/api/v1/detections/blocking \
  -H "Authorization: Bearer oc_your_key" \
  -F "image=@wall.jpg"
json
{
  "id": "7fc…",
  "status": "done",
  "detail": "refined",
  "color": true,
  "stage": null,
  "image": { "width": 2048, "height": 1536 },
  "holds": [{ "polygon": [412, 233, 430, 251], "confidence": 0.96, "color": "#c0392b" }],
  "created_at": "2026-08-28T09:12:00.000Z",
  "finished_at": "2026-08-28T09:12:05.212Z",
  "model_time_ms": 5200,
  "cached": false,
  "error": null
}

The request

Every endpoint below takes the same multipart form.

FieldTypeDefaultDescription
imagefile—Wall photo (JPEG/PNG/WebP/HEIC, max 40 MB). Required.
detailstringrefinedrefined for mask-accurate outlines, raw for the detector's own.
colorbooleantrueSample the dominant colour inside each polygon.
cachebooleantrueWhether this run may serve as the cached result for future identical submissions.
metadataJSON string—Optional object stored with this detection.

The API does not expose a run history or usage endpoint. Keep the detection id returned by a submission when you need to read that individual detection; quota availability is reported by submission responses.

detail

refined is the default and what you want unless you have measured otherwise. raw skips the outline-refinement pass: roughly twice as fast, with polygons that follow the hold more loosely. Both return the same fields.

color

Colour extraction samples the dominant colour inside each finished polygon and returns it as #rrggbb. Turn it off with color=false and the color field is absent from every hold.

cache

Identical submissions are answered from the cache: a finished run for the same image and request serves the next one without spending inference time. Submit cache=false and your run is still answered — including from an earlier cached run — but the row it creates is never considered as a cached result for anyone else, so the next identical submission runs for real.

The response

Every endpoint returns the same object, and a streaming endpoint returns a sequence of them. A detection in progress and a finished one differ only in their field values — never in their shape — so one parser handles both.

FieldDescription
idThe detection's uuid.
statusqueued, running, done or failed.
detailThe detail level this detection ran at.
colorWhether colours were sampled.
stageWhat it is doing now, or null when it is finished or has not started.
image{ width, height } — the space polygon coordinates are in. See below.
holdsThe detections so far. Grows while the run is in progress.
created_atWhen the detection was submitted.
finished_atWhen it finished, or null.
model_time_msHow long inference itself took, or null when that is unknown.
cachedWhether the result was reused from an identical earlier detection.
error{ code, message } when status is failed, otherwise null.

A hold is { "polygon": [x0, y0, x1, y1, …], "confidence": 0.96, "color": "#c0392b" }. color is absent when the detection ran with color=false.

Coordinates

polygon coordinates are in the space of image, not of the file you uploaded. OwnClimb normalises an upload before running detection by resizing its long edge to at most 4000px. The resize preserves the aspect ratio, so neither dimension exceeds 4000px; images already under that cap are left untouched. For example, a 4000×5000 upload returns an image of 3200×4000. Scale by yourWidth / image.width to place holds on your original.

Stages

While a detection runs, stage names what it is doing. Every stream starts with two opening frames: status: queued, stage: null, then status: running, stage: null. These arrive before any stage-specific progress frame. A null stage can therefore mean that work is waiting to start as well as that the detection is finished. Which stages appear depends on the request, so do not wait for one a detection will never report:

RequestStages
detail=refined (default)detecting → refining → coloring
detail=rawdetecting → coloring
detail=raw&color=falsedetecting

Endpoints

POST /api/v1/detections/blocking

Submit an image and wait for the result.

Returns 200 with the finished detection. If it has not finished within 120 seconds you get 202 instead, carrying the detection's id and a Location header — the work continues, so follow it with GET /api/v1/detections/{id} rather than resubmitting.

StatusMeaning
200The finished detection.
202Still running; follow the Location header.
400invalid_request or invalid_image.
401unauthorized.
402quota_exceeded or account_locked.
413image_too_large.
429rate_limited; wait out the retry-after header.

POST /api/v1/detections/polling

Submit an image and return immediately.

Returns 202 with the detection's id and current state, plus a Location header. Read GET /api/v1/detections/{id} until status is done or failed.

bash
curl -X POST https://ownclimb.com/api/v1/detections/polling \
  -H "Authorization: Bearer oc_your_key" \
  -F "image=@wall.jpg" \
  -F "detail=raw"
# → 202 { "id": "7fc…", "status": "queued", "holds": [], … }

POST /api/v1/detections/streaming

Submit an image and stream the result as it is found.

Each item is a complete detection object; holds grows as stages land. The stream ends at the item whose status is done or failed, or after 150 seconds if the detection never finishes.

The default response has Content-Type: application/x-ndjson. It is newline- delimited JSON: each complete detection is one JSON object followed by a newline. Network reads are not frame boundaries; a read may split an object or contain several objects, so buffer incoming text until a newline before parsing.

Newline-delimited JSON by default — one detection per line, so a client needs no event-stream parser:

bash
curl -N -X POST https://ownclimb.com/api/v1/detections/streaming \
  -H "Authorization: Bearer oc_your_key" \
  -F "image=@wall.jpg"
json
{"id":"7fc…","status":"running","stage":"detecting","holds":[…],…}
{"id":"7fc…","status":"running","stage":"refining","holds":[…],…}
{"id":"7fc…","status":"done","stage":null,"holds":[…],…}

Send Accept: text/event-stream for Content-Type: text/event-stream and server-sent events instead, where every item arrives as a state event. Network reads can also split or combine event data, so buffer until the blank line that ends an event. There is only ever one event name: the last item is the one that says done or failed.

GET /api/v1/detections/{id}

Get a detection's current state.

StatusMeaning
200The detection; holds is complete once status is done.
401unauthorized.
404not_found.

Caching

An identical image submitted at the same detail returns the earlier result with cached: true, and does not spend your monthly allowance. detail=raw and detail=refined are cached separately, because their polygons genuinely differ. A detection asked for without colours can be answered from a cached one that has them; the colours are simply left out.

Error codes

Errors come back as { "error": { "code", "message" } }, with an optional details object on validation failures.

CodeStatusMeaning
invalid_request400The request or its fields are not valid.
invalid_image400The upload does not decode as an image.
image_too_large413The image exceeds the size limit.
unauthorized401Missing or invalid API key.
not_found404No such detection, or it belongs to another account.
quota_exceeded402The monthly inference quota is used up.
account_locked402The subscription is inactive.
rate_limited429Too many requests; retry after the delay.
inference_failed500The detection failed.
storage_unavailable503Image storage was unreachable.