Zernio
Zernio
Overview

Social Posting

XInstagramFacebookLinkedInTikTokYouTubePinterestRedditBlueskyThreadsGoogle Business ProfileSnapchat

Messaging

WhatsAppTelegramDiscordSlack

Telephony

Phone NumbersVoice & CallsSMS

Advertising

Meta AdsGoogle AdsLinkedIn AdsTikTok AdsPinterest AdsX AdsOpenAI Ads

Commerce

Shopify
Dashboard
llms.txtOpenAPI
OverviewPlatformsAPI ReferenceResources

YouTube

Publish videos and Shorts to YouTube with the Zernio API, with custom thumbnails, visibility, playlists, COPPA and AI disclosure flags, and description edits after publishing.


Publish videos and Shorts to YouTube with POST /v1/posts and platform: "youtube". The same account serves analytics and comments.

Quick reference

PropertyValue
Title limit100 characters
Description limit5,000 characters
TagsTop-level tags array on the create request; 100 characters per tag, 500 combined
Videos per post1
Video formatsMP4, MOV, AVI, WMV, FLV, 3GP, WebM
Video max size256 GB
Video max duration15 minutes (unverified channel), 12 hours (verified)
Thumbnail formatsJPEG, PNG, GIF
Thumbnail max size2 MB
Post typesVideo, Shorts
SchedulingYes (a public video is uploaded early as private and released by YouTube at the scheduled time)
Editing published postsYes (description only; title and tags through a separate endpoint)
Inbox (comments)Yes
Inbox (DMs)No (YouTube has no DM system)
AnalyticsYes

Before you start

YouTube requires a channel owned by the Google identity you authorize, on a personal Google account or a Brand Account; see Brand Accounts and multiple channels. Every post is exactly one video, so there are no image-only or text-only posts. Unverified channels are limited to 15-minute videos; verify the channel by phone at youtube.com/verify for longer uploads. Daily upload quotas vary by channel, and Shorts are detected from duration and aspect ratio, not chosen with a flag.

If a channel is suspended, every upload fails with a 403. Call Account health before scheduling posts to a channel you do not control.

Connect

Call GET /v1/connect/youtube with profileId on Get OAuth connect URL. The connecting accounts guide covers the OAuth flow and scopes in general; Account health reports what a connected account can do with the scopes the user granted.

OAuth scopes

ScopeWhat it enables
https://www.googleapis.com/auth/youtube.uploadUpload videos to the channel
https://www.googleapis.com/auth/youtubeManage the channel: video metadata, playlists, thumbnails
https://www.googleapis.com/auth/youtube.force-sslRead and post comments
https://www.googleapis.com/auth/yt-analytics.readonlyChannel and video analytics

Brand Accounts and multiple channels

There is no channel picker. YouTube connects straight after Google's OAuth screen, and the channel Zernio connects is the one owned by the Google identity you pick in Google's account chooser. Zernio forces that chooser on every connect (prompt=select_account), so the choice is always yours to make:

  • A channel on your personal Google account: pick your personal identity.
  • A channel that lives on a Brand Account: pick the Brand Account entry in the chooser, not your personal identity. You need owner or manager access to the Brand Account at account.google.com/brandaccounts.
  • YouTube Studio "Manage access" permissions do not grant API access. Someone added as an editor or manager only inside YouTube Studio cannot connect that channel; a Brand Account owner or manager has to do it.

If the identity you picked owns no channel (personal identity chosen by mistake, Studio-only access, or no channel created yet), the connect fails with We couldn't find a YouTube channel for the Google account you authorized.... Restart the flow and pick the right entry in the chooser.

A profile holds one YouTube channel. To connect another one, create a second profile, select it, start the YouTube connect again, and pick the other channel's identity in the chooser. Each channel then has its own accountId.

Publish

A plain post becomes a public video whose title is the first line of content and whose description is the whole of content. YouTube classifies it as a Short on its own when it is 3 minutes or shorter and vertical.

Video

Long-form content: longer than 3 minutes or horizontal. 16:9 is the recommended aspect ratio, and a thumbnail on the media item sets the custom cover.

import Zernio from '@zernio/node';

const zernio = new Zernio();

