Unofficial Node.js SDK for the Yoto API. Built with TypeScript and optimized for Bun runtime.
# Using Bun
bun add yoto-js
# Using npm
npm install yoto-js
# Using yarn
yarn add yoto-jsimport { Yoto } from "yoto-js";
const yoto = new Yoto({ accessToken: "your_access_token" });
// Get content
const content = await yoto.content.get("content_id");
// List devices
const devices = await yoto.devices.list();
// Send device command
await yoto.devices.sendCommand("device_id", { command: "play" });The SDK supports multiple authentication methods. All methods use OAuth 2.0 bearer tokens.
Get your credentials: Before using any authentication method, obtain your client ID from the Yoto Developer Portal.
Choose the approach that fits your use case:
If you already have an access token (e.g., from manual OAuth flow or testing):
const yoto = new Yoto({ accessToken: "your_access_token" });Note: Static tokens don't auto-refresh. For production apps, use an auth provider (see below).
For command-line tools and server-side applications without a browser UI, use the device code flow with automatic token refresh:
import { YotoDeviceAuth, Yoto } from "yoto-js";
// Initialize auth client with your client ID
const auth = new YotoDeviceAuth("your-client-id");
// Start device login
const deviceCode = await auth.initiateDeviceLogin();
// Display to user
console.log("Visit:", deviceCode.verification_uri);
console.log("Enter code:", deviceCode.user_code);
// Poll for completion
const tokens = await auth.pollForToken(
deviceCode.device_code,
deviceCode.interval
);
// Create auth provider with automatic token refresh (recommended)
const authProvider = auth.createProvider(tokens);
const yoto = new Yoto({ auth: authProvider });
// The client will automatically refresh tokens as needed
await yoto.content.listMYO(); // Works indefinitelyLearn more: Yoto Device Code Authentication
For web applications, use the PKCE-based browser flow with automatic token refresh:
import { YotoBrowserAuth, Yoto } from "yoto-js";
// Initialize auth client
const auth = new YotoBrowserAuth(
"your-client-id",
"https://your-app.com/callback"
);
// Step 1: Generate auth URL
const { url, codeVerifier } = await auth.generateAuthUrl();
// Store verifier and redirect
sessionStorage.setItem("pkce_code_verifier", codeVerifier);
window.location.href = url;
// Step 2: On callback, exchange code for tokens
const params = new URLSearchParams(window.location.search);
const code = params.get("code");
const storedVerifier = sessionStorage.getItem("pkce_code_verifier");
const tokens = await auth.exchangeCodeForTokens(code, storedVerifier);
// Create auth provider with automatic token refresh (recommended)
const authProvider = auth.createProvider(tokens);
const yoto = new Yoto({ auth: authProvider });
// The client will automatically refresh tokens as needed
await yoto.content.listMYO(); // Works indefinitelyLearn more: Yoto Browser Authentication
All clients support custom configuration:
const yoto = new Yoto({
accessToken: "your_access_token",
baseUrl: "https://api.yotoplay.com", // Custom API base URL (default)
timeout: 30000, // Request timeout in milliseconds (default: 30000)
maxRetries: 3, // Maximum retry attempts (default: 3)
headers: {
// Custom headers
"X-Custom-Header": "value",
},
});
// Auth clients also support configuration
const auth = new YotoDeviceAuth("client-id", {
authBaseUrl: "https://login.yotoplay.com", // Custom auth URL
apiAudience: "https://api.yotoplay.com", // API audience
timeout: 30000, // Request timeout
});Implement the AuthProvider interface for custom authentication:
import { AuthProvider, Yoto } from "yoto-js";
class CustomAuthProvider implements AuthProvider {
async getAccessToken(): Promise<string> {
// Your custom logic to obtain/refresh tokens
return "access_token";
}
}
const yoto = new Yoto({ auth: new CustomAuthProvider() });Manage Make Your Own (MYO) content.
// Get content by ID
const content = await yoto.content.get("content_id");
// Create content with uploaded audio
const newContent = await yoto.content.create({
title: "My Playlist",
content: {
chapters: [
{
key: "01",
title: "Chapter 1",
overlayLabel: "1",
display: { icon16x16: null },
tracks: [
{
key: "01",
title: "Track 1",
trackUrl: "yoto:#<sha256-hash>",
overlayLabel: "1",
duration: 180,
fileSize: 2048000,
channels: 2,
format: "mp3",
type: "audio",
},
],
},
],
},
metadata: {
description: "A collection of favorite songs",
},
});
// Create content with streaming tracks
const streamingContent = await yoto.content.create({
title: "Weather Updates",
content: {
chapters: [
{
key: "01",
title: "Live Weather",
overlayLabel: "1",
display: { icon16x16: null },
tracks: [
{
key: "01",
title: "Current Weather",
trackUrl: "https://example.com/weather-stream.mp3",
type: "stream",
format: "mp3",
overlayLabel: "1",
duration: 0,
fileSize: 0,
channels: 2,
display: {
icon16x16: "yoto:#<icon-hash>",
},
},
],
},
],
},
metadata: {
description: "Dynamic weather updates",
},
});
// Update content
const updated = await yoto.content.update("content_id", {
title: "Updated Title",
});
// Delete content
await yoto.content.delete("content_id");
// List MYO content
const myoContent = await yoto.content.listMYO();Control and configure Yoto devices.
// List all devices
const devices = await yoto.devices.list();
// Get device status
const status = await yoto.devices.getStatus("device_id");
// Get device configuration
const config = await yoto.devices.getConfig("device_id");
// Update device configuration
await yoto.devices.updateConfig("device_id", {
name: "Living Room Player",
volume: 50,
maxVolume: 80,
nightLightEnabled: true,
});
// Send command to device
await yoto.devices.sendCommand("device_id", { command: "play" });
// Available commands: play, pause, next, previous, volumeUp, volumeDown, stop
// Update shortcuts (beta)
await yoto.devices.updateShortcuts("device_id", {
shortcuts: [
{ position: 1, contentId: "content_id_1" },
{ position: 2, contentId: "content_id_2" },
],
});Manage family images.
// List all family images
const images = await yoto.family.listImages();
// Get specific image
const image = await yoto.family.getImage("image_id");
// Upload image
const uploaded = await yoto.family.uploadImage(fileBuffer, "family-photo.jpg");Organize content into groups.
// List all groups
const groups = await yoto.familyLibraryGroups.list();
// Create group
const group = await yoto.familyLibraryGroups.create({
name: "Bedtime Stories",
description: "Stories for bedtime",
items: [
{ contentId: "content_1" },
{ contentId: "content_2" },
],
});
// Get group
const group = await yoto.familyLibraryGroups.get("group_id");
// Update group
await yoto.familyLibraryGroups.update("group_id", {
name: "Updated Name",
});
// Delete group
await yoto.familyLibraryGroups.delete("group_id");Manage content icons.
// List public icons
const publicIcons = await yoto.icons.listPublic();
// List user icons
const userIcons = await yoto.icons.listUser();
// Upload custom icon
const icon = await yoto.icons.upload(iconBuffer, "my-icon");Handle media uploads.
// Audio upload workflow
// 1. Get upload URL
const { upload } = await yoto.media.getAudioUploadUrl();
// 2. Upload audio file to the URL (using fetch or other HTTP client)
await fetch(upload.uploadUrl, {
method: "PUT",
body: audioFileBuffer,
headers: { "Content-Type": "audio/mpeg" },
});
// 3. Poll for transcode completion
let transcoded = null;
while (!transcoded) {
const status = await yoto.media.getTranscodeStatus(upload.uploadId);
if (status.transcode.transcodedSha256) {
transcoded = status.transcode;
break;
}
await new Promise((resolve) => setTimeout(resolve, 500));
}
// 4. Use the transcoded SHA256 in your content
const trackUrl = `yoto:#${transcoded.transcodedSha256}`;
// Upload cover image
const { coverImage } = await yoto.media.uploadCoverImage(imageBuffer, {
autoconvert: true,
coverType: "default",
});
// Use coverImage.mediaUrl in content metadata.cover.imageLThe SDK provides specific error classes for different failure scenarios:
import {
YotoError,
YotoAuthenticationError,
YotoAPIError,
YotoRateLimitError,
YotoConnectionError,
} from "yoto-js";
try {
const content = await yoto.content.get("invalid_id");
} catch (error) {
if (error instanceof YotoAuthenticationError) {
console.error("Invalid API key");
} else if (error instanceof YotoRateLimitError) {
console.error(`Rate limited. Retry after ${error.retryAfter} seconds`);
} else if (error instanceof YotoAPIError) {
console.error(`API error: ${error.message} (${error.statusCode})`);
} else if (error instanceof YotoConnectionError) {
console.error("Connection failed");
}
}All error classes extend YotoError and include:
message: Error descriptionstatusCode: HTTP status code (when applicable)type: Error type identifierrequestId: Yoto request ID for debugging
The SDK is written in TypeScript and provides full type definitions:
import type { Content, Device, DeviceCommand } from "yoto-js";
const content: Content = await yoto.content.get("content_id");
const command: DeviceCommand = "play";bun run buildbun testbun run format
bun run lint
bun run checkEnd-to-end validation scripts test real API interactions and ensure SDK types match actual API behavior. The Yoto developer documentation isn't always accurate or complete, so these scripts validate against live endpoints.
Set your access token as an environment variable in .env:
export YOTO_ACCESS_TOKEN="your_access_token"Run validations:
# Validate content operations
bun run validate:content
# Validate family library groups
bun run validate:groups
# Validate family images
bun run validate:images
# Validate read-only endpoints
bun run validate:readonly
# Run all validations
bun run validate:all- Node.js 18+ or Bun 1.0+
- TypeScript 5+ (for TypeScript users)
MIT
Contributions are welcome!