Partner API v1

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.

Numbers only
You get the analysis JSON: contacts, rallies, movement, shot types, speed, heatmaps. No videos back, no Padel Vision UI required.
Fixed cameras
Register the twelve court points per camera once; every analysis then runs on the same homography – more precise than any automatic detection.
Asynchronous
Submitting takes a second, the analysis takes minutes. Poll the status or receive a signed webhook when the result is ready.
Manual approval
Access only for vetted partners. Register below and I will get back to you with your key.

How it works

  1. 1 · RegisterFill in the form below. I review the request and send you key and webhook secret.
  2. 2 · Calibrate camerasCreate a camera profile with the twelve court points once per fixed camera (optional, recommended).
  3. 3 · Submit an analysisPOST with the video URL. You get a job id back immediately.
  4. 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.

123456789101112camera side (near)far side
  1. 1corner_near_leftnear baseline, left corner (left as seen in the camera image)
  2. 2corner_near_rightnear baseline, right corner
  3. 3corner_far_leftfar baseline, left corner
  4. 4corner_far_rightfar baseline, right corner
  5. 5service_near_leftnear service line meets the left side wall
  6. 6service_near_rightnear service line meets the right side wall
  7. 7service_far_leftfar service line meets the left side wall
  8. 8service_far_rightfar service line meets the right side wall
  9. 9net_leftnet meets the left side wall – on the floor, not the net tape
  10. 10net_rightnet meets the right side wall – on the floor
  11. 11center_service_nearcentre line meets the near service line
  12. 12center_service_farcentre line meets the far service line
  1. 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.
  2. 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.
  3. 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.
  4. 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

One camera, one profile
Every physical camera gets its own profile – two courts in the same hall are two cameras. Create as many as you like; the name is your key, e.g. “Hall North – Court 3”.
The mapping is yours
Your software knows which court a video comes from and passes the matching camera_id. Padel Vision does not recognise the camera from the picture.
Works without camera_id too
Omit camera_id and I detect the court per video automatically (court.source = “auto” with confidence). That is the right path for changing or unknown cameras – just a bit less precise than a calibrated profile.
Camera moved?
After any rebuild, zoom or tilt, calibrate again (create a new camera and stop using the old one). A wrong profile is worse than none.

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.

By sending you agree that I store your details to process the request.
Partner API: padel match analysis via API | Padel Vision