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
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.
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 for the full
platformOptions reference and 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.
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,
},
},
],
});
Content types
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.
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.
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.
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,
},
},
],
});
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
for the URL ingest vs. pre-upload trade-off.
| 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.
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).
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 ⬇️",
},
],
});
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.
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.
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 ⬇️",
},
],
});