Upload videos

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_id of 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_id is 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:

  1. Start the upload session: Send an initial POST request to the CreateYouTubeVideoUpload upload endpoint (/upload/v26/customers/{customer_id}/youTubeVideoUploads:create) with x-goog-upload-command: start (and optionally x-goog-upload-header-content-length to declare the file size). The response returns the upload session URL in the x-goog-upload-url HTTP response header, along with metadata such as the expected chunk size.
  2. Stream and finalize the video bytes: Send the binary video payload to the x-goog-upload-url endpoint with x-goog-upload-command: upload, finalize. When the upload finishes, the response returns the created resource_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:

  1. Create the YoutubeVideoAsset by passing youtube_video_id to AssetService.MutateAssets:

    POST https://googleads.googleapis.com/v26/customers/{customer_id}/assets:mutate
    {
      "operations": [
        {
          "create": {
            "name": "Product Launch Video",
            "youtubeVideoAsset": {
              "youtubeVideoId": "dQw4w9WgXcQ"
            }
          }
        }
      ]
    }
    
  2. Link the Asset to an AssetGroup or AdGroupAd:

    • Performance Max: Create an AssetGroupAsset using AssetGroupAssetService.MutateAssetGroupAssets with field_type set to YOUTUBE_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 the videos list of a DemandGenVideoResponsiveAdInfo object when calling AdGroupAdService.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"
                  }
                }
              }
            }
          }
        ]
      }
      
  3. Query existing YOUTUBE_VIDEO assets using GoogleAdsService.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.