The AdWords API will sunset on April 27, 2022. Migrate to the Google Ads API to take advantage of the latest Google Ads features.

Extensions Samples

The code samples below provide examples of common extension functions using the AdWords API. Client Library.

Associate a Google My Business feed to that of a customer

#!/usr/bin/env ruby
# Encoding: utf-8
#
# Copyright:: Copyright 2014, Google Inc. All Rights Reserved.
#
# License:: Licensed under the Apache License, Version 2.0 (the "License");
#           you may not use this file except in compliance with the License.
#           You may obtain a copy of the License at
#
#           http://www.apache.org/licenses/LICENSE-2.0
#
#           Unless required by applicable law or agreed to in writing, software
#           distributed under the License is distributed on an "AS IS" BASIS,
#           WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
#           implied.
#           See the License for the specific language governing permissions and
#           limitations under the License.
#
# This example adds a feed that syncs feed items from a Google My Business (GMB)
# account and associates the feed with a customer.

require 'adwords_api'
require 'date'

def add_gmb_location_extensions(gmb_email_address, gmb_access_token,
                                business_account_identifier)
  # AdwordsApi::Api will read a config file from ENV['HOME']/adwords_api.yml
  # when called without parameters.
  adwords = AdwordsApi::Api.new

  # To enable logging of SOAP requests, set the log_level value to 'DEBUG' in
  # the configuration file or provide your own logger:
  # adwords.logger = Logger.new('adwords_xml.log')

  feed_srv = adwords.service(:FeedService, API_VERSION)
  customer_feed_srv = adwords.service(:CustomerFeedService, API_VERSION)

  if gmb_access_token.nil?
    gmb_access_token = adwords.get_auth_handler.get_token(
        adwords.credential_handler.credentials)[:access_token]
  end

  # Create a feed that will sync to the Google My Business account specified
  # by gmb_email_address. Do not add FeedAttributes to this object, as AdWords
  # will add them automatically because this will be a system generated feed.
  gmb_feed = {
    :name => "GMB feed #%d" % (Time.new.to_f * 1000).to_i,
    :system_feed_generation_data => {
      :xsi_type => 'PlacesLocationFeedData',
      :o_auth_info => {
        :http_method => 'GET',
        :http_request_url => 'https://www.googleapis.com/auth/adwords',
        :http_authorization_header => "Bearer %s" % gmb_access_token
      },
      :email_address => gmb_email_address
    },
    # Since this feed's feed items will be managed by AdWords, you must set
    # its origin to ADWORDS.
    :origin => 'ADWORDS'
  }

  # Optional: specify labels to filter Google My Business listings. If
  # specified, only listings that have any of the labels set are synchronized
  # into FeedItems.
  gmb_feed[:system_feed_generation_data][:label_filters] =
      ['Stores in New York City']

  # Only include the business_account_identifier if it's specified.
  # A nil value will cause an invalid request.
  unless business_account_identifier.nil?
    gmb_feed[:system_feed_generation_data][:business_account_identifier] =
        business_account_identifier
  end

  gmb_operation = {
    :operator => 'ADD',
    :operand => gmb_feed
  }

  result = feed_srv.mutate([gmb_operation])
  added_feed = result[:value].first
  puts "Added GMB feed with ID %d" % added_feed[:id]

  # Add a CustomerFeed that associates the feed with this customer for the
  # LOCATION placeholder type.
  customer_feed = {
    :feed_id => added_feed[:id],
    :placeholder_types => [PLACEHOLDER_TYPE_LOCATION],
    :matching_function => {
      :operator => 'IDENTITY',
      :lhs_operand => [{
        :xsi_type => 'ConstantOperand',
        :type => 'BOOLEAN',
        :boolean_value => true
      }]
    }
  }

  customer_feed_operation = {
    :xsi_type => 'CustomerFeedOperation',
    :operator => 'ADD',
    :operand => customer_feed
  }

  added_customer_feed = nil
  number_of_attempts = 0
  while i < MAX_CUSTOMER_FEED_ADD_ATTEMPTS && !added_customer_feed
    number_of_attempts += 1
    begin
      result = customer_feed_srv.mutate([customer_feed_operation])
      added_customer_feed = result[:value].first
      puts "Attempt #%d to add the CustomerFeed was successful" %
          number_of_attempts
    rescue
      sleep_seconds = 5 * (2 ** number_of_attempts)
      puts ("Attempt #%d to add the CustomerFeed was not succeessful. " +
          "Waiting %d seconds before trying again.") %
          [number_of_attempts, sleep_seconds]
      sleep(sleep_seconds)
    end
  end

  unless added_customer_feed
    raise StandardError, ("Could not create the CustomerFeed after %d " +
        "attempts. Please retry the CustomerFeed ADD operation later.") %
        MAX_CUSTOMER_FEED_ADD_ATTEMPTS
  end

  puts "Added CustomerFeed for feed ID %d and placeholder type %d" %
      [added_customer_feed[:id], added_customer_feed[:placeholder_types].first]

  # OPTIONAL: Create a CampaignFeed to specify which FeedItems to use at the
  # Campaign level. This will be similar to the CampaignFeed in the
  # add_site_links example, except you can filter based on the business name
  # and category of each FeedItem by using a FeedAttributeOperand in your
  # matching function.

  # OPTIONAL: Create an AdGroupFeed for even more fine grained control over
  # which feed items are used at the AdGroup level.
