RelayAPI

YouTube API

Schedule and automate YouTube uploads with RelayAPI — videos, Shorts, custom thumbnails, first comments, and COPPA compliance.

Quick Reference

PropertyValue
Platform keyyoutube
Auth methodOAuth 2.0 (Google)
Title limit100 characters
Description limit5,000 characters
Tags limit500 characters total (all tags combined)
Videos per post1 (required)
Video formatsMP4, MOV, AVI, WMV, FLV, 3GP, WebM
Video max size256 GB
Video max duration15 min (unverified), 12 hours (verified)
Thumbnail formatsJPEG, PNG, GIF
Thumbnail max size2 MB
Post typesVideo, Shorts (auto-detected)
SchedulingYes
AnalyticsYes (likes, comments, shares, views)

SDK sourceTypeScript · Python · Go · Java · REST API

Before You Start

YouTube is a video-only platform — every post requires exactly one video file. There are no image-only or text-only posts. Setting made_for_kids: true is permanent and cannot be undone — it disables comments, notifications, and personalized ads. COPPA violations carry fines of $42,000+. Unverified channels are limited to 15-minute video uploads. Large videos (1 GB+) can take 30-60+ minutes to process — do not retry during processing.

Quick Start

Upload a video to YouTube:

import Relay from '@relayapi/sdk';

const client = new Relay();
const post = await client.posts.create({
  content: 'In this tutorial, I walk through building a REST API from scratch.',
  targets: ['youtube'],
  media: [
    { url: 'https://cdn.example.com/tutorial.mp4', type: 'video' },
  ],
  scheduled_at: 'now',
  target_options: {
    youtube: {
      title: 'Build a REST API from Scratch',
      visibility: 'public',
      made_for_kids: false,
    },
  },
});

console.log(post.id); // post_abc123
from relay import Relay

client = Relay()
post = client.posts.create(
    content='In this tutorial, I walk through building a REST API from scratch.',
    targets=['youtube'],
    media=[
        {'url': 'https://cdn.example.com/tutorial.mp4', 'type': 'video'},
    ],
    scheduled_at='now',
    target_options={
        'youtube': {
            'title': 'Build a REST API from Scratch',
            'visibility': 'public',
            'made_for_kids': False,
        },
    },
)

print(post.id)  # post_abc123
client := relaygo.NewClient()

post, err := client.Posts.New(context.TODO(), relaygo.PostNewParams{
    Content: relaygo.F("In this tutorial, I walk through building a REST API from scratch."),
    Targets: relaygo.F([]string{"youtube"}),
    Media: relaygo.F([]relaygo.PostNewParamsMedia{
        {URL: relaygo.F("https://cdn.example.com/tutorial.mp4"), Type: relaygo.F(relaygo.PostNewParamsMediaTypeVideo)},
    }),
    ScheduledAt: relaygo.F("now"),
    TargetOptions: relaygo.F(map[string]interface{}{
        "youtube": map[string]interface{}{
            "title":         "Build a REST API from Scratch",
            "visibility":    "public",
            "made_for_kids": false,
        },
    }),
})

fmt.Println(post.ID) // post_abc123
RelayClient client = RelayOkHttpClient.fromEnv();

PostCreateResponse post = client.posts().create(PostCreateParams.builder()
    .content("In this tutorial, I walk through building a REST API from scratch.")
    .addTarget("youtube")
    .addMedia(PostCreateParams.Media.builder()
        .url("https://cdn.example.com/tutorial.mp4")
        .type(PostCreateParams.Media.Type.VIDEO)
        .build())
    .scheduledAt("now")
    .targetOptions(PostCreateParams.TargetOptions.builder()
        .putAdditionalProperty("youtube", JsonValue.from(Map.of(
            "title", "Build a REST API from Scratch",
            "visibility", "public",
            "made_for_kids", false
        )))
        .build())
    .build());

