Export files to YouTube in .NET C# using open source libraries
Use Google’s open source .NET client to upload a local MP4 to your YouTube channel. This console app requests upload-only access, sets the video to private, disables subscriber notifications, and prints the video ID returned by YouTube. Upload acceptance and finished video processing are separate results.
Prerequisites
This walkthrough uses Bash on Linux and a browser on the same computer. Install the .NET 10 SDK, using a current security patch. The local example was checked with SDK 10.0.403 and runtime 10.0.14. The SDK includes the runtime; the runtime alone cannot create or build a project.
You also need a small, playable MP4 you own or have permission to upload, a YouTube channel, and its channel ID. Follow YouTube’s channel-ID instructions while signed in as the intended channel’s primary owner. Use UTF-8 filenames. This app sends the file as video/mp4; it does not convert other files into video or validate their codecs.
Follow Google’s project and OAuth setup to enable YouTube Data API v3 in a Google Cloud project. Configure the consent screen and add your Google account as a test user when the app is in testing. Create an OAuth client of type Desktop app and download its JSON file to a private location outside your source repository. An API key does not authorize uploads.
Google’s desktop OAuth flow uses a browser and a local callback listener. This example is for an interactive local application; a remote headless server needs a separate authorization design. During consent, select the intended YouTube channel, including the Brand Account when applicable. A Google account can have more than one channel.
Setting up the YouTube API client
Start in a writable folder outside an existing .NET project. The guard below refuses parent project configuration so that an inherited SDK pin or build settings cannot silently change this standalone example. It creates a new youtube-upload folder and leaves your shell in its original directory. An existing folder is preserved and causes setup to stop.
(
command -v dotnet >/dev/null || exit 1
parent=$(pwd -P)
while :; do
for name in global.json Directory.Build.props Directory.Build.targets Directory.Packages.props NuGet.Config nuget.config NuGet.config; do
if [ -e "$parent/$name" ]; then
printf 'Use a folder without parent .NET configuration: %s\n' "$parent/$name" >&2
exit 1
fi
done
[ "$parent" = / ] && break
parent=${parent%/*}
parent=${parent:-/}
done
dotnet --version &&
mkdir youtube-upload &&
cd youtube-upload &&
dotnet new console --framework net10.0 &&
dotnet add package Google.Apis.YouTube.v3 --version 1.78.0.4309 &&
dotnet build
)
The package is Google’s YouTube Data API client for .NET. If creation, installation, or build fails, fix that reported error before continuing. Keep any files already created; do not paste the creation block over an existing project.
Uploading videos
Replace the generated youtube-upload/Program.cs with this complete program. It uses GoogleWebAuthorizationBroker and FileDataStore to obtain and retain user authorization. A token directory is associated with the channel you selected during consent; its name does not select a channel.
using Google.Apis.Auth.OAuth2;
using Google.Apis.Auth.OAuth2.Responses;
using Google.Apis.Services;
using Google.Apis.Upload;
using Google.Apis.Util.Store;
using Google.Apis.YouTube.v3;
using Google.Apis.YouTube.v3.Data;
public static class Program
{
public static async Task<int> Main(string[] args)
{
if (args.Length != 5 || string.IsNullOrWhiteSpace(args[3]) ||
string.IsNullOrWhiteSpace(args[4]) || args[4].Length > 100)
{
Console.Error.WriteLine(
"Usage: client.json video.mp4 token-directory expected-channel-id title");
return 2;
}
if (!File.Exists(args[0]) || !File.Exists(args[1]))
{
Console.Error.WriteLine("OAuth client file or video is missing. No upload started.");
return 2;
}
try
{
using var clientFile = File.OpenRead(args[0]);
using var videoFile = File.OpenRead(args[1]);
using var deadline = new CancellationTokenSource(TimeSpan.FromMinutes(10));
var store = new FileDataStore(Path.GetFullPath(args[2]), true);
var cached = await store.GetAsync<TokenResponse>("channel-owner");
Console.Error.WriteLine(cached is null
? "Opening browser authorization. Select the intended YouTube channel."
: "Using saved authorization; no new channel selection was requested.");
var credential = await GoogleWebAuthorizationBroker.AuthorizeAsync(
GoogleClientSecrets.FromStream(clientFile).Secrets,
new[] { YouTubeService.Scope.YoutubeUpload },
"channel-owner", deadline.Token, store);
var scopes = credential.Token.Scope?.Split(' ', StringSplitOptions.RemoveEmptyEntries);
if (scopes is null || scopes.Length != 1 ||
scopes[0] != YouTubeService.Scope.YoutubeUpload)
{
Console.Error.WriteLine("Expected upload-only consent. Use a fresh token directory.");
return 1;
}
using var youtube = new YouTubeService(new BaseClientService.Initializer
{
HttpClientInitializer = credential,
ApplicationName = "PrivateVideoUpload"
});
youtube.HttpClient.Timeout = TimeSpan.FromSeconds(60);
await UploadPrivateVideoAsync(youtube, videoFile, args[3], args[4], deadline.Token);
Console.WriteLine("Private upload accepted; processing may still be pending.");
return 0;
}
catch (Exception)
{
Console.Error.WriteLine(
"Upload failed or its outcome is unknown. Inspect the selected channel in " +
"YouTube Studio before retrying. Do not automatically repeat this upload.");
return 1;
}
}
public static async Task<string> UploadPrivateVideoAsync(
YouTubeService youtube, Stream source, string expectedChannelId,
string title, CancellationToken cancellationToken)
{
var video = new Video
{
Snippet = new VideoSnippet { Title = title, CategoryId = "22" },
Status = new VideoStatus { PrivacyStatus = "private" }
};
var upload = youtube.Videos.Insert(video, "snippet,status", source, "video/mp4");
upload.NotifySubscribers = false;
Console.Error.WriteLine($"Upload settings: privacy={video.Status.PrivacyStatus}; notifySubscribers={upload.NotifySubscribers}");
upload.ChunkSize = ResumableUpload.MinimumChunkSize;
upload.ProgressChanged += progress =>
{
if (progress.Status == UploadStatus.Uploading)
Console.Error.WriteLine($"Bytes sent: {progress.BytesSent}");
};
var progress = await upload.UploadAsync(cancellationToken);
progress.ThrowOnFailure();
var result = upload.ResponseBody;
if (progress.Status != UploadStatus.Completed || string.IsNullOrWhiteSpace(result?.Id))
throw new InvalidOperationException("No confirmed video ID was returned.");
Console.WriteLine($"Video ID: {result.Id}");
if (result.Snippet?.ChannelId != expectedChannelId || result.Snippet?.Title != title ||
result.Status?.PrivacyStatus != "private")
throw new InvalidOperationException("Returned video metadata did not match the upload.");
return result.Id;
}
}
From the original working folder, run the following command after replacing all five arguments. Choose an unused private token-directory path for the first authorization. The command builds the saved program before running it; -- separates application arguments from .NET options.
dotnet run --project ./youtube-upload -- \
"/absolute/path/client.json" "/absolute/path/my video.mp4" \
"/absolute/private/path/youtube-channel-tokens" \
"YOUR_CHANNEL_ID" "My private upload"
Complete Google’s consent flow in the browser. The program requests only youtube.upload, one of the scopes accepted by videos.insert. It explicitly sends privacyStatus=private and notifySubscribers=false, whose default would otherwise be true.
On acceptance, the program prints Video ID: followed by the actual ID. Save that ID and inspect the video in YouTube Studio under the selected channel. The expected channel ID argument is a check on the returned metadata after upload, not a destination selector or a way to prevent an upload to a wrongly authorized channel. If the check fails, the program exits unsuccessfully but prints the returned ID so you can locate the video.
Bytes sent are transport progress. Even a completed upload with an ID does not prove that YouTube has finished processing a playable video. Check processing in YouTube Studio before publishing; Google documents a separate processingDetails.processingStatus for this stage.
Handling quotas and rate limits
Check your project’s current limits in Google Cloud and the YouTube quota calculator before automating uploads. As of November 5, 2026, the documented default is a separate bucket of 100 videos.insert calls per day, plus separate limits for search and other endpoints. Daily quotas reset at midnight Pacific Time. Invalid requests consume quota too.
A local counter cannot account for other processes, restarts, or every SDK request. This example makes one application-level insert attempt; it does not implement a project-wide quota scheduler.
Handle failures before retrying
A nonzero exit can mean refusal, cancellation, a timeout, or a lost response after YouTube accepted the video. The app requests cancellation of authorization and upload after ten minutes and configures a 60-second HTTP timeout per upload request. Cancellation is cooperative, rather than a process shutdown guarantee, and a slow connection can interrupt an otherwise valid upload. Neither timeout proves that a remote write was rolled back.
The SDK uses resumable upload requests, but this program does not save their session URI for recovery after the process ends. Do not wrap the entire dotnet run command in a retry loop: a new videos.insert can create a second video when the first response was lost. Inspect the intended channel and any printed video ID before deciding whether to try again. Progress output alone cannot resolve an unknown outcome.
Keep authorization tied to its channel
FileDataStore persists tokens; it is not an encrypted credential vault. Restrict access to the client JSON and token directory, and keep both out of source control. Reusing the directory can skip the browser, so a successful cached run is not evidence of a fresh channel selection. To authorize a different channel, choose another unused private token-directory path and complete consent again.
You now have a single-upload console entry point whose confirmed result is a private video ID. Keep the video private until you have checked its channel, content, and processing result in YouTube Studio. For the request fields available beyond this example, consult the official upload reference.
Rather not run this yourself?
Transloadit exports files to S3, Azure, Google Cloud and more as a step in one API call.