end

if __FILE__ == $0
  API_VERSION = :v201809
  PLACEHOLDER_TYPE_LOCATION = 7
  MAX_CUSTOMER_FEED_ADD_ATTEMPTS = 10

  begin
    # The email address of either an owner or a manager of the GMB account.
    gmb_email_address = 'INSERT_GMB_EMAIL_ADDRESS_HERE'

    # If the gmbEmailAddress above is the same as you used to generate your
    # AdWords API refresh token, leave the value below as nil.
    # Otherwise, to obtain an access token for your GMB account, generate a
    # refresh token as you did for AdWords, but make sure you are logged in as
    # the same user as gmb_email_address above when you follow the link, then
    # capture the generated access token
    gmb_access_token = nil

    # If the gmb_email_address above is for a GMB manager instead of
    # the GMB account owner, then set business_account_identifier to the
    # +Page ID of a location for which the manager has access. See the
    # location extensions guide at
    # https://developers.google.com/adwords/api/docs/guides/feed-services-locations
    # for details.
    business_account_identifier = nil

    add_gmb_location_extensions(gmb_email_address, gmb_access_token,
                                business_account_identifier)

  # Authorization error.
  rescue AdsCommon::Errors::OAuth2VerificationRequired => e
    puts "Authorization credentials are not valid. Edit adwords_api.yml for " +
        "OAuth2 client ID and secret and run misc/setup_oauth2.rb example " +
        "to retrieve and store OAuth2 tokens."
    puts "See this wiki page for more details:\n\n  " +
        'https://github.com/googleads/google-api-ads-ruby/wiki/OAuth2'

  # HTTP errors.
  rescue AdsCommon::Errors::HttpError => e
    puts "HTTP Error: %s" % e

  # API errors.
  rescue AdwordsApi::Errors::ApiException => e
    puts "Message: %s" % e.message
    puts 'Errors:'
    e.errors.each_with_index do |error, index|
      puts "\tError [%d]:" % (index + 1)
      error.each do |field, value|
        puts "\t\t%s: %s" % [field, value]
      end
    end
  end
end

Associate a price extension to an account

