01Overview
This documentation describes how to fetch your data from the CC Athletics database through REST API endpoints.
Use of the API is subject to rate limits and fair use — see Rate Limits.
- Get an API key from your ForceMate Cloud dashboard. Open Organisations, find your organisation (you must be an "Owner"), click the gear icon, then click Manage API keys.
- Include the API key in the
X-API-Keyheader for all requests. - Use the endpoints below to retrieve your data.
Base URL
https://europe-west1-forcemate-desktop.cloudfunctions.net
02Authentication
API Key
- Each API request requires a valid API key.
- Include the API key in the
X-API-Keyheader. - API keys are generated and managed in ForceMate Cloud: click your organisation's name at the top of the side navigation to open the Organisation Access page, click the gear icon on your organisation's card, then click Manage API Keys.
- Only organisation Owners can manage API keys — for Coaches the button is disabled. Keys are scoped to the organisation they are created in.
Example request
curl https://europe-west1-forcemate-desktop.cloudfunctions.net/get_athletes \ -H "X-API-Key: fm_your_api_key_here"
03API Endpoints
Description: Retrieve all teams for your organisation.
Request example
curl https://europe-west1-forcemate-desktop.cloudfunctions.net/get_teams \ -H "X-API-Key: fm_your_api_key_here"
Response format
Loading...
Description: Retrieve athletes including all recordings (optionally filtered by team and other parameters).
Query parameters
| Parameter | Type | Required | Description |
|---|---|---|---|
| team_id | string | Optional | Filter athletes by specific team |
| min_birth_year | integer | Optional | Filter athletes born on or after this year (e.g., 1990) |
| max_birth_year | integer | Optional | Filter athletes born on or before this year (e.g., 2005) |
| tests_date_from | string | Optional | Filter tests recorded on or after this date. Format: YYYY-MM-DD (e.g., 2024-01-01) |
| tests_date_to | string | Optional | Filter tests recorded on or before this date. Format: YYYY-MM-DD (e.g., 2024-12-31) |
| analysis_type | string | Optional | Filter by type of analysis. Valid values: "jump", "pogo", "isometric", "cop" (Center of Pressure) |
| jump_types | string | Optional | Comma-separated list of jump types to include. Only applies when analysis_type is "jump" (e.g., "cmj,sj,dj") |
| with_armswing | boolean | Optional | Filter CMJ jumps by armswing. Valid values: "true" (only jumps with armswing), "false" (only jumps without armswing). If not specified, returns all CMJ jumps regardless of armswing. |
| isometric_exercise_names | string | Optional | Comma-separated list of isometric exercise names to include. Only applies when analysis_type is "isometric" (e.g., "Mid-thigh pull,Squat") |
Request examples
Basic request (all athletes):
curl https://europe-west1-forcemate-desktop.cloudfunctions.net/get_athletes \ -H "X-API-Key: fm_your_api_key_here"
Filter by team:
curl "https://europe-west1-forcemate-desktop.cloudfunctions.net/get_athletes?team_id=team_123" \ -H "X-API-Key: fm_your_api_key_here"
Filter by birth year and test dates:
curl "https://europe-west1-forcemate-desktop.cloudfunctions.net/get_athletes?min_birth_year=1995&max_birth_year=2005&tests_date_from=2024-01-01&tests_date_to=2024-12-31" \ -H "X-API-Key: fm_your_api_key_here"
Filter by analysis type (jump tests only):
curl "https://europe-west1-forcemate-desktop.cloudfunctions.net/get_athletes?analysis_type=jump" \ -H "X-API-Key: fm_your_api_key_here"
Filter by specific jump types (CMJ and SJ only):
curl "https://europe-west1-forcemate-desktop.cloudfunctions.net/get_athletes?analysis_type=jump&jump_types=cmj,sj" \ -H "X-API-Key: fm_your_api_key_here"
Filter CMJ jumps with armswing only:
curl "https://europe-west1-forcemate-desktop.cloudfunctions.net/get_athletes?analysis_type=jump&jump_types=cmj&with_armswing=true" \ -H "X-API-Key: fm_your_api_key_here"
Filter CMJ jumps without armswing only:
curl "https://europe-west1-forcemate-desktop.cloudfunctions.net/get_athletes?analysis_type=jump&jump_types=cmj&with_armswing=false" \ -H "X-API-Key: fm_your_api_key_here"
Filter isometric exercises:
curl "https://europe-west1-forcemate-desktop.cloudfunctions.net/get_athletes?analysis_type=isometric&isometric_exercise_names=Mid-thigh%20pull,Squat" \ -H "X-API-Key: fm_your_api_key_here"
Filter by CoP (Center of Pressure) tests:
curl "https://europe-west1-forcemate-desktop.cloudfunctions.net/get_athletes?analysis_type=cop" \ -H "X-API-Key: fm_your_api_key_here"
Response format — basic structure
Loading...
Description: Retrieve a download link for a CSV file of a raw jump or isometric recording (raw Newton values in 1–2 columns, depending on number of plates used).
Query parameters
| Parameter | Type | Required | Description |
|---|---|---|---|
| path | string | Required | Path where the CSV file is stored. Get this from a jump result or isometric trial via the key path_to_this_jump_raw_csv or path_to_raw_csv. |
| downsample_factor | integer | Optional | An integer above 1 to downsample the data. For example, with 960 Hz raw data, factor 20 yields 48 Hz. A Savitzky–Golay filter prevents aliasing. |
Request example
curl "https://europe-west1-forcemate-desktop.cloudfunctions.net/get_csv_download_url?path=organisations/frC6h5a/recordings/raw_data.csv" \ -H "X-API-Key: fm_your_api_key_here"
With downsampling
curl "https://europe-west1-forcemate-desktop.cloudfunctions.net/get_csv_download_url?path=organisations/frC6h5a/recordings/raw_data.csv&downsample_factor=20" \ -H "X-API-Key: fm_your_api_key_here"
Response format
{
"download_url": "https://storage.googleapis.com/forcemate-desktop.appspot.com/organisations/frC6h5a..."
}
04Error Handling
The API uses standard HTTP status codes to indicate success or failure of requests.
Common error codes
Invalid or missing API key. Check that your API key is correct and included in the X-API-Key header.
Rate limit exceeded. Wait until the limit resets (see the Rate Limits section) before retrying.
Unexpected server-side issue. If this persists, contact support.
Error response format
{
"error": "Unauthorized",
"message": "Invalid API key provided"
}
05Rate Limits
Use of the API is subject to rate limiting. Limits apply per API key. We may enforce, change, or introduce limits at any time to keep the API responsive for all customers and to keep the cost of serving raw recording data sustainable.
Staying within fair use
- Sync on a schedule (for example hourly or nightly) rather than polling in a loop.
- Store what you download and fetch only new recordings, using
tests_date_fromon/get_athletes. - Download the raw CSV of a recording once and keep it — recordings never change after the fact.
- Use
downsample_factoron/get_csv_download_urlif you don't need the full sample rate.
If your use case genuinely needs sustained high-volume access, contact support and we'll find a limit that works for you.
Rate limit headers
When a limit is enforced, requests over the limit receive HTTP 429 Too Many Requests, and API responses include the following headers:
X-RateLimit-Limit— The maximum number of requests allowedX-RateLimit-Remaining— The number of requests remainingX-RateLimit-Reset— The time when the rate limit resets (Unix timestamp)
06Webhooks
Rather than polling for new tests, you can register an HTTPS endpoint and be notified the moment a recording is saved. ForceMate sends a small signed POST containing identifiers; you then fetch whatever detail you need through the REST endpoints above.
/get_athletes sweep filtered by date remains the reliable way to guarantee you have every recording.
Registering an endpoint
To register a webhook, contact CC Athletics support with your organisation and the HTTPS endpoint you want deliveries sent to. We will register it and return your signing secret. Self-service management from the ForceMate Cloud dashboard is coming; until then registration goes through support.
Endpoints must be https:// and publicly reachable. Plain HTTP, private ranges, and loopback addresses are rejected at registration.
Events
| Event | Fires when |
|---|---|
| recording.created | A new recording has been saved and its raw data is available for download. |
Bulk operations — data migrations, moving an athlete between organisations, moving a recording between athletes — do not fire webhooks. You will only be notified about genuinely new tests.
Payload
The body is deliberately small: identifiers plus enough context to route the event without a lookup.
{
"event": "recording.created",
"delivery_id": "9f1c2b7e-3a45-4c8d-9e10-77b3d2f5a1c4",
"created_at": 1730800000000,
"data": {
"org_id": "frC6h5a",
"athlete_id": "-NxKq2p8sLm3vTw",
"recording_id": "-NxKq3a1bCd4eFg",
"device_type": "PLATE_DEVICE",
"analysis_type": "jump",
"recorded_at": 1730799990000
}
}
| Field | Type | Description |
|---|---|---|
| event | string | Event name. Currently always recording.created. |
| delivery_id | string | Unique id for this delivery attempt chain. Use it to make your handler idempotent. |
| created_at | integer | When the event was generated, in milliseconds since epoch. |
| data.org_id | string | Organisation the recording belongs to. |
| data.athlete_id | string | Athlete the recording belongs to. |
| data.recording_id | string | The new recording. Resolve full detail via /get_athletes, or raw data via /get_csv_download_url. |
| data.device_type | string | Device that produced the recording, e.g. PLATE_DEVICE. |
| data.analysis_type | string | One of jump, isometric, pogo, cop, golf, or unknown. |
| data.recorded_at | integer | When the test itself was recorded, in milliseconds since epoch. May predate created_at. |
Headers
| Header | Description |
|---|---|
| X-ForceMate-Signature | HMAC-SHA256 signature in the form t=<unix_seconds>,v1=<hex_digest>. |
| X-ForceMate-Event | Event name, matching the event field in the body. |
| X-ForceMate-Delivery | Delivery id, matching delivery_id in the body. Stable across retries of the same delivery. |
Verifying the signature
Every request is signed with your endpoint's secret. Verify it before trusting the payload — otherwise anyone who learns your endpoint URL can post fabricated results into your system.
The signed content is the timestamp from the header, a literal ., then the raw request body:
HMAC-SHA256(secret, "<timestamp>.<raw_body>")
Python
import hashlib
import hmac
import time
def verify_forcemate_signature(secret, signature_header, raw_body, tolerance=300):
parts = dict(p.split('=', 1) for p in signature_header.split(','))
timestamp, received = parts['t'], parts['v1']
# Reject anything too old to be a live delivery (replay protection)
if abs(time.time() - int(timestamp)) > tolerance:
return False
expected = hmac.new(
secret.encode('utf-8'),
f'{timestamp}.{raw_body}'.encode('utf-8'),
hashlib.sha256,
).hexdigest()
return hmac.compare_digest(received, expected)
Node.js
const crypto = require('crypto');
function verifyForceMateSignature(secret, signatureHeader, rawBody, tolerance = 300) {
const parts = Object.fromEntries(
signatureHeader.split(',').map((p) => p.split('='))
);
const timestamp = parts.t;
const received = parts.v1;
// Reject anything too old to be a live delivery (replay protection)
if (Math.abs(Date.now() / 1000 - Number(timestamp)) > tolerance) return false;
const expected = crypto
.createHmac('sha256', secret)
.update(timestamp + '.' + rawBody)
.digest('hex');
const a = Buffer.from(received);
const b = Buffer.from(expected);
return a.length === b.length && crypto.timingSafeEqual(a, b);
}
Because the timestamp is inside the signed content, an attacker replaying an old capture cannot move it forward without invalidating the signature. Rejecting deliveries older than a few minutes therefore gives you replay protection.
Retries and backoff
Respond with any 2xx status to acknowledge. Anything else — or a timeout — is treated as a failure and retried.
| Behaviour | Value |
|---|---|
| Attempts per delivery | Up to 5 |
| Backoff | Exponential, from 10 seconds up to 1 hour |
| Request timeout | 10 seconds |
| Auto-disable | After 5 consecutive deliveries exhaust all attempts |
Acknowledge quickly and do the real work asynchronously. If your handler takes longer than 10 seconds we treat it as a failure and retry, even if it eventually succeeded — which means duplicate processing on your side.
An endpoint that fails repeatedly is disabled automatically, to avoid hammering a dead host. Contact support to re-enable it once your endpoint is healthy again; re-enabling resets the failure count. Note that you are not notified when this happens, so if deliveries stop arriving unexpectedly, check with us — and rely on your /get_athletes reconciliation in the meantime.
Delivery semantics
- At-least-once. A delivery may arrive more than once — for example if your endpoint succeeds but the response is lost. Use
delivery_id, orrecording_id, to make your handler idempotent. - No ordering guarantee. Retries mean recordings can arrive out of sequence. Do not assume the order of arrival reflects the order of testing; use
recorded_atif sequence matters. - Raw data is ready on arrival. The CSV is uploaded before the event fires, so
/get_csv_download_urlworks for the deliveredrecording_idas soon as you receive it. - HTTPS only. Deliveries are never sent over plain HTTP.
recording_id, and return 200 immediately. Fetch details from the REST API in a background worker, and run a nightly /get_athletes reconciliation to catch anything a failed delivery missed.
07Complete Response Examples
These examples show the complete structure of responses you'll receive from the API, based on real data.
08Data Structure Reference
Detailed breakdown of the data structures returned by the API.
Athlete object
| Field | Type | Description |
|---|---|---|
| id | string | Unique identifier for the athlete |
| name | string | Full name of the athlete |
| team_id | string | ID of the team the athlete belongs to |
| player_info | object | Personal information (birth_date, gender, height_cm, weight_kg) |
| recordings | array | Array of recording objects (jump, isometric, pogo tests) |
Recording object
| Field | Type | Description |
|---|---|---|
| id | string | Unique identifier for the recording |
| date | number | Unix timestamp (milliseconds) of when the test was performed |
| device_type | string | Type of device: PLATE_DEVICE, GROIN_DEVICE, CABLE_DEVICE |
| sample_rate | number | Sample rate in Hz (typically 960) |
| jump_analysis | array | Array of jump results (only for jump tests) |
| isometric_analysis | object | Isometric test results (only for isometric tests) |
| pogo_analysis | object | Pogo jump results (only for pogo tests) |
| cop_analysis | object | Center of Pressure (CoP) test results (only for CoP tests) |
Jump metrics (metric_table)
jump_height_ft— Jump height from flight time (meters)jump_height_ni— Jump height from impulse (meters)peak_force— Maximum force during jump (Newtons)peak_power— Maximum power output (Watts)takeoff_velocity— Velocity at takeoff (m/s)contact_time— Time on ground during jump (seconds)flight_time— Time in air (seconds)rsi— Reactive Strength Indexrsi_modified— Modified RSI (jump height / contact time)
The full jump metric_table contains 300+ metrics. See Metric Definitions for complete, versioned catalogues of every metric per test type.
Device types
| Device | Type Value | Supported Tests |
|---|---|---|
| PlateMate | PLATE_DEVICE | Jump (CMJ, SJ, DJ), Pogo, Isometric, CoP |
| GroinMate | GROIN_DEVICE | Isometric (Hip Adduction/Abduction) |
| CableMate | CABLE_DEVICE | Isometric (Cable exercises) |
09Metric Definitions
Every metric that can appear in analysis results is defined in a versioned, machine-readable JSON catalogue. These are the same definition files the ForceMate apps load at runtime, so they are always in sync with the released software. Use them to map the metric IDs in API responses to display names, units, definitions and test availability.
The catalogues are public and require no API key.
| Catalogue | Tests covered | File |
|---|---|---|
| Jump | CMJ, SJ, DJ, CMRJ — bilateral and unilateral | jump_metric_table_en.json |
| Isometric | IMTP and all other isometric tests (PlateMate, GroinMate, CableMate) | isometric_metric_table_en.json |
| Pogo | Pogo jumps | pogo_metric_table_en.json |
| Center of Pressure | Balance / CoP tests | cop_metric_table_en.json |
| Golf | Golf swing analysis | golf_metric_table_en.json |
File structure
{
"version": "1.0.0",
"last_updated": "2026-07-21",
"groups": {
"impulse": { "name": "Impulse", "explanation": "..." }
},
"metrics": {
"fp1_net_impulse": {
"name": "Net Impulse - Left",
"explanation": "The Left impulse (bodyweight subtracted) applied during the braking+propulsive phase",
"unit": "N·s",
"decimals": 0,
"group": "impulse",
"laterality": "left",
"valid_jump_types": ["cmj", "sj", "cmrj"]
}
}
}
| Field | Meaning |
|---|---|
| metric key | The metric ID. Matches the keys used in metric_table objects in API responses 1:1. |
| name | Display name as shown in the ForceMate apps. |
| explanation | Short definition of the metric. |
| unit | Measurement unit. An empty unit means the metric is dimensionless (ratios and indices such as rsi and rsi_modified). BW means multiples of bodyweight; body-mass-normalized metrics state it explicitly (N/kg, W/kg, N·s/kg). |
| decimals | Number of decimals used when the value is displayed. |
| group | Key into groups, used to organize related metrics. |
| laterality | Whether the value describes the left plate, the right plate, or the total. Left/right variants only exist for bilateral trials recorded on dual plates. |
| valid_jump_types | Jump catalogue only: the jump types the metric exists for (cmj, sj, dj, cmrj). |
Versioning
Each catalogue carries a top-level version (semantic version) and last_updated date. The version is bumped whenever anything in the file changes — metrics added, renamed or removed, or changed units, definitions or availability. Pin the version you validated your integration against, and re-check version and last_updated periodically to detect changes.
10Integration Examples
Ready-to-use code examples for integrating with the CC Athletics API.
Python
import requests
import json
from datetime import datetime
# Configuration
API_KEY = 'fm_your_api_key_here'
BASE_URL = 'https://europe-west1-forcemate-desktop.cloudfunctions.net'
# Headers for authentication
headers = {
'X-API-Key': API_KEY
}
# Get all teams
def get_teams():
response = requests.get(f'{BASE_URL}/get_teams', headers=headers)
if response.status_code == 200:
return response.json()
else:
print(f'Error: {response.status_code}')
return None
# Get athletes with filters
def get_athletes(team_id=None, analysis_type=None):
params = {}
if team_id:
params['team_id'] = team_id
if analysis_type:
params['analysis_type'] = analysis_type
response = requests.get(f'{BASE_URL}/get_athletes', headers=headers, params=params)
if response.status_code == 200:
return response.json()
else:
print(f'Error: {response.status_code}')
return None
# Process jump data
def process_jump_data(athletes_data):
for athlete in athletes_data['athletes']:
print(f"Athlete: {athlete['name']}")
for recording in athlete.get('recordings', []):
if 'jump_analysis' in recording:
date = datetime.fromtimestamp(recording['date'] / 1000)
print(f" Test Date: {date.strftime('%Y-%m-%d')}")
for jump in recording['jump_analysis']:
if jump.get('selected_by_user'):
metrics = jump['metric_table']
print(f" Jump Height: {metrics.get('jump_height_ft', 0):.3f} m")
print(f" Peak Force: {metrics.get('peak_force', 0):.1f} N")
print(f" Peak Power: {metrics.get('peak_power', 0):.1f} W")
# Main execution
if __name__ == '__main__':
teams = get_teams()
if teams:
print(f"Found {len(teams['teams'])} teams")
athletes = get_athletes(analysis_type='jump')
if athletes:
print(f"Found {athletes['total']} athletes")
process_jump_data(athletes)
JavaScript / Node.js
const axios = require('axios');
const API_KEY = 'fm_your_api_key_here';
const BASE_URL = 'https://europe-west1-forcemate-desktop.cloudfunctions.net';
const api = axios.create({
baseURL: BASE_URL,
headers: { 'X-API-Key': API_KEY }
});
async function findBestJumper(teamId) {
try {
const response = await api.get('/get_athletes', {
params: { team_id: teamId, analysis_type: 'jump' }
});
let bestJump = { height: 0, athlete: null };
response.data.athletes.forEach(athlete => {
athlete.recordings?.forEach(recording => {
recording.jump_analysis?.forEach(jump => {
if (jump.selected_by_user && jump.metric_table.jump_height_ft > bestJump.height) {
bestJump = {
height: jump.metric_table.jump_height_ft,
athlete: athlete.name,
force: jump.metric_table.peak_force,
power: jump.metric_table.peak_power
};
}
});
});
});
console.log('Best Jump:', bestJump);
return bestJump;
} catch (error) {
console.error('Error:', error.response?.status || error.message);
}
}
async function downloadCSV(path, downsampleFactor = null) {
try {
const params = { path };
if (downsampleFactor) params.downsample_factor = downsampleFactor;
const response = await api.get('/get_csv_download_url', { params });
console.log('Download URL:', response.data.download_url);
return response.data.download_url;
} catch (error) {
console.error('Error getting CSV:', error.response?.status || error.message);
}
}
findBestJumper('team_123');
11Best Practices
Example: secure API key storage
# Store API key in environment variable export CC_API_KEY="fm_your_api_key_here" # Use in your application curl https://europe-west1-forcemate-desktop.cloudfunctions.net/get_teams \ -H "X-API-Key: $CC_API_KEY"
12Support
Need help with the API? We're here to assist you.
Contact
Common issues
API key not working
- Ensure the key is correctly copied (no extra spaces)
- Check that the key hasn't expired
- Verify the key is for the correct environment
- Make sure you're using the
X-API-Keyheader
No data returned
- Check your filter parameters are correct
- Ensure date formats are
YYYY-MM-DD - Verify the
team_idexists in your organisation - Check that data exists for the specified time period
CSV download issues
- Ensure the
pathparameter is URL-encoded - Download the file promptly (URLs expire)
- Check that the path exists in your athlete data
- Verify
downsample_factoris a positive integer