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

# YouTube

> Schedule YouTube video uploads — Shorts, long-form, COPPA flags, and first-comment via the Community surface.

## Quick reference

| Field                  | Value                                                             |
| ---------------------- | ----------------------------------------------------------------- |
| Title limit            | 100 characters                                                    |
| Description limit      | 5,000 characters                                                  |
| Tags limit             | 500 chars total (sum of tags)                                     |
| Videos per post        | 1                                                                 |
| Video formats          | MP4, MOV, WebM, MKV, FLV, AVI                                     |
| Max file size          | 256 GB (or 12 hours, whichever is less)                           |
| Max duration           | 12 hours (verified accounts), 15 minutes otherwise                |
| Shorts duration        | up to 60 seconds                                                  |
| Shorts aspect ratio    | 9:16                                                              |
| Long-form aspect ratio | 16:9 recommended                                                  |
| Post types             | Video upload (Short or long-form, derived from duration + aspect) |
| First comment          | ✅ Posts as a Community comment after upload                       |
| COPPA disclosure       | ✅ Required (`madeForKids` boolean)                                |

## Before you start

<Warning>
  YouTube **requires** every upload to declare `madeForKids: true | false`. The default in Postbreeze is `false`, but the choice matters — videos flagged Made For Kids have comments, notifications, and personalized ads disabled by YouTube. If you flag it wrong, you have to delete and re-upload. Always set this field explicitly so the decision is recorded in your code.
</Warning>

The connected YouTube channel must:

* Have completed the OAuth flow.
* Have a verified phone number (YouTube enforces this for any video > 15 minutes).
* Have `youtube.force-ssl` scope granted (Postbreeze requests this by default — covers upload + first-comment).

Each upload counts as **1,600 quota units** against the channel's 10,000-unit daily quota. The first-comment call costs an additional 50 quota units. Postbreeze surfaces quota errors as `YT_QUOTA_EXCEEDED`.