#!/usr/bin/env ruby
# Encoding: utf-8
#
# Copyright:: Copyright 2016, Google Inc. All Rights Reserved.
#
# License:: Licensed under the Apache License, Version 2.0 (the "License");
#           you may not use this file except in compliance with the License.
#           You may obtain a copy of the License at
#
#           http://www.apache.org/licenses/LICENSE-2.0
#
#           Unless required by applicable law or agreed to in writing, software
#           distributed under the License is distributed on an "AS IS" BASIS,
#           WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
#           implied.
#           See the License for the specific language governing permissions and
#           limitations under the License.
#
# This example adds a price extension and associates it with an account.
# Campaign targeting is also set using the specified campaign ID. To get
# campaigns, run basic_operations/get_campaigns.rb.

require 'adwords_api'

def add_prices(campaign_id)
  # AdwordsApi::Api will read a config file from ENV['HOME']/adwords_api.yml
  # when called without parameters.
  adwords = AdwordsApi::Api.new

  # To enable logging of SOAP requests, set the log_level value to 'DEBUG' in
  # the configuration file or provide your own logger:
  # adwords.logger = Logger.new('adwords_xml.log')

  customer_extension_setting_srv =
      adwords.service(:CustomerExtensionSettingService, API_VERSION)

  price_feed_item = {
    :xsi_type => 'PriceFeedItem',
    :price_extension_type => 'SERVICES',
    # Price qualifier is optional.
    :price_qualifier => 'FROM',
    :tracking_url_template => 'http://tracker.example.com/?u={lpurl}',
    :language => 'en',
    :campaign_targeting => {
      :targeting_campaign_id => campaign_id
    },
    :scheduling => {
      :feed_item_schedules => [
        {
          :day_of_week => 'SUNDAY',
          :start_hour => 10,
          :start_minute => 'ZERO',
          :end_hour => 18,
          :end_minute => 'ZERO'
        },
        {
          :day_of_week => 'SATURDAY',
          :start_hour => 10,
          :start_minute => 'ZERO',
          :end_hour => 22,
          :end_minute => 'ZERO'
        }
      ]
    },
    # To create a price extension, at least three table rows are needed.
    :table_rows => [
      create_price_table_row(
          'Scrubs',
          'Body Scrub, Salt Scrub',
          'http://www.example.com/scrubs',
          'http://m.example.com/scrubs',
          60000000,
          'USD',
          'PER_HOUR'
      ),
      create_price_table_row(
          'Hair Cuts',
          'Once a month',
          'http://www.example.com/haircuts',
          'http://m.example.com/haircuts',
          75000000,
          'USD',
          'PER_MONTH'
      ),
      create_price_table_row(
          'Skin Care Package',
          'Four times a month',
          'http://www.example.com/skincarepackage',
          nil,
          250000000,
          'USD',
          'PER_MONTH'
      )
    ]
  }

  customer_extension_setting = {
    :extension_type => 'PRICE',
    :extension_setting => {
      :extensions => [price_feed_item]
    }
  }

  operation = {
    :operator => 'ADD',
    :operand => customer_extension_setting
  }

  result = customer_extension_setting_srv.mutate([operation])

  new_extension_setting = result[:value].first
  puts "Extension setting with type '%s' was added to your account.\n" %
      new_extension_setting[:extension_type]
end

def create_price_table_row(header, description, final_url, final_mobile_url,
    price_in_micros, currency_code, price_unit)
  ret_val = {
    :header => header,
    :description => description,
    :final_urls => {
      :urls => [final_url]
    },
    :price => {
      :money => {
        :micro_amount => price_in_micros
      },
      :currency_code => currency_code
    },
    :price_unit => price_unit
  }

  # Optional: Set the mobile final URLs.
  unless final_mobile_url.nil? or final_mobile_url.empty?
    ret_val[:final_mobile_urls] = {:urls => [final_mobile_url]}
  end
end

