# OwnClimb Detection API

Asynchronous climbing-hold detection. Submit a wall image, then poll for the
run's result. The [OpenAPI document](/api/v1/openapi) is the machine-readable
contract for the same reference.

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

Submit an image:

```bash
curl -X POST https://ownclimb.com/api/v1/runs \
  -H "Authorization: Bearer oc_your_key" \
  -F "image=@wall.jpg" \
  -F 'metadata={"gym":"north"}'
# → 202 { "id": "…", "status": "queued", … }
```

Poll for the result:

```bash
curl https://ownclimb.com/api/v1/runs/RUN_ID \
  -H "Authorization: Bearer oc_your_key"
# → 200 { "status": "done", "result": { "holds": [ … ] } }
```

## Endpoints

### POST /api/v1/runs

Submit an image for hold detection.

Multipart form with an `image` file and optional JSON fields. Returns 202
immediately; the run is processed asynchronously.

| Field      | Type        | Required | Description                                           |
| ---------- | ----------- | :------: | ----------------------------------------------------- |
| `image`    | file        |   yes    | Wall photo (JPEG/PNG/WebP/HEIC, max 40 MB).           |
| `config`   | string      |    no    | Detection config id. Defaults to the standard config. |
| `metadata` | JSON string |    no    | Optional object echoed back in run history.           |

| Status | Meaning                                            |
| :----: | -------------------------------------------------- |
| `202`  | Run accepted and queued.                           |
| `400`  | `invalid_request` or `invalid_image`.              |
| `401`  | `unauthorized`.                                    |
| `402`  | `quota_exceeded` or `account_locked`.              |
| `413`  | `image_too_large`.                                 |
| `429`  | `rate_limited`; wait out the `retry-after` header. |

### GET /api/v1/runs/{id}

Get a run.

| Path parameter | Required | Description     |
| -------------- | :------: | --------------- |
| `id`           |   yes    | The run's uuid. |

| Status | Meaning                                                             |
| :----: | ------------------------------------------------------------------- |
| `200`  | Current run state; `result.holds` is present once status is `done`. |
| `401`  | `unauthorized`.                                                     |
| `404`  | `not_found`.                                                        |

### GET /api/v1/runs/{id}/progress

Stream a run's progressive stages.

Server-sent events (`text/event-stream`) that deliver the run's stages as they
finish: `status` with the current state, `progress` with the holds each stage
has found so far, then `done` with the final run or `failed` with the error.
The stream ends at the terminal event, or after two minutes if the worker never
finishes.

| Path parameter | Required | Description     |
| -------------- | :------: | --------------- |
| `id`           |   yes    | The run's uuid. |

| Status | Meaning         |
| :----: | --------------- |
| `200`  | The SSE stream. |
| `401`  | `unauthorized`. |
| `404`  | `not_found`.    |

## Error codes

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

| Code                  | Status | Meaning                                        |
| --------------------- | :----: | ---------------------------------------------- |
| `invalid_request`     | `400`  | The request or its fields are not valid.       |
| `invalid_image`       | `400`  | The upload does not decode as an image.        |
| `image_too_large`     | `413`  | The image exceeds the size limit.              |
| `unauthorized`        | `401`  | Missing or invalid API key.                    |
| `not_found`           | `404`  | No such run, or it belongs to another account. |
| `quota_exceeded`      | `402`  | The monthly inference quota is used up.        |
| `account_locked`      | `402`  | The subscription is inactive.                  |
| `rate_limited`        | `429`  | Too many requests; retry after the delay.      |
| `inference_failed`    | `500`  | The detection run failed.                      |
| `storage_unavailable` | `503`  | Image storage was unreachable.                 |
