Overview
The YouTube publishing workflow lets you upload rendered quiz videos directly
to a connected YouTube channel. The typical flow is:
- Render the quiz video (see Video Rendering)
- Generate metadata — use AI to create an optimized title, description, and tags
- Create an upload job — the worker uploads the video to YouTube asynchronously
- Poll or use webhooks to track upload progress
YouTube publishing requires a connected YouTube channel. Connect your channel
in the Quiz Quail dashboard under Settings > YouTube.
Before uploading, you can generate YouTube-optimized metadata from your quiz content:
curl -X POST https://app.quiz-quail.com/api/v1/youtube/generate-metadata \
-H "Authorization: Bearer YOUR_API_KEY" \
-H "Content-Type: application/json" \
-d '{ "quizId": "QUIZ_ID" }'
{
"data": {
"title": "Can You Beat This Ultimate Space Quiz? 🚀",
"description": "Test your knowledge of the cosmos...\n\nMade with Quiz Quail",
"tags": ["space quiz", "astronomy", "trivia", "planets", "solar system"]
}
}
Results are cached per quiz. To regenerate, pass "force": true:
curl -X POST https://app.quiz-quail.com/api/v1/youtube/generate-metadata \
-H "Authorization: Bearer YOUR_API_KEY" \
-H "Content-Type: application/json" \
-d '{ "quizId": "QUIZ_ID", "force": true }'
Create an upload job
Once you have a completed render and your metadata, create an upload job:
curl -X POST https://app.quiz-quail.com/api/v1/youtube/uploads \
-H "Authorization: Bearer YOUR_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"quizId": "QUIZ_ID",
"renderJobId": "RENDER_JOB_ID",
"channelId": "CHANNEL_ID",
"metadata": {
"title": "Can You Beat This Ultimate Space Quiz?",
"description": "Test your knowledge of the cosmos...",
"tags": ["space quiz", "astronomy", "trivia"],
"visibility": "unlisted",
"categoryId": "27",
"madeForKids": false
}
}'
The response has status 202 Accepted:
{
"data": {
"id": "upload_xyz789",
"status": "pending",
"created_at": "2026-03-15T12:05:00Z"
}
}
| Field | Type | Default | Description |
|---|
title | string | Required | Video title (1–100 characters) |
description | string | "" | Video description (max 5,000 characters) |
tags | string[] | [] | YouTube tags for search discovery |
categoryId | string | "27" | YouTube category ID (27 = Education) |
visibility | string | "unlisted" | "public", "unlisted", or "private" |
madeForKids | boolean | false | COPPA compliance flag |
scheduledFor | string | — | ISO 8601 datetime to schedule publication |
Scheduling uploads
Set scheduledFor to publish a video at a future time. The video is uploaded
immediately as private and automatically published at the scheduled time.
{
"metadata": {
"title": "Friday Quiz Challenge",
"visibility": "private",
"scheduledFor": "2026-03-21T18:00:00Z"
}
}
Scheduling constraints:
- Must be in the future
- Cannot be more than 6 months ahead
Poll upload status
curl https://app.quiz-quail.com/api/v1/youtube/uploads/upload_xyz789 \
-H "Authorization: Bearer YOUR_API_KEY"
{
"data": {
"id": "upload_xyz789",
"status": "processing",
"progress": 80,
"title": "Can You Beat This Ultimate Space Quiz?",
"visibility": "unlisted",
"youtube_video_id": null,
"youtube_video_url": null,
"created_at": "2026-03-15T12:05:00Z"
}
}
Upload statuses
| Status | Description |
|---|
pending | Job created, waiting for worker |
uploading | Video is being uploaded to YouTube |
processing | YouTube is processing the video |
scheduled | Video uploaded and scheduled for future publish |
done | Upload complete — video is live |
failed | Upload failed (see error_message) |
cancelled | Upload was cancelled by the user |
When the upload finishes, the response includes the YouTube video URL:
{
"data": {
"id": "upload_xyz789",
"status": "done",
"youtube_video_id": "dQw4w9WgXcQ",
"youtube_video_url": "https://youtube.com/watch?v=dQw4w9WgXcQ",
"completed_at": "2026-03-15T12:08:30Z"
}
}
List uploads
curl "https://app.quiz-quail.com/api/v1/youtube/uploads?status=done&limit=5" \
-H "Authorization: Bearer YOUR_API_KEY"
| Parameter | Type | Description |
|---|
quizId | string | Filter by quiz |
status | string | Filter by upload status |
limit | number | Results per page (1–100, default 20) |
cursor | string | Pagination cursor |
Using webhooks
Register webhooks to get notified when uploads complete or fail.
youtube.uploaded
{
"upload_id": "upload_xyz789",
"quiz_id": "QUIZ_ID",
"youtube_video_id": "dQw4w9WgXcQ",
"youtube_url": "https://youtube.com/watch?v=dQw4w9WgXcQ",
"title": "Can You Beat This Ultimate Space Quiz?",
"uploaded_at": "2026-03-15T12:08:30Z"
}
youtube.failed
{
"upload_id": "upload_xyz789",
"quiz_id": "QUIZ_ID",
"error": "YouTube API quota exceeded",
"failed_at": "2026-03-15T12:06:00Z"
}
This end-to-end example renders a quiz video, generates AI metadata, and uploads
it to YouTube.
const API_KEY = "your_api_key";
const BASE = "https://app.quiz-quail.com/api/v1";
async function renderAndUpload(quizId, channelId) {
const headers = {
Authorization: `Bearer ${API_KEY}`,
"Content-Type": "application/json",
};
// 1. Create render job
const renderRes = await fetch(`${BASE}/renders`, {
method: "POST",
headers,
body: JSON.stringify({ quiz_id: quizId }),
});
const { data: renderJob } = await renderRes.json();
// 2. Poll render until done
let render = renderJob;
while (render.status !== "done" && render.status !== "failed") {
await new Promise((r) => setTimeout(r, 5000));
const res = await fetch(`${BASE}/renders/${render.id}`, { headers });
render = (await res.json()).data;
}
if (render.status === "failed") {
throw new Error(`Render failed: ${render.error_message}`);
}
// 3. Generate YouTube metadata
const metaRes = await fetch(`${BASE}/youtube/generate-metadata`, {
method: "POST",
headers,
body: JSON.stringify({ quizId }),
});
const { data: metadata } = await metaRes.json();
// 4. Create upload job
const uploadRes = await fetch(`${BASE}/youtube/uploads`, {
method: "POST",
headers,
body: JSON.stringify({
quizId,
renderJobId: render.id,
channelId,
metadata: {
title: metadata.title,
description: metadata.description,
tags: metadata.tags,
visibility: "unlisted",
},
}),
});
const { data: uploadJob } = await uploadRes.json();
// 5. Poll upload until done
let upload = uploadJob;
while (upload.status !== "done" && upload.status !== "failed") {
await new Promise((r) => setTimeout(r, 5000));
const res = await fetch(`${BASE}/youtube/uploads/${upload.id}`, { headers });
upload = (await res.json()).data;
}
if (upload.status === "failed") {
throw new Error(`Upload failed: ${upload.error_message}`);
}
console.log(`Published: ${upload.youtube_video_url}`);
return upload;
}
Required scopes: renders:write + renders:read for rendering,
youtube:write + youtube:read for metadata generation and uploads.