Skip to content

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/request
json
{
  "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/result
json
{
  "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_statusMeaning
OCCUPIEDAt least one person is detected in the court’s monitored area.
EMPTYNo person is detected in the monitored area, and the image is not dark.
LIGHTS_OFFThe 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 statusAction
400Check the required JSON fields.
401 / 403Check authentication and camera or recording access.
404Check the device and job IDs. Unknown or inaccessible jobs also return this status.
409Camera unavailable or media not ready; retry later.
422Check recording configuration.
502 / 503 / 504Analysis 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_ID

The 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))

Clutch Partner API · v1