const { data: published } = await zernio.posts.createPost({
  body: {
    content: 'In this tutorial, I walk through building a REST API from scratch.\n\n#programming #tutorial',
    tags: ['rest api', 'node.js', 'backend tutorial'],
    mediaItems: [{
      type: 'video',
      url: 'https://cdn.example.com/long-form-video.mp4',
      thumbnail: 'https://cdn.example.com/thumbnail.jpg'
    }],
    platforms: [{
      platform: 'youtube',
      accountId: '66b2e19d8c3f5a7e9d0b1c2d',
      platformSpecificData: {
        title: 'Build a REST API from scratch',
        visibility: 'public',
        categoryId: '27',
        madeForKids: false
      }
    }],
    publishNow: true
  }
});

console.log(published.post.platforms[0].platformPostUrl);

Response (201):

{
  "post": {
    "_id": "65f1c0a9e2b5af0012ab34cd",
    "status": "published",
    "platforms": [
      {
        "platform": "youtube",
        "status": "published",
        "platformPostUrl": "https://www.youtube.com/watch?v=dQw4w9WgXcQ"
      }
    ]
  }
}

tags sits at the top level of the request, alongside content, and reaches YouTube as snippet.tags on the upload. Zernio strips a leading #, splits a comma-joined entry into separate tags and drops duplicates, then keeps tags in order until the combined length would pass 500 characters. A single tag longer than 100 characters is skipped.

Every sample below changes only the platforms entry of this request.

Shorts

YouTube detects Shorts on its own: a video that is 3 minutes or shorter and vertical (9:16) is classified as a Short. There is no post type or flag to set, so the request is the one above with a short vertical video. Videos under 15 seconds loop automatically, and custom thumbnails are not supported for Shorts through the API.

Playlists

Create a playlist with POST /v1/accounts/{accountId}/youtube-playlists (Create YouTube playlist). Only title is required; privacy defaults to private and also accepts public or unlisted.

const { data: created } = await zernio.connect.createYoutubePlaylist({
  path: { accountId: '66b2e19d8c3f5a7e9d0b1c2d' },
  body: { title: 'Tutorials', description: 'Step-by-step video tutorials', privacy: 'private' }
});

console.log(created.playlist.id);

Response (201):

{
  "playlist": {
    "id": "PLxxxxxxxxxxxxx",
    "title": "Tutorials",
    "description": "Step-by-step video tutorials",
    "privacy": "private",
    "itemCount": 0,
    "thumbnailUrl": ""
  }
}

Creation costs 50 YouTube quota units and requires the youtube or youtube.force-ssl OAuth scope. The playlist starts empty and does not become the account's default. Use the returned playlist.id in platformSpecificData.playlistId when publishing a video.

Playlist creation is not idempotent. Repeating a request can create another playlist, including after a timeout. Zernio does not automatically retry creation; list the channel's playlists before retrying an ambiguous failure.

YouTube's public API does not expose the official series setting. After creating a playlist, enable Set as official series for this playlist manually in YouTube's desktop playlist settings. See YouTube's series playlist requirements.

playlistId adds the video to an existing playlist after upload, for immediate and scheduled uploads alike. Without it the video is uploaded normally. List the channel's playlists with GET /v1/accounts/{accountId}/youtube-playlists (List YouTube playlists):

const { data: playlists } = await zernio.connect.getYoutubePlaylists({
  path: { accountId: '66b2e19d8c3f5a7e9d0b1c2d' }
});

console.log(playlists.playlists);

Response (200):

{
  "playlists": [
    {
      "id": "PLxxxxxxxxxxxxx",
      "title": "Tutorials",
      "privacy": "public",
      "itemCount": 12
    }
  ],
  "defaultPlaylistId": null
}

Then pass the id when creating the post:

{
  "platform": "youtube",
  "accountId": "66b2e19d8c3f5a7e9d0b1c2d",
  "platformSpecificData": {
    "title": "Build a REST API from scratch",
    "visibility": "public",
    "playlistId": "PLxxxxxxxxxxxxx"
  }
}

PUT /v1/accounts/{accountId}/youtube-playlists (Set default YouTube playlist) stores a default playlist on the account to prefill your own UI. It does not apply to posts that omit playlistId:

