Skip to content
The Yoto Developer Challenge is on, with $10,000 to win!

Uploading content to MYO cards

First, get request a secure URL via the /uploadUrl endpoint. This is a temporary URL on our servers where you’ll upload the audio file. This URL is secure and specific to this upload:

const uploadUrlResponse = await fetch(
"https://api.yotoplay.com/media/transcode/audio/uploadUrl",
{
method: "GET",
headers: {
Authorization: `Bearer ${accessToken}`,
Accept: "application/json",
},
}
);
const {
upload: { uploadUrl: audioUploadUrl, uploadId },
} = await uploadUrlResponse.json();
if (!audioUploadUrl) {
throw new Error("Failed to get upload URL");
}

The response gives us an upload object with the following properties:

  • uploadUrl: The URL where we’ll upload our audio file
  • uploadId: A unique identifier we’ll use to check the transcoding status of the audio file.

Now you can upload you actual audio file to the URL we received with a PUT request:

await fetch(audioUploadUrl, {
method: "PUT",
body: new Blob([audioFile], {
type: audioFile.type,
ContentDisposition: audioFile.name,
}),
headers: {
"Content-Type": audioFile.type,
},
});

Make sure to set the Content-Type header to match the type of your audio file.

After upload, Yoto needs to transcode the audio to make it compatible with our Yoto players. We need to keep polling the API until the process is complete:

let transcodedAudio = null;
let attempts = 0;
const maxAttempts = 30;
while (attempts < maxAttempts) {
const transcodeResponse = await fetch(
`https://api.yotoplay.com/media/upload/${uploadId}/transcoded?loudnorm=false`,
{
headers: {
Authorization: `Bearer ${accessToken}`,
Accept: "application/json",
},
}
);
if (transcodeResponse.ok) {
const data = await transcodeResponse.json();
if (data.transcode.transcodedSha256) {
transcodedAudio = data.transcode;
break;
}
}
await new Promise((resolve) => setTimeout(resolve, 500));
attempts++;
}
if (!transcodedAudio) {
throw new Error("Transcoding timed out");
}

We know that the transcoding is complete when we receive a transcodedSha256 in the response. This is a hash of the transcoded audio file. We’ll use this value to put our audio file in a track.

We add our audio file to a track, using our transcoded value, and insert that track into a chapter. This creates a new playlist with your audio content.

// Get media info from the transcoded audio
const mediaInfo = transcodedAudio.transcodedInfo;
const chapterTitle = mediaInfo?.metadata?.title || title;
const chapters = [
{
key: "01",
title: chapterTitle,
overlayLabel: "1",
tracks: [
{
key: "01",
title: chapterTitle,
trackUrl: `yoto:#${transcodedAudio.transcodedSha256}`,
duration: mediaInfo?.duration,
fileSize: mediaInfo?.fileSize,
channels: mediaInfo?.channels,
format: mediaInfo?.format,
type: "audio",
overlayLabel: "1",
display: {
icon16x16: "yoto:#aUm9i3ex3qqAMYBv-i-O-pYMKuMJGICtR3Vhf289u2Q",
},
},
],
display: {
icon16x16: "yoto:#aUm9i3ex3qqAMYBv-i-O-pYMKuMJGICtR3Vhf289u2Q",
},
},
];
// Create the complete content object for a new playlist
const content = {
title: title,
content: {
chapters,
},
metadata: {
media: {
duration: mediaInfo?.duration,
fileSize: mediaInfo?.fileSize,
readableFileSize:
Math.round((mediaInfo?.fileSize / 1024 / 1024) * 10) / 10,
},
},
};
const createResponse = await fetch("https://api.yotoplay.com/content", {
method: "POST",
headers: {
Authorization: `Bearer ${accessToken}`,
"Content-Type": "application/json",
},
body: JSON.stringify(content),
});
if (!createResponse.ok) {
const errorText = await createResponse.text();
throw new Error(`Failed to create playlist: ${errorText}`);
}
const result = await createResponse.json();

