This is an unofficial Go API wrapper for the Whistle smart pet collar. This wrapper handles authentication and interacting with all known endpoints.
Currently, the wrapper only focuses on reading from the API endpoints, though this may change in the future. Most of the endpoints do support standard CRUD actions, however.
If you have discovered endpoints not listed here, please open a PR or submit an issue.
If you're only here for the (previously) undocumented API endpoints provided by https://Whistle.com, check out the Thunder Tests folder. This folder contains the HTTP request collection supported by the Whistle V3 API.
Install the wrapper within your project via Go Get
go get github.com/amattu2/go-whistle-wrapper
The wrapper exposes two ways of instantiating a client.
With Bearer Token
If you already have a bearer token, you can instantiate a new wrapper via
whistle := whistle.InitializeBearer("API_TOKEN_HERE")
This is useful for cases where you want to reduce overhead on page reload. You should ideally use this method as often as possible.
With API Key
**Note**: I believe this is deprecated and should not be used. The mobile application uses HTTP bearer, and this may be removed unpredictably.If you already have an API key (X-Whistle-AuthToken
),
you can instantiate a new wrapper via
whistle := whistle.InitializeToken("API_TOKEN_HERE")
This is useful for cases where you want to reduce overhead on page reload. You should ideally use this method as often as possible.
With Email/Password
If you don't have an active API key, but have credentials that work on the https://Whistle.com mobile app or on https://app.Whistle.com, you can instantiate a new wrapper via
whistle := whistle.Initialize("EMAIL", "PASSWORD")
With Email/Refresh
If you don't have the HTTP bearer token cached, you can reauthenticate using your email and refresh token credentials. This would be preferred over storing a user's password in a cache somewhere. The refresh token is returned during authentication with a email/password.
whistle := whistle.InitializeRefreshToken("EMAIL", "TOKEN")
Manually
In the event that you have an advanced need, you may also
initialize the wrapper directly. You only need email
/password
,
email
/refresh_token
, token
, or bearer
, but never all 4 options together.
If you provide a email
and password
or email
and refresh_token
,
a HTTP bearer will automatically be requested and stored on your first API query.
client := whistle.Client{
email: "ABC", // Option 1
password: "XYZ", // Option 1-1
refreshToken: "XYZ", // Option 1-2
token: "123", // Option 2
bearer: "abc12932", // Option 3
Timeout: 3000,
Env: whistle.ProdEnv, // Or: whistle.StagingEnv
UserAgent: "Custom User Agent",
}
Important note: The Whistle.com API REQUIRES a Accept: application/vnd.whistle.com.v4+json
header to be present in almost ALL REQUESTS otherwise it will return 404.
Occasionally a endpoint (usually a deprecated one) will accept application/json
.
This section covers all implementations relating to the REST API surrounding users
(/api/users
).
DEPRECATED: Users()
Get information about the currently authenticated user. This does NOT provide information about all associated users.
// ...
q := client.Users()
q.StatusCode // "200"
q.Error // nil
fmt.Println(q.Response) // {CreatedAt, ..., Username}
// ...
Me()
Returns information about the authenticated user.
// ...
q := client.Me()
q.StatusCode // "200"
q.Error // nil
fmt.Println(q.Response.User) // {CreatedAt, ..., Username}
// ...
CheckEmail(email string)
Used to check if an email exists within the database.
HTTP 404 - Non existing
HTTP 204 - User exists
// ...
q := client.CheckEmail("abc@gmail.com")
fmt.Println(q.Response) // true = exists, false = non-existing
// ...
InvitationCodes(code string)
List information about a invitation code. Used during the Whistle App invite process.
// ...
q := client.InvitationCodes("code123")
q.StatusCode // "200"
q.Error // nil
fmt.Println(q.Response) // {pet: ...}
// ...
ApplicationState()
Get information about the current application state. Current usage unknown.
// ...
q := client.ApplicationState()
q.StatusCode // "200"
q.Error // nil
fmt.Println(q.Response.ApplicationState) // {...}
// ...
DEPRECATED: CreditCard()
Get information about the current credit card on file. Does not return the actual card number.
// ...
q := client.CreditCard()
q.StatusCode // "200"
q.Error // nil
fmt.Println(q.Response) // {CardType, ..., ZipCode}
// ...
Subscriptions()
Get a list of subscriptions tied to an account, along with any Partner subscriptions.
// ...
q := client.Subscriptions()
q.StatusCode // "200"
q.Error // nil
fmt.Println(q.Response) // {Subscriptions: ..., PartnerServices: ...}
// ...
CancellationPreview()
Current usage unknown.
// ...
q := client.CancellationPreview()
q.StatusCode // "200"
q.Error // nil
fmt.Println(q.Response) // TBD
// ...
CancellationReasons()
Returns a list of reasons to cancel a subscription.
// ...
q := client.CancellationReasons()
q.StatusCode // "200"
q.Error // nil
fmt.Println(q.Response) // {cancellation_reasons: [{id: 123, ...}, ...]}
// ...
This portion of the document outlines the implementations of the smart collar REST api endpoints.
Device(deviceId string)
Provides information about the specified smart collar device.
// ...
q := client.Device("serial_num")
q.StatusCode // "200"
q.Error // nil
fmt.Println(q.Response) // {device: {model_id: ..., ..., has_gps: true, ...}
// ...
DeviceActivationCheck(deviceId string)
Returns HTTP 204 if the device Id is valid, but not registered
Returns HTTP 422 if the device is registered
Returns HTTP 404 if the id is invalid
// ...
q := client.DeviceActivationCheck("serial_num")
q.StatusCode // "204"
q.Error // nil
// ...
DevicePlans(deviceId string)
Provides information about the specified device plans
// ...
q := client.DevicePlans("serial_num")
q.StatusCode // "200"
q.Error // nil
fmt.Println(q.Response) // {paid_through: "", plans: [ ... ] }
// ...
DeviceSubscription(deviceId string)
Provides information about the specified device subscription status
// ...
q := client.DeviceSubscription("serial_num")
q.StatusCode // "200"
q.Error // nil
fmt.Println(q.Response) // {id: 123, ..., plan: {...}}
// ...
DeviceSubscriptionPreview(deviceId string, planId string)
Current usage unknown
// ...
q := client.DeviceSubscriptionPreview("serial_num", "abc")
q.StatusCode // "200"
q.Error // nil
fmt.Println(q.Response) // TBD
// ...
DeviceUpgradePreview(deviceId string)
Current usage unknown
// ...
q := client.DeviceUpgradePreview("serial_num")
q.StatusCode // "200"
q.Error // nil
fmt.Println(q.Response) // TBD
// ...
DeviceWifiNetworks(deviceId string)
Provides a listing of all connected networks associated with a device.
// ...
q := client.DeviceWifiNetworks("serial_num")
q.StatusCode // "200"
q.Error // nil
fmt.Println(q.Response) // [{id: ..., ssid: "xyz"}, ...]
// ...
This section related to all of the endpoints (currently only 1) relating to animal breeds.
Breeds(animal string)
Provides a list of breeds given the current animal species.
Known options are dogs
or cats
// ...
q := client.Breeds("dogs")
q.StatusCode // "200"
q.Error // nil
fmt.Println(q.Response.Breeds) // [{ID: 123, Name: "German Shepherd", ...}, ...]
// ...
These are the operations relating to pets (cats/dogs/etc).
Pets()
Returns a populated array of objects describing a Pet belonging to the authenticated user.
// ...
q := client.Pets()
q.StatusCode // "200"
q.Error // nil
fmt.Println(q.Response.Pets) // {ID: 135, Name: "Baker", ...}
// ...
PetTransfers()
Returns an array of pets that qualify for a transfer. Unsure of the current usage.
// ...
q := client.PetTransfers()
q.StatusCode // "200"
q.Error // nil
fmt.Println(q.Response.Transfers) // [{ID: 123, Name: "Fido", ...}, ...]
// ...
Pet(petId string)
Returns detailed information about a specific pet.
// ...
q := client.Pet("petid123")
q.StatusCode // "200"
q.Error // nil
fmt.Println(q.Response.Pet) // {ID: 123, ..., Name: "Fido"}
// ...
PetOwners(petId string)
Returns an array of people that are tied to a pet as owners.
// ...
q := client.PetOwners("pet1233")
q.StatusCode // "200"
q.Error // nil
fmt.Println(q.Response.Owners) // [{Name: "amattu2", ..., Email: "xyz@gmail.com"}]
// ...
PetWhereabouts(petId string, startDate string, endDate string)
Returns informations about a pet's historical locations. Based on start/end dates. Provides locations and known places.
// ...
q := client.PetWhereabouts("pet321", "2022-03-03", "2024-01-01")
q.StatusCode // "200"
q.Error // nil
fmt.Println(q.Response) // {Locations: [...], Places: [...]}
// ...
PetLocationsRecent(petId string)
Similar to PetWhereabouts, this returns detailed locations about where a pet has been as of recent.
// ...
q := client.PetLocationsRecent("3892821")
q.StatusCode // "200"
q.Error // nil
fmt.Println(q.Response.Locations) // [{...}]
// ...
PetAchievements(petId string)
Returns a list of achievements that a pet CAN make. The achievements indicate whether or not that goal has been met.
// ...
q := client.PetAchievements("3828111")
q.StatusCode // "200"
q.Error // nil
fmt.Println(q.Response.Achievements) // [{ID: 8382, Name: "1 Week Streak"}]
// ...
PetStatistics(petId string)
Returns analytical insights about a pet.
// ...
q := client.PetStatistics("12345")
q.StatusCode // "200"
q.Error // nil
fmt.Println(q.Response.Statistics) // {AverageMinutesActive: 0, ...}
// ...
PetDailies(petId string)
Returns high-level information about a pet's daily activities
// ...
q := client.PetDailies("12345")
q.StatusCode // "200"
q.Error // nil
fmt.Println(q.Response.Dailies) // [{DayNumber: 93381, ..., UpdatedAt: "..."}]
// ...
PetDaily(petId string, dailyId string)
Returns detailed information about a particular pet's daily activity
// ...
q := client.PetDaily("1234", "938191")
q.StatusCode // "200"
q.Error // nil
fmt.Println(q.Response.Daily) // [{DayNumber: 938191, ...}]
// ...
PetDailyItems(petId string, dailyId string)
Returns very low-level, and highly-detailed breakdown of a pet's daily activity.
// ...
q := client.PetDailyItems("1234", "938191")
q.StatusCode // "200"
q.Error // nil
fmt.Println(q.Response.DailyItems) // [...]
// ...
PetHealthTrends(petId string)
Provides health trend information about the specified pet.
// ...
q := client.PetHealthTrends("12345")
q.StatusCode // "200"
q.Error // nil
fmt.Println(q.Response.Trends) // {...}
// ...
PetHealthGraphs(petId string, trend string, days int)
Provides data to generate a graph for the specified health trend. Days limits the number of observations to include.
// ...
q := client.PetHealthTrends("1234", "sleeping", 7)
q.StatusCode // "200"
q.Error // nil
fmt.Println(q.Response) // {Data: [...], PetId: 1234, ...}
// ...
PetNutritionPortions(petId string)
Returns the suggested food portions for the given pet.
// ...
q := client.PetNutritionPortions("1234")
q.StatusCode // "200"
q.Error // nil
fmt.Println(q.Response) // {Treats: ..., SuggestedCalories: 0.0}
// ...
DEPRECATED: PetFoodPortions(petId string)
Returns the suggested food portions for the given pet. Replaced by the above method.
// ...
q := client.PetFoodPortions("1234")
q.StatusCode // "200"
q.Error // nil
fmt.Println(q.Response.PetFoodPortions) // ...
// ...
PetTask(petId string, taskId string)
Returns information about the pet's task.
// ...
q := client.PetTask("1234", "35")
q.StatusCode // "200"
q.Error // nil
fmt.Println(q.Response) // ...
// ...
PetTaskOccurrence(petId string, occurrenceType string)
Current usage unknown.
// ...
q := client.PetTaskOccurrence("1234", "incomplete")
q.StatusCode // "200"
q.Error // nil
fmt.Println(q.Response) // ...
// ...
These are operations not categorized by another API route.
Notifications()
Returns an array of unread notifications for the current user.
// ...
q := client.Notifications()
q.StatusCode // "200"
q.Error // nil
fmt.Println(q.Response) // {Items: [...]}
// ...
PetFoods(foodType string)
Returns a list of pet foods given the food type.
Known options are dog_treat
, dog_food
. Cat variant does not work.
// ...
q := client.PetFoods("dog_food")
q.StatusCode // "200"
q.Error // nil
fmt.Println(q.Response) // [{ID: 321, Name: "Purina XXX"}, ...]
// ...
ReverseGeocode(latitude string, longitude string)
Decode latitude and longitude to a physical address.
// ...
q := client.ReverseGeocode("LAT", "LON")
q.StatusCode // "200"
q.Error // nil
fmt.Println(q.Response.Description) // {address: ..., region: ..., etc}
// ...
Places()
Returns a list of saved places tied to a user account.
// ...
q := client.Places()
q.StatusCode // "200"
q.Error // nil
fmt.Println(q.Response) // [ {address: "123 ABC Lane", ..., id: 123}, ...]
// ...
AdventureCategories()
Returns a list of adventure categories. Current usage unknown.
// ...
q := client.AdventureCategories()
q.StatusCode // "200"
q.Error // nil
fmt.Println(q.Response) // []
// ...
- Go 1.18+ (Required for Generics)
- https://whistle.com account
The following resources were used to compile this API wrapper.
- API Endpoint Reference https://github.com/aolney/WhistleAPI-DOTNET/blob/master/WhistleAPI-DOTNET-Fsharp.ipynb
- API Reference https://github.com/martzcodes/node-whistle
- API Reference https://community.smartthings.com/t/beta-release-whistle-3-pet-tracker-presence-and-battery-dth/156031
- Design Reference https://www.reddit.com/r/golang/comments/d8m5a5/advice_for_creating_an_api_wrapper
- Implementation Reference https://github.com/ovh/go-ovh
These are the remaining action items of this project. Some of them are on hold until I have a device to test with.
- Add support for the async event pusher service (See
realtime_channel
) - Check into the WiFi adding endpoint