const { data: saved } = await zernio.connect.updateYoutubeDefaultPlaylist({
  path: { accountId: '66b2e19d8c3f5a7e9d0b1c2d' },
  body: { defaultPlaylistId: 'PLxxxxxxxxxxxxx', defaultPlaylistName: 'Tutorials' }
});

console.log(saved.success);

Response (200):

{ "success": true }

Scheduling

A post with scheduledFor in the future runs in this order:

  1. Zernio uploads the video ahead of its scheduled time, so YouTube has finished processing it before the video goes live.
  2. A video targeting "public" goes up as "private" and carries YouTube's own publishAt, so YouTube releases it at the scheduled second. A video targeting "private" or "unlisted" is uploaded with that visibility and never changes.
  3. A video URL exists as soon as the upload finishes, but the video is not publicly accessible before the release.
  4. firstComment is posted at the scheduled time, not at upload time.
{
  "platform": "youtube",
  "accountId": "66b2e19d8c3f5a7e9d0b1c2d",
  "scheduledFor": "2027-01-01T12:00:00",
  "platformSpecificData": {
    "title": "Build a REST API from scratch",
    "visibility": "public",
    "firstComment": "Chapters and source code are in the description."
  }
}

Set timezone on the request so the scheduled time is read in the right zone; see post lifecycle.

Edit a published video

Edit post replaces the video description only. There is no time window and no limit on the number of edits, and the video id does not change, so the video keeps its URL. The title is left exactly as it was, including when it was derived from the first line of content at publish time. Title, tags, thumbnail and visibility changes go through Update post metadata, which also works on videos uploaded outside Zernio when you pass videoId and accountId with _ as the post id.

const { data: edited } = await zernio.posts.editPost({
  path: { postId: '65f1c0a9e2b5af0012ab34cd' },
  body: {
    platform: 'youtube',
    content: 'Updated description with corrected chapter timestamps.'
  }
});

console.log(edited.url);

Response (200):

{
  "success": true,
  "id": "dQw4w9WgXcQ",
  "url": "https://www.youtube.com/watch?v=dQw4w9WgXcQ",
  "message": "youtube post edited successfully"
}

The new description is sanitized before it is written: angle brackets (< and >) are stripped, and anything past 5,000 characters is truncated. Neither one fails the call, so a request that hits either rule still returns success with the video id and URL. If the post was published to several YouTube channels, pass accountId to pick which copy to edit; without it, the first youtube entry on the post is edited.

Platform fields

All fields go in platformSpecificData on the YouTube entry.

FieldTypeDefaultDescription
titlestringFirst line of content, or "Untitled Video"Video title, at most 100 characters.
visibility"public" | "private" | "unlisted""public"Who can see the video.
madeForKidsbooleanfalseMarks the video as child-directed for COPPA. true permanently disables comments, the notification bell, personalized ads, end screens and cards on the video. YouTube may block views when the flag is never set.
containsSyntheticMediabooleanfalseDiscloses that the video contains synthetic content that could be mistaken for real. YouTube may add a label to the video.
categoryIdstring"22" (People & Blogs)Video category. Common values: "1" Film, "10" Music, "20" Gaming, "22" People & Blogs, "27" Education, "28" Science & Technology.
playlistIdstringPlaylist to add the video to after upload. See Playlists.
firstCommentstringPosted and pinned as the first comment, at most 10,000 characters. Posted immediately with publishNow, at the scheduled time otherwise.

Media requirements

Files above these limits are rejected: 256 GB per video, 15 minutes on an unverified channel, 2 MB per thumbnail after compression. Large videos (1 GB or more) can take 30 to 60 minutes or longer to process on YouTube's side; the video shows a "processing" state meanwhile, so do not retry the upload.

Videos

PropertyShortsVideo
Max duration3 minutes12 hours (verified), 15 minutes (unverified)
Min duration1 second1 second
Max file size256 GB256 GB
FormatsMP4, MOV, AVI, WMV, FLV, 3GP, WebMMP4, MOV, AVI, WMV, FLV, 3GP, WebM
Aspect ratio9:16 (vertical)16:9 (horizontal)
Resolution1080 x 1920 px1920 x 1080 px (1080p)