The process is complete when this final request returns successfully. Your audio should now appear in your library as a new playlist. You can now link this playlist to a Make Your Own (MYO) card via your Yoto player or the Yoto app.

Here’s the complete code:

export const uploadToCard = async ({ audioFile, title, accessToken }) => {
// Step 1: Get upload URL for audio with SHA256
const uploadUrlResponse = await fetch(
"https://api.yotoplay.com/media/transcode/audio/uploadUrl",
{
method: "GET",
headers: {
Authorization: `Bearer ${accessToken}`,
Accept: "application/json",
},
}
);
const {
upload: { uploadUrl: audioUploadUrl, uploadId },
} = await uploadUrlResponse.json();
if (!audioUploadUrl) {
throw new Error("Failed to get upload URL");
}
// Step 2: Upload the audio file
console.log("Progress: uploading - 0%");
await fetch(audioUploadUrl, {
method: "PUT",
body: new Blob([audioFile], {
type: audioFile.type,
ContentDisposition: audioFile.name,
}),
headers: {
"Content-Type": audioFile.type,
},
});
console.log("Progress: transcoding - 50%");
// Step 3: Wait for transcoding (with timeout)
let transcodedAudio = null;
let attempts = 0;
const maxAttempts = 30;
while (attempts < maxAttempts) {
const transcodeResponse = await fetch(
`https://api.yotoplay.com/media/upload/${uploadId}/transcoded?loudnorm=false`,
{
headers: {
Authorization: `Bearer ${accessToken}`,
Accept: "application/json",
},
}
);
if (transcodeResponse.ok) {
const data = await transcodeResponse.json();
if (data.transcode.transcodedSha256) {
console.log("Transcoded audio:", data.transcode);
transcodedAudio = data.transcode;
break;
}
}
await new Promise((resolve) => setTimeout(resolve, 500));
attempts++;
console.log(
`Progress: transcoding - ${50 + (attempts / maxAttempts) * 25}%`
);
}
if (!transcodedAudio) {
throw new Error("Transcoding timed out");
}
// Get media info from the transcoded audio
const mediaInfo = transcodedAudio.transcodedInfo;
console.log("Media info:", mediaInfo);
// Step 4: Create new card content
console.log("Progress: creating_card - 85%");
const chapterTitle = mediaInfo?.metadata?.title || title;
const chapters = [
{
key: "01",
title: chapterTitle,
overlayLabel: "1",
tracks: [
{
key: "01",
title: chapterTitle,
trackUrl: `yoto:#${transcodedAudio.transcodedSha256}`,
duration: mediaInfo?.duration,
fileSize: mediaInfo?.fileSize,
channels: mediaInfo?.channels,
format: mediaInfo?.format,
type: "audio",
overlayLabel: "1",
display: {
icon16x16: "yoto:#aUm9i3ex3qqAMYBv-i-O-pYMKuMJGICtR3Vhf289u2Q",
},
},
],
display: {
icon16x16: "yoto:#aUm9i3ex3qqAMYBv-i-O-pYMKuMJGICtR3Vhf289u2Q",
},
},
];
// Create the complete content object for a new card
const content = {
title: title,
content: {
chapters,
},
metadata: {
media: {
duration: mediaInfo?.duration,
fileSize: mediaInfo?.fileSize,
readableFileSize:
Math.round((mediaInfo?.fileSize / 1024 / 1024) * 10) / 10,
},
},
};
console.log("Creating card with data:", content);
// Step 5: Create the new card
const createCardResponse = await fetch("https://api.yotoplay.com/content", {
method: "POST",
headers: {
Authorization: `Bearer ${accessToken}`,
"Content-Type": "application/json",
},
body: JSON.stringify(content),
});
if (!createCardResponse.ok) {
const errorText = await createCardResponse.text();
throw new Error(`Failed to create card: ${errorText}`);
}
console.log("Progress: complete - 100%");
return await createCardResponse.json();
};