Uploading to YouTube directly means creating a Google Cloud project, handling Google OAuth and refresh tokens, starting a resumable upload session, streaming the file, and then making separate calls for the thumbnail and comments. With Relaycast it is one POST /v1/posts request, and the same request can publish the video to other networks too.
This tutorial covers every YouTube option the API supports.
Prerequisites
- A Relaycast project and an API key.
- A YouTube channel connected under Integrations in the dashboard (you sign in with Google and approve access).
- A video file at a public http(s) URL, or uploaded to your project's media library.
export RELAYCAST_API_URL="https://api.your-domain.com"
export RELAYCAST_API_KEY="your-key-here"Find the channel's account ID with GET /v1/accounts; it is the entry with "platform": "youtube".
The minimum request
A YouTube target needs exactly one video in media, and nothing else. The post content becomes the video description.
curl -X POST "$RELAYCAST_API_URL/v1/posts" \
-H "x-access-key: $RELAYCAST_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"content": "How we rebuilt our onboarding in two weeks.\n\nChapters:\n0:00 Intro\n1:20 The problem\n4:05 What we changed",
"media": [{ "url": "https://cdn.example.com/onboarding.mp4", "type": "video" }],
"targets": [{ "accountId": "YOUTUBE_ACCOUNT_ID", "options": { "visibility": "private" } }],
"publishNow": true
}'const res = await fetch(`${process.env.RELAYCAST_API_URL}/v1/posts`, {
method: "POST",
headers: { "x-access-key": process.env.RELAYCAST_API_KEY!, "Content-Type": "application/json" },
body: JSON.stringify({
content: "How we rebuilt our onboarding in two weeks.\n\nChapters:\n0:00 Intro\n1:20 The problem\n4:05 What we changed",
media: [{ url: "https://cdn.example.com/onboarding.mp4", type: "video" }],
targets: [{ accountId: "YOUTUBE_ACCOUNT_ID", options: { visibility: "private" } }],
publishNow: true,
}),
});
console.log((await res.json()).data.id);import os, requests
res = requests.post(
f"{os.environ['RELAYCAST_API_URL']}/v1/posts",
headers={"x-access-key": os.environ["RELAYCAST_API_KEY"]},
json={
"content": "How we rebuilt our onboarding in two weeks.\n\nChapters:\n0:00 Intro\n1:20 The problem\n4:05 What we changed",
"media": [{"url": "https://cdn.example.com/onboarding.mp4", "type": "video"}],
"targets": [{"accountId": "YOUTUBE_ACCOUNT_ID", "options": {"visibility": "private"}}],
"publishNow": True,
},
)
print(res.json()["data"]["id"])Without a title option, the title is taken from the first line of the content (here, "How we rebuilt our onboarding in two weeks."), trimmed to 100 characters.
Start with "visibility": "private" while you test. You can change a private video to public in YouTube Studio once you are happy with it.
All YouTube options
YouTube options go in the target's options object.
| Option | Type | Default | Notes |
|---|---|---|---|
title | string | first line of content | Up to 100 characters. Cannot contain < or >. |
visibility | public | unlisted | private | public | |
categoryId | string | "22" (People & Blogs) | A YouTube category ID, e.g. "27" Education, "28" Science & Technology. |
tags | string[] | [] | Up to 50 tags, 500 characters in total. |
madeForKids | boolean | false | The "made for kids" self-declaration. |
thumbnailUrl | URL | none | Custom thumbnail, JPEG or PNG up to 2 MB. |
firstComment | string | none | Posted as a comment once the video is uploaded. |
containsSyntheticMedia | boolean | false | Disclosure for realistic altered or synthetic content. |
The description (your content) can be up to 5,000 characters.
A complete example
{
"content": "A walkthrough of our new analytics dashboard.\n\nRead the announcement: https://example.com/blog/analytics",
"media": [{ "url": "https://cdn.example.com/analytics-demo.mp4", "type": "video" }],
"targets": [
{
"accountId": "YOUTUBE_ACCOUNT_ID",
"options": {
"title": "Analytics dashboard walkthrough",
"visibility": "unlisted",
"categoryId": "28",
"tags": ["analytics", "product demo", "dashboard"],
"madeForKids": false,
"thumbnailUrl": "https://cdn.example.com/analytics-thumb.jpg",
"firstComment": "Questions? Ask below and we will answer in the next video.",
"containsSyntheticMedia": false
}
}
],
"publishNow": true
}Options are validated when the post is created. A 120-character title, a tag list over 500 characters, or a second media item returns a 400 validation_error straight away instead of a failed upload later.
How the upload works
You do not need to manage any of this, but it helps to know what happens:
- A resumable upload session is opened with YouTube, carrying the title, description, category, tags and status settings.
- The video is streamed from your URL straight to YouTube, without being buffered in full first.
- Once YouTube returns the new video ID, the thumbnail and first comment are applied.
Step 3 is best effort. The video already exists at that point, so if setting the thumbnail or posting the comment fails, the upload is still reported as published rather than failing or retrying (which would risk a duplicate video). A common reason the thumbnail is not applied is that the channel has not been verified for custom thumbnails in YouTube.
When the target's status is published, externalUrl is the watch URL for the video, and externalPostId is the video ID.
Google restricts what unverified API projects can do. In particular, videos uploaded through a Google Cloud project that has not passed Google's API audit are locked to private. If you connect YouTube with your own OAuth app, keep that in mind until your project is approved.
Schedule a premiere-style release
To have a video go out at a specific time, use scheduledFor instead of publishNow. Relaycast uploads it at that moment with the visibility you chose:
{
"content": "Episode 12: designing for offline-first.",
"media": [{ "url": "https://cdn.example.com/ep12.mp4", "type": "video" }],
"targets": [{ "accountId": "YOUTUBE_ACCOUNT_ID", "options": { "visibility": "public", "categoryId": "27" } }],
"scheduledFor": "2026-06-02T16:00:00Z"
}Large videos take time to upload and process, so schedule a little ahead of when you want people to see it.
One video, several networks
The same video can go to other video platforms in one request. Each target gets its own options, and each succeeds or fails independently:
{
"content": "60 seconds on our new keyboard shortcuts.",
"media": [{ "url": "https://cdn.example.com/shortcuts.mp4", "type": "video" }],
"targets": [
{ "accountId": "YOUTUBE_ACCOUNT_ID", "options": { "title": "New keyboard shortcuts in 60 seconds", "tags": ["productivity"] } },
{ "accountId": "TIKTOK_ACCOUNT_ID", "options": { "privacyLevel": "PUBLIC_TO_EVERYONE" } },
{ "accountId": "INSTAGRAM_ACCOUNT_ID", "options": { "publishAs": "reel" } }
],
"publishNow": true
}See the YouTube platform guide for more, and the create post reference for the full request format.
Ready to build?
Get an access key and publish your first post in minutes.