Wahoo Cloud API

OAuth 2.0 REST API that connects Wahoo users to third-party mobile and web applications. Manages user profiles, workouts, workout summaries, FIT-file uploads, structured workout plans, GPS routes, and cycling power zones. Delivers workout_summary webhook events when offline_data scope is granted. Sandbox apps are promoted to production after Wahoo review.

Wahoo Cloud API is one of 3 APIs that Wahoo Fitness publishes on the APIs.io network, described by a machine-readable OpenAPI specification and an AsyncAPI event-driven specification.

This API exposes 7 machine-runnable capabilities that can be deployed as REST, MCP, or Agent Skill surfaces via Naftiko and 2 JSON Schema definitions.

Tagged areas include Fitness, Cycling, Workouts, and OAuth. The published artifact set on APIs.io includes API documentation, an OpenAPI specification, an AsyncAPI specification, a JSON-LD context, rate-limit docs, sample payloads, 7 Naftiko capability specs, and 2 JSON Schemas.

Documentation

Specifications

Examples

Schemas & Data

Other Resources

OpenAPI Specification

wahoo-cloud-api-openapi.yml Raw ↑
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 }