Skip to main content

Overview

The YouTube publishing workflow lets you upload rendered quiz videos directly to a connected YouTube channel. The typical flow is:
  1. Render the quiz video (see Video Rendering)
  2. Generate metadata — use AI to create an optimized title, description, and tags
  3. Create an upload job — the worker uploads the video to YouTube asynchronously
  4. 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.

Generate AI metadata

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"
  }
}

Metadata fields

FieldTypeDefaultDescription
titlestringRequiredVideo title (1–100 characters)
descriptionstring""Video description (max 5,000 characters)
tagsstring[][]YouTube tags for search discovery
categoryIdstring"27"YouTube category ID (27 = Education)
visibilitystring"unlisted""public", "unlisted", or "private"
madeForKidsbooleanfalseCOPPA compliance flag
scheduledForstringISO 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

StatusDescription
pendingJob created, waiting for worker
uploadingVideo is being uploaded to YouTube
processingYouTube is processing the video
scheduledVideo uploaded and scheduled for future publish
doneUpload complete — video is live
failedUpload failed (see error_message)
cancelledUpload 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"
ParameterTypeDescription
quizIdstringFilter by quiz
statusstringFilter by upload status
limitnumberResults per page (1–100, default 20)
cursorstringPagination 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"
}

Full example: render, generate metadata, and upload

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.