System.out.println(post.id()); // post_abc123
curl -X POST https://api.relayapi.dev/v1/posts \
  -H "Authorization: Bearer $RELAY_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "content": "In this tutorial, I walk through building a REST API from scratch.",
    "targets": ["youtube"],
    "media": [
      {"url": "https://cdn.example.com/tutorial.mp4", "type": "video"}
    ],
    "scheduled_at": "now",
    "target_options": {
      "youtube": {
        "title": "Build a REST API from Scratch",
        "visibility": "public",
        "made_for_kids": false
      }
    }
  }'

Content Types

Regular Video

Any video longer than 3 minutes or with a horizontal aspect ratio becomes a regular YouTube video. Supports custom thumbnails, tags, and categories.

const post = await client.posts.create({
  content: 'In this tutorial, I walk through building a REST API from scratch.\n\n#programming #tutorial',
  targets: ['youtube'],
  media: [
    { url: 'https://cdn.example.com/tutorial.mp4', type: 'video' },
  ],
  scheduled_at: 'now',
  target_options: {
    youtube: {
      title: 'Build a REST API from Scratch',
      visibility: 'public',
      category_id: '27',
      made_for_kids: false,
      tags: ['api', 'tutorial', 'programming'],
    },
  },
});
post = client.posts.create(
    content='In this tutorial, I walk through building a REST API from scratch.\n\n#programming #tutorial',
    targets=['youtube'],
    media=[
        {'url': 'https://cdn.example.com/tutorial.mp4', 'type': 'video'},
    ],
    scheduled_at='now',
    target_options={
        'youtube': {
            'title': 'Build a REST API from Scratch',
            'visibility': 'public',
            'category_id': '27',
            'made_for_kids': False,
            'tags': ['api', 'tutorial', 'programming'],
        },
    },
)
post, err := client.Posts.New(context.TODO(), relaygo.PostNewParams{
    Content: relaygo.F("In this tutorial, I walk through building a REST API from scratch.\n\n#programming #tutorial"),
    Targets: relaygo.F([]string{"youtube"}),
    Media: relaygo.F([]relaygo.PostNewParamsMedia{
        {URL: relaygo.F("https://cdn.example.com/tutorial.mp4"), Type: relaygo.F(relaygo.PostNewParamsMediaTypeVideo)},
    }),
    ScheduledAt: relaygo.F("now"),
    TargetOptions: relaygo.F(map[string]interface{}{
        "youtube": map[string]interface{}{
            "title":         "Build a REST API from Scratch",
            "visibility":    "public",
            "category_id":   "27",
            "made_for_kids": false,
            "tags":          []string{"api", "tutorial", "programming"},
        },
    }),
})
PostCreateResponse post = client.posts().create(PostCreateParams.builder()
    .content("In this tutorial, I walk through building a REST API from scratch.\n\n#programming #tutorial")
    .addTarget("youtube")
    .addMedia(PostCreateParams.Media.builder()
        .url("https://cdn.example.com/tutorial.mp4")
        .type(PostCreateParams.Media.Type.VIDEO)
        .build())
    .scheduledAt("now")
    .targetOptions(PostCreateParams.TargetOptions.builder()
        .putAdditionalProperty("youtube", JsonValue.from(Map.of(
            "title", "Build a REST API from Scratch",
            "visibility", "public",
            "category_id", "27",
            "made_for_kids", false,
            "tags", List.of("api", "tutorial", "programming")
        )))
        .build())
    .build());
curl -X POST https://api.relayapi.dev/v1/posts \
  -H "Authorization: Bearer $RELAY_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "content": "In this tutorial, I walk through building a REST API from scratch.\n\n#programming #tutorial",
    "targets": ["youtube"],
    "media": [
      {"url": "https://cdn.example.com/tutorial.mp4", "type": "video"}
    ],
    "scheduled_at": "now",
    "target_options": {
      "youtube": {
        "title": "Build a REST API from Scratch",
        "visibility": "public",
        "category_id": "27",
        "made_for_kids": false,
        "tags": ["api", "tutorial", "programming"]
      }
    }
  }'

