Documentation
¶
Overview ¶
Package whoop provides a production-grade Go client for the WHOOP Developer API (v2).
The client handles authentication, rate limiting (100 req/min via token bucket), automatic retries with exponential backoff on 429 responses, webhook signature verification via HMAC-SHA256, and cursor-based pagination.
Quick Start ¶
client := whoop.NewClient(
whoop.WithToken("your_oauth2_token"),
)
profile, err := client.User.GetBasicProfile(ctx)
Pagination ¶
List methods return page objects with a NextPage iterator:
page, _ := client.Cycle.List(ctx, &whoop.ListOptions{Limit: 25})
for {
for _, c := range page.Records { /* process cycle */ }
page, err = page.NextPage(ctx)
if errors.Is(err, whoop.ErrNoNextPage) {
break
}
}
Webhooks ¶
Use ParseWebhook to validate and decode incoming WHOOP webhook payloads:
event, err := whoop.ParseWebhook(r, "webhook_secret")
Index ¶
- Constants
- Variables
- type APIError
- type AuthError
- type BasicProfile
- type BodyMeasurement
- type Client
- type Cycle
- type CyclePage
- type CycleService
- type ListOptions
- type Option
- type RateLimitError
- type Recovery
- type RecoveryPage
- type RecoveryScore
- type RecoveryService
- type Scope
- type Score
- type Sleep
- type SleepNeeded
- type SleepPage
- type SleepScore
- type SleepService
- type StageSummary
- type UserService
- type WebhookEvent
- type Workout
- type WorkoutPage
- type WorkoutScore
- type WorkoutService
- type ZoneDurations
Examples ¶
Constants ¶
const (
// Version is the semantic version of this library.
Version = "1.0.0"
)
Variables ¶
var ErrNoNextPage = errors.New("no next page available")
ErrNoNextPage is returned by NextPage when there are no more pages to fetch.
Functions ¶
This section is empty.
Types ¶
type APIError ¶
type APIError struct {
StatusCode int
Message string
URL string
Err error // Underlying error, if any
}
APIError represents an error returned by the WHOOP API.
type BasicProfile ¶
type BasicProfile struct {
UserID int `json:"user_id"`
Email string `json:"email"`
FirstName string `json:"first_name"`
LastName string `json:"last_name"`
}
BasicProfile represents the user's basic profile information.
type BodyMeasurement ¶
type BodyMeasurement struct {
HeightMeter float64 `json:"height_meter"`
WeightKilogram float64 `json:"weight_kilogram"`
MaxHeartRate int `json:"max_heart_rate"`
}
BodyMeasurement represents the user's physical body measurements.
type Client ¶
type Client struct {
// Services used for communicating with the WHOOP API endpoints.
User *UserService
Cycle *CycleService
Sleep *SleepService
Workout *WorkoutService
Recovery *RecoveryService
// contains filtered or unexported fields
}
Client is the core WHOOP API client.
func NewClient ¶
NewClient creates a new WHOOP API client with the given options.
Example ¶
Create a client with default settings.
package main
import (
"context"
"fmt"
"os"
"github.com/arvarik/whoop-go/whoop"
)
func main() {
client := whoop.NewClient(
whoop.WithToken(os.Getenv("WHOOP_OAUTH_TOKEN")),
)
profile, err := client.User.GetBasicProfile(context.Background())
if err != nil {
fmt.Println("error:", err)
return
}
fmt.Println("Hello,", profile.FirstName)
}
Output:
Example (WithOptions) ¶
Customize backoff, retries, and base URL using functional options.
package main
import (
"time"
"github.com/arvarik/whoop-go/whoop"
)
func main() {
client := whoop.NewClient(
whoop.WithToken("your_token"),
whoop.WithMaxRetries(5),
whoop.WithBackoffBase(1*time.Second),
whoop.WithBackoffMax(2*time.Minute),
whoop.WithBaseURL("https://custom-proxy.example.com"),
)
_ = client
}
Output:
func (*Client) Do ¶
Do executes an HTTP request with context, authentication, rate limiting, and automatic retries on 429 Too Many Requests.
func (*Client) Get ¶ added in v0.3.0
Get performs a GET request to the specified path and decodes the response into v.
type Cycle ¶
type Cycle struct {
ID int `json:"id"`
UserID int `json:"user_id"`
CreatedAt time.Time `json:"created_at"`
UpdatedAt time.Time `json:"updated_at"`
Start time.Time `json:"start"`
End *time.Time `json:"end"`
TimezoneOffset string `json:"timezone_offset"`
ScoreState string `json:"score_state"`
Score *Score `json:"score,omitempty"`
}
Cycle represents a physiological cycle (typically an awake period to the next awake period).
type CyclePage ¶
type CyclePage struct {
Records []Cycle
NextToken string
// contains filtered or unexported fields
}
CyclePage represents a paginated set of Cycles.
type CycleService ¶
type CycleService struct {
// contains filtered or unexported fields
}
CycleService handles communication with the cycle related methods.
func (*CycleService) GetByID ¶
GetByID fetches a single cycle by its ID.
Example ¶
Fetch a single cycle by its numeric ID.
package main
import (
"context"
"fmt"
"github.com/arvarik/whoop-go/whoop"
)
func main() {
client := whoop.NewClient(whoop.WithToken("your_token"))
cycle, err := client.Cycle.GetByID(context.Background(), 12345)
if err != nil {
fmt.Println("error:", err)
return
}
fmt.Printf("Cycle %d: Strain=%.1f\n", cycle.ID, cycle.Score.Strain)
}
Output:
func (*CycleService) List ¶
func (s *CycleService) List(ctx context.Context, opts *ListOptions) (*CyclePage, error)
List fetches a paginated collection of cycles.
Example ¶
Iterate through all physiological cycles using cursor-based pagination.
package main
import (
"context"
"errors"
"fmt"
"github.com/arvarik/whoop-go/whoop"
)
func main() {
client := whoop.NewClient(whoop.WithToken("your_token"))
ctx := context.Background()
page, err := client.Cycle.List(ctx, &whoop.ListOptions{Limit: 25})
if err != nil {
fmt.Println("error:", err)
return
}
for {
for _, c := range page.Records {
fmt.Printf("Cycle %d: Strain=%.1f, ScoreState=%s\n",
c.ID, c.Score.Strain, c.ScoreState)
}
page, err = page.NextPage(ctx)
if err != nil {
if errors.Is(err, whoop.ErrNoNextPage) {
break // All pages consumed
}
fmt.Println("error:", err)
return
}
}
}
Output:
type ListOptions ¶
type ListOptions struct {
// Maximum number of items to return. The API limits this to 50.
Limit int `url:"limit,omitempty"`
// Earliest date of data to fetch (inclusive). Time is ISO-8601 formatted.
Start *time.Time `url:"start,omitempty"`
// Latest date of data to fetch (inclusive). Time is ISO-8601 formatted.
End *time.Time `url:"end,omitempty"`
// Token used to fetch the next page of results. Usually handled automatically by the paginator.
NextToken string `url:"nextToken,omitempty"`
}
ListOptions specifies the optional parameters to various List methods that support pagination.
type Option ¶
type Option func(*Client)
Option is a functional option for configuring the Client.
func WithBackoffBase ¶
WithBackoffBase sets the base duration for exponential backoff during retries. By default, this is 1 second.
func WithBackoffMax ¶
WithBackoffMax sets the maximum duration for exponential backoff during retries. By default, this is 60 seconds.
func WithBaseURL ¶
WithBaseURL overrides the default WHOOP API base URL. This is primarily useful for testing or connecting to a proxy.
func WithHTTPClient ¶
WithHTTPClient sets the underlying HTTP client used for requests. If this is not provided, a default http.Client is used.
func WithMaxRetries ¶
WithMaxRetries sets the maximum number of retries for 429 Too Many Requests responses. By default, the client will retry up to 3 times.
func WithRateLimiting ¶ added in v0.3.0
WithRateLimiting enables or disables client-side rate limiting. This is primarily used for testing and benchmarking.
type RateLimitError ¶
type RateLimitError struct {
RetryAfter int // Suggested retry after duration in seconds, if provided by the API
Err error
}
RateLimitError represents an error indicating that the client is rate-limited. It can occur locally before the request is made or as a response from the API.
func (*RateLimitError) Error ¶
func (e *RateLimitError) Error() string
Error implements the error interface.
func (*RateLimitError) Unwrap ¶
func (e *RateLimitError) Unwrap() error
Unwrap implements errors.Unwrap.
type Recovery ¶
type Recovery struct {
CycleID int `json:"cycle_id"`
SleepID string `json:"sleep_id"`
UserID int `json:"user_id"`
CreatedAt time.Time `json:"created_at"`
UpdatedAt time.Time `json:"updated_at"`
ScoreState string `json:"score_state"`
Score *RecoveryScore `json:"score,omitempty"`
}
Recovery represents the quantified recovery status of the user for a given cycle.
type RecoveryPage ¶
type RecoveryPage struct {
Records []Recovery
NextToken string
// contains filtered or unexported fields
}
RecoveryPage represents a paginated set of Recoveries.
func (*RecoveryPage) NextPage ¶
func (p *RecoveryPage) NextPage(ctx context.Context) (*RecoveryPage, error)
NextPage fetches the subsequent page of Recoveries based on NextToken.
type RecoveryScore ¶
type RecoveryScore struct {
UserCalibrating bool `json:"user_calibrating"`
RecoveryScore float64 `json:"recovery_score"`
RestingHeartRate float64 `json:"resting_heart_rate"`
HrvRmssdMilli float64 `json:"hrv_rmssd_milli"`
Spo2Percentage float64 `json:"spo2_percentage"`
SkinTempCelsius float64 `json:"skin_temp_celsius"`
}
RecoveryScore contains the metrics formulating the recovery calculation.
type RecoveryService ¶
type RecoveryService struct {
// contains filtered or unexported fields
}
RecoveryService handles communication with the recovery related methods.
func (*RecoveryService) GetByID ¶
GetByID fetches a single recovery score by cycle ID.
Example ¶
Fetch recovery data for a specific cycle.
package main
import (
"context"
"fmt"
"github.com/arvarik/whoop-go/whoop"
)
func main() {
client := whoop.NewClient(whoop.WithToken("your_token"))
recovery, err := client.Recovery.GetByID(context.Background(), 12345)
if err != nil {
fmt.Println("error:", err)
return
}
if recovery.Score != nil {
fmt.Printf("Recovery: %.0f%%, SpO2: %.0f%%\n",
recovery.Score.RecoveryScore,
recovery.Score.Spo2Percentage)
}
}
Output:
func (*RecoveryService) List ¶
func (s *RecoveryService) List(ctx context.Context, opts *ListOptions) (*RecoveryPage, error)
List fetches a paginated collection of recovery records.
Example ¶
List recovery scores across recent cycles.
package main
import (
"context"
"fmt"
"github.com/arvarik/whoop-go/whoop"
)
func main() {
client := whoop.NewClient(whoop.WithToken("your_token"))
ctx := context.Background()
page, err := client.Recovery.List(ctx, &whoop.ListOptions{Limit: 7})
if err != nil {
fmt.Println("error:", err)
return
}
for _, r := range page.Records {
if r.Score != nil {
fmt.Printf("Cycle %d: Recovery=%.0f%%, HRV=%.1f, RHR=%.0f\n",
r.CycleID,
r.Score.RecoveryScore,
r.Score.HrvRmssdMilli,
r.Score.RestingHeartRate)
}
}
}
Output:
type Scope ¶
type Scope string
Scope represents an OAuth2 scope required to access specific WHOOP API endpoints.
const ( // ScopeOffline enables refresh token issuance for long-lived sessions. ScopeOffline Scope = "offline" // ScopeReadRecovery allows reading the user's recovery data. ScopeReadRecovery Scope = "read:recovery" // ScopeReadCycles allows reading the user's physiological cycles. ScopeReadCycles Scope = "read:cycles" // ScopeReadSleep allows reading the user's sleep data. ScopeReadSleep Scope = "read:sleep" // ScopeReadWorkout allows reading the user's workout data. ScopeReadWorkout Scope = "read:workout" // ScopeReadProfile allows reading the user's basic profile. ScopeReadProfile Scope = "read:profile" // ScopeReadBodyMeasurement allows reading the user's body measurements. ScopeReadBodyMeasurement Scope = "read:body_measurement" )
type Score ¶
type Score struct {
Strain float64 `json:"strain"`
Kilojoule float64 `json:"kilojoule"`
AverageHeartRate int `json:"average_heart_rate"`
MaxHeartRate int `json:"max_heart_rate"`
}
Score summarizes physiological strains within a Cycle.
type Sleep ¶
type Sleep struct {
ID string `json:"id"`
CycleID int `json:"cycle_id"`
V1ID *int `json:"v1_id,omitempty"`
UserID int `json:"user_id"`
CreatedAt time.Time `json:"created_at"`
UpdatedAt time.Time `json:"updated_at"`
Start time.Time `json:"start"`
End time.Time `json:"end"`
TimezoneOffset string `json:"timezone_offset"`
Nap bool `json:"nap"`
ScoreState string `json:"score_state"`
Score *SleepScore `json:"score,omitempty"`
}
Sleep represents a single sleep event.
type SleepNeeded ¶
type SleepNeeded struct {
BaselineMilli int `json:"baseline_milli"`
NeedFromSleepDebtMilli int `json:"need_from_sleep_debt_milli"`
NeedFromRecentStrainMilli int `json:"need_from_recent_strain_milli"`
NeedFromRecentNapMilli int `json:"need_from_recent_nap_milli"`
}
SleepNeeded defines baseline and calculated sleep needs for the individual.
type SleepPage ¶
type SleepPage struct {
Records []Sleep
NextToken string
// contains filtered or unexported fields
}
SleepPage represents a paginated set of Sleep activities.
type SleepScore ¶
type SleepScore struct {
StageSummary *StageSummary `json:"stage_summary"`
SleepNeeded *SleepNeeded `json:"sleep_needed"`
RespiratoryRate float64 `json:"respiratory_rate"`
SleepPerformancePercentage float64 `json:"sleep_performance_percentage"`
SleepConsistencyPercentage float64 `json:"sleep_consistency_percentage"`
SleepEfficiencyPercentage float64 `json:"sleep_efficiency_percentage"`
}
SleepScore provides calculated metrics for a Sleep.
type SleepService ¶
type SleepService struct {
// contains filtered or unexported fields
}
SleepService handles communication with the sleep related methods.
func (*SleepService) GetByID ¶
GetByID fetches a single sleep event by its UUID.
Example ¶
Fetch a single sleep event by its UUID.
package main
import (
"context"
"fmt"
"time"
"github.com/arvarik/whoop-go/whoop"
)
func main() {
client := whoop.NewClient(whoop.WithToken("your_token"))
sleep, err := client.Sleep.GetByID(context.Background(), "abc-def-456")
if err != nil {
fmt.Println("error:", err)
return
}
fmt.Printf("Sleep %s: Nap=%v\n", sleep.ID, sleep.Nap)
if sleep.Score != nil && sleep.Score.StageSummary != nil {
rem := time.Duration(sleep.Score.StageSummary.TotalRemSleepTimeMilli) * time.Millisecond
fmt.Printf(" REM sleep: %s\n", rem)
}
}
Output:
func (*SleepService) List ¶
func (s *SleepService) List(ctx context.Context, opts *ListOptions) (*SleepPage, error)
List fetches a paginated collection of sleep events.
Example ¶
Iterate through sleep events with pagination.
package main
import (
"context"
"fmt"
"github.com/arvarik/whoop-go/whoop"
)
func main() {
client := whoop.NewClient(whoop.WithToken("your_token"))
ctx := context.Background()
page, err := client.Sleep.List(ctx, &whoop.ListOptions{Limit: 10})
if err != nil {
fmt.Println("error:", err)
return
}
for _, s := range page.Records {
fmt.Printf("Sleep %s: Nap=%v, ScoreState=%s\n", s.ID, s.Nap, s.ScoreState)
if s.Score != nil {
fmt.Printf(" Performance: %.0f%%, Efficiency: %.0f%%\n",
s.Score.SleepPerformancePercentage,
s.Score.SleepEfficiencyPercentage)
}
}
}
Output:
type StageSummary ¶
type StageSummary struct {
TotalInBedTimeMilli int `json:"total_in_bed_time_milli"`
TotalAwakeTimeMilli int `json:"total_awake_time_milli"`
TotalNoDataTimeMilli int `json:"total_no_data_time_milli"`
TotalLightSleepTimeMilli int `json:"total_light_sleep_time_milli"`
TotalSlowWaveSleepTimeMilli int `json:"total_slow_wave_sleep_time_milli"`
TotalRemSleepTimeMilli int `json:"total_rem_sleep_time_milli"`
SleepCycleCount int `json:"sleep_cycle_count"`
DisturbanceCount int `json:"disturbance_count"`
}
StageSummary breaks down durations spent in different sleep stages.
type UserService ¶
type UserService struct {
// contains filtered or unexported fields
}
UserService handles communication with the user related methods.
func (*UserService) GetBasicProfile ¶
func (s *UserService) GetBasicProfile(ctx context.Context) (profile *BasicProfile, err error)
GetBasicProfile fetches the athlete's basic profile.
Example ¶
Fetch the authenticated user's basic profile.
package main
import (
"context"
"fmt"
"github.com/arvarik/whoop-go/whoop"
)
func main() {
client := whoop.NewClient(whoop.WithToken("your_token"))
profile, err := client.User.GetBasicProfile(context.Background())
if err != nil {
fmt.Println("error:", err)
return
}
fmt.Printf("User: %s %s (ID: %d)\n", profile.FirstName, profile.LastName, profile.UserID)
}
Output:
func (*UserService) GetBodyMeasurement ¶
func (s *UserService) GetBodyMeasurement(ctx context.Context) (measurement *BodyMeasurement, err error)
GetBodyMeasurement fetches the athlete's body measurements.
Example ¶
Fetch the authenticated user's body measurements.
package main
import (
"context"
"fmt"
"github.com/arvarik/whoop-go/whoop"
)
func main() {
client := whoop.NewClient(whoop.WithToken("your_token"))
body, err := client.User.GetBodyMeasurement(context.Background())
if err != nil {
fmt.Println("error:", err)
return
}
fmt.Printf("Height: %.2fm, Weight: %.1fkg, Max HR: %d\n",
body.HeightMeter, body.WeightKilogram, body.MaxHeartRate)
}
Output:
type WebhookEvent ¶
type WebhookEvent struct {
UserID int `json:"user_id"`
ID string `json:"id"`
Type string `json:"type"`
TraceID string `json:"trace_id"`
}
WebhookEvent represents a "Skinny Webhook" payload from WHOOP.
func ParseWebhook ¶
func ParseWebhook(r *http.Request, secret string) (*WebhookEvent, error)
ParseWebhook reads and verifies an incoming HTTP request from a WHOOP Webhook. It validates the X-Whoop-Signature HMAC-SHA256 using the provided secret key. The request body is capped at 1 MB to prevent memory exhaustion. Ensure your HTTP handler does NOT consume r.Body before passing it to this function.
Example ¶
Securely verify and parse incoming WHOOP webhook payloads.
package main
import (
"fmt"
"net/http"
"os"
"github.com/arvarik/whoop-go/whoop"
)
func main() {
http.HandleFunc("/whoop/webhook", func(w http.ResponseWriter, r *http.Request) {
event, err := whoop.ParseWebhook(r, os.Getenv("WHOOP_WEBHOOK_SECRET"))
if err != nil {
http.Error(w, "Unauthorized", http.StatusUnauthorized)
return
}
fmt.Printf("Event: type=%s, id=%s, user=%d\n",
event.Type, event.ID, event.UserID)
w.WriteHeader(http.StatusOK)
})
}
Output:
type Workout ¶
type Workout struct {
ID string `json:"id"`
V1ID *int `json:"v1_id,omitempty"`
UserID int `json:"user_id"`
CreatedAt time.Time `json:"created_at"`
UpdatedAt time.Time `json:"updated_at"`
Start time.Time `json:"start"`
End time.Time `json:"end"`
TimezoneOffset string `json:"timezone_offset"`
SportID int `json:"sport_id"`
SportName string `json:"sport_name"`
ScoreState string `json:"score_state"`
Score *WorkoutScore `json:"score,omitempty"`
}
Workout represents a tracked workout session.
type WorkoutPage ¶
type WorkoutPage struct {
Records []Workout
NextToken string
// contains filtered or unexported fields
}
WorkoutPage represents a paginated set of Workouts.
func (*WorkoutPage) NextPage ¶
func (p *WorkoutPage) NextPage(ctx context.Context) (*WorkoutPage, error)
NextPage fetches the subsequent page of Workouts based on NextToken.
type WorkoutScore ¶
type WorkoutScore struct {
Strain float64 `json:"strain"`
AverageHeartRate int `json:"average_heart_rate"`
MaxHeartRate int `json:"max_heart_rate"`
Kilojoule float64 `json:"kilojoule"`
PercentRecorded float64 `json:"percent_recorded"`
DistanceMeter *float64 `json:"distance_meter"`
AltitudeGainMeter *float64 `json:"altitude_gain_meter"`
AltitudeChangeMeter *float64 `json:"altitude_change_meter"`
ZoneDuration *ZoneDurations `json:"zone_durations"`
}
WorkoutScore details the cardiovascular output of a given workout.
type WorkoutService ¶
type WorkoutService struct {
// contains filtered or unexported fields
}
WorkoutService handles communication with the workout related methods.
func (*WorkoutService) GetByID ¶
GetByID fetches a single workout session by its UUID.
Example ¶
Fetch a single workout by its UUID.
package main
import (
"context"
"fmt"
"github.com/arvarik/whoop-go/whoop"
)
func main() {
client := whoop.NewClient(whoop.WithToken("your_token"))
workout, err := client.Workout.GetByID(context.Background(), "abc-def-123")
if err != nil {
fmt.Println("error:", err)
return
}
fmt.Printf("Workout: %s (%s)\n", workout.SportName, workout.ID)
if workout.Score != nil && workout.Score.ZoneDuration != nil {
fmt.Printf(" Zone 5 Time: %dms\n", workout.Score.ZoneDuration.ZoneFiveMilli)
}
}
Output:
func (*WorkoutService) List ¶
func (s *WorkoutService) List(ctx context.Context, opts *ListOptions) (*WorkoutPage, error)
List fetches a paginated collection of workout sessions.
Example ¶
List recent workouts with date filtering.
package main
import (
"context"
"fmt"
"time"
"github.com/arvarik/whoop-go/whoop"
)
func main() {
client := whoop.NewClient(whoop.WithToken("your_token"))
ctx := context.Background()
start := time.Now().AddDate(0, 0, -7) // Last 7 days
page, err := client.Workout.List(ctx, &whoop.ListOptions{
Limit: 10,
Start: &start,
})
if err != nil {
fmt.Println("error:", err)
return
}
for _, w := range page.Records {
fmt.Printf("Workout %s: %s, Strain=%.1f\n", w.ID, w.SportName, w.Score.Strain)
}
}
Output:
type ZoneDurations ¶
type ZoneDurations struct {
ZoneZeroMilli int `json:"zone_zero_milli"`
ZoneOneMilli int `json:"zone_one_milli"`
ZoneTwoMilli int `json:"zone_two_milli"`
ZoneThreeMilli int `json:"zone_three_milli"`
ZoneFourMilli int `json:"zone_four_milli"`
ZoneFiveMilli int `json:"zone_five_milli"`
}
ZoneDurations breaks down the duration spent in different heart rate zones.