Recommended encoding:

PropertyShortsVideo
Resolution1080 x 1920 px3840 x 2160 px (4K)
Frame rate30 fps24 to 60 fps
CodecH.264H.264 or H.265
AudioAAC, 128 kbpsAAC, 384 kbps
Bitrate10 Mbps35 to 68 Mbps (4K)

Custom thumbnails

Custom thumbnails work on videos only, not Shorts. Set thumbnail on the video media item.

PropertyRequirement
FormatJPEG, PNG, GIF
Max size2 MB
Recommended resolution1280 x 720 px (16:9)
Min width640 px

Zernio enforces YouTube's rules before upload: JPEG, PNG or GIF, and 2 MB at most (oversized images are compressed first, and rejected if they are still over 2 MB). YouTube itself only accepts custom thumbnails on phone-verified channels (youtube.com/verify). On an unverified channel the video still uploads and publishes, only the thumbnail is skipped; Zernio remembers the refusal for 7 days and does not retry thumbnails on that channel until then, so after verifying allow up to a week for thumbnails to resume.

Media URLs

The URL must return the video bytes, not an HTML page, with no authentication and no expired link. Or upload through the media endpoint.

Analytics

Call GET /v1/analytics?platform=youtube (Analytics API).

MetricAvailable
Likes
Comments
Shares (through Daily views only)
Views
const { data: analytics } = await zernio.analytics.getAnalytics({
  query: { platform: 'youtube', fromDate: '2026-08-01', toDate: '2026-08-31' }
});

console.log(analytics.posts);

Response (200), one entry per post:

{
  "posts": [
    {
      "_id": "65f1c0a9e2b5af0012ab34cd",
      "platform": "youtube",
      "status": "published",
      "publishedAt": "2026-08-14T10:00:05Z",
      "platformPostUrl": "https://www.youtube.com/watch?v=dQw4w9WgXcQ",
      "analytics": {
        "likes": 342,
        "comments": 28,
        "shares": 0,
        "views": 15420
      }
    }
  ]
}

Four YouTube-only endpoints go deeper:

  • Daily views: per-day views, watch time, subscriber changes and per-day likes, comments and shares for one video. Data has a 2 to 3 day delay.
  • Channel insights: channel-level views, watch time, average view duration and subscribers gained and lost, without looping through every video. Impressions and impressions click-through rate (the thumbnail metrics in YouTube Studio) are not exposed by YouTube's Analytics API v2 for any principal type; the only way to get those is a manual Studio CSV export.
  • Demographics: audience by age, gender and country, for the channel or one video. Age and gender values are viewer percentages (0 to 100), country values are view counts. Based on signed-in viewers only, with a 2 to 3 day delay.
  • Video retention: the audience retention curve of one video, up to 100 points over the whole date range.

Transcripts and captions

To transcribe a YouTube video, read the caption track YouTube already holds for it rather than downloading the file and running your own transcription. Call GET /v1/accounts/{accountId}/youtube-captions with videoId (Get a YouTube video transcript), for one of the connected channel's own videos. Auto-generated (ASR) tracks count: YouTube serves them to the channel owner, which is what the connected account is. An uploaded track wins over an auto-generated one for the same language.

const { data: transcript } = await zernio.connect.getYoutubeCaptions({
  path: { accountId: '66b2e19d8c3f5a7e9d0b1c2d' },
  query: { videoId: 'dQw4w9WgXcQ', language: 'en' }
});

console.log(transcript.text);

Response (200), trimmed:

{
  "videoId": "dQw4w9WgXcQ",
  "language": "en",
  "trackKind": "asr",
  "source": "cache",
  "fetchedAt": "2026-08-27T21:09:54.000Z",
  "text": "Hey, this is Mickey. I'm the founder of this portfolio of three websites.",
  "cues": [
    { "start": 1.6, "end": 8.88, "text": "Hey, this is Mickey. I'm the founder of" }
  ]
}

format=srt returns the raw SubRip body in srt instead of cues; text is there either way. The first read downloads from YouTube and costs 200 quota units, and Zernio stores the result, so source reads youtube once and cache afterwards. Pass refresh=true only when the captions changed on YouTube, since that spends the quota again. availableTracks lists every track on the video so you can request another language.