YouTube Shorts

Auto-detected: video must be 3 minutes or shorter AND have a vertical (9:16) aspect ratio. No separate flag is needed. Custom thumbnails are not supported for Shorts via the API.

const post = await client.posts.create({
  content: 'Quick tip for better API design!',
  targets: ['youtube'],
  media: [
    { url: 'https://cdn.example.com/short.mp4', type: 'video' },
  ],
  scheduled_at: 'now',
  target_options: {
    youtube: {
      title: 'Quick API Tip',
      visibility: 'public',
    },
  },
});
post = client.posts.create(
    content='Quick tip for better API design!',
    targets=['youtube'],
    media=[
        {'url': 'https://cdn.example.com/short.mp4', 'type': 'video'},
    ],
    scheduled_at='now',
    target_options={
        'youtube': {
            'title': 'Quick API Tip',
            'visibility': 'public',
        },
    },
)
post, err := client.Posts.New(context.TODO(), relaygo.PostNewParams{
    Content: relaygo.F("Quick tip for better API design!"),
    Targets: relaygo.F([]string{"youtube"}),
    Media: relaygo.F([]relaygo.PostNewParamsMedia{
        {URL: relaygo.F("https://cdn.example.com/short.mp4"), Type: relaygo.F(relaygo.PostNewParamsMediaTypeVideo)},
    }),
    ScheduledAt: relaygo.F("now"),
    TargetOptions: relaygo.F(map[string]interface{}{
        "youtube": map[string]interface{}{
            "title":      "Quick API Tip",
            "visibility": "public",
        },
    }),
})
PostCreateResponse post = client.posts().create(PostCreateParams.builder()
    .content("Quick tip for better API design!")
    .addTarget("youtube")
    .addMedia(PostCreateParams.Media.builder()
        .url("https://cdn.example.com/short.mp4")
        .type(PostCreateParams.Media.Type.VIDEO)
        .build())
    .scheduledAt("now")
    .targetOptions(PostCreateParams.TargetOptions.builder()
        .putAdditionalProperty("youtube", JsonValue.from(Map.of(
            "title", "Quick API Tip",
            "visibility", "public"
        )))
        .build())
    .build());
curl -X POST https://api.relayapi.dev/v1/posts \
  -H "Authorization: Bearer $RELAY_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "content": "Quick tip for better API design!",
    "targets": ["youtube"],
    "media": [
      {"url": "https://cdn.example.com/short.mp4", "type": "video"}
    ],
    "scheduled_at": "now",
    "target_options": {
      "youtube": {
        "title": "Quick API Tip",
        "visibility": "public"
      }
    }
  }'

Shorts are auto-detected based on duration (3 minutes or less) and aspect ratio (9:16 vertical). Videos under 15 seconds loop automatically. You do not need to set any special flag.

Video with Custom Thumbnail

Upload a custom thumbnail for regular videos. Not supported for Shorts.

