Appearance
Cameras
GET /clutchcam/device/{device_id}/status
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.
Sample response
json
{
"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"
}
}POST /clutchcam/device/{device_id}/schedule
Schedules a recording.
Body:
json
{
"start_time": "2026-08-01T17:00:00Z",
"max_recording_duration_min": 90,
"privacy": "private"
}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.
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.
multi_match is also optional on both; see Multi-match recordings below.
Sample response
json
{
"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
}
}A schedule conflict returns HTTP 409 with data.conflicting_ids.
GET /clutchcam/device/{device_id}/scheduled
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.
Sample response
json
{
"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"
}
]
}GET /clutchcam/device/{device_id}/recordings?from=<ISO-8601>&to=<ISO-8601>&cursor=<opaque>
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.
The list excludes deleted recordings and private recordings created by another account. The authenticated account can see its own private recordings.
Sample response
json
{
"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
}Pass a non-null next_cursor back as cursor with the same device_id, from, and to. Invalid dates, ranges, or cursors return HTTP 400.
POST /clutchcam/device/{device_id}/start
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.
Body:
json
{
"recording_type": "match",
"game_type": "doubles",
"max_recording_duration_min": 120,
"privacy": "private"
}Sample response
json
{
"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
}
}POST /clutchcam/device/{device_id}/stop
Stops the active recording. The camera must be recording or stopping.
Sample response
json
{
"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
}
}Court status for lighting automation
Returns whether a camera’s court is occupied, empty or dark. Your application decides when to switch the lights; this API does not control them.
Both endpoints use the existing Authorization: Bearer <token> header and Content-Type: application/json.
POST /clutchcam/court-status/request
Requests a court observation. Analysis runs in the background.
Request:
javascript
POST https://api.clutchapp.io/v1/clutchcam/court-status/requestjson
{
"device_id": "your-camera-id"
}HTTP 202 — Accepted:
json
{
"job_id": "1b475178-b3b4-4c24-977d-591dd347de29",
"device_id": "your-camera-id",
"error": null
}Use the returned UUID job_id to retrieve the outcome. Acceptance does not confirm that the camera is online or that analysis will succeed.
POST /clutchcam/court-status/result
Retrieves the outcome of a requested observation. Poll every three seconds while the job is pending or running.
Request:
javascript
POST https://api.clutchapp.io/v1/clutchcam/court-status/resultjson
{
"device_id": "your-camera-id",
"job_id": "1b475178-b3b4-4c24-977d-591dd347de29"
}HTTP 202 — Pending or running:
json
{
"job_id": "1b475178-b3b4-4c24-977d-591dd347de29",
"device_id": "your-camera-id",
"state": "RUNNING",
"court_status": null,
"error": null
}HTTP 200 — Completed:
json
{
"job_id": "1b475178-b3b4-4c24-977d-591dd347de29",
"device_id": "your-camera-id",
"state": "COMPLETED",
"court_status": "OCCUPIED",
"error": null
}court_status | Meaning |
|---|---|
OCCUPIED | At least one person is detected in the court’s monitored area. |
EMPTY | No person is detected in the monitored area, and the image is not dark. |
LIGHTS_OFF | The image meets the darkness threshold. This does not confirm the physical light-switch position. |
Job states are PENDING, RUNNING, COMPLETED and FAILED.
Freshness
A request may reuse an active job or a successful observation from the previous five minutes. Results do not include the observation timestamp.
/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.
Jobs have a five-minute deadline, including queue time.
Offline cameras and errors
If the camera is marked unavailable when capture runs, analysis fails with HTTP 409:
json
{
"job_id": "1b475178-b3b4-4c24-977d-591dd347de29",
"device_id": "your-camera-id",
"state": "FAILED",
"court_status": null,
"error": "internal_error"
}A disconnection during capture may instead produce an upstream error or timeout. There is no separate public CAMERA_OFFLINE error code.
| HTTP status | Action |
|---|---|
400 | Check the required JSON fields. |
401 / 403 | Check authentication and camera or recording access. |
404 | Check the device and job IDs. Unknown or inaccessible jobs also return this status. |
409 | Camera unavailable or media not ready; retry later. |
422 | Check recording configuration. |
502 / 503 / 504 | Analysis unavailable or timed out; retry later. |
Authentication errors may use the shared API error format.
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.
Court Status API — Integration Guide
Camera control errors
POST /start, POST /stop and POST /schedule can return:
| Status | Response | Meaning |
|---|---|---|
| 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. |
| 404 | {"errors": {"detail": "Not Found"}}, or {"error": "Device not found: ..."} on /schedule | The camera does not exist. |
| 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. |
| 422 | {"error": "..."} | /start could not be completed. |
| 429 | {"error": "Too many camera control requests from this account; try again later"} | Wait for Retry-After seconds. |
| 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. |
Through 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.
After a timeout or 5xx on these calls, read the camera's status or scheduled list before retrying, because the first request may have worked.
Multi-match recordings
If 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.
Add "multi_match": true when you start or schedule a recording:
POST /clutchcam/device/{device_id}/startPOST /clutchcam/device/{device_id}/schedule
json
{
"recording_type": "match",
"game_type": "doubles",
"max_recording_duration_min": 90,
"multi_match": true
}It needs "game_type": "doubles", and any other game type returns a 400. For normal recordings, leave the field out.
Get match.json from GET /video/{video_id}/results as usual. It now includes a periods list, with one entry for each match:
json
{
"periods": [
{
"period_index": 0,
"start_time": "00:00:07",
"end_time": "00:34:45",
"player_ids": ["player-12", "player-41", "player-7", "player-29"],
"player_ids_mapping_to_pair": {
"pair-1": ["player-12", "player-41"],
"pair-2": ["player-7", "player-29"]
},
"match_stats": {"match_time_minutes": 34.63, "avg_rally_shots": 4.6}
},
{
"period_index": 1,
"start_time": "00:36:00",
"end_time": "01:13:00",
"player_ids": ["player-55", "player-63", "player-70", "player-81"],
"player_ids_mapping_to_pair": {
"pair-1": ["player-55", "player-63"],
"pair-2": ["player-70", "player-81"]
},
"match_stats": {"match_time_minutes": 37.0, "avg_rally_shots": 5.2}
}
]
}- Each match has its own teams, stats and highlights.
player_statsincludes every player from every match.- Each match gets new player IDs, so someone who plays three matches appears under three
player-NIDs. - In
player_crop_urls.json, each player photo has aperiod_indexshowing which match it belongs to.
Through api.clutchapp.io, POST /start does not pass multi_match on yet; use /schedule for multi-match recordings until it does.