Every activity, route, segment, challenge and health metric your users have — behind one athlete-authorised token. 28 endpoints, six webhook events, five official SDKs, and rate limits that let you build something real.
https://api.stridex.app/v1 $ curl "https://api.stridex.app/v1/athlete/activities?per_page=1" \ -H "Authorization: Bearer $STRIDEX_ACCESS_TOKEN" [ { "id": "act1", "sport": "run", "title": "Hulhumalé sunrise long run 🌅", "distance_km": 32.4, "moving_time": 8712, "average_hr": 152, "average_power": 288, "start_date": "2026-07-14T00:42:00Z", "prs": ["Fastest Half Marathon", "Longest Run"], "device": "Garmin Forerunner 965" } ]
Send the athlete to the authorize screen, then exchange the code you get back for an access token. Tokens are athlete-scoped — you never handle a password.
# 1. Send the athlete here (in a browser, not curl) open "https://api.stridex.app/v1/oauth/authorize\ ?client_id=YOUR_CLIENT_ID\ &redirect_uri=https://yourapp.com/callback\ &response_type=code\ &scope=read,activity:read_all\ &state=x7fa29c" # 2. Exchange the ?code= from the redirect for tokens curl -X POST https://api.stridex.app/v1/oauth/token \ -H "Content-Type: application/json" \ -d '{ "client_id": "YOUR_CLIENT_ID", "client_secret": "YOUR_CLIENT_SECRET", "code": "AUTH_CODE_FROM_REDIRECT", "grant_type": "authorization_code" }'
// 1. Send the athlete to the authorize screen const url = new URL('https://api.stridex.app/v1/oauth/authorize'); url.search = new URLSearchParams({ client_id: process.env.STRIDEX_CLIENT_ID, redirect_uri: 'https://yourapp.com/callback', response_type: 'code', scope: 'read,activity:read_all', state: crypto.randomUUID(), // verify this on the way back }); window.location.href = url.toString(); // 2. On your callback route, exchange the code for tokens const res = await fetch('https://api.stridex.app/v1/oauth/token', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ client_id: process.env.STRIDEX_CLIENT_ID, client_secret: process.env.STRIDEX_CLIENT_SECRET, code, grant_type: 'authorization_code', }), }); const { access_token, refresh_token, expires_at } = await res.json();
import os, secrets, requests # 1. Send the athlete to the authorize screen authorize_url = ( "https://api.stridex.app/v1/oauth/authorize" f"?client_id={os.environ['STRIDEX_CLIENT_ID']}" "&redirect_uri=https://yourapp.com/callback" "&response_type=code" "&scope=read,activity:read_all" f"&state={secrets.token_hex(4)}" ) # 2. Exchange the ?code= from the redirect for tokens res = requests.post( "https://api.stridex.app/v1/oauth/token", json={ "client_id": os.environ["STRIDEX_CLIENT_ID"], "client_secret": os.environ["STRIDEX_CLIENT_SECRET"], "code": code, "grant_type": "authorization_code", }, timeout=10, ) res.raise_for_status() tokens = res.json()
import Foundation import AuthenticationServices // 1. Send the athlete to the authorize screen var comps = URLComponents(string: "https://api.stridex.app/v1/oauth/authorize")! comps.queryItems = [ URLQueryItem(name: "client_id", value: clientID), URLQueryItem(name: "redirect_uri", value: "stridexdemo://callback"), URLQueryItem(name: "response_type", value: "code"), URLQueryItem(name: "scope", value: "read,activity:read_all"), ] let session = ASWebAuthenticationSession( url: comps.url!, callbackURLScheme: "stridexdemo" ) { callbackURL, error in guard let code = callbackURL?.queryValue("code") else { return } Task { try await exchange(code: code) } } session.start() // 2. Exchange the code for tokens struct Tokens: Decodable { let accessToken: String let refreshToken: String let expiresAt: Int } func exchange(code: String) async throws { var req = URLRequest(url: URL(string: "https://api.stridex.app/v1/oauth/token")!) req.httpMethod = "POST" req.setValue("application/json", forHTTPHeaderField: "Content-Type") req.httpBody = try JSONEncoder().encode([ "client_id": clientID, "client_secret": clientSecret, "code": code, "grant_type": "authorization_code", ]) let (data, _) = try await URLSession.shared.data(for: req) let decoder = JSONDecoder() decoder.keyDecodingStrategy = .convertFromSnakeCase Keychain.store(try decoder.decode(Tokens.self, from: data)) }
import android.content.Intent import android.net.Uri import okhttp3.* import okhttp3.MediaType.Companion.toMediaType import okhttp3.RequestBody.Companion.toRequestBody import kotlinx.serialization.json.* // 1. Send the athlete to the authorize screen val authorize = Uri.parse("https://api.stridex.app/v1/oauth/authorize") .buildUpon() .appendQueryParameter("client_id", BuildConfig.STRIDEX_CLIENT_ID) .appendQueryParameter("redirect_uri", "stridexdemo://callback") .appendQueryParameter("response_type", "code") .appendQueryParameter("scope", "read,activity:read_all") .build() startActivity(Intent(Intent.ACTION_VIEW, authorize)) // 2. Exchange the code for tokens (on your callback activity) val payload = buildJsonObject { put("client_id", BuildConfig.STRIDEX_CLIENT_ID) put("client_secret", BuildConfig.STRIDEX_CLIENT_SECRET) put("code", code) put("grant_type", "authorization_code") }.toString().toRequestBody("application/json".toMediaType()) val request = Request.Builder() .url("https://api.stridex.app/v1/oauth/token") .post(payload) .build() val tokens = client.newCall(request).execute().use { res -> Json.decodeFromString<Tokens>(res.body!!.string()) }
One authenticated GET. Results are newest-first and paginated at 30 per page (200 max). Always read the rate-limit headers — they are on every response.
curl "https://api.stridex.app/v1/athlete/activities?per_page=10&page=1" \ -H "Authorization: Bearer $STRIDEX_ACCESS_TOKEN" # Response headers worth reading: # X-RateLimit-Limit: 600 # X-RateLimit-Remaining: 594 # X-RateLimit-Reset: 1784102400
const res = await fetch( 'https://api.stridex.app/v1/athlete/activities?per_page=10&page=1', { headers: { Authorization: `Bearer ${accessToken}` } }, ); if (res.status === 429) { const retry = Number(res.headers.get('Retry-After') ?? 60); throw new RateLimited(retry); // back off, do not hammer } if (!res.ok) throw new StrideXError(await res.json()); const activities = await res.json(); console.log(res.headers.get('X-RateLimit-Remaining')); // 594
res = requests.get(
"https://api.stridex.app/v1/athlete/activities",
headers={"Authorization": f"Bearer {access_token}"},
params={"per_page": 10, "page": 1},
timeout=10,
)
if res.status_code == 429:
raise RateLimited(int(res.headers.get("Retry-After", 60)))
res.raise_for_status()
activities = res.json()
print(res.headers["X-RateLimit-Remaining"]) # 594var req = URLRequest(url: URL(string: "https://api.stridex.app/v1/athlete/activities?per_page=10&page=1")!) req.setValue("Bearer \(accessToken)", forHTTPHeaderField: "Authorization") let (data, response) = try await URLSession.shared.data(for: req) guard let http = response as? HTTPURLResponse else { throw StrideXError.transport } if http.statusCode == 429 { let retry = Int(http.value(forHTTPHeaderField: "Retry-After") ?? "60")! throw StrideXError.rateLimited(retryAfter: retry) } let activities = try decoder.decode([Activity].self, from: data)
val request = Request.Builder() .url("https://api.stridex.app/v1/athlete/activities?per_page=10&page=1") .header("Authorization", "Bearer $accessToken") .build() val activities: List<Activity> = client.newCall(request).execute().use { res -> if (res.code == 429) { throw RateLimited(res.header("Retry-After")?.toInt() ?: 60) } if (!res.isSuccessful) throw StrideXError(res.code, res.body?.string()) json.decodeFromString(res.body!!.string()) }
Distances are kilometres, times are seconds, paces are yours to calculate. Every activity carries the same shape whatever the sport — GPS fields are simply null for indoor ones.
curl -s "https://api.stridex.app/v1/athlete/activities?per_page=3" \ -H "Authorization: Bearer $STRIDEX_ACCESS_TOKEN" \ | jq -r '.[] | "\(.sport) · \(.distance_km) km · \(.average_hr) bpm"' # run · 32.4 km · 152 bpm # swim · 3.8 km · 141 bpm # ride · 28.2 km · 118 bpm
for (const act of activities) { const secPerKm = act.moving_time / act.distance_km; const pace = `${Math.floor(secPerKm / 60)}:${String(Math.round(secPerKm % 60)).padStart(2, '0')}`; console.log( `${act.sport} · ${act.distance_km.toFixed(2)} km · ${pace}/km · ${act.average_hr} bpm`, ); } // run · 32.40 km · 4:29/km · 152 bpm // swim · 3.80 km · 17:22/km · 141 bpm
for act in activities: sec_per_km = act["moving_time"] / act["distance_km"] print( f"{act['sport']} · {act['distance_km']:.2f} km " f"· {int(sec_per_km // 60)}:{round(sec_per_km % 60):02d}/km " f"· {act['average_hr']} bpm" ) # run · 32.40 km · 4:29/km · 152 bpm # swim · 3.80 km · 17:22/km · 141 bpm
for act in activities { let secPerKm = Double(act.movingTime) / act.distanceKm let pace = String(format: "%d:%02d", Int(secPerKm) / 60, Int(secPerKm) % 60) print("\(act.sport) · \(String(format: "%.2f", act.distanceKm)) km · \(pace)/km · \(act.averageHr) bpm") } // run · 32.40 km · 4:29/km · 152 bpm // swim · 3.80 km · 17:22/km · 141 bpm
activities.forEach { act -> val secPerKm = act.movingTime / act.distanceKm val pace = "%d:%02d".format((secPerKm / 60).toInt(), (secPerKm % 60).roundToInt()) println("${act.sport} · %.2f km · $pace/km · ${act.averageHr} bpm".format(act.distanceKm)) } // run · 32.40 km · 4:29/km · 152 bpm // swim · 3.80 km · 17:22/km · 141 bpm
Every call is made on behalf of an athlete who explicitly granted your app a set of scopes. There are no API-key-only endpoints for athlete data, no password grant, and no way to widen a scope after the fact — you re-prompt, or you go without.
| Scope | Grants | Default |
|---|---|---|
read | Public profile, public activities, clubs and challenges the athlete has joined. | Implicit |
activity:read_all | Every activity including followers-only and private ones, with full streams. | Opt-in |
activity:write | Create, update and delete activities on the athlete’s behalf. Uploads count against their history. | Opt-in |
profile:read | Email, weight, FTP, HR zones, sport preferences and device list. | Opt-in |
profile:write | Update profile fields, zones and training preferences. | Opt-in |
health:read | Sleep, HRV, resting HR, recovery and body-composition metrics. Sensitive — expect scrutiny at review. | Opt-in |
coach:manage | Read and write plans for athletes who have accepted you as their coach. Coach plan only. | Opt-in |
Athletes see this table almost verbatim on the consent screen. Apps that request health:read or activity:write without a visible reason get refused at roughly triple the rate — ask for them at the moment the feature needs them, not at sign-up.
POST /v1/oauth/token HTTP/1.1 Host: api.stridex.app Content-Type: application/json { "client_id": "84210", "client_secret": "8f2c4a106b7d4e219c33a41f9c2e88b0", "code": "1f5a8e2b7d40c8834b19e5f26a4c19e5", "grant_type": "authorization_code" }
{
"token_type": "Bearer",
"access_token": "sxa_9c33a41f9c2e88b07d643ac15e920f77",
"refresh_token": "sxr_b8d36a4c19e58f2c4a106b7d4e219c33",
"expires_at": 1784124000,
"expires_in": 21600,
"scope": "read,activity:read_all",
"athlete": {
"id": "me",
"handle": "@aiman.runs",
"name": "Aiman Jaleel",
"city": "Malé",
"country": "Maldives",
"premium": true
}
}curl -X POST https://api.stridex.app/v1/oauth/token \ -H "Content-Type: application/json" \ -d '{ "client_id": "84210", "client_secret": "YOUR_CLIENT_SECRET", "refresh_token": "sxr_b8d36a4c19e58f2c4a106b7d4e219c33", "grant_type": "refresh_token" }'
Refresh tokens do not expire, but they are single-use: every refresh returns a new one and invalidates the old. Store the new one before you use the access token, or a crash mid-flight logs the athlete out.
/athlete The authenticated athlete. curl https://api.stridex.app/v1/athlete -H "Authorization: Bearer $TOKEN"{
"id": "me",
"name": "Aiman Jaleel",
"handle": "@aiman.runs",
"initials": "AJ",
"city": "Malé",
"country": "Maldives",
"flag": "🇲🇻",
"verified": true,
"badge": null,
"followers": 1284,
"following": 342,
"ytd_km": 1043.6,
"level": 27,
"xp": 68420,
"streak_days": 34,
"bio": "Chasing a sub-3 marathon from a country with no hills.",
"sports": ["run", "ride", "swim"],
"premium": true,
"created_at": "2024-01-12T04:18:00Z"
}/athletes/{id} Any athlete by id. Respects their privacy settings. | Name | Type | Description |
|---|---|---|
id | string | Athlete id, e.g. a1. Required. |
include_stats | boolean | Attach the YTD/all-time stats block. Default false. |
curl https://api.stridex.app/v1/athletes/a1?include_stats=true -H "Authorization: Bearer $TOKEN"{
"id": "a1",
"name": "Sara Nasheed",
"handle": "@sara.tri",
"city": "Hulhumalé",
"country": "Maldives",
"verified": true,
"badge": "pro",
"followers": 24800,
"ytd_km": 2310.4,
"sports": ["swim", "ride", "run"],
"stats": {
"ytd_activity_count": 412,
"ytd_moving_time": 1284600,
"all_time_km": 9840.2
}
}/athlete Update the authenticated athlete. Needs profile:write. | Name | Type | Description |
|---|---|---|
bio | string | Max 240 characters. |
city | string | Free text. |
weight_kg | number | Used for calorie and power modelling. |
ftp | number | Functional threshold power in watts. |
curl -X PUT https://api.stridex.app/v1/athlete -H "Authorization: Bearer $TOKEN" \ -H "Content-Type: application/json" -d '{"weight_kg": 68.4, "ftp": 268}'
{
"id": "me",
"weight_kg": 68.4,
"ftp": 268,
"updated_at": "2026-07-14T13:42:11Z"
}/athlete/stats Rolled-up totals by sport and period. | Name | Type | Description |
|---|---|---|
period | string | week | month | ytd | all. Default ytd. |
curl "https://api.stridex.app/v1/athlete/stats?period=ytd" -H "Authorization: Bearer $TOKEN"
{
"period": "ytd",
"totals": {
"run": { "count": 186, "distance_km": 1043.6, "moving_time": 402840, "elevation_gain": 2140 },
"ride": { "count": 42, "distance_km": 1284.2, "moving_time": 168600, "elevation_gain": 640 },
"swim": { "count": 61, "distance_km": 148.4, "moving_time": 186420, "elevation_gain": 0 }
},
"biggest_run_km": 32.4,
"longest_streak_days": 96
}/athlete/activities The athlete’s activities, newest first. | Name | Type | Description |
|---|---|---|
page | integer | Page number. Default 1. |
per_page | integer | Default 30, max 200. |
before | integer | Unix seconds — activities started before this. |
after | integer | Unix seconds — activities started after this. |
sport | string | Filter to one sport key, e.g. run. |
curl "https://api.stridex.app/v1/athlete/activities?per_page=2&sport=run" -H "Authorization: Bearer $TOKEN"
[
{
"id": "act1",
"athlete_id": "me",
"sport": "run",
"title": "Hulhumalé sunrise long run 🌅",
"description": "Legs felt springy the whole way. Held marathon pace to 24K.",
"distance_km": 32.4,
"moving_time": 8712,
"elapsed_time": 8964,
"elevation_gain": 86,
"calories": 2140,
"average_hr": 152,
"max_hr": 178,
"average_cadence": 176,
"average_power": 288,
"stride_length": 1.21,
"temperature": 29,
"weather": "Clear",
"wind": "9 km/h NE",
"humidity": 78,
"start_date": "2026-07-14T00:42:00Z",
"start_latlng": [4.2105, 73.5405],
"is_loop": false,
"kudos_count": 148,
"comment_count": 22,
"photo_count": 3,
"prs": ["Fastest Half Marathon", "Longest Run"],
"achievements": ["Marathon-ready", "30K Club"],
"perceived_exertion": 8,
"device": "Garmin Forerunner 965",
"visibility": "public",
"was_live": true,
"map": {
"summary_polyline": "u{~vFvyys@fS]|@rDvBnG`@lAtCbG..."
}
},
{
"id": "act12",
"athlete_id": "me",
"sport": "run",
"title": "Track night — 12×400m",
"distance_km": 11.2,
"moving_time": 3180,
"elevation_gain": 4,
"average_hr": 162,
"max_hr": 184,
"average_cadence": 186,
"average_power": 322,
"start_date": "2026-07-10T14:02:00Z",
"kudos_count": 58,
"prs": ["Fastest 1K"],
"perceived_exertion": 9,
"visibility": "public"
}
]/activities/{id} One activity in full, including splits. | Name | Type | Description |
|---|---|---|
id | string | Activity id. Required. |
include_splits | boolean | Attach per-km splits. Default true. |
curl https://api.stridex.app/v1/activities/act1 -H "Authorization: Bearer $TOKEN"{
"id": "act1",
"athlete_id": "me",
"sport": "run",
"title": "Hulhumalé sunrise long run 🌅",
"distance_km": 32.4,
"moving_time": 8712,
"average_hr": 152,
"splits": [
{ "km": 1, "moving_time": 268, "average_hr": 138, "elevation_gain": 2 },
{ "km": 2, "moving_time": 271, "average_hr": 144, "elevation_gain": 1 },
{ "km": 3, "moving_time": 269, "average_hr": 148, "elevation_gain": 4 }
],
"device": "Garmin Forerunner 965",
"visibility": "public"
}/activities Create an activity. Needs activity:write. | Name | Type | Description |
|---|---|---|
sport | string | One of the 24 sport keys. Required. |
title | string | Required. |
start_date | string | ISO 8601 UTC. Required. |
distance_km | number | Required for GPS sports. |
moving_time | integer | Seconds. Required. |
file | file | Optional GPX/TCX/FIT upload — everything else is then derived. |
curl -X POST https://api.stridex.app/v1/activities -H "Authorization: Bearer $TOKEN" \ -H "Content-Type: application/json" \ -d '{"sport":"run","title":"Easy 8K","start_date":"2026-07-14T00:40:00Z","distance_km":8.0,"moving_time":2880}'
{
"id": "act_9c33a41f",
"athlete_id": "me",
"sport": "run",
"title": "Easy 8K",
"distance_km": 8.0,
"moving_time": 2880,
"start_date": "2026-07-14T00:40:00Z",
"visibility": "public",
"created_at": "2026-07-14T02:11:04Z"
}/activities/{id} Update title, description, sport or visibility. | Name | Type | Description |
|---|---|---|
title | string | |
description | string | |
sport | string | Re-derives metrics if the sport group changes. |
visibility | string | public | followers | private |
curl -X PUT https://api.stridex.app/v1/activities/act1 -H "Authorization: Bearer $TOKEN" \ -H "Content-Type: application/json" -d '{"visibility": "followers"}'
{
"id": "act1",
"visibility": "followers",
"updated_at": "2026-07-14T13:44:52Z"
}/activities/{id} Delete an activity. Irreversible, and it takes its PRs with it. curl -X DELETE https://api.stridex.app/v1/activities/act_9c33a41f -H "Authorization: Bearer $TOKEN"{
"id": "act_9c33a41f",
"deleted": true,
"prs_revoked": 0
}/activities/{id}/streams Raw time-series. The good stuff. | Name | Type | Description |
|---|---|---|
keys | string | Comma-separated: time, latlng, distance, altitude, heartrate, cadence, watts, temp, moving, grade. |
resolution | string | low (100) | medium (1000) | high (all). Default medium. |
curl "https://api.stridex.app/v1/activities/act1/streams?keys=heartrate,latlng,altitude&resolution=low" \ -H "Authorization: Bearer $TOKEN"
{
"activity_id": "act1",
"resolution": "low",
"point_count": 100,
"streams": {
"time": { "unit": "s", "data": [0, 87, 174, 261, 348] },
"heartrate": { "unit": "bpm", "data": [122, 138, 144, 149, 152] },
"altitude": { "unit": "m", "data": [2.1, 2.4, 3.0, 2.8, 2.2] },
"latlng": { "unit": null, "data": [[4.2105, 73.5405], [4.2118, 73.5412]] }
}
}/routes Search routes by area, sport and difficulty. | Name | Type | Description |
|---|---|---|
bounds | string | sw_lat,sw_lng,ne_lat,ne_lng |
sport | string | Sport key. |
min_km | number | |
max_km | number | |
difficulty | string | Easy | Moderate | Hard | Expert |
sort | string | popularity | rating | distance. Default popularity. |
curl "https://api.stridex.app/v1/routes?bounds=4.15,73.45,4.25,73.60&sport=run&sort=popularity" \ -H "Authorization: Bearer $TOKEN"
[
{
"id": "r1",
"name": "Hulhumalé Beach Loop",
"sport": "run",
"distance_km": 8.4,
"elevation_gain": 6,
"city": "Hulhumalé",
"country": "Maldives",
"surface": "Paved + sand",
"difficulty": "Easy",
"rating": 4.8,
"use_count": 18420,
"estimated_time": 2760,
"popularity": 96,
"is_loop": true,
"tags": ["Beach", "Flat", "Sunrise", "Water stops"],
"offline_available": true
}
]/routes/{id} One route with its full polyline and elevation profile. curl https://api.stridex.app/v1/routes/r1 -H "Authorization: Bearer $TOKEN"{
"id": "r1",
"name": "Hulhumalé Beach Loop",
"distance_km": 8.4,
"description": "Flat, fast and forgiving — the island's default long-run loop.",
"centre": [4.2105, 73.5405],
"polyline": "u{~vFvyys@fS]|@rDvBnG`@lAtCbG...",
"elevation_profile": [2.1, 2.4, 3.0, 2.8, 2.2, 1.9],
"waypoints": [
{ "km": 0.0, "type": "start", "name": "Central Park gate" },
{ "km": 4.2, "type": "water", "name": "Phase 2 fountain" }
]
}/routes/{id}/gpx Download as GPX. TCX and FIT also available. | Name | Type | Description |
|---|---|---|
format | string | gpx | tcx | fit. Default gpx. |
curl "https://api.stridex.app/v1/routes/r1/gpx" -H "Authorization: Bearer $TOKEN" -o beach-loop.gpx
HTTP/1.1 200 OK Content-Type: application/gpx+xml Content-Disposition: attachment; filename="hulhumale-beach-loop.gpx" (GPX 1.1 document, 842 track points)
/segments/{id} Segment detail with the current KOM/QOM. curl https://api.stridex.app/v1/segments/s1 -H "Authorization: Bearer $TOKEN"{
"id": "s1",
"name": "Beach Road Sprint",
"sport": "run",
"distance_km": 1.0,
"average_grade": 0.2,
"city": "Hulhumalé",
"kom": { "athlete_id": "a1", "name": "Sara Nasheed", "time": 178 },
"attempt_count": 8420,
"athlete_effort": { "best_time": 198, "rank": 42, "effort_count": 24 }
}/segments/{id}/leaderboard Segment leaderboard, filterable. | Name | Type | Description |
|---|---|---|
scope | string | global | country | club | friends. Default global. |
age_group | string | e.g. 25_34 |
date_range | string | this_year | this_month | today |
per_page | integer | Default 30. |
curl "https://api.stridex.app/v1/segments/s1/leaderboard?scope=country&per_page=3" -H "Authorization: Bearer $TOKEN"
{
"segment_id": "s1",
"scope": "country",
"entry_count": 1842,
"entries": [
{ "rank": 1, "athlete_id": "a1", "name": "Sara Nasheed", "time": 178, "date": "2026-06-02" },
{ "rank": 2, "athlete_id": "x3", "name": "Ibrahim Waheed", "time": 184, "date": "2026-05-18" },
{ "rank": 3, "athlete_id": "x7", "name": "Fatima Ali", "time": 191, "date": "2026-07-01" }
],
"athlete_entry": { "rank": 42, "time": 198, "date": "2026-07-10" }
}/segments/explore Find segments inside a bounding box. | Name | Type | Description |
|---|---|---|
bounds | string | Required. sw_lat,sw_lng,ne_lat,ne_lng |
sport | string | |
min_grade | number | |
max_grade | number |
curl "https://api.stridex.app/v1/segments/explore?bounds=4.15,73.45,4.25,73.60&sport=ride" \ -H "Authorization: Bearer $TOKEN"
{
"segments": [
{ "id": "s2", "name": "Bridge Climb", "distance_km": 2.4, "average_grade": 3.1, "sport": "ride" },
{ "id": "s5", "name": "Airport Causeway TT", "distance_km": 6.8, "average_grade": 0.1, "sport": "ride" }
]
}/challenges Challenges the athlete can see or has joined. | Name | Type | Description |
|---|---|---|
joined | boolean | Only ones they joined. |
type | string | Daily | Weekly | Monthly | Corporate | School | Virtual … |
curl "https://api.stridex.app/v1/challenges?joined=true" -H "Authorization: Bearer $TOKEN"
[
{
"id": "c1",
"name": "July 200K Run",
"sport": "run",
"type": "Monthly",
"goal": "Run 200 km in July",
"target": 200,
"unit": "km",
"progress": 148.2,
"joined": true,
"participant_count": 284100,
"ends_at": "2026-07-31T23:59:59Z",
"days_left": 17,
"sponsor": null
}
]/challenges/{id}/join Join a challenge as the authenticated athlete. | Name | Type | Description |
|---|---|---|
team_id | string | For team challenges. Optional. |
curl -X POST https://api.stridex.app/v1/challenges/c7/join -H "Authorization: Bearer $TOKEN"{
"challenge_id": "c7",
"joined": true,
"progress": 0,
"rank": 62401,
"joined_at": "2026-07-14T13:48:20Z"
}/clubs/{id} Club profile and aggregate stats. curl https://api.stridex.app/v1/clubs/cl1 -H "Authorization: Bearer $TOKEN"{
"id": "cl1",
"name": "Malé Runners",
"sport": "run",
"city": "Malé",
"country": "Maldives",
"member_count": 2840,
"verified": true,
"private": false,
"weekly_km": 8420,
"event_count": 4,
"athlete_role": "moderator"
}/clubs/{id}/members Paginated member list. Moderators see pending requests. | Name | Type | Description |
|---|---|---|
page | integer | |
per_page | integer | Default 30, max 200. |
status | string | active | pending. Moderators only. |
curl "https://api.stridex.app/v1/clubs/cl1/members?per_page=2" -H "Authorization: Bearer $TOKEN"
{
"member_count": 2840,
"page": 1,
"members": [
{ "id": "me", "name": "Aiman Jaleel", "role": "moderator", "joined_at": "2021-03-04" },
{ "id": "a1", "name": "Sara Nasheed", "role": "member", "joined_at": "2019-01-22" }
]
}/events Races and events, filterable by area and date. | Name | Type | Description |
|---|---|---|
country | string | |
sport | string | |
from | string | ISO date. |
to | string | ISO date. |
status | string | Open | Closing soon | Sold out |
curl "https://api.stridex.app/v1/events?country=MV&sport=run" -H "Authorization: Bearer $TOKEN"
[
{
"id": "e1",
"name": "Malé Marathon 2026",
"sport": "run",
"date": "2026-08-08",
"city": "Malé",
"country": "Maldives",
"distances": ["42.2K", "21.1K", "10K", "5K Fun Run"],
"fee": "MVR 350",
"spots": 5000,
"taken": 4218,
"status": "Closing soon",
"registered": true,
"bib": "MM-2416",
"chip_timed": true,
"certificate": true
}
]/events/{id}/register Register the athlete for an event. | Name | Type | Description |
|---|---|---|
distance | string | Must be one of the event distances. Required. |
estimated_finish | integer | Seconds — used for corral seeding. |
emergency_contact | string | Required by most organisers. |
curl -X POST https://api.stridex.app/v1/events/e3/register -H "Authorization: Bearer $TOKEN" \ -H "Content-Type: application/json" -d '{"distance": "120K", "estimated_finish": 14400}'
{
"event_id": "e3",
"registered": true,
"bib": "AGF-0413",
"distance": "120K",
"wave": "A",
"payment": { "status": "pending", "amount": "MVR 500", "expires_at": "2026-07-15T13:50:00Z" }
}/athlete/health Health metrics. Needs health:read — the most tightly reviewed scope we have. | Name | Type | Description |
|---|---|---|
metrics | string | Comma-separated: sleep, hrv, rhr, recovery, weight, body_fat, stress. |
from | string | ISO date. |
to | string | ISO date. |
curl "https://api.stridex.app/v1/athlete/health?metrics=hrv,sleep,recovery&from=2026-07-12" \ -H "Authorization: Bearer $TOKEN"
{
"athlete_id": "me",
"metrics": {
"hrv": { "unit": "ms", "baseline": 54, "series": [{ "date": "2026-07-13", "value": 48 }] },
"sleep": { "unit": "s", "baseline": 27120, "series": [{ "date": "2026-07-13", "value": 22320, "deep": 3480, "rem": 6120, "wake_events": 3 }] },
"recovery": { "unit": "score", "series": [{ "date": "2026-07-14", "value": 68 }] }
},
"note": "HRV 11% below baseline. Recovery is the limiter today."
}/athlete/health Push metrics from your own device or scale. | Name | Type | Description |
|---|---|---|
date | string | ISO date. Required. |
weight_kg | number | |
body_fat_pct | number | |
rhr | integer | |
hrv | integer | ms |
curl -X POST https://api.stridex.app/v1/athlete/health -H "Authorization: Bearer $TOKEN" \ -H "Content-Type: application/json" -d '{"date":"2026-07-14","weight_kg":68.4,"rhr":52}'
{
"date": "2026-07-14",
"accepted": ["weight_kg", "rhr"],
"rejected": [],
"source": "api:84210"
}/leaderboards Ranked athletes by scope and metric. | Name | Type | Description |
|---|---|---|
scope | string | global | country | city | friends | club. Default global. |
club_id | string | Required when scope=club. |
metric | string | distance | elevation | time | activities. Default distance. |
period | string | week | month | year. Default week. |
sport | string |
curl "https://api.stridex.app/v1/leaderboards?scope=club&club_id=cl1&metric=distance&period=week" \ -H "Authorization: Bearer $TOKEN"
{
"scope": "club",
"club_id": "cl1",
"metric": "distance",
"period": "week",
"unit": "km",
"entries": [
{ "rank": 1, "athlete_id": "a1", "name": "Sara Nasheed", "value": 96.2, "delta": 2 },
{ "rank": 2, "athlete_id": "me", "name": "Aiman Jaleel", "value": 82.4, "delta": -1 },
{ "rank": 3, "athlete_id": "x1", "name": "Ibrahim Waheed", "value": 74.8, "delta": 3 }
],
"athlete_rank": 2
}/webhooks Create a subscription. We verify it with a GET challenge first. | Name | Type | Description |
|---|---|---|
callback_url | string | HTTPS only. Required. |
events | array | Event types to receive. Required. |
verify_token | string | Echoed back in the verification GET. |
curl -X POST https://api.stridex.app/v1/webhooks -H "Authorization: Bearer $TOKEN" \ -H "Content-Type: application/json" \ -d '{"callback_url":"https://yourapp.com/hooks/stridex","events":["activity.created"],"verify_token":"x7fa29c"}'
{
"id": "wh_4a106b7d",
"callback_url": "https://yourapp.com/hooks/stridex",
"events": ["activity.created"],
"status": "verifying",
"created_at": "2026-07-14T13:52:00Z"
}/webhooks List your subscriptions and their delivery health. curl https://api.stridex.app/v1/webhooks -H "Authorization: Bearer $TOKEN"[
{
"id": "wh_4a106b7d",
"callback_url": "https://yourapp.com/hooks/stridex",
"events": ["activity.created"],
"status": "active",
"last_delivery": { "at": "2026-07-14T13:50:12Z", "status_code": 200, "latency_ms": 84 },
"success_rate_30d": 0.9998
}
]/webhooks/{id} Unsubscribe. Queued events are dropped immediately. curl -X DELETE https://api.stridex.app/v1/webhooks/wh_4a106b7d -H "Authorization: Bearer $TOKEN"{
"id": "wh_4a106b7d",
"deleted": true,
"queued_events_dropped": 0
}| Event | Fires when | |
|---|---|---|
activity.created | An activity was recorded, uploaded or synced from a wearable. | Most used |
activity.updated | Title, sport, description or visibility changed. | |
athlete.followed | Someone followed the athlete, or they followed someone. | |
challenge.completed | The athlete hit a challenge target. Fires once per challenge. | |
segment.pr | A new personal record on a segment. | |
event.finished | Chip time posted for a registered event. Includes splits. | Timing |
POST /hooks/stridex HTTP/1.1 Host: yourapp.com Content-Type: application/json X-StrideX-Signature: t=1784102400,v1=8f2c4a106b7d4e219c33a41f9c2e88b07d643ac15e920f77b8d36a4c19e5 X-StrideX-Delivery: dl_9c33a41f9c2e88b0 X-StrideX-Event: activity.created { "event": "activity.created", "created_at": 1784102400, "athlete_id": "me", "object_id": "act1", "object_type": "activity", "subscription_id": "wh_4a106b7d", "data": { "id": "act1", "sport": "run", "title": "Hulhumalé sunrise long run 🌅", "distance_km": 32.4, "moving_time": 8712, "start_date": "2026-07-14T00:42:00Z" } }
import crypto from 'node:crypto'; function verify(rawBody, header, secret) { const [tPart, v1Part] = header.split(','); const timestamp = tPart.split('=')[1]; const signature = v1Part.split('=')[1]; // Reject anything older than 5 minutes — stops replay attacks if (Math.abs(Date.now() / 1000 - Number(timestamp)) > 300) return false; const expected = crypto .createHmac('sha256', secret) .update(timestamp + '.' + rawBody) .digest('hex'); // Constant-time compare. Never use === here. return crypto.timingSafeEqual(Buffer.from(expected), Buffer.from(signature)); }
Every delivery carries X-StrideX-Signature in the form t=<unix>,v1=<hex>. The v1 value is an HMAC-SHA256 over timestamp + "." + rawBody, keyed with your endpoint's signing secret.
=== leaks the secret one byte at a time.Answer 2xx within 10 seconds or we retry. Queue the work, don't do it in the handler.
After 15 consecutive failures we disable the endpoint and email you. Redirects are not followed. Anything other than 2xx — including 3xx — counts as a failure.
Limits apply per athlete token, so an app with 10,000 Premium users has 10,000 separate 600/15-min budgets. The 15-minute window is a rolling one, and it resets on the quarter hour.
| Athlete plan | Short window | Daily | Burst | Concurrent |
|---|---|---|---|---|
Free | 100 / 15 min | 1,000 / day | 10 / s | 2 |
Premium | 600 / 15 min | 30,000 / day | 20 / s | 5 |
Coach | 1,200 / 15 min | 100,000 / day | 40 / s | 10 |
Organization | Custom | Contracted | Custom | Custom |
X-RateLimit-Limit: 600 X-RateLimit-Remaining: 594 X-RateLimit-Reset: 1784102400 X-RateLimit-Daily-Limit: 30000 X-RateLimit-Daily-Remaining: 28412 X-Request-Id: req_9c33a41f9c2e88b0
Read Remaining on every response and throttle yourself before we do it for you. Reset is a Unix timestamp, not a duration.
async function call(url, token, attempt = 0) { const res = await fetch(url, { headers: { Authorization: 'Bearer ' + token }, }); if (res.status !== 429) return res; if (attempt >= 5) throw new Error('Rate limited, gave up'); // Honour Retry-After. Add jitter so your whole fleet // does not wake up at the same millisecond. const wait = Number(res.headers.get('Retry-After') ?? 60) * 1000; const jitter = Math.random() * 1000; await new Promise((r) => setTimeout(r, wait + jitter)); return call(url, token, attempt + 1); }
{
"error": "invalid_request",
"message": "distance_km must be greater than 0",
"field": "distance_km",
"request_id": "req_9c33a41f9c2e88b0"
}{
"error": "invalid_token",
"message": "The access token expired at 2026-07-14T07:42:00Z",
"request_id": "req_1f5a8e2b7d40c883"
}{
"error": "insufficient_scope",
"message": "This endpoint requires the health:read scope",
"required_scope": "health:read",
"granted_scopes": ["read", "activity:read_all"],
"request_id": "req_6a4c19e5b0d372f1"
}{
"error": "not_found",
"message": "No activity with id act_deadbeef",
"request_id": "req_42b9c7e1a05d6f38"
}{
"error": "unprocessable_entity",
"message": "Activity rejected by the anomaly engine: average pace 2:14/km is not physically plausible",
"code": "implausible_effort",
"request_id": "req_e5b0d372f18c2e44"
}{
"error": "rate_limit_exceeded",
"message": "600 requests per 15 minutes exceeded",
"retry_after": 284,
"limit": 600,
"remaining": 0,
"reset": 1784102684,
"request_id": "req_84210c8834b19e5f"
}{
"error": "internal_error",
"message": "Something broke on our side. This is logged.",
"request_id": "req_b8d36a4c19e58f2c"
}{
"error": "service_unavailable",
"message": "Scheduled maintenance until 2026-07-20T03:00:00Z",
"retry_after": 1800,
"request_id": "req_0f77b8d36a4c19e5"
}Quote the request_id when you contact support — it pulls the exact request, response and timing out of our logs in seconds. Without it we are guessing alongside you.
Typed client, auto token refresh, webhook signature helper. Works in Node 18+, Deno, Bun and the browser.
npm install @stridex/sdk Sync and async clients, pandas DataFrame export for streams, built-in retry with jitter.
pip install stridex async/await, Codable models, ASWebAuthenticationSession OAuth flow, Keychain token storage.
.package(url: "https://github.com/stridex/stridex-swift") Coroutines, OkHttp, kotlinx.serialization. Ships with a Health Connect bridge.
implementation("app.stridex:sdk:2.6.0") Context-aware, zero dependencies, streaming decoder for large stream payloads.
go get github.com/stridex/stridex-go You can now request low/medium/high resolution on /activities/{id}/streams instead of always paying for every point. High resolution costs 3 rate-limit units per call.
Any app requesting health:read goes through a manual review before it can leave development mode. Existing approved apps are unaffected. Sleep and HRV data is not something we hand out on a form submission.
Chip times from integrated timing systems (Chronotrack, MyLaps, RFID) now fire a webhook with full splits within 90 seconds of the finish line read.
Tokens in query strings end up in logs, proxies and browser history. Use the Authorization header. The query parameter stops working on 1 October 2026 — 6% of traffic still uses it.
Premium goes 300 → 600 per 15 minutes, Coach 600 → 1,200. Free is unchanged. No action needed; the headers tell you the truth as always.
Free to develop against, free to launch, free until you are doing millions of calls a day — and then we will just want to talk to you first.