const post = await client.posts.create({
  content: 'Full tutorial on modern API architecture.',
  targets: ['youtube'],
  media: [
    {
      url: 'https://cdn.example.com/video.mp4',
      type: 'video',
      thumbnail: 'https://cdn.example.com/thumbnail.jpg',
    },
  ],
  scheduled_at: 'now',
  target_options: {
    youtube: {
      title: 'How to Build Modern APIs',
      visibility: 'public',
    },
  },
});
post = client.posts.create(
    content='Full tutorial on modern API architecture.',
    targets=['youtube'],
    media=[
        {
            'url': 'https://cdn.example.com/video.mp4',
            'type': 'video',
            'thumbnail': 'https://cdn.example.com/thumbnail.jpg',
        },
    ],
    scheduled_at='now',
    target_options={
        'youtube': {
            'title': 'How to Build Modern APIs',
            'visibility': 'public',
        },
    },
)
post, err := client.Posts.New(context.TODO(), relaygo.PostNewParams{
    Content: relaygo.F("Full tutorial on modern API architecture."),
    Targets: relaygo.F([]string{"youtube"}),
    Media: relaygo.F([]relaygo.PostNewParamsMedia{
        {
            URL:       relaygo.F("https://cdn.example.com/video.mp4"),
            Type:      relaygo.F(relaygo.PostNewParamsMediaTypeVideo),
            Thumbnail: relaygo.F("https://cdn.example.com/thumbnail.jpg"),
        },
    }),
    ScheduledAt: relaygo.F("now"),
    TargetOptions: relaygo.F(map[string]interface{}{
        "youtube": map[string]interface{}{
            "title":      "How to Build Modern APIs",
            "visibility": "public",
        },
    }),
})
PostCreateResponse post = client.posts().create(PostCreateParams.builder()
    .content("Full tutorial on modern API architecture.")
    .addTarget("youtube")
    .addMedia(PostCreateParams.Media.builder()
        .url("https://cdn.example.com/video.mp4")
        .type(PostCreateParams.Media.Type.VIDEO)
        .thumbnail("https://cdn.example.com/thumbnail.jpg")
        .build())
    .scheduledAt("now")
    .targetOptions(PostCreateParams.TargetOptions.builder()
        .putAdditionalProperty("youtube", JsonValue.from(Map.of(
            "title", "How to Build Modern APIs",
            "visibility", "public"
        )))
        .build())
    .build());
curl -X POST https://api.relayapi.dev/v1/posts \
  -H "Authorization: Bearer $RELAY_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "content": "Full tutorial on modern API architecture.",
    "targets": ["youtube"],
    "media": [
      {
        "url": "https://cdn.example.com/video.mp4",
        "type": "video",
        "thumbnail": "https://cdn.example.com/thumbnail.jpg"
      }
    ],
    "scheduled_at": "now",
    "target_options": {
      "youtube": {
        "title": "How to Build Modern APIs",
        "visibility": "public"
      }
    }
  }'

Video with First Comment

Automatically post and pin a comment after the video is published.

const post = await client.posts.create({
  content: 'Tutorial on building REST APIs from scratch.',
  targets: ['youtube'],
  media: [
    { url: 'https://cdn.example.com/video.mp4', type: 'video' },
  ],
  scheduled_at: 'now',
  target_options: {
    youtube: {
      title: 'API Tutorial',
      visibility: 'public',
      first_comment: 'Thanks for watching! Links to all resources mentioned are below.',
    },
  },
});
post = client.posts.create(
    content='Tutorial on building REST APIs from scratch.',
    targets=['youtube'],
    media=[
        {'url': 'https://cdn.example.com/video.mp4', 'type': 'video'},
    ],
    scheduled_at='now',
    target_options={
        'youtube': {
            'title': 'API Tutorial',
            'visibility': 'public',
            'first_comment': 'Thanks for watching! Links to all resources mentioned are below.',
        },
    },
)
post, err := client.Posts.New(context.TODO(), relaygo.PostNewParams{
    Content: relaygo.F("Tutorial on building REST APIs from scratch."),
    Targets: relaygo.F([]string{"youtube"}),
    Media: relaygo.F([]relaygo.PostNewParamsMedia{
        {URL: relaygo.F("https://cdn.example.com/video.mp4"), Type: relaygo.F(relaygo.PostNewParamsMediaTypeVideo)},
    }),
    ScheduledAt: relaygo.F("now"),
    TargetOptions: relaygo.F(map[string]interface{}{
        "youtube": map[string]interface{}{
            "title":         "API Tutorial",
            "visibility":    "public",
            "first_comment": "Thanks for watching! Links to all resources mentioned are below.",
        },
    }),
})
PostCreateResponse post = client.posts().create(PostCreateParams.builder()
    .content("Tutorial on building REST APIs from scratch.")
    .addTarget("youtube")
    .addMedia(PostCreateParams.Media.builder()
        .url("https://cdn.example.com/video.mp4")
        .type(PostCreateParams.Media.Type.VIDEO)
        .build())
    .scheduledAt("now")
    .targetOptions(PostCreateParams.TargetOptions.builder()
        .putAdditionalProperty("youtube", JsonValue.from(Map.of(
            "title", "API Tutorial",
            "visibility", "public",
            "first_comment", "Thanks for watching! Links to all resources mentioned are below."
        )))
        .build())
    .build());
