openapi: 3.0.3
info:
title: Wahoo Cloud API
description: >-
The Wahoo Cloud API connects Wahoo Fitness users to mobile and web
applications. OAuth 2.0 (with PKCE option) authorizes access to user
profiles, workouts, workout summaries, FIT-file uploads, structured
workout plans, GPS routes, and cycling power zones. Webhooks deliver
workout_summary notifications when offline_data scope is granted.
version: v1
contact:
name: Wahoo Developer Support
email: [email protected]
url: https://developers.wahooligan.com
termsOfService: https://www.wahoofitness.com/wahoo-api-agreement
servers:
- url: https://api.wahooligan.com
description: Production
externalDocs:
description: Wahoo Cloud API Reference
url: https://cloud-api.wahooligan.com/
tags:
- name: Users
description: Authenticated user profile.
- name: Workouts
description: Workout records (CRUD + listing).
- name: Workout Summaries
description: Aggregate results for a completed workout.
- name: Workout File Uploads
description: Asynchronous FIT-file ingestion.
- name: Plans
description: Structured workout plans.
- name: Routes
description: Navigation / course data backed by FIT files.
- name: Power Zones
description: Cycling power training zones.
- name: Permissions
description: Revoke OAuth app access.
security:
- OAuth2: []
paths:
/v1/user:
get:
tags: [Users]
summary: Get Authenticated User
operationId: getUser
security:
- OAuth2: [user_read]
responses:
'200':
description: The authenticated user record.
content:
application/json:
schema:
$ref: '#/components/schemas/User'
put:
tags: [Users]
summary: Update Authenticated User
operationId: updateUser
security:
- OAuth2: [user_write]
requestBody:
required: true
content:
application/json:
schema:
$ref: '#/components/schemas/UserUpdate'
responses:
'200':
description: Updated user record.
content:
application/json:
schema:
$ref: '#/components/schemas/User'
/v1/workouts:
get:
tags: [Workouts]
summary: List Workouts
operationId: listWorkouts
security:
- OAuth2: [workouts_read]
parameters:
- $ref: '#/components/parameters/Page'
- $ref: '#/components/parameters/PerPage'
responses:
'200':
description: Paginated workout list.
content:
application/json:
schema:
$ref: '#/components/schemas/WorkoutList'
post:
tags: [Workouts]
summary: Create Workout
operationId: createWorkout
security:
- OAuth2: [workouts_write]
requestBody:
required: true
content:
application/json:
schema:
$ref: '#/components/schemas/WorkoutCreate'
responses:
'201':
description: Workout created.
content:
application/json:
schema:
$ref: '#/components/schemas/Workout'
/v1/workouts/{id}:
parameters:
- $ref: '#/components/parameters/Id'
get:
tags: [Workouts]
summary: Get Workout
operationId: getWorkout
security:
- OAuth2: [workouts_read]
responses:
'200':
description: Workout record.
content:
application/json:
schema:
$ref: '#/components/schemas/Workout'
put:
tags: [Workouts]
summary: Update Workout
operationId: updateWorkout
security:
- OAuth2: [workouts_write]
requestBody:
required: true
content:
application/json:
schema:
$ref: '#/components/schemas/WorkoutCreate'
responses:
'200':
description: Updated workout record.
content:
application/json:
schema:
$ref: '#/components/schemas/Workout'
delete:
tags: [Workouts]
summary: Delete Workout
operationId: deleteWorkout
security:
- OAuth2: [workouts_write]
responses:
'204':
description: Workout deleted.
/v1/workouts/{id}/workout_summary:
parameters:
- $ref: '#/components/parameters/Id'
get:
tags: [Workout Summaries]
summary: Get Workout Summary
operationId: getWorkoutSummary
security:
- OAuth2: [workouts_read]
responses:
'200':
description: Workout summary record.
content:
application/json:
schema:
$ref: '#/components/schemas/WorkoutSummary'
post:
tags: [Workout Summaries]
summary: Create Workout Summary (Deprecated)
operationId: createWorkoutSummary
deprecated: true
security:
- OAuth2: [workouts_write]
requestBody:
required: true
content:
application/json:
schema:
$ref: '#/components/schemas/WorkoutSummary'
responses:
'201':
description: Summary created.
content:
application/json:
schema:
$ref: '#/components/schemas/WorkoutSummary'
/v1/workout_file_uploads:
post:
tags: [Workout File Uploads]
summary: Upload Workout FIT File
operationId: createWorkoutFileUpload
security:
- OAuth2: [workouts_write]
requestBody:
required: true
content:
multipart/form-data:
schema:
type: object
properties:
file:
type: string
format: binary
description: FIT file to ingest.
required: [file]
responses:
'202':
description: Upload accepted; returns a token for polling.
content:
application/json:
schema:
$ref: '#/components/schemas/WorkoutFileUpload'
/v1/workout_file_uploads/{token}:
get:
tags: [Workout File Uploads]
summary: Get Workout File Upload Status
operationId: getWorkoutFileUpload
security:
- OAuth2: [workouts_write]
parameters:
- in: path
name: token
required: true
schema: { type: string }
responses:
'200':
description: Upload status.
content:
application/json:
schema:
$ref: '#/components/schemas/WorkoutFileUpload'
/v1/plans:
get:
tags: [Plans]
summary: List Plans
operationId: listPlans
security:
- OAuth2: [plans_read]
parameters:
- $ref: '#/components/parameters/Page'
- $ref: '#/components/parameters/PerPage'
responses:
'200':
description: Paginated plan list.
content:
application/json:
schema:
type: array
items: { $ref: '#/components/schemas/Plan' }
post:
tags: [Plans]
summary: Create Plan
operationId: createPlan
security:
- OAuth2: [plans_write]
requestBody:
required: true
content:
application/json:
schema: { $ref: '#/components/schemas/PlanCreate' }
responses:
'201':
description: Plan created.
content:
application/json:
schema: { $ref: '#/components/schemas/Plan' }
/v1/plans/{id}:
parameters:
- $ref: '#/components/parameters/Id'
get:
tags: [Plans]
summary: Get Plan
operationId: getPlan
security:
- OAuth2: [plans_read]
responses:
'200':
description: Plan record.
content:
application/json:
schema: { $ref: '#/components/schemas/Plan' }
put:
tags: [Plans]
summary: Update Plan
operationId: updatePlan
security:
- OAuth2: [plans_write]
requestBody:
required: true
content:
application/json:
schema: { $ref: '#/components/schemas/PlanCreate' }
responses:
'200':
description: Updated plan record.
content:
application/json:
schema: { $ref: '#/components/schemas/Plan' }
delete:
tags: [Plans]
summary: Delete Plan
operationId: deletePlan
security:
- OAuth2: [plans_write]
responses:
'204':
description: Plan deleted.
/v1/workouts/{workout_id}/plans:
get:
tags: [Plans]
summary: List Plans For Workout
operationId: listPlansForWorkout
security:
- OAuth2: [plans_read]
parameters:
- in: path
name: workout_id
required: true
schema: { type: integer, format: int64 }
responses:
'200':
description: Plans associated with the workout.
content:
application/json:
schema:
type: array
items: { $ref: '#/components/schemas/Plan' }
/v1/routes:
get:
tags: [Routes]
summary: List Routes
operationId: listRoutes
security:
- OAuth2: [routes_read]
parameters:
- $ref: '#/components/parameters/Page'
- $ref: '#/components/parameters/PerPage'
responses:
'200':
description: Paginated route list.
content:
application/json:
schema:
type: array
items: { $ref: '#/components/schemas/Route' }
post:
tags: [Routes]
summary: Create Route
operationId: createRoute
security:
- OAuth2: [routes_write]
requestBody:
required: true
content:
multipart/form-data:
schema: { $ref: '#/components/schemas/RouteCreate' }
responses:
'201':
description: Route created.
content:
application/json:
schema: { $ref: '#/components/schemas/Route' }
/v1/routes/{id}:
parameters:
- $ref: '#/components/parameters/Id'
get:
tags: [Routes]
summary: Get Route
operationId: getRoute
security:
- OAuth2: [routes_read]
responses:
'200':
description: Route record.
content:
application/json:
schema: { $ref: '#/components/schemas/Route' }
put:
tags: [Routes]
summary: Update Route
operationId: updateRoute
security:
- OAuth2: [routes_write]
requestBody:
required: true
content:
multipart/form-data:
schema: { $ref: '#/components/schemas/RouteCreate' }
responses:
'200':
description: Updated route record.
content:
application/json:
schema: { $ref: '#/components/schemas/Route' }
delete:
tags: [Routes]
summary: Delete Route
operationId: deleteRoute
security:
- OAuth2: [routes_write]
responses:
'204':
description: Route deleted.
/v1/power_zones:
get:
tags: [Power Zones]
summary: List Power Zones
operationId: listPowerZones
security:
- OAuth2: [power_zones_read]
responses:
'200':
description: Power zones list.
content:
application/json:
schema:
type: array
items: { $ref: '#/components/schemas/PowerZone' }
post:
tags: [Power Zones]
summary: Create Power Zones
operationId: createPowerZones
security:
- OAuth2: [power_zones_write]
requestBody:
required: true
content:
application/json:
schema: { $ref: '#/components/schemas/PowerZoneCreate' }
responses:
'201':
description: Power zones created.
content:
application/json:
schema: { $ref: '#/components/schemas/PowerZone' }
/v1/power_zones/{id}:
parameters:
- $ref: '#/components/parameters/Id'
get:
tags: [Power Zones]
summary: Get Power Zones
operationId: getPowerZones
security:
- OAuth2: [power_zones_read]
responses:
'200':
description: Power zones record.
content:
application/json:
schema: { $ref: '#/components/schemas/PowerZone' }
put:
tags: [Power Zones]
summary: Update Power Zones
operationId: updatePowerZones
security:
- OAuth2: [power_zones_write]
requestBody:
required: true
content:
application/json:
schema: { $ref: '#/components/schemas/PowerZoneCreate' }
responses:
'200':
description: Updated power zones.
content:
application/json:
schema: { $ref: '#/components/schemas/PowerZone' }
delete:
tags: [Power Zones]
summary: Delete Power Zones
operationId: deletePowerZones
security:
- OAuth2: [power_zones_write]
responses:
'204':
description: Power zones deleted.
/v1/permissions:
delete:
tags: [Permissions]
summary: Revoke App Access
operationId: revokeAppAccess
security:
- OAuth2: []
responses:
'204':
description: Permissions revoked for the calling app/user.
components:
parameters:
Id:
in: path
name: id
required: true
schema: { type: integer, format: int64 }
Page:
in: query
name: page
schema: { type: integer, minimum: 1, default: 1 }
PerPage:
in: query
name: per_page
schema: { type: integer, minimum: 1, maximum: 100, default: 30 }
securitySchemes:
OAuth2:
type: oauth2
description: >-
OAuth 2.0 Authorization Code (with PKCE option for public apps).
Access tokens are bearer tokens with a 2-hour TTL; refresh tokens are
single-use. Starting 2026-01-01, apps are limited to 10 unrevoked
access tokens per user.
flows:
authorizationCode:
authorizationUrl: https://api.wahooligan.com/oauth/authorize
tokenUrl: https://api.wahooligan.com/oauth/token
refreshUrl: https://api.wahooligan.com/oauth/token
scopes:
email: Access the user's email address
user_read: Read user profile
user_write: Update user profile
workouts_read: Read workouts and summaries
workouts_write: Create/update/delete workouts and uploads
offline_data: Receive webhook events while the app is closed
plans_read: Read workout plans
plans_write: Manage workout plans
power_zones_read: Read cycling power zones
power_zones_write: Manage cycling power zones
routes_read: Read GPS routes
routes_write: Manage GPS routes
schemas:
User:
type: object
properties:
id: { type: integer, format: int64 }
email: { type: string, format: email }
first: { type: string }
last: { type: string }
birth: { type: string, format: date }
gender: { type: integer, description: '0 = male, 1 = female, 2 = other' }
height: { type: string, description: Meters as string. }
weight: { type: string, description: Kilograms as string. }
created_at: { type: string, format: date-time }
updated_at: { type: string, format: date-time }
UserUpdate:
type: object
properties:
first: { type: string }
last: { type: string }
birth: { type: string, format: date }
gender: { type: integer }
height: { type: string }
weight: { type: string }
Workout:
type: object
properties:
id: { type: integer, format: int64 }
user_id: { type: integer, format: int64 }
starts: { type: string, format: date-time }
minutes: { type: integer }
name: { type: string }
plan_id: { type: integer, format: int64, nullable: true }
workout_token: { type: string }
workout_type_id: { type: integer }
workout_summary:
$ref: '#/components/schemas/WorkoutSummary'
created_at: { type: string, format: date-time }
updated_at: { type: string, format: date-time }
WorkoutCreate:
type: object
required: [starts, minutes, name, workout_type_id]
properties:
starts: { type: string, format: date-time }
minutes: { type: integer }
name: { type: string }
plan_id: { type: integer, format: int64 }
workout_token: { type: string }
workout_type_id: { type: integer }
WorkoutList:
type: object
properties:
workouts:
type: array
items: { $ref: '#/components/schemas/Workout' }
total: { type: integer }
page: { type: integer }
per_page: { type: integer }
order: { type: string }
sort: { type: string }
WorkoutSummary:
type: object
properties:
id: { type: integer, format: int64 }
ascent_accum: { type: string }
calories_accum: { type: string }
distance_accum: { type: string }
duration_active_accum: { type: string }
duration_paused_accum: { type: string }
duration_total_accum: { type: string }
cadence_avg: { type: string }
heart_rate_avg: { type: string }
power_bike_avg: { type: string }
speed_avg: { type: string }
work_accum: { type: string }
file:
type: object
properties:
url: { type: string, format: uri }
WorkoutFileUpload:
type: object
properties:
token: { type: string }
state:
type: string
enum: [queued, processing, completed, failed]
workout_id: { type: integer, format: int64, nullable: true }
error: { type: string, nullable: true }
Plan:
type: object
properties:
id: { type: integer, format: int64 }
name: { type: string }
description: { type: string }
external_id: { type: string }
file:
type: object
properties:
url: { type: string, format: uri }
provider_updated_at: { type: string, format: date-time }
workout_type_family_id: { type: integer }
created_at: { type: string, format: date-time }
updated_at: { type: string, format: date-time }
PlanCreate:
type: object
required: [name, file]
properties:
name: { type: string }
description: { type: string }
external_id: { type: string }
provider_updated_at: { type: string, format: date-time }
workout_type_family_id: { type: integer }
file:
type: string
format: binary
description: JSON plan file.
Route:
type: object
properties:
id: { type: integer, format: int64 }
name: { type: string }
description: { type: string }
external_id: { type: string }
starting_lat: { type: number, format: float }
starting_lng: { type: number, format: float }
ending_lat: { type: number, format: float }
ending_lng: { type: number, format: float }
distance: { type: number, format: float }
ascent: { type: number, format: float }
descent: { type: number, format: float }
workout_type_family_id: { type: integer }
file:
type: object
properties:
url: { type: string, format: uri }
created_at: { type: string, format: date-time }
updated_at: { type: string, format: date-time }
RouteCreate:
type: object
required: [name, file]
properties:
name: { type: string }
description: { type: string }
external_id: { type: string }
starting_lat: { type: number, format: float }
starting_lng: { type: number, format: float }
ending_lat: { type: number, format: float }
ending_lng: { type: number, format: float }
distance: { type: number, format: float }
ascent: { type: number, format: float }
descent: { type: number, format: float }
workout_type_family_id: { type: integer }
file:
type: string
format: binary
description: FIT-format route file.
PowerZone:
type: object
properties:
id: { type: integer, format: int64 }
zone_1: { type: integer }
zone_2: { type: integer }
zone_3: { type: integer }
zone_4: { type: integer }
zone_5: { type: integer }
zone_6: { type: integer }
zone_7: { type: integer }
ftp: { type: integer, description: Functional Threshold Power in watts. }
critical_power: { type: integer }
workout_type_family_id: { type: integer }
created_at: { type: string, format: date-time }
updated_at: { type: string, format: date-time }
PowerZoneCreate:
type: object
required: [ftp, workout_type_family_id]
properties:
ftp: { type: integer }
critical_power: { type: integer }
workout_type_family_id: { type: integer }
zone_1: { type: integer }
zone_2: { type: integer }
zone_3: { type: integer }
zone_4: { type: integer }
zone_5: { type: integer }
zone_6: { type: integer }
zone_7: { type: integer }