if __FILE__ == $0
  API_VERSION = :v201809

  begin
    # Campaign ID to add site link to.
    campaign_id = 'INSERT_CAMPAIGN_ID_HERE'.to_i
    add_prices(campaign_id)

  # Authorization error.
  rescue AdsCommon::Errors::OAuth2VerificationRequired => e
    puts "Authorization credentials are not valid. Edit adwords_api.yml for " +
        "OAuth2 client ID and secret and run misc/setup_oauth2.rb example " +
        "to retrieve and store OAuth2 tokens."
    puts "See this wiki page for more details:\n\n  " +
        'https://github.com/googleads/google-api-ads-ruby/wiki/OAuth2'

  # HTTP errors.
  rescue AdsCommon::Errors::HttpError => e
    puts "HTTP Error: %s" % e

  # API errors.
  rescue AdwordsApi::Errors::ApiException => e
    puts "Message: %s" % e.message
    puts 'Errors:'
    e.errors.each_with_index do |error, index|
      puts "\tError [%d]:" % (index + 1)
      error.each do |field, value|
        puts "\t\t%s: %s" % [field, value]
      end
    end
  end
end

#!/usr/bin/env ruby
# Encoding: utf-8
#
# Copyright:: Copyright 2015, Google Inc. All Rights Reserved.
#
# License:: Licensed under the Apache License, Version 2.0 (the "License");
#           you may not use this file except in compliance with the License.
#           You may obtain a copy of the License at
#
#           http://www.apache.org/licenses/LICENSE-2.0
#
#           Unless required by applicable law or agreed to in writing, software
#           distributed under the License is distributed on an "AS IS" BASIS,
#           WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
#           implied.
#           See the License for the specific language governing permissions and
#           limitations under the License.
#
# This example adds a sitelinks feed and associates it with a campaign.

require 'adwords_api'
require 'date'

def add_site_links(campaign_id)
  # AdwordsApi::Api will read a config file from ENV['HOME']/adwords_api.yml
  # when called without parameters.
  adwords = AdwordsApi::Api.new

  # To enable logging of SOAP requests, set the log_level value to 'DEBUG' in
  # the configuration file or provide your own logger:
  # adwords.logger = Logger.new('adwords_xml.log')

  customer_srv = adwords.service(:CustomerService, API_VERSION)

  # Find the matching customer and its time zone. The get_customers method will
  # return a single customer corresponding to the configured client_customer_id.
  customer = customer_srv.get_customers().first
  customer_time_zone = customer[:date_time_zone]
  puts 'Found customer ID %d with time zone "%s"' %
      [customer[:customer_id], customer_time_zone]

  if customer_time_zone.nil?
    raise StandardError, 'Customer not found for customer ID: ' + customer_id
  end

  campaign_extension_setting_srv =
      adwords.service(:CampaignExtensionSettingService, API_VERSION)

  sitelink_1 = {
    :xsi_type => "SitelinkFeedItem",
    :sitelink_text => "Store Hours",
    :sitelink_final_urls => {
      :urls => ["http://www.example.com/storehours"]
    }
  }

  sitelink_2 = {
    :xsi_type => "SitelinkFeedItem",
    :sitelink_text => "Thanksgiving Specials",
    :sitelink_final_urls => {
      :urls => ["http://www.example.com/thanksgiving"]
    },
    :start_time => DateTime.new(Date.today.year, 11, 20, 0, 0, 0).
        strftime("%Y%m%d %H%M%S ") + customer_time_zone,
    :end_time => DateTime.new(Date.today.year, 11, 27, 23, 59, 59).
        strftime("%Y%m%d %H%M%S ") + customer_time_zone,
    # Target this sitelink for United States only. See
    # https://developers.google.com/adwords/api/docs/appendix/geotargeting
    # for valid geolocation codes.
    :geo_targeting => {
      :id => 2840
    },
    # Restrict targeting only to people physically within the United States.
    # Otherwise, this could also show to people interested in the United States
    # but not physically located there.
    :geo_targeting_restriction => {
      :geo_restriction => 'LOCATION_OF_PRESENCE'
    }
  }

  sitelink_3 = {
    :xsi_type => "SitelinkFeedItem",
    :sitelink_text => "Wifi available",
    :sitelink_final_urls => {
      :urls => ["http://www.example.com/mobile/wifi"]
    },
    :device_preference => {:device_preference => 30001},
    # Target this sitelink only when the ad is triggered by the keyword
    # "free wifi".
    :keyword_targeting => {
      :text => "free wifi",
      :match_type => 'BROAD'
    }
  }

  sitelink_4 = {
    :xsi_type => "SitelinkFeedItem",
    :sitelink_text => "Happy hours",
    :sitelink_final_urls => {
      :urls => ["http://www.example.com/happyhours"]
    },
    :scheduling => {
      :feed_item_schedules => [
        {
          :day_of_week => 'MONDAY',
          :start_hour => 18,
          :start_minute => 'ZERO',
          :end_hour => 21,
          :end_minute => 'ZERO'
        },
        {
          :day_of_week => 'TUESDAY',
          :start_hour => 18,
          :start_minute => 'ZERO',
          :end_hour => 21,
          :end_minute => 'ZERO'
        },
        {
          :day_of_week => 'WEDNESDAY',
          :start_hour => 18,
          :start_minute => 'ZERO',
          :end_hour => 21,
          :end_minute => 'ZERO'
        },
        {
          :day_of_week => 'THURSDAY',
          :start_hour => 18,
          :start_minute => 'ZERO',
          :end_hour => 21,
          :end_minute => 'ZERO'
        },
        {
          :day_of_week => 'FRIDAY',
          :start_hour => 18,
          :start_minute => 'ZERO',
          :end_hour => 21,
          :end_minute => 'ZERO'
        }
      ]
    }
  }

  campaign_extension_setting = {
    :campaign_id => campaign_id,
    :extension_type => 'SITELINK',
    :extension_setting => {
      :extensions => [sitelink_1, sitelink_2, sitelink_3, sitelink_4]
    }
  }

  operation = {
    :operand => campaign_extension_setting,
    :operator => 'ADD'
  }

  response = campaign_extension_setting_srv.mutate([operation])
  if response and response[:value]
    new_extension_setting = response[:value].first
    puts "Extension setting with type = %s was added to campaign ID %d" % [
      new_extension_setting[:extension_type],
      new_extension_setting[:campaign_id]
    ]
  elsif
    puts "No extension settings were created."
  end