curl -X POST https://api.relayapi.dev/v1/posts \
  -H "Authorization: Bearer $RELAY_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "content": "Tutorial on building REST APIs from scratch.",
    "targets": ["youtube"],
    "media": [
      {"url": "https://cdn.example.com/video.mp4", "type": "video"}
    ],
    "scheduled_at": "now",
    "target_options": {
      "youtube": {
        "title": "API Tutorial",
        "visibility": "public",
        "first_comment": "Thanks for watching! Links to all resources mentioned are below."
      }
    }
  }'

Scheduled Video

When scheduling a YouTube video, the upload happens immediately but the video remains private. At the scheduled time, visibility switches to your target setting and the first comment is posted.

const post = await client.posts.create({
  content: 'New video dropping tomorrow!',
  targets: ['youtube'],
  media: [
    { url: 'https://cdn.example.com/video.mp4', type: 'video' },
  ],
  scheduled_at: '2026-03-15T14:00:00Z',
  target_options: {
    youtube: {
      title: 'Big Announcement',
      visibility: 'public',
      first_comment: 'What do you think? Let me know in the comments!',
    },
  },
});
post = client.posts.create(
    content='New video dropping tomorrow!',
    targets=['youtube'],
    media=[
        {'url': 'https://cdn.example.com/video.mp4', 'type': 'video'},
    ],
    scheduled_at='2026-03-15T14:00:00Z',
    target_options={
        'youtube': {
            'title': 'Big Announcement',
            'visibility': 'public',
            'first_comment': 'What do you think? Let me know in the comments!',
        },
    },
)
post, err := client.Posts.New(context.TODO(), relaygo.PostNewParams{
    Content: relaygo.F("New video dropping tomorrow!"),
    Targets: relaygo.F([]string{"youtube"}),
    Media: relaygo.F([]relaygo.PostNewParamsMedia{
        {URL: relaygo.F("https://cdn.example.com/video.mp4"), Type: relaygo.F(relaygo.PostNewParamsMediaTypeVideo)},
    }),
    ScheduledAt: relaygo.F("2026-03-15T14:00:00Z"),
    TargetOptions: relaygo.F(map[string]interface{}{
        "youtube": map[string]interface{}{
            "title":         "Big Announcement",
            "visibility":    "public",
            "first_comment": "What do you think? Let me know in the comments!",
        },
    }),
})
PostCreateResponse post = client.posts().create(PostCreateParams.builder()
    .content("New video dropping tomorrow!")
    .addTarget("youtube")
    .addMedia(PostCreateParams.Media.builder()
        .url("https://cdn.example.com/video.mp4")
        .type(PostCreateParams.Media.Type.VIDEO)
        .build())
    .scheduledAt("2026-03-15T14:00:00Z")
    .targetOptions(PostCreateParams.TargetOptions.builder()
        .putAdditionalProperty("youtube", JsonValue.from(Map.of(
            "title", "Big Announcement",
            "visibility", "public",
            "first_comment", "What do you think? Let me know in the comments!"
        )))
        .build())
    .build());
curl -X POST https://api.relayapi.dev/v1/posts \
  -H "Authorization: Bearer $RELAY_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "content": "New video dropping tomorrow!",
    "targets": ["youtube"],
    "media": [
      {"url": "https://cdn.example.com/video.mp4", "type": "video"}
    ],
    "scheduled_at": "2026-03-15T14:00:00Z",
    "target_options": {
      "youtube": {
        "title": "Big Announcement",
        "visibility": "public",
        "first_comment": "What do you think? Let me know in the comments!"
      }
    }
  }'

