go-yoto Client
go-yoto is a zero-dependency Go client for the Yoto API. Used internally by YotoShelf and available for other Go projects.
gitlab.com/yotoshelf/go-yoto
is a zero-dependency Go client for the Yoto API.
It handles OAuth2 PKCE and device code authentication, content library access,
device management, MYO card CRUD, and media uploads with SHA256 deduplication.
YotoShelf uses it as its upstream API layer.
Version status
| Version | Status | Notes |
|---|---|---|
v0.3.1 | Current | In use by YotoShelf main branch |
v0.3.2 | Planned | Minor additions; no breaking changes expected |
v1.0 | Horizon | API stabilisation; possible Channels type change; GetDeviceStatus removal |
Installation
go get gitlab.com/yotoshelf/go-yoto Quick start
import "gitlab.com/yotoshelf/go-yoto"
client := yoto.NewClient("YOUR_CLIENT_ID", nil, nil) Authentication
The Yoto API uses Authorization Code + PKCE. Two flows are supported:
Browser OAuth2 PKCE (web applications)
verifier, err := yoto.GenerateCodeVerifier()
authURL := client.BuildAuthorizeURL(redirectURI, "random-state", verifier)
// redirect user to authURL, receive code in callback
tokens, err := client.ExchangeCode(ctx, code, redirectURI, verifier) Device code flow (CLI / headless)
dcr, err := client.RequestDeviceCode(ctx)
fmt.Printf("Visit %s and enter code: %s\n", dcr.VerificationURI, dcr.UserCode)
tokens, err := client.PollForToken(ctx, dcr) Token persistence
Implement the TokenStore interface to persist tokens across restarts.
A MemoryTokenStore is included for testing. Tokens are automatically
refreshed 30 seconds before expiry. Refresh token rotation is mutex-protected and
safe for concurrent use.
client := yoto.NewClient("YOUR_CLIENT_ID", &yoto.MemoryTokenStore{}, nil) API coverage
26 of 27 upstream endpoints (96%) are implemented.
149 of 149 response fields (100%) are validated against the live API
via the yoto-explorer harness. Coverage is verified on each CI smoke run
via response shape assertions.
Devices
| Upstream endpoint | Go method | Status |
|---|---|---|
GET /device-v2/devices/mine | ListDevices | Working |
GET /device-v2/{id}/config | GetDeviceInfo | Working |
GET /device-v2/{id}/config | GetDeviceSettings | Working (settings only) |
PUT /device-v2/{id}/config | UpdateDeviceSettings | Working |
POST /device-v2/{id}/command/status | SendCommand | Working |
PUT /device-v2/{id}/shortcuts | UpdateShortcutConfig | Working (beta) |
GET /device-v2/{id}/status | GetDeviceStatus | Deprecated — returns 403 |
Content
| Upstream endpoint | Go method | Status |
|---|---|---|
GET /content/mine | ListMyCards | Working |
GET /content/mine?showdeleted | ListMyCardsWithOptions | Working |
GET /content/{id} | GetCard | Working |
GET /content/{id}?playable&signingType | GetCardPlayable | Working |
GET /content/{id}?timezone | GetCardWithOptions | Working |
POST /content | CreateCard / UpdateCard | Working |
DELETE /content/{id} | DeleteCard | Working |
Family & library
| Upstream endpoint | Go method | Status |
|---|---|---|
GET /user/family | GetUserFamily | Working |
GET /card/family/library/groups | ListLibraryGroups | Working |
GET /card/family/library/groups/{id} | GetLibraryGroup | Working |
POST /card/family/library/groups | CreateLibraryGroup | Working |
PUT /card/family/library/groups/{id} | UpdateLibraryGroup | Working |
DELETE /card/family/library/groups/{id} | DeleteLibraryGroup | Working |
Media
| Upstream endpoint | Go method | Status |
|---|---|---|
GET /media/transcode/audio/uploadUrl | UploadAudio | Working |
POST /media/displayIcons/user/me/upload | UploadIcon | Working |
POST /media/coverImage/user/me/upload | UploadCoverImage | Working |
GET /media/displayIcons/user/yoto | ListPublicIcons | Working |
GET /media/displayIcons/user/me | ListMyIcons | Working |
GET /media/family/images | ListFamilyImages | Working |
GET /media/family/images/{id} | GetFamilyImageURL | Working |
POST /media/family/images | UploadFamilyImage | Working |
Known API behaviours
MYO vs streaming cards
All cards appear in /content/mine. Distinguish them by:
- MYO / purchased:
config.OnlineOnlyisfalse(or absent), noeditSettings.RssUrl. Track format is"aac"with type"audio". - Streaming / podcast:
config.OnlineOnlyistrue, haseditSettings.RssUrl. Track type is"stream".
Device status embedded in config
GET /device-v2/{id}/status returns HTTP 403. Device status
(active card, battery level, charging state, etc.) is embedded in the config response.
Use GetDeviceInfo to access both settings and status.
Account email via family endpoint
/userinfo requires the openid scope (not in the default scope set).
To get the account email, call GetUserFamily() — the primary address is at
family.Members[0].Email.
Signed audio URLs
To get playable audio URLs, fetch a card with GetCardPlayable.
Track URLs are signed S3 URLs and are time-limited.
Live validation
A weekly scheduled CI pipeline runs the full integration test suite against the real Yoto API to detect upstream drift. The pipeline has two stages:
- refresh-token — exchanges the stored refresh token for a fresh access token and persists the rotated refresh token back to the CI variable. Yoto refresh tokens are single-use.
- smoke-test — runs integration tests with the fresh access token, including response shape assertions against every tracked field.
If a smoke run detects shape drift (new or removed fields), the pipeline fails and the
library is updated before the next YotoShelf release.
To rebaseline the field manifest after an intentional upstream change, update the
explore/ fixtures and re-run with REBASELINE=1.
Error handling
if errors.Is(err, yoto.ErrNotFound) {} // 404
if errors.Is(err, yoto.ErrUnauthorized) {} // 401
if errors.Is(err, yoto.ErrRateLimited) {} // 429
var apiErr *yoto.APIError
if errors.As(err, &apiErr) {
fmt.Printf("API error %d: %s\n", apiErr.StatusCode, apiErr.Message)
}