Appearance
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.
Python example
Download court_status.py. Requires Python 3.9+ and uses only the standard library.
Set CLUTCH_API_TOKEN to your existing bearer token, then run:
sh
python3 court_status.py YOUR_CAMERA_IDThe 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.
python
#!/usr/bin/env python3
"""Request a court observation and poll for its result. Never controls lights."""
import argparse
import json
import os
import time
import urllib.request
BASE_URL = "https://api.clutchapp.io/v1/clutchcam/court-status"
def post_json(path, token, body, timeout=15):
request = urllib.request.Request(
f"{BASE_URL}/{path}",
data=json.dumps(body).encode("utf-8"),
headers={"Authorization": f"Bearer {token}", "Content-Type": "application/json"},
method="POST",
)
# urllib raises HTTPError for non-2xx responses. Callers must preserve light
# state on this error; no failure is interpreted as an empty court.
with urllib.request.urlopen(request, timeout=timeout) as response:
return response.status, json.load(response)
def observe_court(device_id, token, wait_seconds=330, poll_interval=3):
if not device_id or not token:
raise ValueError("A device ID and bearer token are required")
if wait_seconds <= 0 or poll_interval <= 0:
raise ValueError("Polling interval and timeout must be positive")
deadline = time.monotonic() + wait_seconds
status, ack = post_json("request", token, {"device_id": device_id}, min(15, wait_seconds))
if status != 202 or not ack.get("job_id") or ack.get("error"):
raise RuntimeError("Court observation was not accepted")
while True:
remaining = deadline - time.monotonic()
if remaining <= 0:
raise TimeoutError("Court status unavailable; keep the current light state")
time.sleep(min(poll_interval, remaining))
remaining = deadline - time.monotonic()
if remaining <= 0:
raise TimeoutError("Court status unavailable; keep the current light state")
status, result = post_json(
"result", token,
{"device_id": device_id, "job_id": ack["job_id"]},
min(15, remaining),
)
if status == 200 and result.get("state") == "COMPLETED" and result.get("error") is None:
court_status = result.get("court_status")
if court_status not in {"OCCUPIED", "EMPTY", "LIGHTS_OFF"}:
raise RuntimeError("Unknown court status; keep the current light state")
return court_status
if status != 202 or result.get("state") not in {"PENDING", "RUNNING"} or result.get("error"):
raise RuntimeError("Court observation failed; keep the current light state")
if __name__ == "__main__":
parser = argparse.ArgumentParser(description=__doc__)
parser.add_argument("device_id", help="Clutch camera ID")
args = parser.parse_args()
token = os.environ.get("CLUTCH_API_TOKEN", "")
if not token:
parser.error("Set CLUTCH_API_TOKEN to your existing bearer token")
print(observe_court(args.device_id, token))