Scheduled YouTube videos upload immediately as private. The video URL exists right away, but the video is not publicly visible until the scheduled time. The first_comment is posted at the scheduled time, not at upload time.

Media Requirements

Videos

PropertyShortsRegular Video
Max duration3 minutes12 hours (verified), 15 min (unverified)
Max file size256 GB256 GB
FormatsMP4, MOV, AVI, WMV, FLV, 3GP, WebMSame
Aspect ratio9:16 (vertical)16:9 (horizontal)
Recommended resolution1080 x 1920 px3840 x 2160 px (4K)
Frame rate30 fps24-60 fps
CodecH.264H.264 or H.265
AudioAAC, 128 kbpsAAC, 384 kbps

Thumbnails

PropertyRequirement
FormatsJPEG, PNG, GIF
Max size2 MB
Recommended1280 x 720 px (16:9)
Min width640 px
Shorts supportNot supported via API

target_options Fields

All fields go inside target_options.youtube on your post request.

FieldTypeDescription
contentstringOverride video description for YouTube specifically
titlestringVideo title (max 100 chars). Falls back to first line of content if not provided.
visibilitystring"public", "private", or "unlisted" (default: "public")
made_for_kidsbooleanCOPPA compliance flag. Default: false. Permanently disables comments if set to true.
contains_synthetic_mediabooleanAI-generated content disclosure
category_idstringYouTube category. Default: "22" (People & Blogs). Common: "1" Film, "10" Music, "20" Gaming, "27" Education, "28" Sci & Tech
tagsstring[]Video tags. Each tag max 100 chars, total max 500 chars combined.
first_commentstringAuto-posted and pinned comment (max 10,000 chars)

Common Errors

ErrorCauseFix
Account suspended (403)YouTube channel is suspendedCheck channel status via the account health endpoint.
Upload initialization failed (403)Channel suspended, quota exceeded, or missing permissionsVerify channel status, quota, and OAuth scopes.
Failed to fetch video (404)Video URL expired or inaccessibleVerify the media URL is still valid and publicly accessible.
Permission errorMissing required OAuth scopesReconnect the account with youtube.upload and youtube scopes.

Known Quirks

  • Video-only platform — every post requires exactly one video file. No image or text posts.
  • Shorts are auto-detected from duration (3 minutes or less) AND vertical aspect ratio (9:16).
  • Videos under 15 seconds loop automatically when detected as Shorts.
  • Custom thumbnails not supported for Shorts via the API.
  • Large videos (1 GB+) can take 30-60+ minutes to process. Do not retry during processing.
  • made_for_kids: true is permanent — it cannot be undone and disables comments, notifications, and personalized ads.
  • COPPA violations carry fines of $42,000+ — use the made_for_kids flag carefully.
  • Unverified channels are limited to 15-minute video uploads.
  • Daily upload quotas vary by channel and are enforced by YouTube.
  • Scheduled videos upload immediately as private — the URL exists right away but is not publicly visible.

Automations

YouTube automation uses Data API v3. PubSubHubbub drives new-video events; comments + live chat are polling-based.

Triggers

TypeFires on
youtube_commentNew comment on a video
youtube_live_chatLive chat message during a broadcast
youtube_new_videoNew video published (PubSub)

Send nodes

Base: https://www.googleapis.com/youtube/v3.

NodeEndpointRequired fields
youtube_reply_to_commentPOST /comments?part=snippettext, parent_id
youtube_send_live_chatPOST /liveChat/messages?part=snippettext, live_chat_id
youtube_moderate_commentPOST /comments/setModerationStatuscomment_id, moderation_status ∈ heldForReview|published|rejected

Rate limits

  • 10,000 quota units/day. Comment inserts ≈ 50 units, live chat messages ≈ 5 units. Budget accordingly.

Found something wrong? Help us improve this page.

On this page

Submit an Issue
Requires a GitHub account.View repo