The YouTubeVideoUploadService
lets you upload videos directly to YouTube through the Google Ads API. These
videos can then be used to create video assets in various ad types, such as
Performance Max campaigns or
Demand Gen campaigns.
This service streamlines the workflow of creating video ads by handling the YouTube upload process, ensuring that the videos are correctly associated with your account.
Key concepts
Before you begin, it's important to understand how video uploads are managed and the different states they can go through.
End-to-end workflow
Uploading a video file and serving it in an ad requires four sequential steps:
| Step | Service and method | Summary |
|---|---|---|
| 1. Initiate and upload video bytes | CreateYouTubeVideoUpload on YouTubeVideoUploadService |
Upload the video metadata and binary file to create a YouTubeVideoUpload resource (customers/{customer_id}/youTubeVideoUploads/{video_upload_id}). |
| 2. Poll processing state | SearchStream on GoogleAdsService |
Query you_tube_video_upload until state = 'PROCESSED' and the 11-character YouTube video_id is populated. |
| 3. Create a reusable video asset | MutateAssets on AssetService |
Create a YoutubeVideoAsset (youtube_video_id = video_id) to get an asset resource name (customers/{customer_id}/assets/{asset_id}). |
| 4. Link the asset to campaigns or ads | MutateAssetGroupAssets on AssetGroupAssetService, or MutateAdGroupAds on AdGroupAdService |
Link the asset to a Performance Max AssetGroupAsset (field_type = YOUTUBE_VIDEO) or a Demand Gen DemandGenVideoResponsiveAdInfo (videos[].asset). |
Channel ownership
When you upload a video, you can specify the destination YouTube channel using
the channel_id field in the YouTubeVideoUpload
resource:
- Advertiser-owned (brand) channel: Provide the
channel_idof an existing YouTube channel owned by the advertiser. This is only supported with the user authentication flow; it cannot be used with service accounts. - Google-managed channel: If
channel_idis omitted, the video is uploaded to a Google-managed YouTube channel associated with the Google Ads account.
Upload states
The lifecycle of a YouTube video upload is tracked by the state field.
The YouTubeVideoUploadState
enum defines the following states:
| State | Description |
|---|---|
PENDING |
The video is being uploaded. |
UPLOADED |
The video has been successfully uploaded and is being processed by YouTube. |
PROCESSED |
The video has been successfully processed and is ready for use. |
FAILED |
The upload or processing failed and cannot be completed. |
REJECTED |
The video was rejected due to validation or policy reasons. |
UNAVAILABLE |
The video state is unavailable; it may have been removed from YouTube. |
Privacy settings
The video_privacy field controls who can see the uploaded video. The
YouTubeVideoPrivacy
enum supports:
PUBLIC: The video is available to anyone on YouTube. (Only allowed for brand channels).UNLISTED: The video is not searchable but can be viewed by anyone with the link. This is the default and the only option for Google-managed channels.
Generative content disclosure
Starting in v26, under regulations such as the EU AI Act, advertisers can disclose whether uploaded video content contains AI-generated or synthetic media.
The video_content_metadata field in YouTubeVideoUpload contains
VideoContentMetadata. Set its nested
generative_content_disclosure field using the
YouTubeVideoUploadGenerativeContentDisclosure
enum:
| Value | Description |
|---|---|
NON_GENERATIVE_CONTENT |
The advertiser disclosed the video as non-AI generated. |
GENERATIVE_CONTENT |
The advertiser disclosed the video as AI generated. |
Upload a video
To upload a video, you must use a multi-part request to the
CreateYouTubeVideoUpload method. The request contains both the metadata for
the upload and the video file itself.
1. Initiate the upload
Construct a CreateYouTubeVideoUploadRequest with the following fields:
| Field | Requirement | Description and constraints |
|---|---|---|
customer_id |
Required | The 10-digit Google Ads customer ID (without hyphens). |
you_tube_video_upload.video_title |
Required (INPUT_ONLY, IMMUTABLE) |
Title of the uploaded video on YouTube (up to 100 characters; cannot contain < or >). |
you_tube_video_upload.video_description |
Optional (INPUT_ONLY, IMMUTABLE) |
Description of the video on YouTube (up to 5,000 characters; cannot contain < or >). |
you_tube_video_upload.channel_id |
Optional (IMMUTABLE) |
Destination 24-character YouTube channel ID (starting with UC) for an advertiser-owned brand channel. If omitted, uploads to a Google-managed channel. |
you_tube_video_upload.video_privacy |
Optional (mutable on brand channels) | YouTubeVideoPrivacy setting (UNLISTED default, or PUBLIC for advertiser-owned brand channels). |
you_tube_video_upload.video_content_metadata |
Optional (INPUT_ONLY, IMMUTABLE) |
Available starting in v26: VideoContentMetadata with generative_content_disclosure set to a YouTubeVideoUploadGenerativeContentDisclosure value (NON_GENERATIVE_CONTENT or GENERATIVE_CONTENT). |
If you are using a client library, call the
CreateYouTubeVideoUpload
method, passing your video file, and the video upload will be handled
internally.
Java
String videoUploadResourceName; try (YouTubeVideoUploadServiceClient youTubeVideoUploadServiceClient = googleAdsClient.getLatestVersion().createYouTubeVideoUploadServiceClient()) { YouTubeVideoUpload youTubeVideoUpload = YouTubeVideoUpload.newBuilder() .setVideoTitle("Test Video") .setVideoDescription("Test Video Description") .setVideoPrivacy(YouTubeVideoPrivacy.UNLISTED) .build(); CreateYouTubeVideoUploadRequest createUploadRequest = CreateYouTubeVideoUploadRequest.newBuilder() .setCustomerId(Long.toString(customerId)) .setYouTubeVideoUpload(youTubeVideoUpload) .build(); CreateYouTubeVideoUploadResponse response = youTubeVideoUploadServiceClient.createYouTubeVideoUpload( createUploadRequest, () -> new FileInputStream(videoFile), null); videoUploadResourceName = response.getResourceName(); System.out.printf("Created YouTube video upload: %s%n", videoUploadResourceName); }
C#
YouTubeVideoUploadServiceClient ytService = client.GetService( Services.V26.YouTubeVideoUploadService); CreateYouTubeVideoUploadRequest createUploadRequest = new CreateYouTubeVideoUploadRequest() { CustomerId = customerId.ToString(), YouTubeVideoUpload = new YouTubeVideoUpload() { VideoTitle = "Test Video", VideoDescription = "Test Video Description", VideoPrivacy = YouTubeVideoPrivacy.Unlisted } }; string videoUploadResourceName; using (FileStream stream = File.OpenRead(videoFilePath)) { ResumableUploadSession<CreateYouTubeVideoUploadRequest, CreateYouTubeVideoUploadResponse> session = ytService.CreateYouTubeVideoUpload(); CreateYouTubeVideoUploadResponse response = session.BeginUploadAsync(createUploadRequest, stream).Result; videoUploadResourceName = response.ResourceName; Console.WriteLine($"Created YouTube video upload: {videoUploadResourceName}"); }
PHP
$youTubeVideoUploadServiceClient = $googleAdsClient->getYouTubeVideoUploadServiceClient(); $youTubeVideoUpload = new YouTubeVideoUpload([ 'video_title' => 'Test Video', 'video_description' => 'Test Video Description', 'video_privacy' => YouTubeVideoPrivacy::UNLISTED ]); $createYouTubeVideoUploadRequest = CreateYouTubeVideoUploadRequest::build( $customerId, $youTubeVideoUpload ); /** @var ResumableUpload $resumableUpload */ $resumableUpload = $youTubeVideoUploadServiceClient->createYouTubeVideoUpload( $createYouTubeVideoUploadRequest ); $stream = Utils::streamFor(fopen($videoFilePath, 'rb')); /** @var CreateYouTubeVideoUploadResponse $response */ $response = $resumableUpload->startUpload($stream); $videoUploadResourceName = $response->getResourceName(); printf("Created YouTube video upload: '%s'%s", $videoUploadResourceName, PHP_EOL);
Python
yt_service: YouTubeVideoUploadServiceClient = client.get_service( "YouTubeVideoUploadService" ) create_upload_request: CreateYouTubeVideoUploadRequest = ( youtube_video_upload_service.CreateYouTubeVideoUploadRequest() ) create_upload_request.customer_id = customer_id create_upload_request.you_tube_video_upload.video_title = "Test Video" create_upload_request.you_tube_video_upload.video_description = ( "Test Video Description" ) create_upload_request.you_tube_video_upload.video_privacy = ( client.enums.YouTubeVideoPrivacyEnum.UNLISTED ) video_upload_resource_name: str with open(video_file_path, "rb") as stream: response: CreateYouTubeVideoUploadResponse = ( yt_service.create_you_tube_video_upload( stream=stream, request=create_upload_request, retry=None, ) ) video_upload_resource_name = response.resource_name print(f"Created YouTube video upload: {video_upload_resource_name}")
Ruby
video_upload = client.resource.you_tube_video_upload do |v| v.video_title = "Test Video" v.video_description = "Test Video Description" v.video_privacy = :UNLISTED end upload = client.service.you_tube_video_upload.create_you_tube_video_upload( customer_id: customer_id, you_tube_video_upload: video_upload, ) video_upload_resource_name = nil File.open(video_file_path, "rb") do |stream| response = upload.start( stream: stream, content_type: "application/octet-stream", ) video_upload_resource_name = response.resource_name puts "Created YouTube video upload: #{video_upload_resource_name}" end
Perl
This example is not yet available in Perl; you can take a look at the other languages.
curl
# # Use the --i curl parameter to capture response headers in the $RESPONSE # variable. FILE_SIZE=$(wc -c < "${VIDEO_FILE_NAME}" | tr -d '\r') RESPONSE=$(curl -i -f -v -s --request POST \ "https://googleads.googleapis.com/resumable/upload/v${API_VERSION}/customers/${CUSTOMER_ID}/youTubeVideoUploads:create" \ --header "Content-Type: application/json" \ --header "login-customer-id: ${MANAGER_CUSTOMER_ID}" \ --header "Authorization: Bearer ${OAUTH2_ACCESS_TOKEN}" \ --header "X-Goog-Upload-Protocol: resumable" \ --header "X-Goog-Upload-Command: start" \ --header "X-Goog-Upload-Header-Content-Length: ${FILE_SIZE}" \ --data @- <<EOF { "customer_id": "${CUSTOMER_ID}", "you_tube_video_upload": { "video_title": "${VIDEO_TITLE}", "video_description": "${VIDEO_DESCRIPTION}", "video_privacy": "UNLISTED" } } EOF ) # Extract the value of the "x-goog-upload-url" header from the HTTP response. UPLOAD_URL=$(echo "${RESPONSE}" \ | grep -i '^x-goog-upload-url' \ | awk '{print $2}' \ | tr -d '\r') CHUNK_SIZE=$(echo "${RESPONSE}" \ | grep -i '^x-goog-upload-chunk-granularity' \ | awk '{print $2}' \ | tr -d '\r')
If you are using REST, the following section describes how to manage the video upload.
2. Upload the video
When uploading with REST, CreateYouTubeVideoUpload uses a two-step
resumable upload protocol:
- Start the upload session: Send an initial
POSTrequest to theCreateYouTubeVideoUploadupload endpoint (/upload/v26/customers/{customer_id}/youTubeVideoUploads:create) withx-goog-upload-command: start(and optionallyx-goog-upload-header-content-lengthto declare the file size). The response returns the upload session URL in thex-goog-upload-urlHTTP response header, along with metadata such as the expected chunk size. - Stream and finalize the video bytes: Send the binary video payload to
the
x-goog-upload-urlendpoint withx-goog-upload-command: upload, finalize. When the upload finishes, the response returns the createdresource_name(customers/{customer_id}/youTubeVideoUploads/{video_upload_id}).
For a full description of the HTTP headers used in the video upload protocol, refer to the following code example:
# Take the first ${CHUNK_SIZE} bytes of the video file and upload them. head -c ${CHUNK_SIZE} ${VIDEO_FILE_NAME} | curl -i -v -X PUT "${UPLOAD_URL}" \ --header "Authorization: Bearer ${OAUTH2_ACCESS_TOKEN}" \ --header "X-Goog-Upload-Offset: 0" \ --header "X-Goog-Upload-Command: upload" \ --header "Content-Length: ${CHUNK_SIZE}" \ --data-binary @- # Query the status of the upload. QUERY_RESPONSE=$(curl -i -s -X POST "${UPLOAD_URL}" \ --header "Authorization: Bearer ${OAUTH2_ACCESS_TOKEN}" \ --header "X-Goog-Upload-Command: query") # Extract the value of the "x-goog-upload-size-received" header from the HTTP # response. UPLOADED_BYTES=$(echo "${QUERY_RESPONSE}" \ | grep -i '^x-goog-upload-size-received' \ | awk '{print $2}' \ | tr -d '\r') echo "Uploaded ${UPLOADED_BYTES} bytes." REMAINING_BYTES=$((FILE_SIZE - UPLOADED_BYTES)) echo "${REMAINING_BYTES} bytes remaining to upload." FINALIZE_RESPONSE=$(tail -c ${REMAINING_BYTES} ${VIDEO_FILE_NAME} | curl -v -X PUT "${UPLOAD_URL}" \ --header "Authorization: Bearer ${OAUTH2_ACCESS_TOKEN}" \ --header "X-Goog-Upload-Offset: ${UPLOADED_BYTES}" \ --header "X-Goog-Upload-Command: upload, finalize" \ --data-binary @-) UPLOADED_VIDEO_RESOURCE_NAME=$(echo $FINALIZE_RESPONSE | jq -r '.resourceName')
3. Retrieve the video upload state
After starting a video upload, poll its processing state by querying the
selectable fields of the you_tube_video_upload resource
(resource_name, video_upload_id, channel_id, video_id, state, and
video_privacy) with GAQL until state = 'PROCESSED' (INPUT_ONLY fields
cannot be selected in GAQL):
Java
// Retrieve the metadata of the newly uploaded video. String query = String.format( "SELECT " + "you_tube_video_upload.resource_name, " + "you_tube_video_upload.video_id, " + "you_tube_video_upload.state " + "FROM you_tube_video_upload " + "WHERE you_tube_video_upload.resource_name = '%s'", videoUploadResourceName); try (GoogleAdsServiceClient googleAdsServiceClient = googleAdsClient.getLatestVersion().createGoogleAdsServiceClient()) { SearchGoogleAdsStreamRequest request = SearchGoogleAdsStreamRequest.newBuilder() .setCustomerId(Long.toString(customerId)) .setQuery(query) .build(); ServerStream<SearchGoogleAdsStreamResponse> stream = googleAdsServiceClient.searchStreamCallable().call(request); for (SearchGoogleAdsStreamResponse response : stream) { for (GoogleAdsRow googleAdsRow : response.getResultsList()) { YouTubeVideoUpload video = googleAdsRow.getYouTubeVideoUpload(); System.out.printf( "Video with ID %s was found in state %s.%n", video.getVideoId(), video.getState()); } } }
C#
// Retrieve the metadata of the newly uploaded video. string query = $@" SELECT you_tube_video_upload.resource_name, you_tube_video_upload.video_id, you_tube_video_upload.state FROM you_tube_video_upload WHERE you_tube_video_upload.resource_name = '{videoUploadResourceName}'"; GoogleAdsServiceClient gaService = client.GetService( Services.V26.GoogleAdsService); gaService.SearchStream(customerId.ToString(), query, delegate (SearchGoogleAdsStreamResponse resp) { foreach (GoogleAdsRow row in resp.Results) { Console.WriteLine( $"Video with ID {row.YouTubeVideoUpload.VideoId} was found in " + $"state {row.YouTubeVideoUpload.State}."); } } );
PHP
// Retrieve the metadata of the newly uploaded video. $query = sprintf( "SELECT you_tube_video_upload.resource_name, " . "you_tube_video_upload.video_id, " . "you_tube_video_upload.state " . "FROM you_tube_video_upload " . "WHERE you_tube_video_upload.resource_name = '%s'", $videoUploadResourceName ); $googleAdsServiceClient = $googleAdsClient->getGoogleAdsServiceClient(); $stream = $googleAdsServiceClient->searchStream( SearchGoogleAdsStreamRequest::build($customerId, $query) ); foreach ($stream->iterateAllElements() as $googleAdsRow) { /** @var GoogleAdsRow $googleAdsRow */ printf( "Video with ID '%s' was found in state '%s'.%s", $googleAdsRow->getYouTubeVideoUpload()->getVideoId(), YouTubeVideoUploadState::name($googleAdsRow->getYouTubeVideoUpload()->getState()), PHP_EOL ); }
Python
# Retrieve the metadata of the newly uploaded video. query: str = f""" SELECT you_tube_video_upload.resource_name, you_tube_video_upload.video_id, you_tube_video_upload.state FROM you_tube_video_upload WHERE you_tube_video_upload.resource_name = '{video_upload_resource_name}'""" ga_service: GoogleAdsServiceClient = client.get_service("GoogleAdsService") stream: Iterator[SearchGoogleAdsStreamResponse] = ga_service.search_stream( customer_id=customer_id, query=query ) for row in itertools.chain.from_iterable(batch.results for batch in stream): video = row.you_tube_video_upload print( f"Video with ID {row.you_tube_video_upload.video_id} was found in state {row.you_tube_video_upload.state}." )
Ruby
# Retrieve the metadata of the newly uploaded video. query = <<~EOQ SELECT you_tube_video_upload.resource_name, you_tube_video_upload.video_id, you_tube_video_upload.state FROM you_tube_video_upload WHERE you_tube_video_upload.resource_name = '#{video_upload_resource_name}' EOQ responses = client.service.google_ads.search_stream( customer_id: customer_id, query: query, ) responses.each do |batch| batch.results.each do |row| puts "Video with ID #{row.you_tube_video_upload.video_id} was found in " \ "state #{row.you_tube_video_upload.state}." end end
Perl
This example is not yet available in Perl; you can take a look at the other languages.
curl
curl -i -v -X POST \ "https://qa-prod-googleads.sandbox.googleapis.com/v${API_VERSION}/customers/${CUSTOMER_ID}/googleAds:search" \ --header "Content-Type: application/json" \ --header "Developer-Token: ${DEVELOPER_TOKEN}" \ --header "login-customer-id: ${MANAGER_CUSTOMER_ID}" \ --header "Authorization: Bearer ${OAUTH2_ACCESS_TOKEN}" \ --data @- <<EOF { "query": "SELECT you_tube_video_upload.resource_name, you_tube_video_upload.video_id, you_tube_video_upload.state FROM you_tube_video_upload WHERE you_tube_video_upload.resource_name = '$UPLOADED_VIDEO_RESOURCE_NAME'" } EOF
Manage uploads
After a video upload is completed, you can use it as a video asset.
Use the uploaded video
After a video reaches the PROCESSED state, you can find its 11-character
YouTube video ID in the video_id field of the YouTubeVideoUpload resource.
Use this video_id to create a YoutubeVideoAsset with
MutateAssets and link the resulting asset resource name
(customers/{customer_id}/assets/{asset_id}) to your campaigns or ads:
Create the
YoutubeVideoAssetby passingyoutube_video_idtoAssetService.MutateAssets:POST https://googleads.googleapis.com/v26/customers/{customer_id}/assets:mutate { "operations": [ { "create": { "name": "Product Launch Video", "youtubeVideoAsset": { "youtubeVideoId": "dQw4w9WgXcQ" } } } ] }Link the
Assetto anAssetGrouporAdGroupAd:Performance Max: Create an
AssetGroupAssetusingAssetGroupAssetService.MutateAssetGroupAssetswithfield_typeset toYOUTUBE_VIDEO:POST https://googleads.googleapis.com/v26/customers/{customer_id}/assetGroupAssets:mutate { "operations": [ { "create": { "assetGroup": "customers/{customer_id}/assetGroups/{asset_group_id}", "asset": "customers/{customer_id}/assets/{asset_id}", "fieldType": "YOUTUBE_VIDEO" } } ] }Demand Gen: Reference
customers/{customer_id}/assets/{asset_id}in thevideoslist of aDemandGenVideoResponsiveAdInfoobject when callingAdGroupAdService.MutateAdGroupAds:POST https://googleads.googleapis.com/v26/customers/{customer_id}/adGroupAds:mutate { "operations": [ { "create": { "adGroup": "customers/{customer_id}/adGroups/{ad_group_id}", "status": "PAUSED", "ad": { "name": "Demand Gen Video Ad", "finalUrls": ["https://www.example.com"], "demandGenVideoResponsiveAd": { "videos": [ { "asset": "customers/{customer_id}/assets/{asset_id}" } ], "logoImages": [ { "asset": "customers/{customer_id}/assets/{logo_asset_id}" } ], "headlines": [ { "text": "Example Headline" } ], "longHeadlines": [ { "text": "Example Long Headline" } ], "descriptions": [ { "text": "Example Description" } ], "businessName": { "text": "Example Business" } } } } } ] }
Query existing
YOUTUBE_VIDEOassets usingGoogleAdsService.SearchStream:SELECT asset.id, asset.resource_name, asset.name, asset.type, asset.youtube_video_asset.youtube_video_id, asset.youtube_video_asset.youtube_video_title FROM asset WHERE asset.type = 'YOUTUBE_VIDEO' AND asset.youtube_video_asset.youtube_video_id = '{video_id}'
Update metadata
You can update the privacy setting of a video uploaded through this API using
the UpdateYouTubeVideoUpload method with update_mask
set to "video_privacy". Only the video_privacy field (PUBLIC or
UNLISTED, for videos uploaded to an advertiser-owned brand channel) can be
updated after creation; video_title, video_description, channel_id, and
video_content_metadata are immutable.
Remove uploads
If you need to delete videos uploaded with the Google Ads API, pass their
customers/{customer_id}/youTubeVideoUploads/{video_upload_id} resource names
in the resource_names field of the
RemoveYouTubeVideoUpload method. This removes the
video from both the Google Ads asset library and YouTube.