YouTube
Upload a video to a selected YouTube channel with explicit audience, visibility, media processing, and reconciliation fields.
The direct YouTube adapter accepts an existing OAuth access token and channel ID. The server package also provides youtubeOAuth for Google authorization-code connections and channel discovery; use ConnectionManager to keep the attempt server-side, present discovered channels for selection, and persist credentials through an encrypted credentialSink.
Setup
Configure Google OAuth
Request https://www.googleapis.com/auth/youtube.upload when the connected workflow uploads
videos. Add https://www.googleapis.com/auth/youtube.force-ssl for captions and playlist
mutations, https://www.googleapis.com/auth/youtube to cancel a scheduled video, and
https://www.googleapis.com/auth/yt-analytics.readonly for Analytics API reports. Refresh with
refreshOAuthToken("youtube", ...) and persist any rotated refresh token.
Store credentials server-side
Set YOUTUBE_ACCESS_TOKEN and YOUTUBE_CHANNEL_ID in server configuration. Upload bytes and
resumable session URIs are secrets too.
Configure the adapter
Construct the backend and select the channel through a connected-account reference.
import { createSocial, connectedAccountRef } from "@opencoredev/social-sdk";
import { youtube } from "@opencoredev/social-sdk/youtube";
const social = createSocial({
backend: youtube({
auth: {
accessToken: process.env.YOUTUBE_ACCESS_TOKEN!,
channelId: process.env.YOUTUBE_CHANNEL_ID!,
},
}),
});
const account = connectedAccountRef({
backend: "default",
platform: "youtube",
accountId: process.env.YOUTUBE_CHANNEL_ID!,
});
Examples
Upload a video
Preparation requires exactly one video, an actual video/* MIME type, a title from 1–100 characters, explicit visibility, and an explicit madeForKids value.
const media = {
kind: "video" as const,
source: { kind: "blob" as const, blob, fingerprint: contentHash },
mimeType: "video/mp4",
byteSize: blob.size,
};
const result = await social.posts.publish({
targets: [
{
account,
options: { title: "A release", visibility: "private", madeForKids: false },
},
],
content: { text: "Description", media: [media] },
});
Schedule a video
Pass schedule to upload now and let YouTube publish later. The adapter uploads the video as private with status.publishAt set to the scheduled time, and YouTube makes it public at that time. The time must be in the future.
const result = await social.posts.publish({
targets: [
{
account,
options: { title: "A release", visibility: "private", madeForKids: false },
},
],
content: { text: "Description", media: [media] },
schedule: { at: "2026-10-01T15:00:00Z" },
});
While the video waits for its publish time, the outcome is scheduled and carries a job reference whose jobId is the video ID. After the time passes and YouTube publishes the video, getDelivery reports it as published.
Cancel a scheduled video
posts.cancelScheduled(job) stops YouTube from publishing the video at the scheduled time. It does not delete the video. The adapter reads the video, checks that it belongs to the configured channel, is still private, and has a publishAt in the future, then calls videos.update with part=status. The result is { state: "cancelled", backendRecord: "retained" }: the video stays uploaded and private with no scheduled time.
const outcome = result.outcomes[0];
if (outcome?.state === "scheduled") {
await social.posts.cancelScheduled(outcome.job);
}
videos.update replaces the whole status part and resets any field left out, so the adapter resends the status fields it just read (license, embeddable, publicStatsViewable, selfDeclaredMadeForKids, and containsSyntheticMedia) and leaves out only publishAt. If YouTube does not return the made-for-kids declaration, the adapter stops before writing rather than risk clearing it.
Cancellation needs the https://www.googleapis.com/auth/youtube scope; youtube.upload alone cannot call videos.update. It costs 51 quota units: 1 for videos.list and 50 for videos.update. A video that is public, already past its publish time, or has no schedule is rejected with invalid_input and nothing is written. The update is sent once. If the response is lost or does not show a private video without publishAt, the call raises ambiguous_outcome with reconcile-first; read the video before trying again. To remove the video entirely, use posts.removeFromPlatform instead.
Track upload and processing separately
YouTube separates upload completion from processing. A private video with a future publishAt maps to scheduled. Otherwise uploaded maps to processing, processed maps to published, and failed/rejected maps to a confirmed media failure. The returned watch URL is produced only with a verified video ID.
const outcome = result.outcomes[0];
if (outcome?.state === "processing" && outcome.delivery) {
const delivery = await social.posts.getDelivery(outcome.delivery);
console.log(delivery.state);
}
Resume an interrupted upload
Resumable upload sessions can be persisted with saveUploadSession. A non-complete upload is unknown and must be resumed or reconciled explicitly; the adapter does not resume in the background.
Receive push notifications
YouTube sends new and updated videos through the PubSubHubbub hub. Subscribe with a hub.secret and pass the same value as webhookSecret; the hub then signs each delivery with X-Hub-Signature, and adapter.webhooks.verify checks it. Without a secret the hub sends unsigned notifications, so read the video through the API before acting on one. Answer the hub’s GET verification with answerYouTubeWebhookChallenge, listing the topics you subscribed to. The decoded event lists the videos in data.videos with type unknown. See Process webhooks.
Native operations
Provider-shaped operations are available on social.native:
const native = social.native("default", { acknowledgeUnsafe: true });
await native.playlists({ action: "list", mine: true, context });
await native.playlistItems({
action: "insert",
playlistId: "PL...",
body: { snippet: { playlistId: "PL...", resourceId: { kind: "youtube#video", videoId: "v1" } } },
context,
});
await native.setThumbnail({ videoId: "v1", thumbnail, context });
await native.updateVideo({
videoId: "v1",
body: {
snippet: { title: "Updated title", description: "...", tags: ["sdk"], categoryId: "22" },
status: { privacyStatus: "private" },
},
context,
});
await native.rateVideo({ videoId: "v1", rating: "like", context });
await native.captions({
action: "insert",
videoId: "v1",
caption,
body: { snippet: { language: "en", name: "English" } },
context,
});
await native.subscriptions({ action: "list", context });
await native.commentsModeration({
action: "setModerationStatus",
commentId: "c1",
moderationStatus: "heldForReview",
context,
});
await native.deleteComment({ account, commentId: "Ugz...", context });
Update channel details
native.updateProfile writes one part of the configured channel through channels.update. Use part: "brandingSettings" with a channel object for the description, keywords, country, default language, tracking analytics account, and unsubscribed trailer. Use part: "localizations" for a map of language codes to localized title and description.
await native.updateProfile({
part: "brandingSettings",
value: { channel: { description: "New channel description", keywords: "sdk social" } },
context,
});
await native.updateProfile({
part: "localizations",
value: { de: { title: "Mein Kanal", description: "Beschreibung" }, fr: null },
context,
});
YouTube deletes any mutable property that a write leaves out, so the adapter reads the current part with channels.list and merges your fields into it before the write. A null field removes that property or localization. Set brandingSettings.channel.defaultLanguage before you add localizations. YouTube rejects a changed channel title with channelTitleUpdateForbidden, and the API has no write for the channel avatar or handle. The adapter resends the current banner URL unchanged and drops deprecated branding fields. Changing the banner takes a channelBanners.insert upload followed by a channels.update that sets brandingSettings.image.bannerExternalUrl, which updateProfile does not support. The call needs https://www.googleapis.com/auth/youtube and costs 51 quota units: 1 for the read and 50 for the write.
updateVideo fetches the current snippet and status first, then sends the complete resource required by videos.update. Caption downloads use action: "download"; insert and update require explicit metadata and media. Held-for-review threads use native.heldComments. deleteComment calls comments.delete with youtube.force-ssl and costs 50 quota units. Pass a top-level comment ID; comment threads have no delete method. Google does not document which comments a channel may delete, and a 403 raises missing_permission. To remove another user’s comment from your video, use commentsModeration with moderationStatus: "rejected". The normalized social.search.posts(account, { query }) maps to public search.list results with type=video; YouTube accepts only the recent scope and limits pages to 50 items. Use native.search for provider filters such as channelId, order, and date bounds.
Limits and requirements
- Quota, channel eligibility, scopes, and API audit status remain upstream conditions.
- Data API quota costs are method-specific: list/search/get operations generally cost 1 unit; playlist and video mutations cost 50, so schedule cancellation costs 51 including its read;
thumbnails.setcosts approximately 50; captions list/delete cost 50, insert 400, update 450, and download 200; subscriptions mutations cost 50; comment moderation mutations cost 50.search.listis also subject to Google’s documented 100-calls-per-day Search Queries limit. - Playlist and video management generally uses
youtubeoryoutube.readonly; ratings and captions useyoutube.force-ssl; uploads useyoutube.upload. Google OAuth verification, channel ownership, and project quota remain approval or account conditions. - Video uploads get at least 15 minutes, even when
retryBudget.maxElapsedMsis shorter, because the default 30-second budget cannot fit a real upload. Pass a longer budget for very large files, and passsignalto cancel an upload early. - Current evidence is contract coverage, not a live channel verification.
See YouTube’s videos.insert documentation for current audit and quota rules, videos.update and the videos resource for status.publishAt rules, and channels.update for the channel fields you can write.