end

if __FILE__ == $0
  API_VERSION = :v201809

  begin
    # Campaign ID to add site link to.
    campaign_id = 'INSERT_CAMPAIGN_ID_HERE'.to_i
    add_site_links(campaign_id)

  # Authorization error.
  rescue AdsCommon::Errors::OAuth2VerificationRequired => e
    puts "Authorization credentials are not valid. Edit adwords_api.yml for " +
        "OAuth2 client ID and secret and run misc/setup_oauth2.rb example " +
        "to retrieve and store OAuth2 tokens."
    puts "See this wiki page for more details:\n\n  " +
        'https://github.com/googleads/google-api-ads-ruby/wiki/OAuth2'

  # HTTP errors.
  rescue AdsCommon::Errors::HttpError => e
    puts "HTTP Error: %s" % e

  # API errors.
  rescue AdwordsApi::Errors::ApiException => e
    puts "Message: %s" % e.message
    puts 'Errors:'
    e.errors.each_with_index do |error, index|
      puts "\tError [%d]:" % (index + 1)
      error.each do |field, value|
        puts "\t\t%s: %s" % [field, value]
      end
    end
  end
end

#!/usr/bin/env ruby
# Encoding: utf-8
#
# Copyright:: Copyright 2013, Google Inc. All Rights Reserved.
#
# License:: Licensed under the Apache License, Version 2.0 (the "License");
#           you may not use this file except in compliance with the License.
#           You may obtain a copy of the License at
#
#           http://www.apache.org/licenses/LICENSE-2.0
#
#           Unless required by applicable law or agreed to in writing, software
#           distributed under the License is distributed on an "AS IS" BASIS,
#           WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
#           implied.
#           See the License for the specific language governing permissions and
#           limitations under the License.
#
# This example adds a sitelinks feed and associates it with a campaign.