Only videos owned by the connected channel are readable; anything else is a 404. A video with no track in the requested language is also a 404, with code: "captions_not_found". YouTube generates auto-captions only for videos with recognizable speech and can take a few hours after upload to publish them, so treat that 404 as "not yet" rather than "never". contentDetails.caption in YouTube's own API reads false on videos that do have a serving auto-generated track, so it is not a usable availability signal.

Inbox

YouTube supports comments only; the platform has no DMs.

FeatureSupported
List comments on videos
Reply to comments
Delete comments
Moderate comments (YouTube only: approve, reject or hold, with an optional author ban)
Like comments (no API available)

Read and reply with the Comments API. Work a moderation queue with POST /v1/inbox/comments/{postId}/{commentId}/moderation: moderationStatus takes published to approve, rejected to remove or heldForReview to send it back to the queue, and banAuthor: true (valid only alongside rejected) auto-rejects that author from then on. You have to own the channel or the video (Set comment moderation status).

Like a video as any connected channel with POST /v1/inbox/posts/{postId}/like, and clear the rating again with DELETE. Each call spends 50 of the project's 10,000 daily quota units, the tightest per-day ceiling of any platform here, and a video whose owner turned ratings off returns 403.

What you cannot do

YouTube's API does not expose:

  • Community posts
  • Going live or scheduling Premieres
  • End screens, cards or chapters (timestamps in the description do work)
  • Monetization settings
  • Creating or deleting playlists (you can list playlists and add videos to an existing one)
  • Disliking a video (YouTube's rating call takes a like or no rating, so a like can only be set or cleared)
  • Uploading captions or subtitles (reading an existing track is supported; see Transcripts and captions)
  • Liking comments

Common errors

ErrorCauseFix
"The YouTube account of the authenticated user is suspended." (403)YouTube suspended the channelCheck the channel status on YouTube. Use Account health.
"Social account not found"The account was disconnected or deleted from ZernioReconnect the YouTube account. Subscribe to the account.disconnected webhook.
"Account was deleted"The user deleted the accountReconnect the account.
"Failed to fetch video from URL: 404"The video URL returned a 404Check that the URL is still valid and public. Links expire on some hosts.
"YouTube permission error: Ensure the channel has required scopes and features enabled."The OAuth token lacks a required scopeReconnect the YouTube account and grant every scope.
"YouTube upload initialization failed: 403"YouTube rejected the upload before the file transfer beganCheck whether the channel is suspended, the upload quota is exhausted, or permissions are missing.

A publishNow: true post that YouTube rejects returns 207 with post.status: "failed" and the message in platforms[].errorMessage:

{
  "message": "Post created but publishing failed",
  "error": "All platforms failed",
  "post": {
    "_id": "65f1c0a9e2b5af0012ab34cd",
    "status": "failed",
    "platforms": [
      {
        "platform": "youtube",
        "status": "failed",
        "errorMessage": "The YouTube account of the authenticated user is suspended."
      }
    ]
  }
}

207 is a 2xx status, so fetch(...).ok is true; branch on the status code and on post.status. Error handling covers the envelope.

Related

  • Connecting accounts: the OAuth flow.
  • Create post: every field of the request.
  • Edit post and Update post metadata: change a published video.
  • Media uploads: upload videos instead of hosting them.
  • Analytics: post performance metrics.
  • Comments: read and reply to comments.
  • Get a YouTube video transcript: every parameter of the captions call.
  • Pricing: what analytics, the inbox and outbound messages cost, and which replies count.
Was this page helpful?

TikTok

Publish videos and photo carousels to TikTok with the Zernio API, with creator privacy levels, duet and stitch controls, custom covers, AI disclosure and Creator Inbox drafts.

Pinterest

Publish image and video pins to Pinterest boards with the Zernio API, with destination links, cover images, board creation and description edits.

On this page

Quick referenceBefore you startConnectOAuth scopesBrand Accounts and multiple channelsPublishVideoShortsPlaylistsSchedulingEdit a published videoPlatform fieldsMedia requirementsVideosCustom thumbnailsMedia URLsAnalyticsTranscripts and captionsInboxWhat you cannot doCommon errorsRelated