{
  "openapi": "3.1.0",
  "info": {
    "title": "Clutch Partner API",
    "version": "v1",
    "description": "Camera controls, recordings, court observations and player integrations. Public Gateway behavior is documented, including compatibility caveats. See the guides for retries, consent, webhooks and complete workflow examples.",
    "contact": {
      "name": "Brennan Lee — API access, improvements and bug reports",
      "email": "brennan@clutchapp.io"
    }
  },
  "servers": [
    {
      "url": "https://api.clutchapp.io/v1",
      "description": "Production Partner API"
    }
  ],
  "security": [
    {
      "bearerAuth": []
    }
  ],
  "tags": [
    {
      "name": "authentication",
      "description": "See the authentication integration reference for full workflow guidance."
    },
    {
      "name": "cameras",
      "description": "See the cameras integration reference for full workflow guidance."
    },
    {
      "name": "videos",
      "description": "See the videos integration reference for full workflow guidance."
    },
    {
      "name": "player-links",
      "description": "See the player-links integration reference for full workflow guidance."
    },
    {
      "name": "annotations",
      "description": "See the annotations integration reference for full workflow guidance."
    },
    {
      "name": "recordings",
      "description": "See the recordings integration reference for full workflow guidance."
    },
    {
      "name": "history",
      "description": "See the history integration reference for full workflow guidance."
    },
    {
      "name": "webhooks",
      "description": "See the webhooks integration reference for full workflow guidance."
    }
  ],
  "paths": {
    "/auth/login": {
      "post": {
        "operationId": "post_auth_login",
        "summary": "Log in",
        "tags": [
          "authentication"
        ],
        "description": "Creates a Partner JWT valid for 12 hours. Share one token across backend workers and reuse it until shortly before its `exp` time. Allow only one login in flight per account.\n\nOn an authenticated endpoint’s `401`, renew once and retry once; stop if it still fails. A login `401` means the credentials are wrong or API access is not enabled for the account; correct the credentials or ask Clutch to enable access before trying again. A `403` requires checking permissions. For login `429`, wait for `Retry-After` seconds across all workers. For login `503` or a network failure, use exponential backoff with jitter, starting at one second, capped at 30 seconds, with at most three retries. Other writes follow their endpoint’s retry rules.\n\n**Body:**\n\n```json\n{\n  \"email\": \"partner@example.com\",\n  \"password\": \"<partner-password>\"\n}\n```\n\n**Sample response**\n\n```json\n{\n  \"data\": {\n    \"id\": \"dd2dfc8c-4244-4a6e-b410-eec76f9254c0\",\n    \"token\": \"<partner-jwt>\"\n  }\n}\n```",
        "parameters": [],
        "responses": {
          "200": {
            "description": "Successful response. Read the endpoint-specific status and error fields.",
            "content": {
              "application/json": {
                "schema": {
                  "type": [
                    "object",
                    "null"
                  ],
                  "additionalProperties": true,
                  "properties": {
                    "data": {
                      "type": [
                        "object",
                        "null"
                      ],
                      "additionalProperties": true,
                      "properties": {
                        "id": {
                          "type": [
                            "string",
                            "null"
                          ]
                        },
                        "token": {
                          "type": [
                            "string",
                            "null"
                          ]
                        }
                      }
                    }
                  }
                },
                "example": {
                  "data": {
                    "id": "dd2dfc8c-4244-4a6e-b410-eec76f9254c0",
                    "token": "<partner-jwt>"
                  }
                }
              }
            }
          },
          "401": {
            "description": "Invalid credentials, expired token or API access not enabled.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "429": {
            "description": "Rate limit reached.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            },
            "headers": {
              "Retry-After": {
                "description": "Seconds to wait before retrying. Gateway /start may instead return HTTP 200 without this header; see its documented caveat.",
                "schema": {
                  "type": "integer",
                  "minimum": 0
                }
              }
            }
          },
          "503": {
            "description": "Service unavailable.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        },
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/Login"
              },
              "example": {
                "email": "partner@example.com",
                "password": "<partner-password>"
              }
            }
          }
        },
        "security": [],
        "x-codeSamples": [
          {
            "lang": "curl",
            "source": "curl --request POST \\\n  'https://api.clutchapp.io/v1/auth/login' \\\n  --header 'Content-Type: application/json' \\\n  --data '{\"email\":\"partner@example.com\",\"password\":\"<partner-password>\"}'"
          }
        ]
      }
    },
    "/clutchcam/device/{device_id}/status": {
      "get": {
        "operationId": "get_clutchcam_device_device_id_status",
        "summary": "Get camera status",
        "tags": [
          "cameras"
        ],
        "description": "Returns the camera status and active recording. `status` can be `available`, `unavailable` (offline), `rebooting`, `initializing`, `recording`, `stopping`, `uploading`, `uploaded` or `error`. A recording can start only when it is `available`.\n\n**Sample response**\n\n```json\n{\n  \"data\": {\n    \"id\": \"4580f207-b672-4df0-83eb-110cc408466b\",\n    \"name\": \"Example Court\",\n    \"status\": \"available\",\n    \"video_id\": null,\n    \"max_recording_duration_min\": null,\n    \"recording_started_at\": null,\n    \"used_by\": null,\n    \"sport\": \"padel\"\n  }\n}\n```",
        "parameters": [
          {
            "name": "device_id",
            "in": "path",
            "required": true,
            "description": "URL-encoded device_id.",
            "schema": {
              "type": "string"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Successful response. Read the endpoint-specific status and error fields.",
            "content": {
              "application/json": {
                "schema": {
                  "type": [
                    "object",
                    "null"
                  ],
                  "additionalProperties": true,
                  "properties": {
                    "data": {
                      "type": [
                        "object",
                        "null"
                      ],
                      "additionalProperties": true,
                      "properties": {
                        "id": {
                          "type": [
                            "string",
                            "null"
                          ]
                        },
                        "name": {
                          "type": [
                            "string",
                            "null"
                          ]
                        },
                        "status": {
                          "type": [
                            "string",
                            "null"
                          ]
                        },
                        "video_id": {},
                        "max_recording_duration_min": {},
                        "recording_started_at": {},
                        "used_by": {},
                        "sport": {
                          "type": [
                            "string",
                            "null"
                          ]
                        }
                      }
                    }
                  }
                },
                "example": {
                  "data": {
                    "id": "4580f207-b672-4df0-83eb-110cc408466b",
                    "name": "Example Court",
                    "status": "available",
                    "video_id": null,
                    "max_recording_duration_min": null,
                    "recording_started_at": null,
                    "used_by": null,
                    "sport": "padel"
                  }
                }
              }
            }
          },
          "400": {
            "description": "Invalid request.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "401": {
            "description": "Invalid credentials, expired token or API access not enabled.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              },
              "text/plain": {
                "schema": {
                  "type": "string"
                },
                "example": "Unauthorized"
              }
            }
          },
          "403": {
            "description": "Access denied.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "404": {
            "description": "Resource missing or inaccessible.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        },
        "x-codeSamples": [
          {
            "lang": "curl",
            "source": "curl --request GET \\\n  'https://api.clutchapp.io/v1/clutchcam/device/{device_id}/status' \\\n  --header 'Authorization: Bearer YOUR_TOKEN'"
          }
        ]
      }
    },
    "/clutchcam/device/{device_id}/schedule": {
      "post": {
        "operationId": "post_clutchcam_device_device_id_schedule",
        "summary": "Schedule a recording",
        "tags": [
          "cameras"
        ],
        "description": "Schedules a recording.\n\n**Body:**\n\n```json\n{\n  \"start_time\": \"2026-08-01T17:00:00Z\",\n  \"max_recording_duration_min\": 90,\n  \"privacy\": \"private\"\n}\n```\n\n`start_time` and `max_recording_duration_min` are required. `recording_type` is optional and accepts `match` or `standard`, defaulting to `match`. A `standard` recording is video only, with no AI analysis. `game_type` is optional and accepts `singles` or `doubles`, defaulting to `doubles`, so send `\"game_type\": \"singles\"` for a singles match.\n\n`privacy` is optional on both schedule and start. It accepts `public` or `private` and defaults to `public`. A private recording is hidden from other users’ feeds and profiles; the owner and direct links still work.\n\n`multi_match` is also optional on both; see Multi-match recordings below.\n\n**Sample response**\n\n```json\n{\n  \"data\": {\n    \"id\": \"4580f207-b672-4df0-83eb-110cc408466b\",\n    \"name\": \"Example Court\",\n    \"status\": \"SCHEDULED\",\n    \"used_by\": null,\n    \"video_id\": \"B92BFC8A-2454-4632-9580-F96E99FB7C85\",\n    \"sport\": \"padel\",\n    \"recording_starts_at\": \"2026-08-01T17:00:00Z\",\n    \"recording_duration\": 90\n  }\n}\n```\n\nA schedule conflict returns HTTP 409 with `data.conflicting_ids`.\n\n\n\n`POST /start`, `POST /stop` and `POST /schedule` can return:\n\n| Status | Response | Meaning |\n| --- | --- | --- |\n| 400 | `{\"error\": \"...\"}`, or `{\"message\": \"Invalid request body\"}` on `/start` | A field is invalid, for example `privacy` or a past `start_time`. On `/start`, a missing or invalid `recording_type`, `game_type` or `max_recording_duration_min` returns the second form. |\n| 404 | `{\"errors\": {\"detail\": \"Not Found\"}}`, or `{\"error\": \"Device not found: ...\"}` on `/schedule` | The camera does not exist. |\n| 409 | `{\"error\": \"...\"}` | `/start` would run into a scheduled recording (IDs in `conflicting_recordings`) or the account has too many recordings running. `/stop` could not stop the recording in its current state. `/schedule` conflicts are described above. |\n| 422 | `{\"error\": \"...\"}` | `/start` could not be completed. |\n| 429 | `{\"error\": \"Too many camera control requests from this account; try again later\"}` | Wait for `Retry-After` seconds. |\n| 503 | `{\"errors\": {\"detail\": \"Service Unavailable\"}}` | The camera is not in the state `/start` or `/stop` needs. Read `GET /clutchcam/device/{device_id}/status` and try again once it changes. |\n\nThrough `api.clutchapp.io`, a rate-limited `POST /start` currently returns HTTP 200 with the same `error` body and no `Retry-After`. Treat a start response without `data` as failed, and wait 10 minutes before trying again.\n\nAfter a timeout or 5xx on these calls, read the camera's status or scheduled list before retrying, because the first request may have worked.\n\n## Multi-match recordings\n\nIf a recording covers several back-to-back matches with different players, such as an americano, mixer, round robin or tournament, Clutch can analyse each match separately.\n\nAdd `\"multi_match\": true` when you start or schedule a recording:\n\n- `POST /clutchcam/device/{device_id}/start`\n- `POST /clutchcam/device/{device_id}/schedule`\n\n```json\n{\n  \"recording_type\": \"match\",\n  \"game_type\": \"doubles\",\n  \"max_recording_duration_min\": 90,\n  \"multi_match\": true\n}\n```\n\nIt needs `\"game_type\": \"doubles\"`, and any other game type returns a 400. For normal recordings, leave the field out.\n\nGet `match.json` from `GET /video/{video_id}/results` as usual. It now includes a `periods` list, with one entry for each match:\n\n```json\n{\n\"periods\": [\n  {\n    \"period_index\": 0,\n    \"start_time\": \"00:00:07\",\n    \"end_time\": \"00:34:45\",\n    \"player_ids\": [\"player-12\", \"player-41\", \"player-7\", \"player-29\"],\n    \"player_ids_mapping_to_pair\": {\n      \"pair-1\": [\"player-12\", \"player-41\"],\n      \"pair-2\": [\"player-7\", \"player-29\"]\n    },\n    \"match_stats\": {\"match_time_minutes\": 34.63, \"avg_rally_shots\": 4.6}\n  },\n  {\n    \"period_index\": 1,\n    \"start_time\": \"00:36:00\",\n    \"end_time\": \"01:13:00\",\n    \"player_ids\": [\"player-55\", \"player-63\", \"player-70\", \"player-81\"],\n    \"player_ids_mapping_to_pair\": {\n      \"pair-1\": [\"player-55\", \"player-63\"],\n      \"pair-2\": [\"player-70\", \"player-81\"]\n    },\n    \"match_stats\": {\"match_time_minutes\": 37.0, \"avg_rally_shots\": 5.2}\n  }\n]\n}\n```\n\n- Each match has its own teams, stats and highlights.\n- `player_stats` includes every player from every match.\n- Each match gets new player IDs, so someone who plays three matches appears under three `player-N` IDs.\n- In `player_crop_urls.json`, each player photo has a `period_index` showing which match it belongs to.\n\nThrough `api.clutchapp.io`, `POST /start` does not pass `multi_match` on yet; use `/schedule` for multi-match recordings until it does.\n",
        "parameters": [
          {
            "name": "device_id",
            "in": "path",
            "required": true,
            "description": "URL-encoded device_id.",
            "schema": {
              "type": "string"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Successful response. Read the endpoint-specific status and error fields.",
            "content": {
              "application/json": {
                "schema": {
                  "type": [
                    "object",
                    "null"
                  ],
                  "additionalProperties": true,
                  "properties": {
                    "data": {
                      "type": [
                        "object",
                        "null"
                      ],
                      "additionalProperties": true,
                      "properties": {
                        "id": {
                          "type": [
                            "string",
                            "null"
                          ]
                        },
                        "name": {
                          "type": [
                            "string",
                            "null"
                          ]
                        },
                        "status": {
                          "type": [
                            "string",
                            "null"
                          ]
                        },
                        "used_by": {},
                        "video_id": {
                          "type": [
                            "string",
                            "null"
                          ]
                        },
                        "sport": {
                          "type": [
                            "string",
                            "null"
                          ]
                        },
                        "recording_starts_at": {
                          "type": [
                            "string",
                            "null"
                          ]
                        },
                        "recording_duration": {
                          "type": [
                            "number",
                            "null"
                          ]
                        }
                      }
                    }
                  }
                },
                "example": {
                  "data": {
                    "id": "4580f207-b672-4df0-83eb-110cc408466b",
                    "name": "Example Court",
                    "status": "SCHEDULED",
                    "used_by": null,
                    "video_id": "B92BFC8A-2454-4632-9580-F96E99FB7C85",
                    "sport": "padel",
                    "recording_starts_at": "2026-08-01T17:00:00Z",
                    "recording_duration": 90
                  }
                }
              }
            }
          },
          "400": {
            "description": "Invalid request.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "401": {
            "description": "Invalid credentials, expired token or API access not enabled.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              },
              "text/plain": {
                "schema": {
                  "type": "string"
                },
                "example": "Unauthorized"
              }
            }
          },
          "403": {
            "description": "Access denied.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "404": {
            "description": "Resource missing or inaccessible.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "409": {
            "description": "Conflict, unavailable camera/media, missing consent or annotation revision conflict.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "422": {
            "description": "Invalid configuration or recording not scheduled.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "429": {
            "description": "Rate limit reached.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            },
            "headers": {
              "Retry-After": {
                "description": "Seconds to wait before retrying. Gateway /start may instead return HTTP 200 without this header; see its documented caveat.",
                "schema": {
                  "type": "integer",
                  "minimum": 0
                }
              }
            }
          },
          "503": {
            "description": "Service unavailable.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        },
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/Schedule"
              },
              "example": {
                "start_time": "2026-08-01T17:00:00Z",
                "max_recording_duration_min": 90,
                "privacy": "private"
              }
            }
          }
        },
        "x-codeSamples": [
          {
            "lang": "curl",
            "source": "curl --request POST \\\n  'https://api.clutchapp.io/v1/clutchcam/device/{device_id}/schedule' \\\n  --header 'Authorization: Bearer YOUR_TOKEN' \\\n  --header 'Content-Type: application/json' \\\n  --data '{\"start_time\":\"2026-08-01T17:00:00Z\",\"max_recording_duration_min\":90,\"privacy\":\"private\"}'"
          }
        ]
      }
    },
    "/clutchcam/device/{device_id}/scheduled": {
      "get": {
        "operationId": "get_clutchcam_device_device_id_scheduled",
        "summary": "List scheduled recordings",
        "tags": [
          "cameras"
        ],
        "description": "Returns scheduled recordings for a camera. A camera with nothing scheduled, or an unknown `device_id`, returns HTTP 404 with `{\"error\": \"No scheduled recordings found for device <device_id>\"}` rather than an empty list.\n\n**Sample response**\n\n```json\n{\n  \"data\": [\n    {\n      \"id\": \"B92BFC8A-2454-4632-9580-F96E99FB7C85\",\n      \"status\": \"SCHEDULED\",\n      \"scheduled_at\": \"2026-08-01T17:00:00Z\",\n      \"end_time\": \"2026-08-01T18:30:00Z\",\n      \"sport\": \"padel\",\n      \"recording_type\": \"match\",\n      \"game_type\": \"doubles\"\n    }\n  ]\n}\n```",
        "parameters": [
          {
            "name": "device_id",
            "in": "path",
            "required": true,
            "description": "URL-encoded device_id.",
            "schema": {
              "type": "string"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Successful response. Read the endpoint-specific status and error fields.",
            "content": {
              "application/json": {
                "schema": {
                  "type": [
                    "object",
                    "null"
                  ],
                  "additionalProperties": true,
                  "properties": {
                    "data": {
                      "type": "array",
                      "items": {
                        "type": [
                          "object",
                          "null"
                        ],
                        "additionalProperties": true,
                        "properties": {
                          "id": {
                            "type": [
                              "string",
                              "null"
                            ]
                          },
                          "status": {
                            "type": [
                              "string",
                              "null"
                            ]
                          },
                          "scheduled_at": {
                            "type": [
                              "string",
                              "null"
                            ]
                          },
                          "end_time": {
                            "type": [
                              "string",
                              "null"
                            ]
                          },
                          "sport": {
                            "type": [
                              "string",
                              "null"
                            ]
                          },
                          "recording_type": {
                            "type": [
                              "string",
                              "null"
                            ]
                          },
                          "game_type": {
                            "type": [
                              "string",
                              "null"
                            ]
                          }
                        }
                      }
                    }
                  }
                },
                "example": {
                  "data": [
                    {
                      "id": "B92BFC8A-2454-4632-9580-F96E99FB7C85",
                      "status": "SCHEDULED",
                      "scheduled_at": "2026-08-01T17:00:00Z",
                      "end_time": "2026-08-01T18:30:00Z",
                      "sport": "padel",
                      "recording_type": "match",
                      "game_type": "doubles"
                    }
                  ]
                }
              }
            }
          },
          "400": {
            "description": "Invalid request.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "401": {
            "description": "Invalid credentials, expired token or API access not enabled.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              },
              "text/plain": {
                "schema": {
                  "type": "string"
                },
                "example": "Unauthorized"
              }
            }
          },
          "403": {
            "description": "Access denied.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "404": {
            "description": "Resource missing or inaccessible.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        },
        "x-codeSamples": [
          {
            "lang": "curl",
            "source": "curl --request GET \\\n  'https://api.clutchapp.io/v1/clutchcam/device/{device_id}/scheduled' \\\n  --header 'Authorization: Bearer YOUR_TOKEN'"
          }
        ]
      }
    },
    "/clutchcam/device/{device_id}/recordings": {
      "get": {
        "operationId": "get_clutchcam_device_device_id_recordings",
        "summary": "List camera recordings",
        "tags": [
          "cameras"
        ],
        "description": "Lists visible recordings whose `start_time` falls within the inclusive date range, newest first. Without `from` or `to`, the range defaults to the last seven days. Each page contains at most 100 recordings.\n\nThe list excludes deleted recordings and private recordings created by another account. The authenticated account can see its own private recordings.\n\n**Sample response**\n\n```json\n{\n  \"data\": [\n    {\n      \"id\": \"B92BFC8A-2454-4632-9580-F96E99FB7C85\",\n      \"status\": \"OK\",\n      \"recording_type\": \"match\",\n      \"game_type\": \"doubles\",\n      \"sport\": \"padel\",\n      \"title\": \"Example Court\",\n      \"start_time\": \"2026-08-01T17:02:00Z\",\n      \"end_time\": \"2026-08-01T18:30:00Z\"\n    }\n  ],\n  \"next_cursor\": null\n}\n```\n\nPass a non-null `next_cursor` back as `cursor` with the same `device_id`, `from`, and `to`. Invalid dates, ranges, or cursors return HTTP 400.",
        "parameters": [
          {
            "name": "device_id",
            "in": "path",
            "required": true,
            "description": "URL-encoded device_id.",
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "from",
            "in": "query",
            "description": "Inclusive UTC ISO 8601 boundary; defaults to the last seven days.",
            "schema": {
              "type": "string",
              "format": "date-time"
            }
          },
          {
            "name": "to",
            "in": "query",
            "description": "Inclusive UTC ISO 8601 boundary; defaults to the last seven days.",
            "schema": {
              "type": "string",
              "format": "date-time"
            }
          },
          {
            "name": "cursor",
            "in": "query",
            "description": "Opaque next_cursor from the preceding page. Preserve the other filters.",
            "schema": {
              "type": "string"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Successful response. Read the endpoint-specific status and error fields.",
            "content": {
              "application/json": {
                "schema": {
                  "type": [
                    "object",
                    "null"
                  ],
                  "additionalProperties": true,
                  "properties": {
                    "data": {
                      "type": "array",
                      "items": {
                        "type": [
                          "object",
                          "null"
                        ],
                        "additionalProperties": true,
                        "properties": {
                          "id": {
                            "type": [
                              "string",
                              "null"
                            ]
                          },
                          "status": {
                            "type": [
                              "string",
                              "null"
                            ]
                          },
                          "recording_type": {
                            "type": [
                              "string",
                              "null"
                            ]
                          },
                          "game_type": {
                            "type": [
                              "string",
                              "null"
                            ]
                          },
                          "sport": {
                            "type": [
                              "string",
                              "null"
                            ]
                          },
                          "title": {
                            "type": [
                              "string",
                              "null"
                            ]
                          },
                          "start_time": {
                            "type": [
                              "string",
                              "null"
                            ]
                          },
                          "end_time": {
                            "type": [
                              "string",
                              "null"
                            ]
                          }
                        }
                      }
                    },
                    "next_cursor": {}
                  }
                },
                "example": {
                  "data": [
                    {
                      "id": "B92BFC8A-2454-4632-9580-F96E99FB7C85",
                      "status": "OK",
                      "recording_type": "match",
                      "game_type": "doubles",
                      "sport": "padel",
                      "title": "Example Court",
                      "start_time": "2026-08-01T17:02:00Z",
                      "end_time": "2026-08-01T18:30:00Z"
                    }
                  ],
                  "next_cursor": null
                }
              }
            }
          },
          "400": {
            "description": "Invalid request.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "401": {
            "description": "Invalid credentials, expired token or API access not enabled.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              },
              "text/plain": {
                "schema": {
                  "type": "string"
                },
                "example": "Unauthorized"
              }
            }
          },
          "403": {
            "description": "Access denied.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        },
        "x-codeSamples": [
          {
            "lang": "curl",
            "source": "curl --request GET \\\n  'https://api.clutchapp.io/v1/clutchcam/device/{device_id}/recordings' \\\n  --header 'Authorization: Bearer YOUR_TOKEN'"
          }
        ]
      }
    },
    "/clutchcam/device/{device_id}/start": {
      "post": {
        "operationId": "post_clutchcam_device_device_id_start",
        "summary": "Start a recording",
        "tags": [
          "cameras"
        ],
        "description": "Starts a recording. The camera must be `available`. `recording_type`, `game_type` and `max_recording_duration_min` are required. Schedule above lists the accepted values, and `max_recording_duration_min` must be 1 to 120.\n\n**Body:**\n\n```json\n{\n  \"recording_type\": \"match\",\n  \"game_type\": \"doubles\",\n  \"max_recording_duration_min\": 120,\n  \"privacy\": \"private\"\n}\n```\n\n**Sample response**\n\n```json\n{\n  \"data\": {\n    \"id\": \"4580f207-b672-4df0-83eb-110cc408466b\",\n    \"name\": \"Example Court\",\n    \"status\": \"initializing\",\n    \"used_by\": \"dd2dfc8c-4244-4a6e-b410-eec76f9254c0\",\n    \"video_id\": \"B92BFC8A-2454-4632-9580-F96E99FB7C85\",\n    \"sport\": \"padel\",\n    \"recording_started_at\": null,\n    \"max_recording_duration_min\": 120\n  }\n}\n```\n\n\n\n`POST /start`, `POST /stop` and `POST /schedule` can return:\n\n| Status | Response | Meaning |\n| --- | --- | --- |\n| 400 | `{\"error\": \"...\"}`, or `{\"message\": \"Invalid request body\"}` on `/start` | A field is invalid, for example `privacy` or a past `start_time`. On `/start`, a missing or invalid `recording_type`, `game_type` or `max_recording_duration_min` returns the second form. |\n| 404 | `{\"errors\": {\"detail\": \"Not Found\"}}`, or `{\"error\": \"Device not found: ...\"}` on `/schedule` | The camera does not exist. |\n| 409 | `{\"error\": \"...\"}` | `/start` would run into a scheduled recording (IDs in `conflicting_recordings`) or the account has too many recordings running. `/stop` could not stop the recording in its current state. `/schedule` conflicts are described above. |\n| 422 | `{\"error\": \"...\"}` | `/start` could not be completed. |\n| 429 | `{\"error\": \"Too many camera control requests from this account; try again later\"}` | Wait for `Retry-After` seconds. |\n| 503 | `{\"errors\": {\"detail\": \"Service Unavailable\"}}` | The camera is not in the state `/start` or `/stop` needs. Read `GET /clutchcam/device/{device_id}/status` and try again once it changes. |\n\nThrough `api.clutchapp.io`, a rate-limited `POST /start` currently returns HTTP 200 with the same `error` body and no `Retry-After`. Treat a start response without `data` as failed, and wait 10 minutes before trying again.\n\nAfter a timeout or 5xx on these calls, read the camera's status or scheduled list before retrying, because the first request may have worked.\n\n## Multi-match recordings\n\nIf a recording covers several back-to-back matches with different players, such as an americano, mixer, round robin or tournament, Clutch can analyse each match separately.\n\nAdd `\"multi_match\": true` when you start or schedule a recording:\n\n- `POST /clutchcam/device/{device_id}/start`\n- `POST /clutchcam/device/{device_id}/schedule`\n\n```json\n{\n  \"recording_type\": \"match\",\n  \"game_type\": \"doubles\",\n  \"max_recording_duration_min\": 90,\n  \"multi_match\": true\n}\n```\n\nIt needs `\"game_type\": \"doubles\"`, and any other game type returns a 400. For normal recordings, leave the field out.\n\nGet `match.json` from `GET /video/{video_id}/results` as usual. It now includes a `periods` list, with one entry for each match:\n\n```json\n{\n\"periods\": [\n  {\n    \"period_index\": 0,\n    \"start_time\": \"00:00:07\",\n    \"end_time\": \"00:34:45\",\n    \"player_ids\": [\"player-12\", \"player-41\", \"player-7\", \"player-29\"],\n    \"player_ids_mapping_to_pair\": {\n      \"pair-1\": [\"player-12\", \"player-41\"],\n      \"pair-2\": [\"player-7\", \"player-29\"]\n    },\n    \"match_stats\": {\"match_time_minutes\": 34.63, \"avg_rally_shots\": 4.6}\n  },\n  {\n    \"period_index\": 1,\n    \"start_time\": \"00:36:00\",\n    \"end_time\": \"01:13:00\",\n    \"player_ids\": [\"player-55\", \"player-63\", \"player-70\", \"player-81\"],\n    \"player_ids_mapping_to_pair\": {\n      \"pair-1\": [\"player-55\", \"player-63\"],\n      \"pair-2\": [\"player-70\", \"player-81\"]\n    },\n    \"match_stats\": {\"match_time_minutes\": 37.0, \"avg_rally_shots\": 5.2}\n  }\n]\n}\n```\n\n- Each match has its own teams, stats and highlights.\n- `player_stats` includes every player from every match.\n- Each match gets new player IDs, so someone who plays three matches appears under three `player-N` IDs.\n- In `player_crop_urls.json`, each player photo has a `period_index` showing which match it belongs to.\n\nThrough `api.clutchapp.io`, `POST /start` does not pass `multi_match` on yet; use `/schedule` for multi-match recordings until it does.\n",
        "parameters": [
          {
            "name": "device_id",
            "in": "path",
            "required": true,
            "description": "URL-encoded device_id.",
            "schema": {
              "type": "string"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Recording accepted, or a Gateway rate-limit error body without data. Treat the latter as failure and wait 10 minutes before retrying.",
            "content": {
              "application/json": {
                "schema": {
                  "oneOf": [
                    {
                      "type": [
                        "object",
                        "null"
                      ],
                      "additionalProperties": true,
                      "properties": {
                        "data": {
                          "type": [
                            "object",
                            "null"
                          ],
                          "additionalProperties": true,
                          "properties": {
                            "id": {
                              "type": [
                                "string",
                                "null"
                              ]
                            },
                            "name": {
                              "type": [
                                "string",
                                "null"
                              ]
                            },
                            "status": {
                              "type": [
                                "string",
                                "null"
                              ]
                            },
                            "used_by": {
                              "type": [
                                "string",
                                "null"
                              ]
                            },
                            "video_id": {
                              "type": [
                                "string",
                                "null"
                              ]
                            },
                            "sport": {
                              "type": [
                                "string",
                                "null"
                              ]
                            },
                            "recording_started_at": {},
                            "max_recording_duration_min": {
                              "type": [
                                "number",
                                "null"
                              ]
                            }
                          }
                        }
                      },
                      "required": [
                        "data"
                      ]
                    },
                    {
                      "$ref": "#/components/schemas/Error"
                    }
                  ]
                },
                "example": {
                  "data": {
                    "id": "4580f207-b672-4df0-83eb-110cc408466b",
                    "name": "Example Court",
                    "status": "initializing",
                    "used_by": "dd2dfc8c-4244-4a6e-b410-eec76f9254c0",
                    "video_id": "B92BFC8A-2454-4632-9580-F96E99FB7C85",
                    "sport": "padel",
                    "recording_started_at": null,
                    "max_recording_duration_min": 120
                  }
                }
              }
            }
          },
          "400": {
            "description": "Invalid request.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "401": {
            "description": "Invalid credentials, expired token or API access not enabled.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              },
              "text/plain": {
                "schema": {
                  "type": "string"
                },
                "example": "Unauthorized"
              }
            }
          },
          "403": {
            "description": "Access denied.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "404": {
            "description": "Resource missing or inaccessible.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "409": {
            "description": "Conflict, unavailable camera/media, missing consent or annotation revision conflict.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "422": {
            "description": "Invalid configuration or recording not scheduled.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "429": {
            "description": "Rate limit reached.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            },
            "headers": {
              "Retry-After": {
                "description": "Seconds to wait before retrying. Gateway /start may instead return HTTP 200 without this header; see its documented caveat.",
                "schema": {
                  "type": "integer",
                  "minimum": 0
                }
              }
            }
          },
          "503": {
            "description": "Service unavailable.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        },
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/Start"
              },
              "example": {
                "recording_type": "match",
                "game_type": "doubles",
                "max_recording_duration_min": 120,
                "privacy": "private"
              }
            }
          }
        },
        "x-codeSamples": [
          {
            "lang": "curl",
            "source": "curl --request POST \\\n  'https://api.clutchapp.io/v1/clutchcam/device/{device_id}/start' \\\n  --header 'Authorization: Bearer YOUR_TOKEN' \\\n  --header 'Content-Type: application/json' \\\n  --data '{\"recording_type\":\"match\",\"game_type\":\"doubles\",\"max_recording_duration_min\":120,\"privacy\":\"private\"}'"
          }
        ]
      }
    },
    "/clutchcam/device/{device_id}/stop": {
      "post": {
        "operationId": "post_clutchcam_device_device_id_stop",
        "summary": "Stop a recording",
        "tags": [
          "cameras"
        ],
        "description": "Stops the active recording. The camera must be `recording` or `stopping`.\n\n**Sample response**\n\n```json\n{\n  \"data\": {\n    \"id\": \"4580f207-b672-4df0-83eb-110cc408466b\",\n    \"name\": \"Example Court\",\n    \"status\": \"stopping\",\n    \"used_by\": \"dd2dfc8c-4244-4a6e-b410-eec76f9254c0\",\n    \"video_id\": \"B92BFC8A-2454-4632-9580-F96E99FB7C85\",\n    \"sport\": \"padel\",\n    \"recording_started_at\": \"2026-08-01T17:00:00Z\",\n    \"max_recording_duration_min\": 120\n  }\n}\n```\n\n\n\n`POST /start`, `POST /stop` and `POST /schedule` can return:\n\n| Status | Response | Meaning |\n| --- | --- | --- |\n| 400 | `{\"error\": \"...\"}`, or `{\"message\": \"Invalid request body\"}` on `/start` | A field is invalid, for example `privacy` or a past `start_time`. On `/start`, a missing or invalid `recording_type`, `game_type` or `max_recording_duration_min` returns the second form. |\n| 404 | `{\"errors\": {\"detail\": \"Not Found\"}}`, or `{\"error\": \"Device not found: ...\"}` on `/schedule` | The camera does not exist. |\n| 409 | `{\"error\": \"...\"}` | `/start` would run into a scheduled recording (IDs in `conflicting_recordings`) or the account has too many recordings running. `/stop` could not stop the recording in its current state. `/schedule` conflicts are described above. |\n| 422 | `{\"error\": \"...\"}` | `/start` could not be completed. |\n| 429 | `{\"error\": \"Too many camera control requests from this account; try again later\"}` | Wait for `Retry-After` seconds. |\n| 503 | `{\"errors\": {\"detail\": \"Service Unavailable\"}}` | The camera is not in the state `/start` or `/stop` needs. Read `GET /clutchcam/device/{device_id}/status` and try again once it changes. |\n\nThrough `api.clutchapp.io`, a rate-limited `POST /start` currently returns HTTP 200 with the same `error` body and no `Retry-After`. Treat a start response without `data` as failed, and wait 10 minutes before trying again.\n\nAfter a timeout or 5xx on these calls, read the camera's status or scheduled list before retrying, because the first request may have worked.\n\n## Multi-match recordings\n\nIf a recording covers several back-to-back matches with different players, such as an americano, mixer, round robin or tournament, Clutch can analyse each match separately.\n\nAdd `\"multi_match\": true` when you start or schedule a recording:\n\n- `POST /clutchcam/device/{device_id}/start`\n- `POST /clutchcam/device/{device_id}/schedule`\n\n```json\n{\n  \"recording_type\": \"match\",\n  \"game_type\": \"doubles\",\n  \"max_recording_duration_min\": 90,\n  \"multi_match\": true\n}\n```\n\nIt needs `\"game_type\": \"doubles\"`, and any other game type returns a 400. For normal recordings, leave the field out.\n\nGet `match.json` from `GET /video/{video_id}/results` as usual. It now includes a `periods` list, with one entry for each match:\n\n```json\n{\n\"periods\": [\n  {\n    \"period_index\": 0,\n    \"start_time\": \"00:00:07\",\n    \"end_time\": \"00:34:45\",\n    \"player_ids\": [\"player-12\", \"player-41\", \"player-7\", \"player-29\"],\n    \"player_ids_mapping_to_pair\": {\n      \"pair-1\": [\"player-12\", \"player-41\"],\n      \"pair-2\": [\"player-7\", \"player-29\"]\n    },\n    \"match_stats\": {\"match_time_minutes\": 34.63, \"avg_rally_shots\": 4.6}\n  },\n  {\n    \"period_index\": 1,\n    \"start_time\": \"00:36:00\",\n    \"end_time\": \"01:13:00\",\n    \"player_ids\": [\"player-55\", \"player-63\", \"player-70\", \"player-81\"],\n    \"player_ids_mapping_to_pair\": {\n      \"pair-1\": [\"player-55\", \"player-63\"],\n      \"pair-2\": [\"player-70\", \"player-81\"]\n    },\n    \"match_stats\": {\"match_time_minutes\": 37.0, \"avg_rally_shots\": 5.2}\n  }\n]\n}\n```\n\n- Each match has its own teams, stats and highlights.\n- `player_stats` includes every player from every match.\n- Each match gets new player IDs, so someone who plays three matches appears under three `player-N` IDs.\n- In `player_crop_urls.json`, each player photo has a `period_index` showing which match it belongs to.\n\nThrough `api.clutchapp.io`, `POST /start` does not pass `multi_match` on yet; use `/schedule` for multi-match recordings until it does.\n",
        "parameters": [
          {
            "name": "device_id",
            "in": "path",
            "required": true,
            "description": "URL-encoded device_id.",
            "schema": {
              "type": "string"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Successful response. Read the endpoint-specific status and error fields.",
            "content": {
              "application/json": {
                "schema": {
                  "type": [
                    "object",
                    "null"
                  ],
                  "additionalProperties": true,
                  "properties": {
                    "data": {
                      "type": [
                        "object",
                        "null"
                      ],
                      "additionalProperties": true,
                      "properties": {
                        "id": {
                          "type": [
                            "string",
                            "null"
                          ]
                        },
                        "name": {
                          "type": [
                            "string",
                            "null"
                          ]
                        },
                        "status": {
                          "type": [
                            "string",
                            "null"
                          ]
                        },
                        "used_by": {
                          "type": [
                            "string",
                            "null"
                          ]
                        },
                        "video_id": {
                          "type": [
                            "string",
                            "null"
                          ]
                        },
                        "sport": {
                          "type": [
                            "string",
                            "null"
                          ]
                        },
                        "recording_started_at": {
                          "type": [
                            "string",
                            "null"
                          ]
                        },
                        "max_recording_duration_min": {
                          "type": [
                            "number",
                            "null"
                          ]
                        }
                      }
                    }
                  }
                },
                "example": {
                  "data": {
                    "id": "4580f207-b672-4df0-83eb-110cc408466b",
                    "name": "Example Court",
                    "status": "stopping",
                    "used_by": "dd2dfc8c-4244-4a6e-b410-eec76f9254c0",
                    "video_id": "B92BFC8A-2454-4632-9580-F96E99FB7C85",
                    "sport": "padel",
                    "recording_started_at": "2026-08-01T17:00:00Z",
                    "max_recording_duration_min": 120
                  }
                }
              }
            }
          },
          "400": {
            "description": "Invalid request.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "401": {
            "description": "Invalid credentials, expired token or API access not enabled.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              },
              "text/plain": {
                "schema": {
                  "type": "string"
                },
                "example": "Unauthorized"
              }
            }
          },
          "403": {
            "description": "Access denied.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "404": {
            "description": "Resource missing or inaccessible.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "409": {
            "description": "Conflict, unavailable camera/media, missing consent or annotation revision conflict.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "429": {
            "description": "Rate limit reached.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            },
            "headers": {
              "Retry-After": {
                "description": "Seconds to wait before retrying. Gateway /start may instead return HTTP 200 without this header; see its documented caveat.",
                "schema": {
                  "type": "integer",
                  "minimum": 0
                }
              }
            }
          },
          "503": {
            "description": "Service unavailable.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        },
        "x-codeSamples": [
          {
            "lang": "curl",
            "source": "curl --request POST \\\n  'https://api.clutchapp.io/v1/clutchcam/device/{device_id}/stop' \\\n  --header 'Authorization: Bearer YOUR_TOKEN'"
          }
        ]
      }
    },
    "/clutchcam/court-status/request": {
      "post": {
        "operationId": "post_clutchcam_court_status_request",
        "summary": "Request a court observation",
        "tags": [
          "cameras"
        ],
        "description": "# Court status for lighting automation\n\nReturns whether a camera’s court is occupied, empty or dark. Your application decides when to switch the lights; this API does not control them.\n\nBoth endpoints use the existing `Authorization: Bearer <token>` header and `Content-Type: application/json`.\n\n### `POST /clutchcam/court-status/request`\n\nRequests a court observation. Analysis runs in the background.\n\n**Request:**\n\n```javascript\nPOST https://api.clutchapp.io/v1/clutchcam/court-status/request\n```\n\n```json\n{\n  \"device_id\": \"your-camera-id\"\n}\n```\n\n**HTTP 202 — Accepted:**\n\n```json\n{\n  \"job_id\": \"1b475178-b3b4-4c24-977d-591dd347de29\",\n  \"device_id\": \"your-camera-id\",\n  \"error\": null\n}\n```\n\nUse the returned UUID `job_id` to retrieve the outcome. Acceptance does not confirm that the camera is online or that analysis will succeed.\n\n### `POST /clutchcam/court-status/result`\n\nRetrieves the outcome of a requested observation. Poll every three seconds while the job is pending or running.\n\n**Request:**\n\n```javascript\nPOST https://api.clutchapp.io/v1/clutchcam/court-status/result\n```\n\n```json\n{\n  \"device_id\": \"your-camera-id\",\n  \"job_id\": \"1b475178-b3b4-4c24-977d-591dd347de29\"\n}\n```\n\n**HTTP 202 — Pending or running:**\n\n```json\n{\n  \"job_id\": \"1b475178-b3b4-4c24-977d-591dd347de29\",\n  \"device_id\": \"your-camera-id\",\n  \"state\": \"RUNNING\",\n  \"court_status\": null,\n  \"error\": null\n}\n```\n\n**HTTP 200 — Completed:**\n\n```json\n{\n  \"job_id\": \"1b475178-b3b4-4c24-977d-591dd347de29\",\n  \"device_id\": \"your-camera-id\",\n  \"state\": \"COMPLETED\",\n  \"court_status\": \"OCCUPIED\",\n  \"error\": null\n}\n```\n\n| `court_status` | Meaning |\n| --- | --- |\n| `OCCUPIED` | At least one person is detected in the court’s monitored area. |\n| `EMPTY` | No person is detected in the monitored area, and the image is not dark. |\n| `LIGHTS_OFF` | The image meets the darkness threshold. This does not confirm the physical light-switch position. |\n\nJob states are `PENDING`, `RUNNING`, `COMPLETED` and `FAILED`.\n\n### Freshness\n\nA request may reuse an active job or a successful observation from the previous five minutes. Results do not include the observation timestamp.\n\n`/result` retrieves the original outcome; it does not refresh the observation. Call `/request` again whenever you need an updated court state. Completed results remain readable while retained and access remains valid.\n\nJobs have a five-minute deadline, including queue time.\n\n### Offline cameras and errors\n\nIf the camera is marked unavailable when capture runs, analysis fails with **HTTP 409**:\n\n```json\n{\n  \"job_id\": \"1b475178-b3b4-4c24-977d-591dd347de29\",\n  \"device_id\": \"your-camera-id\",\n  \"state\": \"FAILED\",\n  \"court_status\": null,\n  \"error\": \"internal_error\"\n}\n```\n\nA disconnection during capture may instead produce an upstream error or timeout. There is no separate public `CAMERA_OFFLINE` error code.\n\n| HTTP status | Action |\n| --- | --- |\n| `400` | Check the required JSON fields. |\n| `401 / 403` | Check authentication and camera or recording access. |\n| `404` | Check the device and job IDs. Unknown or inaccessible jobs also return this status. |\n| `409` | Camera unavailable or media not ready; retry later. |\n| `422` | Check recording configuration. |\n| `502 / 503 / 504` | Analysis unavailable or timed out; retry later. |\n\nAuthentication errors may use the shared API error format.\n\n**When analysis fails or ****`court_status`**** is ****`null`****, keep the current light state. Never interpret an error as ****`EMPTY`****.** A successful observation may be cached and does not confirm current camera connectivity. Apply your booking rules and vacancy delay before switching lights off.\n\n## Python example\n\n<a href=\"/examples/court_status.py\" download>Download court_status.py</a>. Requires Python 3.9+ and uses only the standard library.\n\nSet `CLUTCH_API_TOKEN` to your existing bearer token, then run:\n\n```sh\npython3 court_status.py YOUR_CAMERA_ID\n```\n\nThe script submits an observation, polls every three seconds, and prints the completed court status. It stops on HTTP errors, invalid responses, or timeout. It does not issue lighting commands.\n\n```python\n#!/usr/bin/env python3\n\"\"\"Request a court observation and poll for its result. Never controls lights.\"\"\"\nimport argparse\nimport json\nimport os\nimport time\nimport urllib.request\n\nBASE_URL = \"https://api.clutchapp.io/v1/clutchcam/court-status\"\n\n\ndef post_json(path, token, body, timeout=15):\n    request = urllib.request.Request(\n        f\"{BASE_URL}/{path}\",\n        data=json.dumps(body).encode(\"utf-8\"),\n        headers={\"Authorization\": f\"Bearer {token}\", \"Content-Type\": \"application/json\"},\n        method=\"POST\",\n    )\n    # urllib raises HTTPError for non-2xx responses. Callers must preserve light\n    # state on this error; no failure is interpreted as an empty court.\n    with urllib.request.urlopen(request, timeout=timeout) as response:\n        return response.status, json.load(response)\n\n\ndef observe_court(device_id, token, wait_seconds=330, poll_interval=3):\n    if not device_id or not token:\n        raise ValueError(\"A device ID and bearer token are required\")\n    if wait_seconds <= 0 or poll_interval <= 0:\n        raise ValueError(\"Polling interval and timeout must be positive\")\n    deadline = time.monotonic() + wait_seconds\n    status, ack = post_json(\"request\", token, {\"device_id\": device_id}, min(15, wait_seconds))\n    if status != 202 or not ack.get(\"job_id\") or ack.get(\"error\"):\n        raise RuntimeError(\"Court observation was not accepted\")\n    while True:\n        remaining = deadline - time.monotonic()\n        if remaining <= 0:\n            raise TimeoutError(\"Court status unavailable; keep the current light state\")\n        time.sleep(min(poll_interval, remaining))\n        remaining = deadline - time.monotonic()\n        if remaining <= 0:\n            raise TimeoutError(\"Court status unavailable; keep the current light state\")\n        status, result = post_json(\n            \"result\", token,\n            {\"device_id\": device_id, \"job_id\": ack[\"job_id\"]},\n            min(15, remaining),\n        )\n        if status == 200 and result.get(\"state\") == \"COMPLETED\" and result.get(\"error\") is None:\n            court_status = result.get(\"court_status\")\n            if court_status not in {\"OCCUPIED\", \"EMPTY\", \"LIGHTS_OFF\"}:\n                raise RuntimeError(\"Unknown court status; keep the current light state\")\n            return court_status\n        if status != 202 or result.get(\"state\") not in {\"PENDING\", \"RUNNING\"} or result.get(\"error\"):\n            raise RuntimeError(\"Court observation failed; keep the current light state\")\n\n\nif __name__ == \"__main__\":\n    parser = argparse.ArgumentParser(description=__doc__)\n    parser.add_argument(\"device_id\", help=\"Clutch camera ID\")\n    args = parser.parse_args()\n    token = os.environ.get(\"CLUTCH_API_TOKEN\", \"\")\n    if not token:\n        parser.error(\"Set CLUTCH_API_TOKEN to your existing bearer token\")\n    print(observe_court(args.device_id, token))\n\n```\n",
        "parameters": [],
        "responses": {
          "202": {
            "description": "Accepted or not ready; follow the endpoint-specific polling rules.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/CourtAcknowledgement"
                },
                "example": {
                  "job_id": "1b475178-b3b4-4c24-977d-591dd347de29",
                  "device_id": "your-camera-id",
                  "error": null
                }
              }
            }
          },
          "400": {
            "description": "Invalid request.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "description": "Generic internal_error with device_id and job_id; result errors also include FAILED and null court_status. Authentication errors may use shared formats."
                }
              }
            }
          },
          "401": {
            "description": "Invalid credentials, expired token or API access not enabled.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "description": "Generic internal_error with device_id and job_id; result errors also include FAILED and null court_status. Authentication errors may use shared formats."
                }
              },
              "text/plain": {
                "schema": {
                  "type": "string"
                },
                "example": "Unauthorized"
              }
            }
          },
          "403": {
            "description": "Access denied.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "description": "Generic internal_error with device_id and job_id; result errors also include FAILED and null court_status. Authentication errors may use shared formats."
                }
              }
            }
          },
          "404": {
            "description": "Resource missing or inaccessible.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "description": "Generic internal_error with device_id and job_id; result errors also include FAILED and null court_status. Authentication errors may use shared formats."
                }
              }
            }
          },
          "503": {
            "description": "Service unavailable.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "description": "Generic internal_error with device_id and job_id; result errors also include FAILED and null court_status. Authentication errors may use shared formats."
                }
              }
            }
          }
        },
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/CourtRequest"
              },
              "example": {
                "device_id": "your-camera-id"
              }
            }
          }
        },
        "x-codeSamples": [
          {
            "lang": "curl",
            "source": "curl --request POST \\\n  'https://api.clutchapp.io/v1/clutchcam/court-status/request' \\\n  --header 'Authorization: Bearer YOUR_TOKEN' \\\n  --header 'Content-Type: application/json' \\\n  --data '{\"device_id\":\"your-camera-id\"}'"
          }
        ]
      }
    },
    "/clutchcam/court-status/result": {
      "post": {
        "operationId": "post_clutchcam_court_status_result",
        "summary": "Poll a court observation",
        "tags": [
          "cameras"
        ],
        "description": "# Court status for lighting automation\n\nReturns whether a camera’s court is occupied, empty or dark. Your application decides when to switch the lights; this API does not control them.\n\nBoth endpoints use the existing `Authorization: Bearer <token>` header and `Content-Type: application/json`.\n\n### `POST /clutchcam/court-status/request`\n\nRequests a court observation. Analysis runs in the background.\n\n**Request:**\n\n```javascript\nPOST https://api.clutchapp.io/v1/clutchcam/court-status/request\n```\n\n```json\n{\n  \"device_id\": \"your-camera-id\"\n}\n```\n\n**HTTP 202 — Accepted:**\n\n```json\n{\n  \"job_id\": \"1b475178-b3b4-4c24-977d-591dd347de29\",\n  \"device_id\": \"your-camera-id\",\n  \"error\": null\n}\n```\n\nUse the returned UUID `job_id` to retrieve the outcome. Acceptance does not confirm that the camera is online or that analysis will succeed.\n\n### `POST /clutchcam/court-status/result`\n\nRetrieves the outcome of a requested observation. Poll every three seconds while the job is pending or running.\n\n**Request:**\n\n```javascript\nPOST https://api.clutchapp.io/v1/clutchcam/court-status/result\n```\n\n```json\n{\n  \"device_id\": \"your-camera-id\",\n  \"job_id\": \"1b475178-b3b4-4c24-977d-591dd347de29\"\n}\n```\n\n**HTTP 202 — Pending or running:**\n\n```json\n{\n  \"job_id\": \"1b475178-b3b4-4c24-977d-591dd347de29\",\n  \"device_id\": \"your-camera-id\",\n  \"state\": \"RUNNING\",\n  \"court_status\": null,\n  \"error\": null\n}\n```\n\n**HTTP 200 — Completed:**\n\n```json\n{\n  \"job_id\": \"1b475178-b3b4-4c24-977d-591dd347de29\",\n  \"device_id\": \"your-camera-id\",\n  \"state\": \"COMPLETED\",\n  \"court_status\": \"OCCUPIED\",\n  \"error\": null\n}\n```\n\n| `court_status` | Meaning |\n| --- | --- |\n| `OCCUPIED` | At least one person is detected in the court’s monitored area. |\n| `EMPTY` | No person is detected in the monitored area, and the image is not dark. |\n| `LIGHTS_OFF` | The image meets the darkness threshold. This does not confirm the physical light-switch position. |\n\nJob states are `PENDING`, `RUNNING`, `COMPLETED` and `FAILED`.\n\n### Freshness\n\nA request may reuse an active job or a successful observation from the previous five minutes. Results do not include the observation timestamp.\n\n`/result` retrieves the original outcome; it does not refresh the observation. Call `/request` again whenever you need an updated court state. Completed results remain readable while retained and access remains valid.\n\nJobs have a five-minute deadline, including queue time.\n\n### Offline cameras and errors\n\nIf the camera is marked unavailable when capture runs, analysis fails with **HTTP 409**:\n\n```json\n{\n  \"job_id\": \"1b475178-b3b4-4c24-977d-591dd347de29\",\n  \"device_id\": \"your-camera-id\",\n  \"state\": \"FAILED\",\n  \"court_status\": null,\n  \"error\": \"internal_error\"\n}\n```\n\nA disconnection during capture may instead produce an upstream error or timeout. There is no separate public `CAMERA_OFFLINE` error code.\n\n| HTTP status | Action |\n| --- | --- |\n| `400` | Check the required JSON fields. |\n| `401 / 403` | Check authentication and camera or recording access. |\n| `404` | Check the device and job IDs. Unknown or inaccessible jobs also return this status. |\n| `409` | Camera unavailable or media not ready; retry later. |\n| `422` | Check recording configuration. |\n| `502 / 503 / 504` | Analysis unavailable or timed out; retry later. |\n\nAuthentication errors may use the shared API error format.\n\n**When analysis fails or ****`court_status`**** is ****`null`****, keep the current light state. Never interpret an error as ****`EMPTY`****.** A successful observation may be cached and does not confirm current camera connectivity. Apply your booking rules and vacancy delay before switching lights off.\n\n## Python example\n\n<a href=\"/examples/court_status.py\" download>Download court_status.py</a>. Requires Python 3.9+ and uses only the standard library.\n\nSet `CLUTCH_API_TOKEN` to your existing bearer token, then run:\n\n```sh\npython3 court_status.py YOUR_CAMERA_ID\n```\n\nThe script submits an observation, polls every three seconds, and prints the completed court status. It stops on HTTP errors, invalid responses, or timeout. It does not issue lighting commands.\n\n```python\n#!/usr/bin/env python3\n\"\"\"Request a court observation and poll for its result. Never controls lights.\"\"\"\nimport argparse\nimport json\nimport os\nimport time\nimport urllib.request\n\nBASE_URL = \"https://api.clutchapp.io/v1/clutchcam/court-status\"\n\n\ndef post_json(path, token, body, timeout=15):\n    request = urllib.request.Request(\n        f\"{BASE_URL}/{path}\",\n        data=json.dumps(body).encode(\"utf-8\"),\n        headers={\"Authorization\": f\"Bearer {token}\", \"Content-Type\": \"application/json\"},\n        method=\"POST\",\n    )\n    # urllib raises HTTPError for non-2xx responses. Callers must preserve light\n    # state on this error; no failure is interpreted as an empty court.\n    with urllib.request.urlopen(request, timeout=timeout) as response:\n        return response.status, json.load(response)\n\n\ndef observe_court(device_id, token, wait_seconds=330, poll_interval=3):\n    if not device_id or not token:\n        raise ValueError(\"A device ID and bearer token are required\")\n    if wait_seconds <= 0 or poll_interval <= 0:\n        raise ValueError(\"Polling interval and timeout must be positive\")\n    deadline = time.monotonic() + wait_seconds\n    status, ack = post_json(\"request\", token, {\"device_id\": device_id}, min(15, wait_seconds))\n    if status != 202 or not ack.get(\"job_id\") or ack.get(\"error\"):\n        raise RuntimeError(\"Court observation was not accepted\")\n    while True:\n        remaining = deadline - time.monotonic()\n        if remaining <= 0:\n            raise TimeoutError(\"Court status unavailable; keep the current light state\")\n        time.sleep(min(poll_interval, remaining))\n        remaining = deadline - time.monotonic()\n        if remaining <= 0:\n            raise TimeoutError(\"Court status unavailable; keep the current light state\")\n        status, result = post_json(\n            \"result\", token,\n            {\"device_id\": device_id, \"job_id\": ack[\"job_id\"]},\n            min(15, remaining),\n        )\n        if status == 200 and result.get(\"state\") == \"COMPLETED\" and result.get(\"error\") is None:\n            court_status = result.get(\"court_status\")\n            if court_status not in {\"OCCUPIED\", \"EMPTY\", \"LIGHTS_OFF\"}:\n                raise RuntimeError(\"Unknown court status; keep the current light state\")\n            return court_status\n        if status != 202 or result.get(\"state\") not in {\"PENDING\", \"RUNNING\"} or result.get(\"error\"):\n            raise RuntimeError(\"Court observation failed; keep the current light state\")\n\n\nif __name__ == \"__main__\":\n    parser = argparse.ArgumentParser(description=__doc__)\n    parser.add_argument(\"device_id\", help=\"Clutch camera ID\")\n    args = parser.parse_args()\n    token = os.environ.get(\"CLUTCH_API_TOKEN\", \"\")\n    if not token:\n        parser.error(\"Set CLUTCH_API_TOKEN to your existing bearer token\")\n    print(observe_court(args.device_id, token))\n\n```\n",
        "parameters": [],
        "responses": {
          "200": {
            "description": "Successful response. Read the endpoint-specific status and error fields.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/CourtResult"
                },
                "example": {
                  "job_id": "1b475178-b3b4-4c24-977d-591dd347de29",
                  "device_id": "your-camera-id",
                  "state": "COMPLETED",
                  "court_status": "OCCUPIED",
                  "error": null
                }
              }
            }
          },
          "202": {
            "description": "Accepted or not ready; follow the endpoint-specific polling rules.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/CourtResult"
                },
                "example": {
                  "job_id": "1b475178-b3b4-4c24-977d-591dd347de29",
                  "device_id": "your-camera-id",
                  "state": "RUNNING",
                  "court_status": null,
                  "error": null
                }
              }
            }
          },
          "400": {
            "description": "Invalid request.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "description": "Generic internal_error with device_id and job_id; result errors also include FAILED and null court_status. Authentication errors may use shared formats."
                }
              }
            }
          },
          "401": {
            "description": "Invalid credentials, expired token or API access not enabled.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "description": "Generic internal_error with device_id and job_id; result errors also include FAILED and null court_status. Authentication errors may use shared formats."
                }
              },
              "text/plain": {
                "schema": {
                  "type": "string"
                },
                "example": "Unauthorized"
              }
            }
          },
          "403": {
            "description": "Access denied.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "description": "Generic internal_error with device_id and job_id; result errors also include FAILED and null court_status. Authentication errors may use shared formats."
                }
              }
            }
          },
          "404": {
            "description": "Resource missing or inaccessible.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "description": "Generic internal_error with device_id and job_id; result errors also include FAILED and null court_status. Authentication errors may use shared formats."
                }
              }
            }
          },
          "409": {
            "description": "Conflict, unavailable camera/media, missing consent or annotation revision conflict.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "description": "Generic internal_error with device_id and job_id; result errors also include FAILED and null court_status. Authentication errors may use shared formats."
                }
              }
            }
          },
          "422": {
            "description": "Invalid configuration or recording not scheduled.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "description": "Generic internal_error with device_id and job_id; result errors also include FAILED and null court_status. Authentication errors may use shared formats."
                }
              }
            }
          },
          "502": {
            "description": "Upstream response unavailable or invalid.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "description": "Generic internal_error with device_id and job_id; result errors also include FAILED and null court_status. Authentication errors may use shared formats."
                }
              }
            }
          },
          "503": {
            "description": "Service unavailable.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "description": "Generic internal_error with device_id and job_id; result errors also include FAILED and null court_status. Authentication errors may use shared formats."
                }
              }
            }
          },
          "504": {
            "description": "Capture or analysis timed out.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "description": "Generic internal_error with device_id and job_id; result errors also include FAILED and null court_status. Authentication errors may use shared formats."
                }
              }
            }
          }
        },
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/CourtPoll"
              },
              "example": {
                "device_id": "your-camera-id",
                "job_id": "1b475178-b3b4-4c24-977d-591dd347de29"
              }
            }
          }
        },
        "x-codeSamples": [
          {
            "lang": "curl",
            "source": "curl --request POST \\\n  'https://api.clutchapp.io/v1/clutchcam/court-status/result' \\\n  --header 'Authorization: Bearer YOUR_TOKEN' \\\n  --header 'Content-Type: application/json' \\\n  --data '{\"device_id\":\"your-camera-id\",\"job_id\":\"1b475178-b3b4-4c24-977d-591dd347de29\"}'"
          }
        ]
      }
    },
    "/video/{video_id}/status": {
      "get": {
        "operationId": "get_video_video_id_status",
        "summary": "Get recording status",
        "tags": [
          "videos"
        ],
        "description": "Returns `id`, `device_id`, `status`, `start_time`, `end_time`, `delayed_processing`, `delayed_processing_since`, and `updated_at`.\n\n| Status | Meaning |\n| --- | --- |\n| `SCHEDULED` | A future recording exists and has not started. |\n| `QUEUED` | The initial processing job is waiting for a worker. |\n| `RECORDING` | The camera is capturing the recording. |\n| `PROCESSING` | The initial Clutch analysis is running. |\n| `UPLOADING` | The camera is uploading the full recording. |\n| `UPLOADED` | The upload finished. This is terminal for standard recordings and may be transient for match recordings. |\n| `UPLOAD_FAILED` | The full-recording upload failed. |\n| `COMPLETED` | Initial analysis is complete. Stats, highlights, and `video_stream` are ready. The recording may remain in `COMPLETED` while the camera uploads the raw video. Clutch may reprocess it if poor internet caused excessive frame loss. |\n| `COMPLETED_EMPTY_COURT` | Initial analysis found no usable match. The stream may be ready. Match stats and highlights are omitted. |\n| `DELAYED_PROCESSING` | Initial processing failed or was incomplete. Clutch will retry through delayed processing, usually within 1 to 2 hours. |\n| `OK` | All processing is complete. The full video and available match outputs are ready. |\n| `OK_EMPTY_COURT` | All processing is complete and no usable match was found. The full video may be available. Match stats and highlights are omitted. |\n| `FAILED_DEVICE_OCCUPIED` | The scheduled recording could not start because the camera remained busy. |\n| `FAILED_DEVICE_OFFLINE` | The scheduled recording could not start because the camera was offline. |\n| `CANCELLED` | The scheduled recording was cancelled before it started. |\n\n`delayed_processing` reports whether a second processing pass is expected, running, or finished: `not_expected`, `expected`, `in_progress`, or `done`. `expected` means Clutch detected a degraded stream while the recording was in progress and will reprocess once the camera’s recording uploads.\n\n`delayed_processing_since` is when the pending re-run was detected for `expected`, not when the re-run starts. For `in_progress` and `done` it is when the re-run was dispatched. Null otherwise.\n\nAn internal `FAILED` state is returned as `DELAYED_PROCESSING`. Treat any undocumented transient status as in progress.\n\n**Sample response**\n\n```json\n{\n  \"data\": {\n    \"id\": \"B92BFC8A-2454-4632-9580-F96E99FB7C85\",\n    \"device_id\": \"4580f207-b672-4df0-83eb-110cc408466b\",\n    \"status\": \"PROCESSING\",\n    \"start_time\": \"2026-08-01T17:02:00Z\",\n    \"end_time\": \"2026-08-01T18:30:00Z\",\n    \"delayed_processing\": \"not_expected\",\n    \"delayed_processing_since\": null,\n    \"updated_at\": \"2026-08-01T19:05:00Z\"\n  }\n}\n```",
        "parameters": [
          {
            "name": "video_id",
            "in": "path",
            "required": true,
            "description": "URL-encoded video_id.",
            "schema": {
              "type": "string"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Successful response. Read the endpoint-specific status and error fields.",
            "content": {
              "application/json": {
                "schema": {
                  "type": [
                    "object",
                    "null"
                  ],
                  "additionalProperties": true,
                  "properties": {
                    "data": {
                      "type": [
                        "object",
                        "null"
                      ],
                      "additionalProperties": true,
                      "properties": {
                        "id": {
                          "type": [
                            "string",
                            "null"
                          ]
                        },
                        "device_id": {
                          "type": [
                            "string",
                            "null"
                          ]
                        },
                        "status": {
                          "type": [
                            "string",
                            "null"
                          ]
                        },
                        "start_time": {
                          "type": [
                            "string",
                            "null"
                          ]
                        },
                        "end_time": {
                          "type": [
                            "string",
                            "null"
                          ]
                        },
                        "delayed_processing": {
                          "type": [
                            "string",
                            "null"
                          ]
                        },
                        "delayed_processing_since": {},
                        "updated_at": {
                          "type": [
                            "string",
                            "null"
                          ]
                        }
                      }
                    }
                  }
                },
                "example": {
                  "data": {
                    "id": "B92BFC8A-2454-4632-9580-F96E99FB7C85",
                    "device_id": "4580f207-b672-4df0-83eb-110cc408466b",
                    "status": "PROCESSING",
                    "start_time": "2026-08-01T17:02:00Z",
                    "end_time": "2026-08-01T18:30:00Z",
                    "delayed_processing": "not_expected",
                    "delayed_processing_since": null,
                    "updated_at": "2026-08-01T19:05:00Z"
                  }
                }
              }
            }
          },
          "400": {
            "description": "Invalid request.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "401": {
            "description": "Invalid credentials, expired token or API access not enabled.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              },
              "text/plain": {
                "schema": {
                  "type": "string"
                },
                "example": "Unauthorized"
              }
            }
          },
          "403": {
            "description": "Access denied.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "404": {
            "description": "Resource missing or inaccessible.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        },
        "x-codeSamples": [
          {
            "lang": "curl",
            "source": "curl --request GET \\\n  'https://api.clutchapp.io/v1/video/{video_id}/status' \\\n  --header 'Authorization: Bearer YOUR_TOKEN'"
          }
        ]
      }
    },
    "/video/{video_id}/results": {
      "get": {
        "operationId": "get_video_video_id_results",
        "summary": "Get recording outputs",
        "tags": [
          "videos"
        ],
        "description": "Returns the Clutch app link and output URLs. Treat URLs as opaque and call the endpoint again for current values. HTTP 202 means there are no results to return right now, and the message names the current status, for example `{\"error\": \"Video is not yet processed. Status: PROCESSING\"}`.\n\n`CANCELLED`, `FAILED_DEVICE_OCCUPIED` and `FAILED_DEVICE_OFFLINE` also return 202 but will never have results, so stop polling on those statuses. `UPLOAD_FAILED` also returns 202, and results only follow if the camera retries the upload successfully.\n\nHTTP 200 does not always mean the analysis is ready. During `DELAYED_PROCESSING` this endpoint returns 200 with only `video` or an empty `outputs` list, and `court_keypoints` is `null`. Use `GET /video/{video_id}/status` or the webhook to see when analysis has finished.\n\n**Sample response**\n\n```json\n{\n  \"data\": {\n    \"id\": \"B92BFC8A-2454-4632-9580-F96E99FB7C85\",\n    \"device_id\": \"4580f207-b672-4df0-83eb-110cc408466b\",\n    \"app_link\": \"https://app.clutchapp.io/match/B92BFC8A-2454-4632-9580-F96E99FB7C85\",\n    \"outputs\": [\n      {\n        \"key\": \"match.json\",\n        \"url\": \"https://assets.example.com/public/dd2dfc8c-4244-4a6e-b410-eec76f9254c0/output/B92BFC8A-2454-4632-9580-F96E99FB7C85/data/match.json\"\n      },\n      {\n        \"key\": \"video_stream\",\n        \"url\": \"https://assets.example.com/public/dd2dfc8c-4244-4a6e-b410-eec76f9254c0/B92BFC8A-2454-4632-9580-F96E99FB7C85_stream.mp4\"\n      }\n    ],\n    \"court_keypoints\": {\n      \"frame\": { \"width\": 1920, \"height\": 1080 },\n      \"points\": {\n        \"tol\": [741, 165],\n        \"tor\": [1187, 165],\n        \"point_7\": [112, 810],\n        \"point_9\": [1803, 815]\n      }\n    }\n  }\n}\n```\n\nOutput keys may include `video`, `video_stream`, `match.json`, `player_crop_urls.json`, thumbnails, highlight manifests, and highlight videos.\n\n`court_keypoints` gives the court points the AI used for the recording, in pixels of a 1920×1080 frame, or `null` when none were saved. The AI analyses every recording at that size, so scale by `width / 1920` and `height / 1080` before drawing on a 4K `video`. The points apply to `video` and `video_stream`, not to the cropped highlight clips. Each recording keeps the points it was processed with, even if the camera is moved later.\n\nA padel court is 10 m wide and 20 m long. `tol` is the origin, the far baseline corner on the left of the image. `x` runs across the court and `y` towards the camera, with the net at `y = 10` and the service lines at `y = 3` and `y = 17`. Current cameras label four points: `tol` (0, 0), `tor` (10, 0), `point_7` (0, 17) and `point_9` (10, 17), in metres. A perspective transform over those four pairs, such as OpenCV's `getPerspectiveTransform`, maps pixels to court metres and back. The wide lens bends lines slightly near the frame edges, so the transform is exact at the labelled points and approximate elsewhere. Recordings from before 2025 can carry other point names; other sports label the court corners `tol`, `tor`, `bol` and `bor`.",
        "parameters": [
          {
            "name": "video_id",
            "in": "path",
            "required": true,
            "description": "URL-encoded video_id.",
            "schema": {
              "type": "string"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Successful response. Read the endpoint-specific status and error fields.",
            "content": {
              "application/json": {
                "schema": {
                  "type": [
                    "object",
                    "null"
                  ],
                  "additionalProperties": true,
                  "properties": {
                    "data": {
                      "type": [
                        "object",
                        "null"
                      ],
                      "additionalProperties": true,
                      "properties": {
                        "id": {
                          "type": [
                            "string",
                            "null"
                          ]
                        },
                        "device_id": {
                          "type": [
                            "string",
                            "null"
                          ]
                        },
                        "app_link": {
                          "type": [
                            "string",
                            "null"
                          ]
                        },
                        "outputs": {
                          "type": "array",
                          "items": {
                            "type": [
                              "object",
                              "null"
                            ],
                            "additionalProperties": true,
                            "properties": {
                              "key": {
                                "type": [
                                  "string",
                                  "null"
                                ]
                              },
                              "url": {
                                "type": [
                                  "string",
                                  "null"
                                ]
                              }
                            }
                          }
                        },
                        "court_keypoints": {
                          "type": [
                            "object",
                            "null"
                          ],
                          "additionalProperties": true,
                          "properties": {
                            "frame": {
                              "type": [
                                "object",
                                "null"
                              ],
                              "additionalProperties": true,
                              "properties": {
                                "width": {
                                  "type": [
                                    "number",
                                    "null"
                                  ]
                                },
                                "height": {
                                  "type": [
                                    "number",
                                    "null"
                                  ]
                                }
                              }
                            },
                            "points": {
                              "type": [
                                "object",
                                "null"
                              ],
                              "additionalProperties": true,
                              "properties": {
                                "tol": {
                                  "type": "array",
                                  "items": {
                                    "type": [
                                      "number",
                                      "null"
                                    ]
                                  }
                                },
                                "tor": {
                                  "type": "array",
                                  "items": {
                                    "type": [
                                      "number",
                                      "null"
                                    ]
                                  }
                                },
                                "point_7": {
                                  "type": "array",
                                  "items": {
                                    "type": [
                                      "number",
                                      "null"
                                    ]
                                  }
                                },
                                "point_9": {
                                  "type": "array",
                                  "items": {
                                    "type": [
                                      "number",
                                      "null"
                                    ]
                                  }
                                }
                              }
                            }
                          }
                        }
                      }
                    }
                  }
                },
                "example": {
                  "data": {
                    "id": "B92BFC8A-2454-4632-9580-F96E99FB7C85",
                    "device_id": "4580f207-b672-4df0-83eb-110cc408466b",
                    "app_link": "https://app.clutchapp.io/match/B92BFC8A-2454-4632-9580-F96E99FB7C85",
                    "outputs": [
                      {
                        "key": "match.json",
                        "url": "https://assets.example.com/public/dd2dfc8c-4244-4a6e-b410-eec76f9254c0/output/B92BFC8A-2454-4632-9580-F96E99FB7C85/data/match.json"
                      },
                      {
                        "key": "video_stream",
                        "url": "https://assets.example.com/public/dd2dfc8c-4244-4a6e-b410-eec76f9254c0/B92BFC8A-2454-4632-9580-F96E99FB7C85_stream.mp4"
                      }
                    ],
                    "court_keypoints": {
                      "frame": {
                        "width": 1920,
                        "height": 1080
                      },
                      "points": {
                        "tol": [
                          741,
                          165
                        ],
                        "tor": [
                          1187,
                          165
                        ],
                        "point_7": [
                          112,
                          810
                        ],
                        "point_9": [
                          1803,
                          815
                        ]
                      }
                    }
                  }
                }
              }
            }
          },
          "202": {
            "description": "Accepted or not ready; follow the endpoint-specific polling rules.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                },
                "example": {
                  "error": "Video is not yet processed. Status: PROCESSING"
                }
              }
            }
          },
          "400": {
            "description": "Invalid request.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "401": {
            "description": "Invalid credentials, expired token or API access not enabled.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              },
              "text/plain": {
                "schema": {
                  "type": "string"
                },
                "example": "Unauthorized"
              }
            }
          },
          "403": {
            "description": "Access denied.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "404": {
            "description": "Resource missing or inaccessible.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        },
        "x-codeSamples": [
          {
            "lang": "curl",
            "source": "curl --request GET \\\n  'https://api.clutchapp.io/v1/video/{video_id}/results' \\\n  --header 'Authorization: Bearer YOUR_TOKEN'"
          }
        ]
      }
    },
    "/video/{video_id}/cancel": {
      "delete": {
        "operationId": "delete_video_video_id_cancel",
        "summary": "Cancel a scheduled recording",
        "tags": [
          "videos"
        ],
        "description": "Cancels a scheduled recording.\n\n**Sample response**\n\n```json\n{\n  \"message\": \"Recording scheduled for start-time: 2026-08-01 17:00:00Z, end-time: 2026-08-01 18:30:00Z has now been cancelled.\",\n  \"video_id\": \"B92BFC8A-2454-4632-9580-F96E99FB7C85\"\n}\n```",
        "parameters": [
          {
            "name": "video_id",
            "in": "path",
            "required": true,
            "description": "URL-encoded video_id.",
            "schema": {
              "type": "string"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Successful response. Read the endpoint-specific status and error fields.",
            "content": {
              "application/json": {
                "schema": {
                  "type": [
                    "object",
                    "null"
                  ],
                  "additionalProperties": true,
                  "properties": {
                    "message": {
                      "type": [
                        "string",
                        "null"
                      ]
                    },
                    "video_id": {
                      "type": [
                        "string",
                        "null"
                      ]
                    }
                  }
                },
                "example": {
                  "message": "Recording scheduled for start-time: 2026-08-01 17:00:00Z, end-time: 2026-08-01 18:30:00Z has now been cancelled.",
                  "video_id": "B92BFC8A-2454-4632-9580-F96E99FB7C85"
                }
              }
            }
          },
          "400": {
            "description": "Invalid request.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "401": {
            "description": "Invalid credentials, expired token or API access not enabled.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              },
              "text/plain": {
                "schema": {
                  "type": "string"
                },
                "example": "Unauthorized"
              }
            }
          },
          "403": {
            "description": "Access denied.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "404": {
            "description": "Resource missing or inaccessible.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "422": {
            "description": "Invalid configuration or recording not scheduled.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        },
        "x-codeSamples": [
          {
            "lang": "curl",
            "source": "curl --request DELETE \\\n  'https://api.clutchapp.io/v1/video/{video_id}/cancel' \\\n  --header 'Authorization: Bearer YOUR_TOKEN'"
          }
        ]
      }
    },
    "/partner/player-links": {
      "post": {
        "operationId": "post_partner_player_links",
        "summary": "Create a player connection or tagging link",
        "tags": [
          "player-links"
        ],
        "description": "Request annotation permission:\n\n```json\n{\n  \"partner_player_id\": \"player_1042\",\n  \"purpose\": \"annotation\",\n  \"return_url\": \"clutchpartner://clutch/return\",\n  \"state\": \"5hj9R4Wpi7zj2Zy8S7qzMQ\"\n}\n```\n\n`return_url` is optional; the other fields are required for `purpose: \"annotation\"`. Omit `video_id` and keep the recording and selected player in your app. Omitting `purpose` preserves the Clutch-hosted tagging flow below.\n\nBind a fresh random `state` (at least 128 bits) to the signed-in player and saved destination. If supplied, `return_url` must exactly match a registered HTTPS app link or custom scheme; ask Clutch to register it. Reserve its `state` and `status` query names.\n\n`201 Created` when consent is needed:\n\n```json\n{\n  \"data\": {\n    \"clutch_user_id\": null,\n    \"annotation_allowed\": false,\n    \"link_url\": \"https://dashboard.clutchapp.io/partner/connect#token=EXAMPLE_TOKEN\",\n    \"expires_at\": \"2026-10-30T17:00:00Z\"\n  }\n}\n```\n\nOpen `link_url` in a webview. It expires after 24 hours. An already authorized player receives `200 OK`; skip the webview:\n\n```json\n{\n  \"data\": {\n    \"clutch_user_id\": \"user_alex_example\",\n    \"annotation_allowed\": true,\n    \"link_url\": null,\n    \"expires_at\": null\n  }\n}\n```\n\n\n\nUse `partner_player_id` to identify a player from your system, such as their booking platform user ID. After the player signs in and gives consent, Clutch links that ID to their Clutch account.\n\n## `POST /partner/player-links`\n\nOmitting `purpose` keeps the existing Clutch-hosted flow. It creates a hosted link. For a player who has not linked yet it is a connect link: the player signs in or registers, gives consent, and joins the recording when `video_id` is given. For a player who is already linked, with a `video_id`, it is a tag link: it opens that recording so the player can tag themselves, with no consent step.\n\n![Hosted account consent screen.](/images/clutch-partner-consent.svg)\n\n**Body:**\n\n```json\n{\n  \"partner_player_id\": \"partner_player_813742\",\n  \"video_id\": \"B92BFC8A-2454-4632-9580-F96E99FB7C85\",\n  \"return_url\": \"partnerapp://clutch/done\"\n}\n```\n\n`partner_player_id` is required. `video_id` is optional and must belong to the Partner. `return_url` is optional and must exactly match a URL registered for your account; ask Clutch to register them. `https` URLs and custom app schemes are accepted.\n\n**Sample response**\n\n```json\n{\n  \"data\": {\n    \"partner_player_id\": \"partner_player_813742\",\n    \"status\": \"pending\",\n    \"link_url\": \"https://dashboard.clutchapp.io/partner/connect#token=HsE-la9Lcf9olvzt8E4KNlbhITBQ0S8dnqcxuMR1bnE\",\n    \"expires_at\": \"2026-08-02T16:00:00Z\"\n  }\n}\n```\n\nConnect links expire after 24 hours and work once. A new connect link replaces the player's earlier unused connect links, whichever recording they were for. Tag links expire after 7 days, can be opened again, and only work for the linked Clutch account. Tag links for different recordings can be open at the same time; a new tag link for the same player and recording replaces the previous one. Several Partner player IDs may map to the same Clutch account.\n\nWhen `video_id` is provided, the player is added to the recording and lands on its Clutch match page. Once processing has finished, they first identify the players there, matching the AI's player crops to each other, and then tag themselves. The page opens on the identification step until someone has completed it.\n\nWhen `return_url` is provided, the hosted flow sends the player back to it with `status`, `video_id`, and `partner_player_id` as query parameters. `status` is `linked` after consent on a link without a recording, or `tagged` once the player has tagged themselves. A player may also close the page instead; the webhook and `GET /partner/recordings/{video_id}/players` report the result either way, so treat `status` as a hint.\n\nAn already linked player without `video_id` returns `status: linked`, `link_url: null` and `linked_at`, the time they consented. A `return_url` that is not registered for your account returns HTTP 400 with `return_url_not_allowed`.\n",
        "parameters": [],
        "responses": {
          "200": {
            "description": "Successful response. Read the endpoint-specific status and error fields.",
            "content": {
              "application/json": {
                "schema": {
                  "type": [
                    "object",
                    "null"
                  ],
                  "additionalProperties": true,
                  "properties": {
                    "data": {
                      "type": [
                        "object",
                        "null"
                      ],
                      "additionalProperties": true,
                      "properties": {
                        "clutch_user_id": {
                          "type": [
                            "string",
                            "null"
                          ]
                        },
                        "annotation_allowed": {
                          "type": [
                            "boolean",
                            "null"
                          ]
                        },
                        "link_url": {},
                        "expires_at": {}
                      }
                    }
                  }
                },
                "example": {
                  "data": {
                    "clutch_user_id": "user_alex_example",
                    "annotation_allowed": true,
                    "link_url": null,
                    "expires_at": null
                  }
                }
              }
            }
          },
          "201": {
            "description": "Player consent link created.",
            "content": {
              "application/json": {
                "schema": {
                  "type": [
                    "object",
                    "null"
                  ],
                  "additionalProperties": true,
                  "properties": {
                    "data": {
                      "type": [
                        "object",
                        "null"
                      ],
                      "additionalProperties": true,
                      "properties": {
                        "clutch_user_id": {},
                        "annotation_allowed": {
                          "type": [
                            "boolean",
                            "null"
                          ]
                        },
                        "link_url": {
                          "type": [
                            "string",
                            "null"
                          ]
                        },
                        "expires_at": {
                          "type": [
                            "string",
                            "null"
                          ]
                        }
                      }
                    }
                  }
                },
                "example": {
                  "data": {
                    "clutch_user_id": null,
                    "annotation_allowed": false,
                    "link_url": "https://dashboard.clutchapp.io/partner/connect#token=EXAMPLE_TOKEN",
                    "expires_at": "2026-10-30T17:00:00Z"
                  }
                }
              }
            }
          },
          "400": {
            "description": "Invalid request.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "401": {
            "description": "Invalid credentials, expired token or API access not enabled.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              },
              "text/plain": {
                "schema": {
                  "type": "string"
                },
                "example": "Unauthorized"
              }
            }
          },
          "403": {
            "description": "Access denied.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        },
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/PlayerLink"
              },
              "example": {
                "partner_player_id": "partner_player_813742",
                "video_id": "B92BFC8A-2454-4632-9580-F96E99FB7C85",
                "return_url": "partnerapp://clutch/done"
              }
            }
          }
        },
        "x-codeSamples": [
          {
            "lang": "curl",
            "source": "curl --request POST \\\n  'https://api.clutchapp.io/v1/partner/player-links' \\\n  --header 'Authorization: Bearer YOUR_TOKEN' \\\n  --header 'Content-Type: application/json' \\\n  --data '{\"partner_player_id\":\"partner_player_813742\",\"video_id\":\"B92BFC8A-2454-4632-9580-F96E99FB7C85\",\"return_url\":\"partnerapp://clutch/done\"}'"
          }
        ]
      }
    },
    "/partner/players/{partner_player_id}/link": {
      "get": {
        "operationId": "get_partner_players_partner_player_id_link",
        "summary": "Get a player connection",
        "tags": [
          "player-links"
        ],
        "description": "Read the current connection and annotation permission. No request body.\n\n`GET /partner/players/player_1042/link` → `200 OK`:\n\n```json\n{\n  \"data\": {\n    \"clutch_user_id\": \"user_alex_example\",\n    \"annotation_allowed\": true\n  }\n}\n```\n\nAn unlinked player returns a null ID and false permission. An older link can return an ID with false permission. Show Connected only when permission is true; otherwise offer Connect Clutch.\n\n### Returning to your app\n\nWith `return_url`, Clutch navigates to:\n\n```txt\nclutchpartner://clutch/return?state=5hj9R4Wpi7zj2Zy8S7qzMQ&status=linked\n```\n\nThe callback carries `state` and a `linked`, `cancelled` or `expired` hint. Match the registered scheme, host, path and fixed parameters, then close the webview. Your backend verifies state against the signed-in player and calls `GET /partner/players/{partner_player_id}/link` before resuming the saved screen.\n\nOmit `return_url` or send null to finish on a Clutch confirmation screen. Keep a native close control and check the link whenever the webview closes without a callback. Preserve the selection on cancellation or expiry and offer a fresh link.\n\n### Screen flows\n\nBlue screens represent the Clutch partner app. Clutch login uses a web screenshot; consent uses the Clutch component with sample data. Partner and return screens illustrate the integration.\n\n**Connect from settings**\n\n![Connect from settings.](/images/flow-settings.png)\n\n**Connect before assigning a player**\n\n![Connect before assigning a player.](/images/flow-annotation.png)\n\n**Create a Clutch account**\n\n![Create a Clutch account.](/images/new-account-branch.png)\n\n**Cancel or retry an expired link**\n\n![Cancel or retry an expired link.](/images/cancel-and-retry.png)",
        "parameters": [
          {
            "name": "partner_player_id",
            "in": "path",
            "required": true,
            "description": "URL-encoded partner_player_id.",
            "schema": {
              "type": "string"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Successful response. Read the endpoint-specific status and error fields.",
            "content": {
              "application/json": {
                "schema": {
                  "type": [
                    "object",
                    "null"
                  ],
                  "additionalProperties": true,
                  "properties": {
                    "data": {
                      "type": [
                        "object",
                        "null"
                      ],
                      "additionalProperties": true,
                      "properties": {
                        "clutch_user_id": {
                          "type": [
                            "string",
                            "null"
                          ]
                        },
                        "annotation_allowed": {
                          "type": [
                            "boolean",
                            "null"
                          ]
                        }
                      }
                    }
                  }
                },
                "example": {
                  "data": {
                    "clutch_user_id": "user_alex_example",
                    "annotation_allowed": true
                  }
                }
              }
            }
          },
          "400": {
            "description": "Invalid request.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "401": {
            "description": "Invalid credentials, expired token or API access not enabled.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              },
              "text/plain": {
                "schema": {
                  "type": "string"
                },
                "example": "Unauthorized"
              }
            }
          },
          "403": {
            "description": "Access denied.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        },
        "x-codeSamples": [
          {
            "lang": "curl",
            "source": "curl --request GET \\\n  'https://api.clutchapp.io/v1/partner/players/{partner_player_id}/link' \\\n  --header 'Authorization: Bearer YOUR_TOKEN'"
          }
        ]
      }
    },
    "/partner/recordings/{video_id}/annotations": {
      "get": {
        "operationId": "get_partner_recordings_video_id_annotations",
        "summary": "Get player annotations",
        "tags": [
          "annotations"
        ],
        "description": "No request body. `200 OK`:\n\n**Sample response**\n\n```json\n{\n  \"data\": {\n    \"status\": \"ready\",\n    \"reason\": null,\n    \"revision\": \"rev_7\",\n    \"crops\": [\n      { \"track_id\": \"player-2\", \"crop_url\": \"https://assets.example.com/2.jpg\", \"is_reference\": true },\n      { \"track_id\": \"player-8\", \"crop_url\": \"https://assets.example.com/8.jpg\", \"is_reference\": true },\n      { \"track_id\": \"player-14\", \"crop_url\": \"https://assets.example.com/14.jpg\", \"is_reference\": true },\n      { \"track_id\": \"player-21\", \"crop_url\": \"https://assets.example.com/21.jpg\", \"is_reference\": true },\n      { \"track_id\": \"player-29\", \"crop_url\": \"https://assets.example.com/29.jpg\", \"is_reference\": false, \"suggested_ai_player_id\": \"player-2\" },\n      { \"track_id\": \"player-31\", \"crop_url\": \"https://assets.example.com/31.jpg\", \"is_reference\": false }\n    ],\n    \"player_mapping\": {},\n    \"skipped_track_ids\": [],\n    \"assignments\": {\n      \"player-8\": { \"clutch_user_id\": \"user_jamie_example\", \"partner_player_ids\": [\"player_2051\"] }\n    },\n    \"teams\": {\n      \"pair-1\": [\"player-2\", \"player-8\"],\n      \"pair-2\": [\"player-14\", \"player-21\"]\n    }\n  }\n}\n```\n\nReference crop IDs are the `ai_player_id` slots. Use crop URLs directly. Suggestions are optional. Assignments return canonical Clutch IDs and linked IDs in your account; unoccupied slots are omitted. Unmapped, unskipped non-reference tracks are unreviewed.\n\nEdit only when `ready`. Otherwise status is `processing` or `unavailable`, with null revision and empty collections. Unavailable reasons: `no_ai`, `multiple_periods`, `no_players`, `crops_missing`, `processing_failed`. This contract covers player annotation for a single singles/doubles match.",
        "parameters": [
          {
            "name": "video_id",
            "in": "path",
            "required": true,
            "description": "URL-encoded video_id.",
            "schema": {
              "type": "string"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Successful response. Read the endpoint-specific status and error fields.",
            "content": {
              "application/json": {
                "schema": {
                  "type": [
                    "object",
                    "null"
                  ],
                  "additionalProperties": true,
                  "properties": {
                    "data": {
                      "type": [
                        "object",
                        "null"
                      ],
                      "additionalProperties": true,
                      "properties": {
                        "status": {
                          "type": [
                            "string",
                            "null"
                          ]
                        },
                        "reason": {},
                        "revision": {
                          "type": [
                            "string",
                            "null"
                          ]
                        },
                        "crops": {
                          "type": "array",
                          "items": {
                            "type": [
                              "object",
                              "null"
                            ],
                            "additionalProperties": true,
                            "properties": {
                              "track_id": {
                                "type": [
                                  "string",
                                  "null"
                                ]
                              },
                              "crop_url": {
                                "type": [
                                  "string",
                                  "null"
                                ]
                              },
                              "is_reference": {
                                "type": [
                                  "boolean",
                                  "null"
                                ]
                              }
                            }
                          }
                        },
                        "player_mapping": {
                          "type": [
                            "object",
                            "null"
                          ],
                          "additionalProperties": true,
                          "properties": {}
                        },
                        "skipped_track_ids": {
                          "type": "array",
                          "items": {}
                        },
                        "assignments": {
                          "type": [
                            "object",
                            "null"
                          ],
                          "additionalProperties": true,
                          "properties": {
                            "player-8": {
                              "type": [
                                "object",
                                "null"
                              ],
                              "additionalProperties": true,
                              "properties": {
                                "clutch_user_id": {
                                  "type": [
                                    "string",
                                    "null"
                                  ]
                                },
                                "partner_player_ids": {
                                  "type": "array",
                                  "items": {
                                    "type": [
                                      "string",
                                      "null"
                                    ]
                                  }
                                }
                              }
                            }
                          }
                        },
                        "teams": {
                          "type": [
                            "object",
                            "null"
                          ],
                          "additionalProperties": true,
                          "properties": {
                            "pair-1": {
                              "type": "array",
                              "items": {
                                "type": [
                                  "string",
                                  "null"
                                ]
                              }
                            },
                            "pair-2": {
                              "type": "array",
                              "items": {
                                "type": [
                                  "string",
                                  "null"
                                ]
                              }
                            }
                          }
                        }
                      }
                    }
                  }
                },
                "example": {
                  "data": {
                    "status": "ready",
                    "reason": null,
                    "revision": "rev_7",
                    "crops": [
                      {
                        "track_id": "player-2",
                        "crop_url": "https://assets.example.com/2.jpg",
                        "is_reference": true
                      },
                      {
                        "track_id": "player-8",
                        "crop_url": "https://assets.example.com/8.jpg",
                        "is_reference": true
                      },
                      {
                        "track_id": "player-14",
                        "crop_url": "https://assets.example.com/14.jpg",
                        "is_reference": true
                      },
                      {
                        "track_id": "player-21",
                        "crop_url": "https://assets.example.com/21.jpg",
                        "is_reference": true
                      },
                      {
                        "track_id": "player-29",
                        "crop_url": "https://assets.example.com/29.jpg",
                        "is_reference": false,
                        "suggested_ai_player_id": "player-2"
                      },
                      {
                        "track_id": "player-31",
                        "crop_url": "https://assets.example.com/31.jpg",
                        "is_reference": false
                      }
                    ],
                    "player_mapping": {},
                    "skipped_track_ids": [],
                    "assignments": {
                      "player-8": {
                        "clutch_user_id": "user_jamie_example",
                        "partner_player_ids": [
                          "player_2051"
                        ]
                      }
                    },
                    "teams": {
                      "pair-1": [
                        "player-2",
                        "player-8"
                      ],
                      "pair-2": [
                        "player-14",
                        "player-21"
                      ]
                    }
                  }
                }
              }
            }
          },
          "400": {
            "description": "Invalid request.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "401": {
            "description": "Invalid credentials, expired token or API access not enabled.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              },
              "text/plain": {
                "schema": {
                  "type": "string"
                },
                "example": "Unauthorized"
              }
            }
          },
          "403": {
            "description": "Access denied.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        },
        "x-codeSamples": [
          {
            "lang": "curl",
            "source": "curl --request GET \\\n  'https://api.clutchapp.io/v1/partner/recordings/{video_id}/annotations' \\\n  --header 'Authorization: Bearer YOUR_TOKEN'"
          }
        ]
      },
      "patch": {
        "operationId": "patch_partner_recordings_video_id_annotations",
        "summary": "Save player annotations",
        "tags": [
          "annotations"
        ],
        "description": "Request using either your player ID or the Clutch account ID:\n\n```json\n{\n  \"revision\": \"rev_7\",\n  \"player_mapping\": { \"player-29\": \"player-2\" },\n  \"skipped_track_ids\": [\"player-31\"],\n  \"assignments\": {\n    \"player-2\": { \"partner_player_id\": \"player_1042\" },\n    \"player-8\": { \"clutch_user_id\": \"user_jamie_example\" }\n  },\n  \"teams\": {\n    \"pair-1\": [\"player-2\", \"player-14\"],\n    \"pair-2\": [\"player-8\", \"player-21\"]\n  }\n}\n```\n\n`200 OK` confirms the entire edit committed:\n\n```json\n{ \"data\": { \"revision\": \"rev_8\" } }\n```\n\nSend a revision and at least one annotation field. Use the returned revision for the next edit. GET returns saved state; omitted fields stay unchanged.\n\n| Field | Save behavior |\n| --- | --- |\n| `player_mapping` | Patch named non-reference tracks to reference slots. Null clears a track's grouping. Reference crops map to themselves automatically. |\n| `skipped_track_ids` | Replace skipped non-reference tracks. `[]` clears the list. Skipped tracks cannot also be mapped. |\n| `assignments` | Patch named slots. Supply exactly one ID form per slot, or null to unassign it. |\n| `teams` | Replace both teams. Every reference slot appears once: one per team for singles, two for doubles. Member order within a team is ignored. |\n\nBoth ID forms require an active link and consent, including accounts replaced or removed. A user occupies one slot; clear or reassign their old slot in the same save when moving them. Grouping, skips and teams can be saved before linking. Account assignments require linking first.\n\nUnassigning can remove a non-creator's recording membership, post and invite acceptance. Creators keep membership with slot/stats cleared. Account links remain intact.\n\nSaves are atomic. Retry timeouts/5xx with the same body:\n\n- Same analysis run and requested values already saved: `200` with the current revision, without repeated writes or side effects.\n- Values differ and revision is current: save the edit.\n- Conflicting edit or reprocessing: `409 revision_conflict`; reload and review.\n\nAn unrelated edit may change the returned revision. Refetch when reopening the editor.\n\nErrors use `{\"error\":{\"code\":\"revision_conflict\",\"message\":\"Reload annotations.\"}}`.\n\n| Error | Action |\n| --- | --- |\n| `400 invalid_request` | Correct the body and resend. Common causes: a reference crop mapped, cleared or skipped; a track both mapped and skipped; one user on two slots; an unknown ID; or `teams` not listing every reference slot once. |\n| `409 link_required` | Connect or renew annotation consent for the affected player. |\n| `409 revision_conflict` | Reload crops/state and have the user review the edit again. |\n| `409 annotation_unavailable` | Reload readiness; save when ready. |\n\nStandard authentication, ownership, not-found and rate-limit errors also apply. Recordings and account links are scoped to your partner account.\n\n### How slots work\n\nA reference crop's `track_id` is the slot, so images never move between slots. You cannot map, clear or skip a reference crop. To link a player to a picture, find the reference crop that shows them and assign them to its `track_id`. Then map their other non-reference crops to the same ID.\n\nLink a player to the `player-14` picture and group another crop of them:\n\n```json\n{\n  \"revision\": \"rev_7\",\n  \"player_mapping\": { \"player-29\": \"player-14\" },\n  \"assignments\": { \"player-14\": { \"partner_player_id\": \"player_1042\" } }\n}\n```\n\nMove that player to the `player-2` picture. Clear the old slot in the same save, because a user can hold only one slot:\n\n```json\n{\n  \"revision\": \"rev_8\",\n  \"player_mapping\": { \"player-29\": \"player-2\" },\n  \"assignments\": {\n    \"player-14\": null,\n    \"player-2\": { \"partner_player_id\": \"player_1042\" }\n  }\n}\n```\n\nUndo a grouping by clearing the non-reference track:\n\n```json\n{ \"revision\": \"rev_9\", \"player_mapping\": { \"player-29\": null } }\n```\n\nMap a track you skipped earlier by removing it from `skipped_track_ids` in the same save. That field replaces the whole list:\n\n```json\n{\n  \"revision\": \"rev_10\",\n  \"skipped_track_ids\": [],\n  \"player_mapping\": { \"player-31\": \"player-2\" }\n}\n```\n\nThis body returns `400 invalid_request`, because `player-14` is a reference crop:\n\n```json\n{ \"revision\": \"rev_7\", \"player_mapping\": { \"player-14\": \"player-2\" } }\n```\n\n**Clutch-hosted tagging (omit purpose)**\n\nUse `partner_player_id` to identify a player from your system, such as their booking platform user ID. After the player signs in and gives consent, Clutch links that ID to their Clutch account.",
        "parameters": [
          {
            "name": "video_id",
            "in": "path",
            "required": true,
            "description": "URL-encoded video_id.",
            "schema": {
              "type": "string"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Successful response. Read the endpoint-specific status and error fields.",
            "content": {
              "application/json": {
                "schema": {
                  "type": [
                    "object",
                    "null"
                  ],
                  "additionalProperties": true,
                  "properties": {
                    "data": {
                      "type": [
                        "object",
                        "null"
                      ],
                      "additionalProperties": true,
                      "properties": {
                        "revision": {
                          "type": [
                            "string",
                            "null"
                          ]
                        }
                      }
                    }
                  }
                },
                "example": {
                  "data": {
                    "revision": "rev_8"
                  }
                }
              }
            }
          },
          "400": {
            "description": "Invalid request.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "401": {
            "description": "Invalid credentials, expired token or API access not enabled.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              },
              "text/plain": {
                "schema": {
                  "type": "string"
                },
                "example": "Unauthorized"
              }
            }
          },
          "403": {
            "description": "Access denied.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "409": {
            "description": "Conflict, unavailable camera/media, missing consent or annotation revision conflict.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        },
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/AnnotationEdit"
              },
              "example": {
                "revision": "rev_7",
                "player_mapping": {
                  "player-29": "player-2"
                },
                "skipped_track_ids": [
                  "player-31"
                ],
                "assignments": {
                  "player-2": {
                    "partner_player_id": "player_1042"
                  },
                  "player-8": {
                    "clutch_user_id": "user_jamie_example"
                  }
                },
                "teams": {
                  "pair-1": [
                    "player-2",
                    "player-14"
                  ],
                  "pair-2": [
                    "player-8",
                    "player-21"
                  ]
                }
              }
            }
          }
        },
        "x-codeSamples": [
          {
            "lang": "curl",
            "source": "curl --request PATCH \\\n  'https://api.clutchapp.io/v1/partner/recordings/{video_id}/annotations' \\\n  --header 'Authorization: Bearer YOUR_TOKEN' \\\n  --header 'Content-Type: application/json' \\\n  --data '{\"revision\":\"rev_7\",\"player_mapping\":{\"player-29\":\"player-2\"},\"skipped_track_ids\":[\"player-31\"],\"assignments\":{\"player-2\":{\"partner_player_id\":\"player_1042\"},\"player-8\":{\"clutch_user_id\":\"user_jamie_example\"}},\"teams\":{\"pair-1\":[\"player-2\",\"player-14\"],\"pair-2\":[\"player-8\",\"player-21\"]}}'"
          }
        ]
      }
    },
    "/partner/recordings": {
      "get": {
        "operationId": "get_partner_recordings",
        "summary": "List partner recordings",
        "tags": [
          "recordings"
        ],
        "description": "Returns recordings created by the authenticated Partner, newest first. `limit` accepts 1 to 100. `delayed_processing` is described under `GET /video/{video_id}/status`.\n\n**Sample response**\n\n```json\n{\n  \"data\": [\n    {\n      \"video_id\": \"B92BFC8A-2454-4632-9580-F96E99FB7C85\",\n      \"device_id\": \"4580f207-b672-4df0-83eb-110cc408466b\",\n      \"venue_id\": \"ac8e2b99-9dce-44fe-a317-4bfdfee12187\",\n      \"sport\": \"padel\",\n      \"game_type\": \"doubles\",\n      \"recording_type\": \"match\",\n      \"status\": \"OK\",\n      \"delayed_processing\": \"not_expected\",\n      \"title\": \"Example Court\",\n      \"scheduled_at\": \"2026-08-01T17:00:00Z\",\n      \"start_time\": \"2026-08-01T17:02:00Z\",\n      \"end_time\": \"2026-08-01T18:30:00Z\",\n      \"created_at\": \"2026-08-01T16:00:00Z\",\n      \"app_link\": \"https://app.clutchapp.io/match/B92BFC8A-2454-4632-9580-F96E99FB7C85\"\n    }\n  ],\n  \"next_cursor\": null\n}\n```\n\nUse `next_cursor` to request the next page.",
        "parameters": [
          {
            "name": "limit",
            "in": "query",
            "description": "Page size, 1–100; default 50.",
            "schema": {
              "type": "integer",
              "minimum": 1,
              "maximum": 100,
              "default": 50
            }
          },
          {
            "name": "cursor",
            "in": "query",
            "description": "Opaque next_cursor from the preceding page. Preserve the other filters.",
            "schema": {
              "type": "string"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Successful response. Read the endpoint-specific status and error fields.",
            "content": {
              "application/json": {
                "schema": {
                  "type": [
                    "object",
                    "null"
                  ],
                  "additionalProperties": true,
                  "properties": {
                    "data": {
                      "type": "array",
                      "items": {
                        "type": [
                          "object",
                          "null"
                        ],
                        "additionalProperties": true,
                        "properties": {
                          "video_id": {
                            "type": [
                              "string",
                              "null"
                            ]
                          },
                          "device_id": {
                            "type": [
                              "string",
                              "null"
                            ]
                          },
                          "venue_id": {
                            "type": [
                              "string",
                              "null"
                            ]
                          },
                          "sport": {
                            "type": [
                              "string",
                              "null"
                            ]
                          },
                          "game_type": {
                            "type": [
                              "string",
                              "null"
                            ]
                          },
                          "recording_type": {
                            "type": [
                              "string",
                              "null"
                            ]
                          },
                          "status": {
                            "type": [
                              "string",
                              "null"
                            ]
                          },
                          "delayed_processing": {
                            "type": [
                              "string",
                              "null"
                            ]
                          },
                          "title": {
                            "type": [
                              "string",
                              "null"
                            ]
                          },
                          "scheduled_at": {
                            "type": [
                              "string",
                              "null"
                            ]
                          },
                          "start_time": {
                            "type": [
                              "string",
                              "null"
                            ]
                          },
                          "end_time": {
                            "type": [
                              "string",
                              "null"
                            ]
                          },
                          "created_at": {
                            "type": [
                              "string",
                              "null"
                            ]
                          },
                          "app_link": {
                            "type": [
                              "string",
                              "null"
                            ]
                          }
                        }
                      }
                    },
                    "next_cursor": {}
                  }
                },
                "example": {
                  "data": [
                    {
                      "video_id": "B92BFC8A-2454-4632-9580-F96E99FB7C85",
                      "device_id": "4580f207-b672-4df0-83eb-110cc408466b",
                      "venue_id": "ac8e2b99-9dce-44fe-a317-4bfdfee12187",
                      "sport": "padel",
                      "game_type": "doubles",
                      "recording_type": "match",
                      "status": "OK",
                      "delayed_processing": "not_expected",
                      "title": "Example Court",
                      "scheduled_at": "2026-08-01T17:00:00Z",
                      "start_time": "2026-08-01T17:02:00Z",
                      "end_time": "2026-08-01T18:30:00Z",
                      "created_at": "2026-08-01T16:00:00Z",
                      "app_link": "https://app.clutchapp.io/match/B92BFC8A-2454-4632-9580-F96E99FB7C85"
                    }
                  ],
                  "next_cursor": null
                }
              }
            }
          },
          "400": {
            "description": "Invalid request.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "401": {
            "description": "Invalid credentials, expired token or API access not enabled.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              },
              "text/plain": {
                "schema": {
                  "type": "string"
                },
                "example": "Unauthorized"
              }
            }
          },
          "403": {
            "description": "Access denied.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        },
        "x-codeSamples": [
          {
            "lang": "curl",
            "source": "curl --request GET \\\n  'https://api.clutchapp.io/v1/partner/recordings' \\\n  --header 'Authorization: Bearer YOUR_TOKEN'"
          }
        ]
      }
    },
    "/partner/recordings/{video_id}/players": {
      "get": {
        "operationId": "get_partner_recordings_video_id_players",
        "summary": "Get recording player slots",
        "tags": [
          "recordings"
        ],
        "description": "Returns the recording's player slots, each with its team and the linked Partner player who holds it. Use it to reconcile `player.identification_changed` events.\n\n**Sample response**\n\n```json\n{\n  \"data\": {\n    \"video_id\": \"B92BFC8A-2454-4632-9580-F96E99FB7C85\",\n    \"players\": [\n      {\n        \"ai_player_id\": \"player-14\",\n        \"pair\": \"pair-1\",\n        \"partner_player_id\": \"partner_player_813742\"\n      },\n      {\n        \"ai_player_id\": \"player-158\",\n        \"pair\": \"pair-1\",\n        \"partner_player_id\": null\n      },\n      {\n        \"ai_player_id\": \"player-25\",\n        \"pair\": \"pair-2\",\n        \"partner_player_id\": null\n      },\n      {\n        \"ai_player_id\": \"player-27\",\n        \"pair\": \"pair-2\",\n        \"partner_player_id\": \"partner_player_205118\"\n      }\n    ]\n  }\n}\n```\n\n`ai_player_id` is the player's slot in this recording and `pair` is the team, `pair-1` or `pair-2`. A slot that no linked player holds has `partner_player_id: null`. A linked player who joined the recording but has not tagged themselves yet appears with `ai_player_id` and `pair` null. A recording that is still processing has no slots yet. The recording must belong to the Partner; otherwise HTTP 403 with `video_not_owned`.",
        "parameters": [
          {
            "name": "video_id",
            "in": "path",
            "required": true,
            "description": "URL-encoded video_id.",
            "schema": {
              "type": "string"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Successful response. Read the endpoint-specific status and error fields.",
            "content": {
              "application/json": {
                "schema": {
                  "type": [
                    "object",
                    "null"
                  ],
                  "additionalProperties": true,
                  "properties": {
                    "data": {
                      "type": [
                        "object",
                        "null"
                      ],
                      "additionalProperties": true,
                      "properties": {
                        "video_id": {
                          "type": [
                            "string",
                            "null"
                          ]
                        },
                        "players": {
                          "type": "array",
                          "items": {
                            "type": [
                              "object",
                              "null"
                            ],
                            "additionalProperties": true,
                            "properties": {
                              "ai_player_id": {
                                "type": [
                                  "string",
                                  "null"
                                ]
                              },
                              "pair": {
                                "type": [
                                  "string",
                                  "null"
                                ]
                              },
                              "partner_player_id": {
                                "type": [
                                  "string",
                                  "null"
                                ]
                              }
                            }
                          }
                        }
                      }
                    }
                  }
                },
                "example": {
                  "data": {
                    "video_id": "B92BFC8A-2454-4632-9580-F96E99FB7C85",
                    "players": [
                      {
                        "ai_player_id": "player-14",
                        "pair": "pair-1",
                        "partner_player_id": "partner_player_813742"
                      },
                      {
                        "ai_player_id": "player-158",
                        "pair": "pair-1",
                        "partner_player_id": null
                      },
                      {
                        "ai_player_id": "player-25",
                        "pair": "pair-2",
                        "partner_player_id": null
                      },
                      {
                        "ai_player_id": "player-27",
                        "pair": "pair-2",
                        "partner_player_id": "partner_player_205118"
                      }
                    ]
                  }
                }
              }
            }
          },
          "400": {
            "description": "Invalid request.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "401": {
            "description": "Invalid credentials, expired token or API access not enabled.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              },
              "text/plain": {
                "schema": {
                  "type": "string"
                },
                "example": "Unauthorized"
              }
            }
          },
          "403": {
            "description": "Access denied.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        },
        "x-codeSamples": [
          {
            "lang": "curl",
            "source": "curl --request GET \\\n  'https://api.clutchapp.io/v1/partner/recordings/{video_id}/players' \\\n  --header 'Authorization: Bearer YOUR_TOKEN'"
          }
        ]
      }
    },
    "/partner/players/{partner_player_id}/history": {
      "get": {
        "operationId": "get_partner_players_partner_player_id_history",
        "summary": "Get linked player history",
        "tags": [
          "history"
        ],
        "description": "Returns the linked player's worldwide Clutch history, including recordings they joined but have not tagged themselves in yet.\n\n**Sample response**\n\n```json\n{\n  \"data\": {\n    \"partner_player_id\": \"partner_player_813742\",\n    \"consented_at\": \"2026-07-15T10:00:00Z\",\n    \"summary\": {\n      \"player_rating\": 7.5,\n      \"rating_confidence\": 82,\n      \"rating_provisional\": false,\n      \"rated_matches\": 22,\n      \"rating_scale\": {\n        \"minimum\": 1,\n        \"maximum\": 10,\n        \"unrated_value\": null\n      },\n      \"total_matches\": 24,\n      \"scored_matches\": 22,\n      \"win_loss\": {\n        \"wins\": 14,\n        \"losses\": 8\n      }\n    },\n    \"matches\": [\n      {\n        \"video_id\": \"B92BFC8A-2454-4632-9580-F96E99FB7C85\",\n        \"recorded_at\": \"2026-08-01T17:02:00Z\",\n        \"device_id\": \"4580f207-b672-4df0-83eb-110cc408466b\",\n        \"venue_id\": \"ac8e2b99-9dce-44fe-a317-4bfdfee12187\",\n        \"sport\": \"padel\",\n        \"game_type\": \"doubles\",\n        \"status\": \"OK\",\n        \"title\": \"Example Match\",\n        \"ai_player_id\": \"player-14\",\n        \"result\": \"win\",\n        \"score\": {},\n        \"metrics\": {\n          \"player_rating\": 7.42,\n          \"fastest_smash\": 112.5,\n          \"first_serve_percentage\": 74.2,\n          \"first_serve_speed\": 88.4,\n          \"aces\": 3,\n          \"winners\": 12,\n          \"unforced_errors\": 8,\n          \"winner_to_unforced_error_ratio\": 1.5,\n          \"net_points_won\": 10,\n          \"average_rally_length\": 6.8,\n          \"double_faults\": 2,\n          \"second_serve_speed\": 72.1,\n          \"average_shot_speed\": 61.7,\n          \"calories\": 544.3,\n          \"distance_ran\": 1234.5,\n          \"steps\": 1662,\n          \"smash_error_rate\": 20.0,\n          \"smash_conversion_rate\": 40.0,\n          \"lob_recovery_rate\": 66.67,\n          \"net_approach_success_rate\": 58.33,\n          \"volley_winners\": 5,\n          \"volley_errors\": 2,\n          \"average_lob_height\": 5.4\n        },\n        \"app_link\": \"https://app.clutchapp.io/match/B92BFC8A-2454-4632-9580-F96E99FB7C85\"\n      }\n    ],\n    \"next_cursor\": null\n  }\n}\n```\n\n`ai_player_id` is the slot the player holds in that recording, matching `GET /partner/recordings/{video_id}/players`, and is null until they tag themselves. Unrated players return `player_rating: null`. `summary.player_rating` is the padel doubles Clutch Score. `rating_confidence` (0 to 100) shows how much recent evidence supports it; it builds with play, fades during breaks, and is not an accuracy probability. `rating_provisional` is true with fewer than five qualifying matches, and `rated_matches` counts the qualifying matches. `rating_confidence` and `rating_provisional` are null without a score. HTTP 409 with `player_not_linked` means consent is incomplete.",
        "parameters": [
          {
            "name": "partner_player_id",
            "in": "path",
            "required": true,
            "description": "URL-encoded partner_player_id.",
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "limit",
            "in": "query",
            "description": "Page size, 1–100; default 50.",
            "schema": {
              "type": "integer",
              "minimum": 1,
              "maximum": 100,
              "default": 50
            }
          },
          {
            "name": "cursor",
            "in": "query",
            "description": "Opaque next_cursor from the preceding page. Preserve the other filters.",
            "schema": {
              "type": "string"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Successful response. Read the endpoint-specific status and error fields.",
            "content": {
              "application/json": {
                "schema": {
                  "type": [
                    "object",
                    "null"
                  ],
                  "additionalProperties": true,
                  "properties": {
                    "data": {
                      "type": [
                        "object",
                        "null"
                      ],
                      "additionalProperties": true,
                      "properties": {
                        "partner_player_id": {
                          "type": [
                            "string",
                            "null"
                          ]
                        },
                        "consented_at": {
                          "type": [
                            "string",
                            "null"
                          ]
                        },
                        "summary": {
                          "type": [
                            "object",
                            "null"
                          ],
                          "additionalProperties": true,
                          "properties": {
                            "player_rating": {
                              "type": [
                                "number",
                                "null"
                              ]
                            },
                            "rating_confidence": {
                              "type": [
                                "number",
                                "null"
                              ]
                            },
                            "rating_provisional": {
                              "type": [
                                "boolean",
                                "null"
                              ]
                            },
                            "rated_matches": {
                              "type": [
                                "number",
                                "null"
                              ]
                            },
                            "rating_scale": {
                              "type": [
                                "object",
                                "null"
                              ],
                              "additionalProperties": true,
                              "properties": {
                                "minimum": {
                                  "type": [
                                    "number",
                                    "null"
                                  ]
                                },
                                "maximum": {
                                  "type": [
                                    "number",
                                    "null"
                                  ]
                                },
                                "unrated_value": {}
                              }
                            },
                            "total_matches": {
                              "type": [
                                "number",
                                "null"
                              ]
                            },
                            "scored_matches": {
                              "type": [
                                "number",
                                "null"
                              ]
                            },
                            "win_loss": {
                              "type": [
                                "object",
                                "null"
                              ],
                              "additionalProperties": true,
                              "properties": {
                                "wins": {
                                  "type": [
                                    "number",
                                    "null"
                                  ]
                                },
                                "losses": {
                                  "type": [
                                    "number",
                                    "null"
                                  ]
                                }
                              }
                            }
                          }
                        },
                        "matches": {
                          "type": "array",
                          "items": {
                            "type": [
                              "object",
                              "null"
                            ],
                            "additionalProperties": true,
                            "properties": {
                              "video_id": {
                                "type": [
                                  "string",
                                  "null"
                                ]
                              },
                              "recorded_at": {
                                "type": [
                                  "string",
                                  "null"
                                ]
                              },
                              "device_id": {
                                "type": [
                                  "string",
                                  "null"
                                ]
                              },
                              "venue_id": {
                                "type": [
                                  "string",
                                  "null"
                                ]
                              },
                              "sport": {
                                "type": [
                                  "string",
                                  "null"
                                ]
                              },
                              "game_type": {
                                "type": [
                                  "string",
                                  "null"
                                ]
                              },
                              "status": {
                                "type": [
                                  "string",
                                  "null"
                                ]
                              },
                              "title": {
                                "type": [
                                  "string",
                                  "null"
                                ]
                              },
                              "ai_player_id": {
                                "type": [
                                  "string",
                                  "null"
                                ]
                              },
                              "result": {
                                "type": [
                                  "string",
                                  "null"
                                ]
                              },
                              "score": {
                                "type": [
                                  "object",
                                  "null"
                                ],
                                "additionalProperties": true,
                                "properties": {}
                              },
                              "metrics": {
                                "type": [
                                  "object",
                                  "null"
                                ],
                                "additionalProperties": true,
                                "properties": {
                                  "player_rating": {
                                    "type": [
                                      "number",
                                      "null"
                                    ]
                                  },
                                  "fastest_smash": {
                                    "type": [
                                      "number",
                                      "null"
                                    ]
                                  },
                                  "first_serve_percentage": {
                                    "type": [
                                      "number",
                                      "null"
                                    ]
                                  },
                                  "first_serve_speed": {
                                    "type": [
                                      "number",
                                      "null"
                                    ]
                                  },
                                  "aces": {
                                    "type": [
                                      "number",
                                      "null"
                                    ]
                                  },
                                  "winners": {
                                    "type": [
                                      "number",
                                      "null"
                                    ]
                                  },
                                  "unforced_errors": {
                                    "type": [
                                      "number",
                                      "null"
                                    ]
                                  },
                                  "winner_to_unforced_error_ratio": {
                                    "type": [
                                      "number",
                                      "null"
                                    ]
                                  },
                                  "net_points_won": {
                                    "type": [
                                      "number",
                                      "null"
                                    ]
                                  },
                                  "average_rally_length": {
                                    "type": [
                                      "number",
                                      "null"
                                    ]
                                  },
                                  "double_faults": {
                                    "type": [
                                      "number",
                                      "null"
                                    ]
                                  },
                                  "second_serve_speed": {
                                    "type": [
                                      "number",
                                      "null"
                                    ]
                                  },
                                  "average_shot_speed": {
                                    "type": [
                                      "number",
                                      "null"
                                    ]
                                  },
                                  "calories": {
                                    "type": [
                                      "number",
                                      "null"
                                    ]
                                  },
                                  "distance_ran": {
                                    "type": [
                                      "number",
                                      "null"
                                    ]
                                  },
                                  "steps": {
                                    "type": [
                                      "number",
                                      "null"
                                    ]
                                  },
                                  "smash_error_rate": {
                                    "type": [
                                      "number",
                                      "null"
                                    ]
                                  },
                                  "smash_conversion_rate": {
                                    "type": [
                                      "number",
                                      "null"
                                    ]
                                  },
                                  "lob_recovery_rate": {
                                    "type": [
                                      "number",
                                      "null"
                                    ]
                                  },
                                  "net_approach_success_rate": {
                                    "type": [
                                      "number",
                                      "null"
                                    ]
                                  },
                                  "volley_winners": {
                                    "type": [
                                      "number",
                                      "null"
                                    ]
                                  },
                                  "volley_errors": {
                                    "type": [
                                      "number",
                                      "null"
                                    ]
                                  },
                                  "average_lob_height": {
                                    "type": [
                                      "number",
                                      "null"
                                    ]
                                  }
                                }
                              },
                              "app_link": {
                                "type": [
                                  "string",
                                  "null"
                                ]
                              }
                            }
                          }
                        },
                        "next_cursor": {}
                      }
                    }
                  }
                },
                "example": {
                  "data": {
                    "partner_player_id": "partner_player_813742",
                    "consented_at": "2026-07-15T10:00:00Z",
                    "summary": {
                      "player_rating": 7.5,
                      "rating_confidence": 82,
                      "rating_provisional": false,
                      "rated_matches": 22,
                      "rating_scale": {
                        "minimum": 1,
                        "maximum": 10,
                        "unrated_value": null
                      },
                      "total_matches": 24,
                      "scored_matches": 22,
                      "win_loss": {
                        "wins": 14,
                        "losses": 8
                      }
                    },
                    "matches": [
                      {
                        "video_id": "B92BFC8A-2454-4632-9580-F96E99FB7C85",
                        "recorded_at": "2026-08-01T17:02:00Z",
                        "device_id": "4580f207-b672-4df0-83eb-110cc408466b",
                        "venue_id": "ac8e2b99-9dce-44fe-a317-4bfdfee12187",
                        "sport": "padel",
                        "game_type": "doubles",
                        "status": "OK",
                        "title": "Example Match",
                        "ai_player_id": "player-14",
                        "result": "win",
                        "score": {},
                        "metrics": {
                          "player_rating": 7.42,
                          "fastest_smash": 112.5,
                          "first_serve_percentage": 74.2,
                          "first_serve_speed": 88.4,
                          "aces": 3,
                          "winners": 12,
                          "unforced_errors": 8,
                          "winner_to_unforced_error_ratio": 1.5,
                          "net_points_won": 10,
                          "average_rally_length": 6.8,
                          "double_faults": 2,
                          "second_serve_speed": 72.1,
                          "average_shot_speed": 61.7,
                          "calories": 544.3,
                          "distance_ran": 1234.5,
                          "steps": 1662,
                          "smash_error_rate": 20,
                          "smash_conversion_rate": 40,
                          "lob_recovery_rate": 66.67,
                          "net_approach_success_rate": 58.33,
                          "volley_winners": 5,
                          "volley_errors": 2,
                          "average_lob_height": 5.4
                        },
                        "app_link": "https://app.clutchapp.io/match/B92BFC8A-2454-4632-9580-F96E99FB7C85"
                      }
                    ],
                    "next_cursor": null
                  }
                }
              }
            }
          },
          "400": {
            "description": "Invalid request.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "401": {
            "description": "Invalid credentials, expired token or API access not enabled.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              },
              "text/plain": {
                "schema": {
                  "type": "string"
                },
                "example": "Unauthorized"
              }
            }
          },
          "403": {
            "description": "Access denied.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "409": {
            "description": "Conflict, unavailable camera/media, missing consent or annotation revision conflict.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        },
        "x-codeSamples": [
          {
            "lang": "curl",
            "source": "curl --request GET \\\n  'https://api.clutchapp.io/v1/partner/players/{partner_player_id}/history' \\\n  --header 'Authorization: Bearer YOUR_TOKEN'"
          }
        ]
      }
    },
    "/partner/webhook": {
      "get": {
        "operationId": "get_partner_webhook",
        "summary": "Get webhook registration",
        "tags": [
          "webhooks"
        ],
        "description": "Returns the current URL and `updated_at`, or `data: null` when no webhook is registered. The token cannot be read after registration.",
        "parameters": [],
        "responses": {
          "200": {
            "description": "Successful response. Read the endpoint-specific status and error fields.",
            "content": {
              "application/json": {
                "schema": {
                  "type": [
                    "object",
                    "null"
                  ],
                  "additionalProperties": true,
                  "properties": {
                    "data": {
                      "type": [
                        "object",
                        "null"
                      ],
                      "additionalProperties": true,
                      "properties": {
                        "url": {
                          "type": [
                            "string",
                            "null"
                          ]
                        },
                        "updated_at": {
                          "type": [
                            "string",
                            "null"
                          ]
                        }
                      }
                    }
                  }
                },
                "example": {
                  "data": {
                    "url": "https://partner.example.com/clutch-webhook",
                    "updated_at": "2026-08-01T16:00:00Z"
                  }
                }
              }
            }
          },
          "400": {
            "description": "Invalid request.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "401": {
            "description": "Invalid credentials, expired token or API access not enabled.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              },
              "text/plain": {
                "schema": {
                  "type": "string"
                },
                "example": "Unauthorized"
              }
            }
          },
          "403": {
            "description": "Access denied.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        },
        "x-codeSamples": [
          {
            "lang": "curl",
            "source": "curl --request GET \\\n  'https://api.clutchapp.io/v1/partner/webhook' \\\n  --header 'Authorization: Bearer YOUR_TOKEN'"
          }
        ]
      },
      "put": {
        "operationId": "put_partner_webhook",
        "summary": "Register or replace a webhook",
        "tags": [
          "webhooks"
        ],
        "description": "Registers or replaces the webhook. The URL must use HTTPS and a public hostname; IP addresses and internal hostnames are rejected.\n\n**Body:**\n\n```json\n{\n  \"url\": \"https://partner.example.com/clutch-webhook\"\n}\n```\n\n**Sample response**\n\n```json\n{\n  \"data\": {\n    \"url\": \"https://partner.example.com/clutch-webhook\",\n    \"token\": \"<webhook-token>\"\n  }\n}\n```\n\nThe token is shown only in this response. Every `PUT` generates a new token and invalidates the previous one.",
        "parameters": [],
        "responses": {
          "200": {
            "description": "Successful response. Read the endpoint-specific status and error fields.",
            "content": {
              "application/json": {
                "schema": {
                  "type": [
                    "object",
                    "null"
                  ],
                  "additionalProperties": true,
                  "properties": {
                    "data": {
                      "type": [
                        "object",
                        "null"
                      ],
                      "additionalProperties": true,
                      "properties": {
                        "url": {
                          "type": [
                            "string",
                            "null"
                          ]
                        },
                        "token": {
                          "type": [
                            "string",
                            "null"
                          ]
                        }
                      }
                    }
                  }
                },
                "example": {
                  "data": {
                    "url": "https://partner.example.com/clutch-webhook",
                    "token": "<webhook-token>"
                  }
                }
              }
            }
          },
          "400": {
            "description": "Invalid request.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "401": {
            "description": "Invalid credentials, expired token or API access not enabled.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              },
              "text/plain": {
                "schema": {
                  "type": "string"
                },
                "example": "Unauthorized"
              }
            }
          },
          "403": {
            "description": "Access denied.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        },
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/WebhookRegistration"
              },
              "example": {
                "url": "https://partner.example.com/clutch-webhook"
              }
            }
          }
        },
        "x-codeSamples": [
          {
            "lang": "curl",
            "source": "curl --request PUT \\\n  'https://api.clutchapp.io/v1/partner/webhook' \\\n  --header 'Authorization: Bearer YOUR_TOKEN' \\\n  --header 'Content-Type: application/json' \\\n  --data '{\"url\":\"https://partner.example.com/clutch-webhook\"}'"
          }
        ]
      },
      "delete": {
        "operationId": "delete_partner_webhook",
        "summary": "Delete webhook registration",
        "tags": [
          "webhooks"
        ],
        "description": "Removes the registration. `deleted` is `false` when no webhook was registered.\n\n```json\n{\n  \"data\": {\n    \"deleted\": true\n  }\n}\n```",
        "parameters": [],
        "responses": {
          "200": {
            "description": "Successful response. Read the endpoint-specific status and error fields.",
            "content": {
              "application/json": {
                "schema": {
                  "type": [
                    "object",
                    "null"
                  ],
                  "additionalProperties": true,
                  "properties": {
                    "data": {
                      "type": [
                        "object",
                        "null"
                      ],
                      "additionalProperties": true,
                      "properties": {
                        "deleted": {
                          "type": [
                            "boolean",
                            "null"
                          ]
                        }
                      }
                    }
                  }
                },
                "example": {
                  "data": {
                    "deleted": true
                  }
                }
              }
            }
          },
          "400": {
            "description": "Invalid request.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "401": {
            "description": "Invalid credentials, expired token or API access not enabled.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              },
              "text/plain": {
                "schema": {
                  "type": "string"
                },
                "example": "Unauthorized"
              }
            }
          },
          "403": {
            "description": "Access denied.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        },
        "x-codeSamples": [
          {
            "lang": "curl",
            "source": "curl --request DELETE \\\n  'https://api.clutchapp.io/v1/partner/webhook' \\\n  --header 'Authorization: Bearer YOUR_TOKEN'"
          }
        ]
      }
    }
  },
  "components": {
    "securitySchemes": {
      "bearerAuth": {
        "type": "http",
        "scheme": "bearer",
        "bearerFormat": "JWT",
        "description": "12-hour token from POST /auth/login. Keep credentials and tokens on your backend."
      }
    },
    "schemas": {
      "Login": {
        "type": "object",
        "properties": {
          "email": {
            "type": "string",
            "description": "API-enabled account email.",
            "format": "email"
          },
          "password": {
            "type": "string",
            "description": "Account password.",
            "format": "password",
            "writeOnly": true
          }
        },
        "required": [
          "email",
          "password"
        ]
      },
      "Start": {
        "type": "object",
        "properties": {
          "recording_type": {
            "type": "string",
            "description": "match includes AI analysis; standard is video only.",
            "enum": [
              "match",
              "standard"
            ]
          },
          "game_type": {
            "type": "string",
            "description": "Singles or doubles.",
            "enum": [
              "singles",
              "doubles"
            ]
          },
          "max_recording_duration_min": {
            "type": "integer",
            "minimum": 1,
            "maximum": 120,
            "description": "Maximum recording duration in minutes."
          },
          "privacy": {
            "type": "string",
            "description": "Private recordings are hidden from other users’ feeds and profiles; owner and direct links still work.",
            "enum": [
              "public",
              "private"
            ],
            "default": "public"
          },
          "multi_match": {
            "type": "boolean",
            "description": "Several matches with different players. Requires doubles. Gateway /start does not forward this field yet; use /schedule."
          }
        },
        "required": [
          "recording_type",
          "game_type",
          "max_recording_duration_min"
        ]
      },
      "Schedule": {
        "type": "object",
        "properties": {
          "recording_type": {
            "type": "string",
            "description": "match includes AI analysis; standard is video only.",
            "enum": [
              "match",
              "standard"
            ],
            "default": "match"
          },
          "game_type": {
            "type": "string",
            "description": "Singles or doubles.",
            "enum": [
              "singles",
              "doubles"
            ],
            "default": "doubles"
          },
          "max_recording_duration_min": {
            "type": "integer",
            "description": "Requested recording duration in minutes."
          },
          "privacy": {
            "type": "string",
            "description": "Private recordings are hidden from other users’ feeds and profiles; owner and direct links still work.",
            "enum": [
              "public",
              "private"
            ],
            "default": "public"
          },
          "multi_match": {
            "type": "boolean",
            "description": "Several matches with different players. Requires doubles. Gateway /start does not forward this field yet; use /schedule."
          },
          "start_time": {
            "type": "string",
            "description": "UTC ISO 8601 start time.",
            "format": "date-time"
          }
        },
        "required": [
          "start_time",
          "max_recording_duration_min"
        ]
      },
      "CourtRequest": {
        "type": "object",
        "properties": {
          "device_id": {
            "type": "string",
            "description": "Camera ID."
          }
        },
        "required": [
          "device_id"
        ]
      },
      "CourtPoll": {
        "type": "object",
        "properties": {
          "device_id": {
            "type": "string",
            "description": "Camera ID."
          },
          "job_id": {
            "type": "string",
            "description": "UUID returned by /request.",
            "format": "uuid"
          }
        },
        "required": [
          "device_id",
          "job_id"
        ]
      },
      "CourtAcknowledgement": {
        "type": "object",
        "properties": {
          "job_id": {
            "type": "string",
            "description": "UUID returned by /request.",
            "format": "uuid"
          },
          "device_id": {
            "type": "string",
            "description": "Camera ID."
          },
          "error": {
            "type": "null"
          }
        },
        "required": [
          "job_id",
          "device_id",
          "error"
        ]
      },
      "CourtResult": {
        "type": "object",
        "properties": {
          "job_id": {
            "type": "string",
            "description": "UUID returned by /request.",
            "format": "uuid"
          },
          "device_id": {
            "type": "string",
            "description": "Camera ID."
          },
          "state": {
            "type": "string",
            "enum": [
              "PENDING",
              "RUNNING",
              "COMPLETED",
              "FAILED"
            ]
          },
          "court_status": {
            "type": [
              "string",
              "null"
            ],
            "enum": [
              "OCCUPIED",
              "EMPTY",
              "LIGHTS_OFF",
              null
            ]
          },
          "error": {
            "type": [
              "string",
              "null"
            ],
            "enum": [
              "internal_error",
              null
            ]
          }
        },
        "required": [
          "job_id",
          "device_id",
          "state",
          "court_status",
          "error"
        ]
      },
      "PlayerLink": {
        "oneOf": [
          {
            "type": "object",
            "properties": {
              "partner_player_id": {
                "type": "string",
                "description": "Player ID in your system."
              },
              "purpose": {
                "const": "annotation",
                "type": "string"
              },
              "state": {
                "type": "string",
                "description": "Fresh random state, at least 128 bits, bound to the signed-in player."
              },
              "return_url": {
                "anyOf": [
                  {
                    "type": "string",
                    "description": "Exactly registered HTTPS app link or custom scheme."
                  },
                  {
                    "type": "null"
                  }
                ]
              }
            },
            "required": [
              "partner_player_id",
              "purpose",
              "state"
            ]
          },
          {
            "type": "object",
            "properties": {
              "partner_player_id": {
                "type": "string",
                "description": "Player ID in your system."
              },
              "video_id": {
                "type": "string",
                "description": "Optional recording owned by your account."
              },
              "return_url": {
                "anyOf": [
                  {
                    "type": "string",
                    "description": "Exactly registered return URL."
                  },
                  {
                    "type": "null"
                  }
                ]
              }
            },
            "required": [
              "partner_player_id"
            ],
            "not": {
              "properties": {
                "purpose": {}
              },
              "required": [
                "purpose"
              ]
            }
          }
        ],
        "description": "Annotation consent or the existing hosted tagging flow. For annotation, omit video_id."
      },
      "AnnotationEdit": {
        "type": "object",
        "properties": {
          "revision": {
            "type": "string",
            "description": "Revision from GET annotations or the preceding save."
          },
          "player_mapping": {
            "type": "object",
            "additionalProperties": {
              "type": [
                "string",
                "null"
              ]
            },
            "description": "Patch non-reference tracks to reference slots. Null clears grouping."
          },
          "skipped_track_ids": {
            "type": "array",
            "items": {
              "type": "string",
              "description": "Non-reference track ID. Replaces the full skipped list."
            }
          },
          "assignments": {
            "type": "object",
            "additionalProperties": {
              "anyOf": [
                {
                  "oneOf": [
                    {
                      "type": "object",
                      "properties": {
                        "partner_player_id": {
                          "type": "string",
                          "description": "Linked player ID in your system."
                        }
                      },
                      "required": [
                        "partner_player_id"
                      ]
                    },
                    {
                      "type": "object",
                      "properties": {
                        "clutch_user_id": {
                          "type": "string",
                          "description": "Linked Clutch account ID."
                        }
                      },
                      "required": [
                        "clutch_user_id"
                      ]
                    }
                  ]
                },
                {
                  "type": "null"
                }
              ]
            },
            "description": "Patch assignments. Null unassigns. Exactly one ID form per slot."
          },
          "teams": {
            "type": "object",
            "properties": {
              "pair-1": {
                "type": "array",
                "items": {
                  "type": "string",
                  "description": "Reference slot ID."
                }
              },
              "pair-2": {
                "type": "array",
                "items": {
                  "type": "string",
                  "description": "Reference slot ID."
                }
              }
            },
            "required": [
              "pair-1",
              "pair-2"
            ]
          }
        },
        "required": [
          "revision"
        ],
        "anyOf": [
          {
            "required": [
              "player_mapping"
            ]
          },
          {
            "required": [
              "skipped_track_ids"
            ]
          },
          {
            "required": [
              "assignments"
            ]
          },
          {
            "required": [
              "teams"
            ]
          }
        ]
      },
      "WebhookRegistration": {
        "type": "object",
        "properties": {
          "url": {
            "type": "string",
            "description": "Public HTTPS hostname; IP addresses and internal hosts are rejected.",
            "format": "uri",
            "pattern": "^https://"
          }
        },
        "required": [
          "url"
        ]
      },
      "Error": {
        "description": "Public error formats vary by endpoint. See Errors and retries.",
        "oneOf": [
          {
            "type": "object",
            "properties": {
              "error": {
                "type": "string",
                "description": "Error message."
              }
            },
            "required": [
              "error"
            ]
          },
          {
            "type": "object",
            "properties": {
              "error": {
                "type": "object",
                "properties": {
                  "code": {
                    "type": "string",
                    "description": "Machine-readable error code."
                  },
                  "message": {
                    "type": "string",
                    "description": "Error description."
                  }
                },
                "required": [
                  "code",
                  "message"
                ]
              }
            },
            "required": [
              "error"
            ]
          },
          {
            "type": "object",
            "properties": {
              "errors": {
                "type": "object",
                "properties": {
                  "detail": {
                    "type": "string",
                    "description": "Not Found, Unauthorized or Service Unavailable."
                  }
                },
                "required": [
                  "detail"
                ]
              }
            },
            "required": [
              "errors"
            ]
          },
          {
            "type": "object",
            "properties": {
              "message": {
                "type": "string",
                "description": "Gateway validation message."
              }
            },
            "required": [
              "message"
            ]
          }
        ]
      }
    }
  },
  "webhooks": {
    "video.status_changed": {
      "post": {
        "operationId": "receive_video_status_changed",
        "summary": "Receive video.status_changed",
        "description": "Clutch POSTs this JSON payload to your registered public HTTPS webhook URL. Compare the registration token, acknowledge within five seconds, and reconcile out-of-order or missed delivery using the read endpoints. See the webhook guide for event-specific rules.",
        "security": [],
        "parameters": [
          {
            "name": "X-Clutch-Webhook-Token",
            "in": "header",
            "required": true,
            "description": "Token returned once by PUT /partner/webhook. Compare with your stored value.",
            "schema": {
              "type": "string"
            }
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "additionalProperties": true,
                "properties": {
                  "event": {
                    "type": "string",
                    "const": "video.status_changed"
                  },
                  "video_id": {
                    "type": [
                      "string",
                      "null"
                    ]
                  },
                  "status": {
                    "type": [
                      "string",
                      "null"
                    ]
                  },
                  "device_id": {
                    "type": [
                      "string",
                      "null"
                    ]
                  },
                  "start_time": {
                    "type": [
                      "string",
                      "null"
                    ]
                  },
                  "end_time": {
                    "type": [
                      "string",
                      "null"
                    ]
                  },
                  "title": {
                    "type": [
                      "string",
                      "null"
                    ]
                  },
                  "run_version": {
                    "type": [
                      "number",
                      "null"
                    ]
                  },
                  "occurred_at": {
                    "type": [
                      "string",
                      "null"
                    ]
                  }
                },
                "required": [
                  "event"
                ]
              },
              "example": {
                "event": "video.status_changed",
                "video_id": "B92BFC8A-2454-4632-9580-F96E99FB7C85",
                "status": "OK",
                "device_id": "4580f207-b672-4df0-83eb-110cc408466b",
                "start_time": "2026-08-01T17:02:00Z",
                "end_time": "2026-08-01T18:30:00Z",
                "title": "Example Court",
                "run_version": 1,
                "occurred_at": "2026-08-01T18:31:00Z"
              }
            }
          }
        },
        "responses": {
          "2XX": {
            "description": "Acknowledge receipt within five seconds."
          }
        }
      }
    },
    "player.identification_changed": {
      "post": {
        "operationId": "receive_player_identification_changed",
        "summary": "Receive player.identification_changed",
        "description": "Clutch POSTs this JSON payload to your registered public HTTPS webhook URL. Compare the registration token, acknowledge within five seconds, and reconcile out-of-order or missed delivery using the read endpoints. See the webhook guide for event-specific rules.",
        "security": [],
        "parameters": [
          {
            "name": "X-Clutch-Webhook-Token",
            "in": "header",
            "required": true,
            "description": "Token returned once by PUT /partner/webhook. Compare with your stored value.",
            "schema": {
              "type": "string"
            }
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "additionalProperties": true,
                "properties": {
                  "event": {
                    "type": "string",
                    "const": "player.identification_changed"
                  },
                  "event_id": {
                    "type": [
                      "string",
                      "null"
                    ]
                  },
                  "video_id": {
                    "type": [
                      "string",
                      "null"
                    ]
                  },
                  "partner_player_id": {
                    "type": [
                      "string",
                      "null"
                    ]
                  },
                  "ai_player_id": {
                    "type": [
                      "string",
                      "null"
                    ]
                  },
                  "previous_ai_player_id": {},
                  "pair": {
                    "type": [
                      "string",
                      "null"
                    ]
                  },
                  "occurred_at": {
                    "type": [
                      "string",
                      "null"
                    ]
                  }
                },
                "required": [
                  "event"
                ]
              },
              "example": {
                "event": "player.identification_changed",
                "event_id": "5f0e6c2a-8f0b-4d2a-9c31-3e0b9f7c1a44",
                "video_id": "B92BFC8A-2454-4632-9580-F96E99FB7C85",
                "partner_player_id": "partner_player_813742",
                "ai_player_id": "player-14",
                "previous_ai_player_id": null,
                "pair": "pair-1",
                "occurred_at": "2026-08-01T19:40:00Z"
              }
            }
          }
        },
        "responses": {
          "2XX": {
            "description": "Acknowledge receipt within five seconds."
          }
        }
      }
    }
  }
}
