Match analysis via API
Your software sends me a video, I analyse it and you fetch the numbers – no UI, no manual steps. Built for booking systems, venue apps, camera systems and coaching platforms.
How it works
- 1 · RegisterFill in the form below. I review the request and send you key and webhook secret.
- 2 · Calibrate camerasCreate a camera profile with the twelve court points once per fixed camera (optional, recommended).
- 3 · Submit an analysisPOST with the video URL. You get a job id back immediately.
- 4 · Fetch the resultWait for the webhook or poll the status; at “done” the result is part of the same call.
Access
Every request carries your key in the Authorization header. The key is bound to your partner record, optionally restricted to IP addresses, and can be revoked at any time. Keep it server-side – never in apps or browsers.
Authorization: Bearer pv_live_… Base URL: https://padelvision.padelradar.io/api/v1
Calibrate cameras
A fixed camera always sees the court the same way. Register the twelve court points in pixels once per camera and receive a camera_id to pass with every analysis. Without a camera profile I detect the court automatically – usually fine, but less precise.
curl -X POST https://padelvision.padelradar.io/api/v1/cameras \
-H "Authorization: Bearer pv_live_…" \
-H "Content-Type: application/json" \
-d '{
"name": "Court 1 – Halle Nord",
"frame_width": 1920,
"frame_height": 1080,
"keypoints": [
{ "label": "corner_near_left", "x": 212, "y": 1040 },
{ "label": "corner_near_right", "x": 1710, "y": 1038 },
{ "label": "corner_far_left", "x": 702, "y": 318 },
{ "label": "corner_far_right", "x": 1222, "y": 316 },
{ "label": "service_near_left", "x": 330, "y": 870 },
{ "label": "service_near_right", "x": 1590, "y": 868 },
{ "label": "service_far_left", "x": 740, "y": 372 },
{ "label": "service_far_right", "x": 1184, "y": 371 },
{ "label": "net_left", "x": 560, "y": 540 },
{ "label": "net_right", "x": 1360, "y": 538 },
{ "label": "center_service_near", "x": 960, "y": 869 },
{ "label": "center_service_far", "x": 962, "y": 371 }
]
}'“near” is the camera side, net points are on the floor (not the net tape), pixels refer to frame_width × frame_height. GET /cameras lists your cameras.
Determining the twelve points
The points are line intersections on the floor – exactly the ones you find by eye in a still frame. The sketch shows the court from above with the camera side (“near”) at the bottom. Numbers match the table; the order in the JSON does not matter, the label does.
- 1corner_near_leftnear baseline, left corner (left as seen in the camera image)
- 2corner_near_rightnear baseline, right corner
- 3corner_far_leftfar baseline, left corner
- 4corner_far_rightfar baseline, right corner
- 5service_near_leftnear service line meets the left side wall
- 6service_near_rightnear service line meets the right side wall
- 7service_far_leftfar service line meets the left side wall
- 8service_far_rightfar service line meets the right side wall
- 9net_leftnet meets the left side wall – on the floor, not the net tape
- 10net_rightnet meets the right side wall – on the floor
- 11center_service_nearcentre line meets the near service line
- 12center_service_farcentre line meets the far service line
- Take a still frame from this exact camera at the resolution your videos will have (e.g. 1920 × 1080). If the camera is later moved, zoomed or reconfigured, calibrate it again.
- Read the pixel position of each of the twelve points – in any image tool that shows coordinates (GIMP, Photoshop, Preview, Paint) or the browser inspector. x counts from the left, y from the top.
- If a point lies outside the image (e.g. the near baseline below the bottom edge), still give the position where it would be – negative values or values beyond the image are allowed. A camera that sees all lines is better.
- Create the camera with POST /cameras and store the camera_id. To verify, submit a short test analysis: court.source in the result must be “profile”.
Let me propose the points
If you do not want to measure by hand: send a still frame (JPG/PNG) or a short clip from the camera to POST /cameras/detect. The same detector used in the analysis returns the twelve points with a confidence. Check them against the image – at low confidence the proposal is only a trapezoid – and create the camera from them.
curl -X POST https://padelvision.padelradar.io/api/v1/cameras/detect \
-H "Authorization: Bearer pv_live_…" \
-H "Content-Type: application/json" \
-d '{ "image_url": "https://cdn.example.com/court-1-still.jpg" }'
{ "frame_width": 1920, "frame_height": 1080, "detected": true, "confidence": 0.93,
"keypoints": [ { "label": "corner_near_left", "x": 212, "y": 1040 }, … ] }Several venues and cameras
Submit an analysis
POST /analyses – responds 202 with the job id. I download the video in the background.
curl -X POST https://padelvision.padelradar.io/api/v1/analyses \
-H "Authorization: Bearer pv_live_…" \
-H "Content-Type: application/json" \
-d '{
"video_url": "https://cdn.example.com/match-48213.mp4?token=…",
"camera_id": "2f9c…",
"external_id": "match-48213",
"callback_url": "https://example.com/padelvision/webhook",
"trim_start_s": 0,
"trim_end_s": 5400
}'- video_url
- Required. https URL I fetch the video from – MP4/MOV, up to 4 GB, up to 120 minutes of analysed footage. Signed, expiring URLs are ideal.
- camera_id
- Camera profile (see above). Alternatively court_points with the same shape as when creating a camera.
- external_id
- Your identifier. The same id returns the same job – no duplicate analysis on retries.
- callback_url
- Webhook for this job; otherwise the webhook URL stored with your partner record.
- trim_start_s / trim_end_s
- Analyse only this segment (seconds).
{ "id": "7c1e…", "external_id": "match-48213", "status": "queued",
"result_url": "/api/v1/analyses/7c1e…" }Fetch the result
GET /analyses/{id} returns the status and – at status “done” – the result. Players are positions, not identities: near_left and near_right on the camera side, far_left and far_right opposite. You map names yourself. Add ?tracks=1 for the position tracks (several MB).
{
"id": "7c1e…",
"external_id": "match-48213",
"status": "done",
"stage": null,
"progress": 100,
"error": null,
"court": { "source": "profile", "confidence": null },
"result": {
"schema_version": 1,
"duration_s": 5388.2,
"total_ball_contacts": 2914,
"avg_rally_length_shots": 7.4,
"per_player": [
{ "player": "near_left", "ball_contacts": 742, "distance_covered_m": 3120.5 },
{ "player": "near_right", "ball_contacts": 701, "distance_covered_m": 2988.1 },
{ "player": "far_left", "ball_contacts": 745, "distance_covered_m": 3201.9 },
{ "player": "far_right", "ball_contacts": 726, "distance_covered_m": 3044.0 }
],
"rallies": [ { "index": 0, "start_t": 12.4, "end_t": 21.9, "shots": 9 }, … ],
"contacts": [ { "t": 12.4, "player": "far_left", "court_xy": [-6.1, 2.3],
"speed_kmh": 41.2, "shot_type": "serve" }, … ],
"heatmaps": [ … ],
"ball_speed": { "bin_edges_kmh": [ … ], "counts": [ … ], "max_kmh": 118.4, "avg_kmh": 46.7 }
}
}- status
- queued · processing · done · failed. While processing, stage tells where the analysis is (ingest, court, players, ball, hits, aggregate …), progress in percent.
- result.contacts[]
- Every ball contact: time t in seconds, player position, court position in metres (origin at court centre), speed in km/h, shot type (serve, volley, overhead, groundstroke, bandeja, vibora, topspin_smash …).
- result.rallies[]
- Rallies with start, end and number of shots.
- result.per_player[]
- Contacts and distance covered per position; heatmaps and ball_speed alongside.
- court
- Where the homography came from: profile (your camera), clicked, auto (with confidence) or synthetic.
Webhook
At done or failed I POST to the callback_url – three attempts (immediately, after 10 s, after 60 s) until your server answers 2xx. The result stays available at result_url regardless.
POST https://example.com/padelvision/webhook
X-PadelVision-Signature: t=1759600000,v1=9d4c…
{ "event": "analysis.done", "id": "7c1e…", "external_id": "match-48213",
"status": "done", "error": null, "result_url": "/api/v1/analyses/7c1e…" }Verify the signature: v1 = HMAC-SHA256(secret, "<t>.<body>") in hex; t is the Unix timestamp. Reject stale t (e.g. older than five minutes) to prevent replays.
Errors
- 400
- Incomplete request – the error field explains.
- 401 / 403
- Key missing, invalid, revoked or wrong IP.
- 404
- Analysis does not belong to your key.
- 429
- Hourly limit reached (60 new analyses per hour).
- 503
- Analysis currently unavailable.
- failed
- Errors during the analysis live on the job: status “failed” plus error, e.g. “video download failed: HTTP 403”.
Limits
- 60 new analyses per hour and partner.
- Video up to 4 GB, analysed segment up to 120 minutes.
- A 90-minute match is usually done in under an hour – poll no more than every 30 seconds, better use the webhook.
Data
- The original video is kept only for the analysis; the numbers remain available.
- Videos show people – please inform your players about the analysis. A data processing agreement is available on request.
- No face recognition, no identification: players are positions on the court.
Register as a partner
I review every request personally and usually reply within two working days with key and secret.