The Plyomat Open API gives you secure, read-only access to your organization's training data: athletes, groups, devices, test sessions, individual jumps, and assessment results. It is a plain REST API that returns JSON, so you can sync Plyomat into your own dashboards, athlete-management system, or data warehouse. Included free for Plyomat owners.
The API is read-only by design. It exposes the same training data you see in the app, structured as clean REST resources you can pull on your own schedule.
Your roster and how it is organized: names, gender, body weight, external IDs, and the groups each athlete belongs to.
The mats and controllers registered to your organization, so you can attribute data back to the hardware that captured it.
Every test session and the sets inside it. Each set embeds its individual jumps (reps) with flight time, contact time, jump height, and RSI.
Computed assessment results, ready to drop into reports, leaderboards, or longitudinal tracking on your side.
The coaches and staff on your account, including archived members when you ask for them.
Pull it once or sync it continuously. Either way the data matches what your coaches captured on the mat, to 0.001 seconds.
Every endpoint hangs off a single versioned base URL.
https://api.plyomat.com/v1Every request needs your API key sent as a Bearer token in the Authorization header:
Authorization: Bearer pk_live_xxxxxxxxxxxxTreat the key like a password: it grants read access to your whole organization. You can rotate or revoke it anytime from the Plyomat dashboard under Settings › Integrations. Keys are scoped to read-only access, so they can never modify or delete your data.
Getting your key. API keys are self-serve from your own organization account, no request needed:
List your athletes with a single authenticated request.
curl https://api.plyomat.com/v1/athletes \
-H "Authorization: Bearer pk_live_xxxxxxxxxxxx"Every list endpoint returns the same response envelope: a data array of records plus a total.
{
"data": [
{
"id": "...",
"first_name": "...",
"last_name": "...",
"gender": "...",
"body_weight_kg": 0,
"external_id": null,
"external_source": null,
"created_at": "...",
"updated_at": "..."
}
],
"total": 1,
"limit": 100,
"offset": 0,
"has_more": false
}Eight resources, each a list and (where it makes sense) a detail endpoint, with a few filters to keep responses small.
| Resource | List | Detail | Filters |
|---|---|---|---|
| Athletes | GET /athletes | GET /athletes/{id} | group_id |
| Groups | GET /groups | GET /groups/{id} | None |
| Devices | GET /devices | GET /devices/{id} | None |
| Sessions | GET /sessions | GET /sessions/{id} | athlete_id, since |
| Sets | GET /sets | GET /sets/{id} | athlete_id, session_id, since, mode |
| Assessments | GET /assessments | None | None |
| Org members | GET /org/members | None | include_archived |
Each set embeds its reps (rep_index, flight_time_ms, contact_time_ms, jump_height_cm, rsi, side, is_kept, captured_at), so a single GET /sets?session_id=... gives you a session's full jump-by-jump detail in one call.
Every list endpoint is paginated. Add ?limit=N (default 100, max 500) and ?offset=N (default 0). The envelope tells you everything:
{
"data": [ /* ...rows... */ ],
"total": 525, // TRUE total across all pages, not just this page
"limit": 100,
"offset": 0,
"has_more": true // more rows exist past this page
}Page by incrementing offset (0, 100, 200, ...) until has_more is false.
To pull large datasets reliably, filter rather than page. Two patterns cover almost everything:
Poll the since filter for anything new since your last run.
curl "https://api.plyomat.com/v1/sets?since=2026-06-01T00:00:00Z" \
-H "Authorization: Bearer pk_live_xxxxxxxxxxxx"Run the same call against /sessions?since=... and you have a complete, low-cost incremental sync.
For a one-time historical pull, loop your athletes and request their sets one athlete at a time, which keeps every call well under the row limit.
curl "https://api.plyomat.com/v1/sets?athlete_id=<id>" \
-H "Authorization: Bearer pk_live_xxxxxxxxxxxx"The REST API is pull: you ask for data. Webhooks are push: Plyomat POSTs to your endpoint the moment something happens. Most integrations use both, a webhook tells you when something changed, then you pull the full detail from the API using the ids.
whsec_...). It is shown once, so store it now: it is what verifies delivery signatures.Subscribe to any of these. set.completed is the workhorse: it fires the moment a coach saves a set.
set.completedsession.startedsession.endedathlete.createdathlete.updatedathlete.archivedassessment.createdassessment.updatedassessment.archivedgroup.deletedleaderboard_share.createdleaderboard_share.revokedEvery delivery is a JSON POST to your URL:
{
"event_type": "set.completed",
"event_id": "<uuid>",
"source_version": 1,
"delivery_id": "<uuid>",
"payload": { /* the changed record */ }
}The payload is the record that changed. Pull richer detail (for example, a set's reps) from the read API using the ids.
Every delivery carries these headers:
X-Plyomat-Event: set.completed
X-Plyomat-Delivery-Id: <uuid>
X-Plyomat-Timestamp: 1733590000
X-Plyomat-Signature: t=1733590000,v1=<hex>Signatures are Stripe-style. Compute HMAC-SHA256(signing_secret, "<timestamp>.<raw_request_body>") and compare the hex digest to v1 from the X-Plyomat-Signature header. Reject the delivery if the timestamp is more than 5 minutes old.
// Node
const crypto = require("crypto");
function verify(rawBody, sigHeader, secret) {
const parts = Object.fromEntries(sigHeader.split(",").map(p => p.split("=")));
const expected = crypto.createHmac("sha256", secret)
.update(parts.t + "." + rawBody).digest("hex");
const fresh = Math.abs(Date.now() / 1000 - Number(parts.t)) < 300;
return fresh && crypto.timingSafeEqual(
Buffer.from(expected), Buffer.from(parts.v1));
}# Python
import hmac, hashlib, time
def verify(raw_body: bytes, sig_header: str, secret: str) -> bool:
parts = dict(p.split("=") for p in sig_header.split(","))
signed = f"{parts['t']}.".encode() + raw_body
expected = hmac.new(secret.encode(), signed, hashlib.sha256).hexdigest()
fresh = abs(time.time() - int(parts["t"])) < 300
return fresh and hmac.compare_digest(expected, parts["v1"])Deliveries are near-real-time, typically under a minute. set.completed has a roughly 30-second coalesce window, so quick rep edits do not double-fire.
Failed deliveries retry with backoff (1m, 5m, 15m, 1h, 6h). After repeated failures the subscription auto-pauses, and resumes on the next successful delivery.
https://api.plyomat.com/v1, and the full interactive reference lives at app.plyomat.com/developers.Authorization: Bearer pk_live_xxxxxxxxxxxx. Treat the key like a password, since it grants read access to your whole organization. Rotate or revoke it anytime from the dashboard under Settings then Integrations.GET /sessions?since=<ISO8601> and GET /sets?since=<ISO8601> for anything new since your last run. For a full backfill, loop your athletes and call GET /sets?athlete_id=<id> per athlete.The Plyomat Open API is included free for owners. Pull your athletes, sessions, and every jump into your own tools, with a read-only key you control.
Open the interactive reference →