See [Platform settings](/concepts/platform-settings#youtube) for the full
`platformOptions` reference and [Media uploads](/concepts/media-uploads)
for the two ways to attach the video file (`mediaItems` URL ingest vs.
pre-uploaded `mediaIds`).

## Quick start

Workspace is inferred from your API key — no `workspaceId` argument.
The flat shape (`content` + `platforms`) is canonical; pass YouTube's
`madeForKids` flag in `platformOptions` on every upload.

<CodeGroup>
  ```js Node.js theme={null}
  import Postbreeze from "@postbreeze/node";

  const postbreeze = new Postbreeze({ apiKey: process.env.POSTBREEZE_API_KEY });

  await postbreeze.posts.create({
    content: "60-second product demo",
    scheduledFor: new Date(Date.now() + 30_000).toISOString(),
    mediaItems: [
      { type: "video", url: "https://cdn.example.com/demo.mp4" },
    ],
    platforms: [
      {
        accountId: "soc_youtube_…",
        platformOptions: {
          platform: "YOUTUBE",
          visibility: "PUBLIC",
          madeForKids: false,
        },
      },
    ],
  });
  ```

  ```python Python theme={null}
  import os
  from datetime import datetime, timedelta, timezone
  import requests

  API = "https://api.postbreeze.ai/api/v1"

  res = requests.post(
      f"{API}/posts",
      headers={"Authorization": f"Bearer {os.environ['POSTBREEZE_API_KEY']}"},
      json={
          "content": "60-second product demo",
          "scheduledFor": (datetime.now(timezone.utc) + timedelta(seconds=30)).isoformat(),
          "mediaItems": [
              {"type": "video", "url": "https://cdn.example.com/demo.mp4"},
          ],
          "platforms": [
              {
                  "accountId": "soc_youtube_…",
                  "platformOptions": {
                      "platform": "YOUTUBE",
                      "visibility": "PUBLIC",
                      "madeForKids": False,
                  },
              },
          ],
      },
  )
  res.raise_for_status()
  ```

  ```bash curl theme={null}
  curl -X POST https://api.postbreeze.ai/api/v1/posts \
    -H "Authorization: Bearer $POSTBREEZE_API_KEY" \
    -H "Content-Type: application/json" \
    -d '{
      "content": "60-second product demo",
      "scheduledFor": "2026-06-01T12:00:30Z",
      "mediaItems": [
        { "type": "video", "url": "https://cdn.example.com/demo.mp4" }
      ],
      "platforms": [
        {
          "accountId": "soc_youtube_…",
          "platformOptions": {
            "platform": "YOUTUBE",
            "visibility": "PUBLIC",
            "madeForKids": false
          }
        }
      ]
    }'
  ```
</CodeGroup>

## Content types

### Long-form video

Any video over 3 minutes, or any video that is not 9:16. The post `content` becomes the YouTube description; `youtubeTitle` (if set) becomes the YouTube title. If you don't pass `youtubeTitle`, the first line of `content` (≤ 100 chars) is used.

```js theme={null}
await postbreeze.posts.create({
  content: "Deep dive on our new dashboard.\n\nChapters, links, and resources below.",
  scheduledFor: "2026-06-10T15:00:00Z",
  mediaItems: [
    { type: "video", url: "https://cdn.example.com/dashboard-deep-dive.mp4" },
  ],
  platforms: [
    {
      accountId: "soc_youtube_…",
      platformOptions: {
        platform: "YOUTUBE",
        visibility: "PUBLIC",
        madeForKids: false,
        youtubeTitle: "Our new dashboard — deep dive (16 min)",
      },
    },
  ],
});
```

### Short

A video ≤ 3 minutes in 9:16 aspect ratio. YouTube auto-classifies it as a Short on their end — you don't pass a Shorts flag; just ensure the file matches Shorts requirements.

```js theme={null}
await postbreeze.posts.create({
  content: "How we ship on Fridays",
  scheduledFor: "2026-06-10T15:00:00Z",
  mediaItems: [
    { type: "video", url: "https://cdn.example.com/friday-ship.mp4" },
  ],
  platforms: [
    {
      accountId: "soc_youtube_…",
      platformOptions: {
        platform: "YOUTUBE",
        visibility: "PUBLIC",
        madeForKids: false,
      },
    },
  ],
});
```

### Unlisted preview

Set `visibility: "UNLISTED"` to upload without making the video discoverable. Useful for sharing with reviewers before public launch.

```js theme={null}
await postbreeze.posts.create({
  content: "Reviewer preview — please don't share",
  scheduledFor: new Date(Date.now() + 30_000).toISOString(),
  mediaItems: [
    { type: "video", url: "https://cdn.example.com/preview.mp4" },
  ],
  platforms: [
    {
      accountId: "soc_youtube_…",
      platformOptions: {
        platform: "YOUTUBE",
        visibility: "UNLISTED",
        madeForKids: false,
      },
    },
  ],
});
```

## Media requirements

### Videos

| Property               | Requirement                                             |
| ---------------------- | ------------------------------------------------------- |
| Videos per post        | 1                                                       |
| Formats                | MP4, MOV, WebM, MKV, FLV, AVI                           |
| Max file size          | 256 GB                                                  |
| Max duration           | 12 hours (verified accounts), 15 minutes (non-verified) |
| Shorts duration        | up to 3 minutes                                         |
| Shorts aspect ratio    | 9:16                                                    |
| Long-form aspect ratio | 16:9 recommended                                        |
| Codecs                 | H.264, VP9, AV1                                         |
| Frame rate             | 24–60 fps                                               |
| Audio codecs           | AAC, MP3, Vorbis, Opus                                  |

YouTube targets accept a **single video**. Images, GIFs, and documents
are rejected at validation. See [Media uploads](/concepts/media-uploads)
for the URL ingest vs. pre-upload trade-off.

## Platform-specific fields

| Field          | Type                                      | Required              | Description                                                                                       |
| -------------- | ----------------------------------------- | --------------------- | ------------------------------------------------------------------------------------------------- |
| `platform`     | `"YOUTUBE"`                               | Yes                   | Discriminator.                                                                                    |
| `visibility`   | `"PUBLIC"` \| `"UNLISTED"` \| `"PRIVATE"` | No (default `PUBLIC`) | Honored by YouTube exactly as set.                                                                |
| `madeForKids`  | boolean                                   | No (default `false`)  | COPPA disclosure. Disables comments + personalized ads when true. Set explicitly on every upload. |
| `youtubeTitle` | string                                    | No                    | Override for the title (≤ 100 chars). Falls back to first line of `content`.                      |

Full schema in [Platform settings → YouTube](/concepts/platform-settings#youtube).

## First comment

YouTube exposes a `commentThreads.insert` endpoint that lets the channel owner post a top-level comment on a video they own. Postbreeze calls it after the upload completes — it costs 50 quota units, comes back with a `commentId`, and posts as the channel itself (not as a personal Google account).

```js theme={null}
await postbreeze.posts.create({
  content: "Our new dashboard in 60 seconds",
  scheduledFor: new Date(Date.now() + 30_000).toISOString(),
  mediaItems: [
    { type: "video", url: "https://cdn.example.com/demo.mp4" },
  ],
  platforms: [
    {
      accountId: "soc_youtube_…",
      platformOptions: {
        platform: "YOUTUBE",
        visibility: "PUBLIC",
        madeForKids: false,
      },
      firstComment: "Timestamps in the description ⬇️",
    },
  ],
});
```

<Warning>
  If the video has comments disabled (e.g. `madeForKids: true`, or you've set channel-wide comments off), the first-comment call fails with `YT_COMMENTS_DISABLED` and the warning banner appears on the post. The main upload still succeeds.
</Warning>

Comment limit: 10,000 characters.

## Analytics

| Metric                                   | Available                                    |
| ---------------------------------------- | -------------------------------------------- |
| Views                                    | ✅                                            |
| Watch time (minutes)                     | ✅                                            |
| Avg view duration                        | ✅                                            |
| Likes                                    | ✅                                            |
| Dislikes                                 | ❌ (YouTube removed this from the public API) |
| Comments                                 | ✅                                            |
| Subscribers gained from video            | ✅                                            |
| Shares                                   | ✅                                            |
| Click-through rate (impressions → views) | ✅                                            |
| Audience demographics                    | ✅ (channel-level)                            |

Refresh cadence: every **14 days**. Per YouTube's retention rule, raw `MetricSnapshot` rows are purged on disconnect or on platform 404 (video deleted, channel suspended).

## Common errors

| Error                       | Meaning                                                       | Fix                                                                                                       |
| --------------------------- | ------------------------------------------------------------- | --------------------------------------------------------------------------------------------------------- |
| `YT_QUOTA_EXCEEDED`         | Channel hit the 10,000-unit daily quota                       | Wait for the daily reset. The first-comment call is skipped if the upload alone burns most of the budget. |
| `YT_COMMENTS_DISABLED`      | First-comment failed because the video doesn't allow comments | Disable `madeForKids` for the video, or accept that first-comment is impossible on this content.          |
| `YT_INVALID_VIDEO_FORMAT`   | Container or codec YouTube doesn't accept                     | Re-encode to H.264 + AAC.                                                                                 |
| `YT_MADE_FOR_KIDS_REQUIRED` | The COPPA disclosure was missing                              | Pass `madeForKids` explicitly.                                                                            |
| `YT_UPLOAD_QUOTA_EXCEEDED`  | More than the daily upload count for unverified accounts      | Verify the channel's phone number.                                                                        |
| `YT_TITLE_TOO_LONG`         | Title > 100 chars                                             | Trim.                                                                                                     |
| `YT_DESCRIPTION_TOO_LONG`   | Description > 5,000 chars                                     | Trim.                                                                                                     |
| `YT_TOKEN_EXPIRED`          | OAuth refresh failed                                          | Reconnect.                                                                                                |

## What you can't do

* ❌ Edit video metadata after upload (planned for v2)
* ❌ Schedule using YouTube Studio's native scheduler (we run our own queue)
* ❌ Upload to a Brand Account other than the connected one
* ❌ Set a custom thumbnail via API (works only for long-form videos on
  verified channels and isn't exposed in v1)
* ❌ Pass tags, override the category, or flag synthetic media —
  Postbreeze applies server-side defaults (category `22` — People & Blogs)
* ❌ Add to playlists at upload time
* ❌ Premieres or Live streams
* ❌ Community posts (text/poll/image posts) — only video uploads + first-comments
* ❌ Set end screens or cards

## Full control: nested shape

The nested shape (`caption` + `targets`, `socialAccountId` per target) is
the lower-level surface the flat shape compiles down to. Use it when you
want a single payload that targets multiple platforms with distinct
per-platform captions or media. The YouTube `platformOptions` are
identical.

```js theme={null}
import Postbreeze from "@postbreeze/node";

const postbreeze = new Postbreeze({ apiKey: process.env.POSTBREEZE_API_KEY });

await postbreeze.posts.create({
  caption: "60-second product demo",
  scheduledAt: new Date(Date.now() + 30_000).toISOString(),
  mediaIds: ["med_video_…"],
  targets: [
    {
      socialAccountId: "soc_youtube_…",
      platformOptions: {
        platform: "YOUTUBE",
        visibility: "PUBLIC",
        madeForKids: false,
        youtubeTitle: "Our new dashboard in 60s",
      },
      firstComment: "Timestamps in the description ⬇️",
    },
  ],
});
```