require 'adwords_api'

def add_site_links(campaign_id, ad_group_id)
  # AdwordsApi::Api will read a config file from ENV['HOME']/adwords_api.yml
  # when called without parameters.
  adwords = AdwordsApi::Api.new

  # To enable logging of SOAP requests, set the log_level value to 'DEBUG' in
  # the configuration file or provide your own logger:
  # adwords.logger = Logger.new('adwords_xml.log')

  feed_srv = adwords.service(:FeedService, API_VERSION)
  feed_item_srv = adwords.service(:FeedItemService, API_VERSION)
  feed_item_target_srv = adwords.service(:FeedItemTargetService, API_VERSION)
  feed_mapping_srv = adwords.service(:FeedMappingService, API_VERSION)
  campaign_feed_srv = adwords.service(:CampaignFeedService, API_VERSION)

  sitelinks_data = {}

  # Create site links feed first.
  site_links_feed = {
    :name => 'Feed For Site Links',
    :attributes => [
      {:type => 'STRING', :name => 'Link Text'},
      {:type => 'URL_LIST', :name => 'Final URLs'},
      {:type => 'STRING', :name => 'Line 2 Description'},
      {:type => 'STRING', :name => 'Line 3 Description'}
    ]
  }

  response = feed_srv.mutate([
      {:operator => 'ADD', :operand => site_links_feed}
  ])
  unless response.nil? || response[:value].nil?
    feed = response[:value].first
    # Attribute of type STRING.
    link_text_feed_attribute_id = feed[:attributes][0][:id]
    # Attribute of type URL_LIST.
    final_url_feed_attribute_id = feed[:attributes][1][:id]
    # Attribute of type STRING.
    line_2_feed_attribute_id = feed[:attributes][2][:id]
    # Attribute of type STRING.
    line_3_feed_attribute_id = feed[:attributes][3][:id]
    puts "Feed with name '%s' and ID %d was added with" %
        [feed[:name], feed[:id]]
    puts ("\tText attribute ID %d and Final URLs attribute ID %d " +
        "and Line 2 attribute ID %d and Line 3 attribute ID %d.") % [
          link_text_feed_attribute_id,
          final_url_feed_attribute_id,
          line_2_feed_attribute_id,
          line_3_feed_attribute_id
        ]

    sitelinks_data[:feed_id] = feed[:id]
    sitelinks_data[:link_text_feed_id] = link_text_feed_attribute_id
    sitelinks_data[:final_url_feed_id] = final_url_feed_attribute_id
    sitelinks_data[:line_2_feed_id] = line_2_feed_attribute_id
    sitelinks_data[:line_3_feed_id] = line_3_feed_attribute_id
  else
    raise new StandardError, 'No feeds were added.'
  end

  # Create site links feed items.
  items_data = [
    {
      :text => 'Home',
      :final_urls => ['http://www.example.com'],
      :line_2 => 'Home line 2',
      :line_3 => 'Home line 3'
    },
    {
      :text => 'Stores',
      :final_urls => ['http://www.example.com/stores'],
      :line_2 => 'Stores line 2',
      :line_3 => 'Stores line 3'
     },
    {
      :text => 'On Sale',
      :final_urls => ['http://www.example.com/sale'],
      :line_2 => 'On Sale line 2',
      :line_3 => 'On Sale line 3'
    },
    {
      :text => 'Support',
      :final_urls => ['http://www.example.com/support'],
      :line_2 => 'Support line 2',
      :line_3 => 'Support line 3'
    },
    {
      :text => 'Products',
      :final_urls => ['http://www.example.com/products'],
      :line_2 => 'Products line 2',
      :line_3 => 'Products line 3'
    },
    {
      :text => 'About Us',
      :final_urls => ['http://www.example.com/about'],
      :line_2 => 'About line 2',
      :line_3 => 'About line 3'
    }
  ]

  feed_items = items_data.map do |item|
    {
      :feed_id => sitelinks_data[:feed_id],
      :attribute_values => [
        {
          :feed_attribute_id => sitelinks_data[:link_text_feed_id],
          :string_value => item[:text]
        },
        {
          :feed_attribute_id => sitelinks_data[:final_url_feed_id],
          :string_values => item[:final_urls]
        },
        {
          :feed_attribute_id => sitelinks_data[:line_2_feed_id],
          :string_value => item[:line_2]
        },
        {
          :feed_attribute_id => sitelinks_data[:line_3_feed_id],
          :string_value => item[:line_3]
        }
      ]
    }
  end
  # The "About us" site link is using geographical targeting to use
  # LOCATION_OF_PRESENCE.
  feed_items.last[:geo_targeting_restriction] = {
    :geo_restriction => 'LOCATION_OF_PRESENCE'
  }

  feed_items_operations = feed_items.map do |item|
    {:operator => 'ADD', :operand => item}
  end

  response = feed_item_srv.mutate(feed_items_operations)
  unless response.nil? || response[:value].nil?
    sitelinks_data[:feed_item_ids] = []
    response[:value].each do |feed_item|
      puts 'Feed item with ID %d was added.' % feed_item[:feed_item_id]
      sitelinks_data[:feed_item_ids] << feed_item[:feed_item_id]
    end
  else
    raise new StandardError, 'No feed items were added.'
  end

  # Target the "About Us" sitelink to geographically target California.
  # See https://developers.google.com/adwords/api/docs/appendix/geotargeting
  # for location criteria for supported locations.
  criterion_target = {
    :xsi_type => 'FeedItemCriterionTarget',
    :feed_id => feed_items[5][:feed_id],
    :feed_item_id => sitelinks_data[:feed_item_ids][5],
    :criterion => {
      :xsi_type => 'Location',
      :id => 21137 # California
    }
  }

  retval = feed_item_target_srv.mutate([{
    :operator => 'ADD',
    :operand => criterion_target
  }])
  new_location_target = retval[:value].first
  puts ('Feed item target for feed ID %d and feed item ID %d was created to' +
      'restrict serving to location ID %d.') % [new_location_target[:feed_id],
      new_location_target[:feed_item_id], new_location_target[:criterion][:id]]

  # Create site links feed mapping.
  feed_mapping = {
    :placeholder_type => PLACEHOLDER_SITELINKS,
    :feed_id => sitelinks_data[:feed_id],
    :attribute_field_mappings => [
      {
        :feed_attribute_id => sitelinks_data[:link_text_feed_id],
        :field_id => PLACEHOLDER_FIELD_SITELINK_LINK_TEXT
      },
      {
        :feed_attribute_id => sitelinks_data[:final_url_feed_id],
        :field_id => PLACEHOLDER_FIELD_SITELINK_FINAL_URLS
      },
      {
        :feed_attribute_id => sitelinks_data[:line_2_feed_id],
        :field_id => PLACEHOLDER_FIELD_SITELINK_LINE_2_TEXT
      },
      {
        :feed_attribute_id => sitelinks_data[:line_3_feed_id],
        :field_id => PLACEHOLDER_FIELD_SITELINK_LINE_3_TEXT
      }
    ]
  }

  response = feed_mapping_srv.mutate([
      {:operator => 'ADD', :operand => feed_mapping}
  ])
  unless response.nil? || response[:value].nil?
    feed_mapping = response[:value].first
    puts ('Feed mapping with ID %d and placeholder type %d was saved for feed' +
        ' with ID %d.') % [
          feed_mapping[:feed_mapping_id],
          feed_mapping[:placeholder_type],
          feed_mapping[:feed_id]
        ]
  else
    raise new StandardError, 'No feed mappings were added.'
  end

  # Construct a matching function that associates the sitelink feeditems to the
  # campaign, and set the device preference to Mobile. See the matching function
  # guide at:
  # https://developers.google.com/adwords/api/docs/guides/feed-matching-functions
  # for more details.
  matching_function_string =
      "AND(IN(FEED_ITEM_ID, {%s}), EQUALS(CONTEXT.DEVICE, 'Mobile'))" %
      sitelinks_data[:feed_item_ids].join(',')

  # Create site links campaign feed.
  campaign_feed = {
    :feed_id => sitelinks_data[:feed_id],
    :campaign_id => campaign_id,
    :matching_function => {:function_string => matching_function_string},
    # Specifying placeholder types on the CampaignFeed allows the same feed
    # to be used for different placeholders in different Campaigns.
    :placeholder_types => [PLACEHOLDER_SITELINKS]
  }

  response = campaign_feed_srv.mutate([
      {:operator => 'ADD', :operand => campaign_feed}
  ])
  unless response.nil? || response[:value].nil?
    campaign_feed = response[:value].first
    puts 'Campaign with ID %d was associated with feed with ID %d.' %
      [campaign_feed[:campaign_id], campaign_feed[:feed_id]]
  else
    raise new StandardError, 'No campaign feeds were added.'
  end

  # Optional: Restrict the first feed item to only serve with ads for the
  # specified ad group ID.
  if !ad_group_id.nil? && ad_group_id != 0
    feed_item_target = {
      :xsi_type => 'FeedItemAdGroupTarget',
      :feed_id => sitelinks_data[:feed_id],
      :feed_item_id => sitelinks_data[:feed_item_ids].first,
      :ad_group_id => ad_group_id
    }

    operation = {
      :operator => 'ADD',
      :operand => feed_item_target
    }

    response = feed_item_target_srv.mutate([operation])
    unless response.nil? || response[:value].nil?
      feed_item_target = response[:value].first
      puts ('Feed item target for feed ID %d and feed item ID %d' +
          ' was created to restrict serving to ad group ID %d') %
          [feed_item_target[:feed_id], feed_item_target[:feed_item_id],
          feed_item_target[:ad_group_id]]
    end
  end
