> ## Documentation Index
> Fetch the complete documentation index at: https://docs.quiz-quail.com/llms.txt
> Use this file to discover all available pages before exploring further.

# YouTube Publishing

> Upload rendered videos to YouTube with AI-generated metadata

## 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](/guides/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

<Note>
  YouTube publishing requires a connected YouTube channel. Connect your channel
  in the Quiz Quail dashboard under **Settings > YouTube**.
</Note>

## Generate AI metadata

Before uploading, you can generate YouTube-optimized metadata from your quiz content:

```bash theme={null}
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" }'
```

```json theme={null}
{
  "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`:

```bash theme={null}
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:

```bash theme={null}
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`:

```json theme={null}
{
  "data": {
    "id": "upload_xyz789",
    "status": "pending",
    "created_at": "2026-03-15T12:05:00Z"
  }
}
```

### Metadata fields

| 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.

```json theme={null}
{
  "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

```bash theme={null}
curl https://app.quiz-quail.com/api/v1/youtube/uploads/upload_xyz789 \
  -H "Authorization: Bearer YOUR_API_KEY"
```

```json theme={null}
{
  "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:

```json theme={null}
{
  "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

```bash theme={null}
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`

```json theme={null}
{
  "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`

```json theme={null}
{
  "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.

```javascript theme={null}
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;
}
```

<Info>
  **Required scopes:** `renders:write` + `renders:read` for rendering,
  `youtube:write` + `youtube:read` for metadata generation and uploads.
</Info>
