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.

Quick Start
  1. 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.
  2. Include the API key in the X-API-Key header for all requests.
  3. 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-Key header.
  • 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"
Security note: Never share your API key publicly or commit it to version control. Treat it as you would a password.

03API Endpoints

GET /get_teams

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...
View teams.json →
GET /get_athletes

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...
View athletes_basic.json →

Complete response with jump recording

Loading...
View athlete_with_jump.json →
GET /get_csv_download_url

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..."
}
Note: The download URL is temporary and will expire. Download the file promptly after receiving the URL.

04Error Handling

The API uses standard HTTP status codes to indicate success or failure of requests.

Common error codes

401 Unauthorized

Invalid or missing API key. Check that your API key is correct and included in the X-API-Key header.

429 Too Many Requests

Rate limit exceeded. Wait until the limit resets (see the Rate Limits section) before retrying.

500 Internal Server Error

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.

Fair use: The API is intended for periodic export and synchronisation of your organisation's data — for example a scheduled sync into your own database or analysis pipeline. Continuous high-frequency polling, or repeatedly re-downloading data you already hold, may be throttled or blocked.

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_from on /get_athletes.
  • Download the raw CSV of a recording once and keep it — recordings never change after the fact.
  • Use downsample_factor on /get_csv_download_url if 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 allowed
  • X-RateLimit-Remaining — The number of requests remaining
  • X-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.

Webhooks are best-effort — keep polling as your backstop. Delivery can fail for reasons outside our control: your endpoint is down, a network path breaks, or an endpoint is auto-disabled after repeated failures. Treat webhooks as a latency optimisation, not a system of record. A periodic /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.

The signing secret is shown once. It is issued at registration and is not retrievable afterwards — store it somewhere safe immediately. If you lose it, the endpoint has to be removed and registered again to get a new one.

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>")
Sign the raw body, exactly as received. Do not parse the JSON and re-serialise it before verifying — key order and whitespace will differ, and the signature will never match. Capture the raw bytes before your framework deserialises them.

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, or recording_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_at if sequence matters.
  • Raw data is ready on arrival. The CSV is uploaded before the event fires, so /get_csv_download_url works for the delivered recording_id as soon as you receive it.
  • HTTPS only. Deliveries are never sent over plain HTTP.
Recommended pattern: have your webhook handler verify the signature, enqueue the 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.

Athlete with jump recording (CMJ test)

Loading...
View athlete_with_jump.json →

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)

Common jump metrics
  • 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 Index
  • rsi_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

Change detection

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');
Pro tip: Use these examples as a starting point for your integration. Handle errors properly and implement appropriate caching strategies for production use.

11Best Practices

Keep your API key confidential. Never share it publicly or commit it to version control.
Use HTTPS for all API requests. All endpoints require secure connections.
Implement proper error handling. Always check response status codes and handle errors gracefully.
Cache responses when appropriate. Reduce unnecessary API calls by caching data that doesn't change frequently.
Use query parameters efficiently. Filter data at the API level to reduce payload size and processing time.
Monitor your usage. Keep track of your API usage to avoid hitting rate limits.

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-Key header
No data returned
  • Check your filter parameters are correct
  • Ensure date formats are YYYY-MM-DD
  • Verify the team_id exists in your organisation
  • Check that data exists for the specified time period
CSV download issues
  • Ensure the path parameter is URL-encoded
  • Download the file promptly (URLs expire)
  • Check that the path exists in your athlete data
  • Verify downsample_factor is a positive integer