end

if __FILE__ == $0
  API_VERSION = :v201809

  # See the Placeholder reference page for a list of all the placeholder types
  # and fields, see:
  #     https://developers.google.com/adwords/api/docs/appendix/placeholders
  PLACEHOLDER_SITELINKS = 1
  PLACEHOLDER_FIELD_SITELINK_LINK_TEXT = 1
  PLACEHOLDER_FIELD_SITELINK_FINAL_URLS = 5
  PLACEHOLDER_FIELD_SITELINK_LINE_2_TEXT = 3
  PLACEHOLDER_FIELD_SITELINK_LINE_3_TEXT = 4

  begin
    # Campaign ID to add site link to.
    campaign_id = 'INSERT_CAMPAIGN_ID_HERE'.to_i
    # Optional: Ad group to restrict targeting to.
    ad_group_id = 'INSERT_AD_GROUP_ID_HERE'.to_i
    add_site_links(campaign_id, ad_group_id)

  # Authorization error.
  rescue AdsCommon::Errors::OAuth2VerificationRequired => e
    puts "Authorization credentials are not valid. Edit adwords_api.yml for " +
        "OAuth2 client ID and secret and run misc/setup_oauth2.rb example " +
        "to retrieve and store OAuth2 tokens."
    puts "See this wiki page for more details:\n\n  " +
        'https://github.com/googleads/google-api-ads-ruby/wiki/OAuth2'

  # HTTP errors.
  rescue AdsCommon::Errors::HttpError => e
    puts "HTTP Error: %s" % e

  # API errors.
  rescue AdwordsApi::Errors::ApiException => e
    puts "Message: %s" % e.message
    puts 'Errors:'
    e.errors.each_with_index do |error, index|
      puts "\tError [%d]:" % (index + 1)
      error.each do |field, value|
        puts "\t\t%s: %s" % [field, value]
      end
    end
  end
end