Automating CM360 (Part 2): The Standard Display Campaign Workflow

Summarize with AI

Welcome back to the Automating CM360 series.

In Part 1: YouTube Tracker Campaign (1×1 Placement Setup) with R, we tackled the basics of setting up a tracker campaign. That was a specialized use case. Today, we are moving to the bread and butter of digital trafficking: The Standard Display Campaign.

In this installment, we are deploying an R script for Standard Display Campaign that builds a complete campaign hierarchy. Specifically, we will automate a structure consisting of 1 Campaign containing 1 Placement, which houses multiple ads (one main Standard Ad for your creatives, plus the necessary Default Ads for every size).

Why Use an Automation Script for Standard Display Campaign?

While a tracker campaign is simple, a Display setup is where the CM360 UI starts to feel repetitive.

  • Handling Default Ads: When you create a placement, CM360 automatically generates “Default Ads” for each size. Manually assigning tracking groups to these auto-generated ads is easy to forget.
  • Consistency: Naming conventions are hard-coded or generated dynamically. You will never accidentally name a 300×250 ad “728×90” again.
  • Universal Creative Group Assignment: If your campaign utilizes multiple Creative Groups (e.g., Creative Group 1 and Creative Group 2), you typically need both assigned to every ad to ensure your reporting hierarchy is intact. Instead of manually linking these groups one by one, this R script for Standard Display Campaign automatically assigns both groups to every ad in the rotation—including the defaults.

Phase 1: Configuring Your Google Cloud Gateway

Before your script can talk to CM360, you must establish a secure connection via the Google Cloud Platform (GCP). Follow these 25 steps to generate your authentication credentials:

  1. Navigate to the Google Cloud Console.
  2. In the top navigation bar, click on Select a project.
  3. Click the New Project button in the top right.
  4. Enter a Project name (e.g., “CM360 Automator”).
  5. Click Create.
  6. Once the project is ready, select it from the top dropdown.
  7. Click APIs & Services on the home screen sidebar.
  8. Click on Library.
  9. In the search bar, type “Campaign”.
  10. Select Campaign Manager 360 API from the list.
  11. Click the Campaign Manager 360 API box.
  12. Click the blue Enable button.
  13. Once enabled, click OAuth consent screen on the left sidebar.
  14. Click the Get Started button.
  15. Enter your preferred App name (e.g., “CM360 Automation R-Script”).
  16. Under User support email, select your email address.
  17. Under Audience, select External and click Next.
  18. In the Developer contact information, enter your email address again.
  19. Click Next, review the summary, and click Continue/Create.
  20. Now, click the Credentials tab on the left sidebar.
  21. Click Create Credentials > OAuth client ID.
  22. In the Application type dropdown, select Desktop app.
  23. Give it a name (e.g., “R-Studio Connection”) and click Create.
  24. A window will appear with your Client ID; click Download JSON. Save this file securely on your computer.
  25. Final Step: Go back to the OAuth consent screen, find the Test users section, click Add Users, and enter the email address that has access to your CM360 account. Click Save.

Phase 2: Understanding the Logic

Before looking at the code, let’s look at the “Waterfall” logic this R script for Standard Display Campaign performs. We are building a specific hierarchy: Campaign > Placement > Ads (Standard + Default).

  • Creative Upload: The R script for Standard Display Campaign scans a folder, detects dimensions using magick, and uploads the images.
  • Campaign & Landing Page: It builds the campaign shell and creates the necessary Landing Page.
  • The Placement: It creates one master placement. Crucially, it calculates all the unique sizes from your uploaded images and adds them to this placement.
  • Auto-Generation: The moment the Placement is created, the CM360 API automatically generates Default Ads (one for each size defined in the placement).
  • The Standard Ad: The R script for Standard Display Campaign explicitly creates one Standard Ad that contains all your uploaded creatives.
  • The “Patch” Loop: This is the most important step. The R script for Standard Display Campaign loops through ALL ads associated with the campaign (the Standard Ad we made + the Default Ads CM360 made) and force-assigns all campaign Creative Groups to them. This ensures 100% of your impressions—even defaults—are tracked correctly.

This R script for Standard Display Campaign is designed for Static Display (PNG, JPG, JPEG). It assumes you have a folder on your computer full of image files ready to go.

The R Code

Below is the complete, functional R script for Standard Display Campaign. I have anonymized the IDs and paths—you will need to plug in your specific profile_id, advertiser_id, and site_id.

# --- 
# Title: CM360 Standard Display Campaign Workflow
# Series: Automating CM360 (Part 2)
# Use Case: Standard Display Campaign Workflow
# Author: Marketer.tools
# # =========================================================
# CM360 COMPLETE WORKFLOW - UPLOAD, SETUP & FULL ASSIGNMENT
# =========================================================

library(googleAuthR)
library(httr)
library(jsonlite)
library(mime)
library(magick)
library(tools)

# --- CONFIGURATION ---
# REPLACE THESE WITH YOUR ACTUAL IDS
profile_id    <- "YOUR_PROFILE_ID"       # Found in URL or User Profile settings
advertiser_id <- "YOUR_ADVERTISER_ID"    # Found in CM360 Advertiser properties
site_id       <- "YOUR_SITE_ID"          # The ID of the site/publisher you are running on

# Path to your images
creative_folder <- "/path/to/your/creative/folder"

# *** CONFIGURABLE NAMES ***
campaign_name    <- "Display_Consolidated_Automated_v1" # Custom name
placement_name   <- "Placement_Automated_Main" # Custom name
ad_name          <- "Ad_Display_Main" # Custom name

start_date       <- "2026-01-10"
end_date         <- "2026-12-31"
landing_page_url <- "https://www.yourlandingpage.com"

# Creative Group Names for A/B Testing or Reporting
creative_group_1_name <- "Creative_Group_A"
creative_group_2_name <- "Creative_Group_B"

# --- AUTHENTICATION ---
options(googleAuthR.httr_oauth_cache = FALSE)
# Point this to your JSON key from Part 1
gar_set_client("/path/to/your/client_secret.json") 
gar_deauth()
gar_auth(scopes = "https://www.googleapis.com/auth/dfatrafficking")


# =========================================================
# HELPER FUNCTIONS (API WRAPPERS)
# =========================================================

get_access_token <- function() {
  token_obj <- gar_token()
  if (inherits(token_obj, "request")) return(token_obj$auth_token$credentials$access_token)
  if (inherits(token_obj, "Token2.0")) return(token_obj$credentials$access_token)
  return(token_obj)
}

# Generic POST
cm360_post <- function(endpoint, body) {
  access_token <- get_access_token()
  url <- sprintf("https://dfareporting.googleapis.com/dfareporting/v4/userprofiles/%s/%s", profile_id, endpoint)
  response <- httr::POST(
    url = url, 
    httr::add_headers("Authorization" = paste("Bearer", access_token), "Content-Type" = "application/json"),
    body = jsonlite::toJSON(body, auto_unbox = TRUE), encode = "raw"
  )
  if (httr::status_code(response) >= 300) stop("API Error: ", httr::content(response, "text"))
  return(httr::content(response, "parsed"))
}

# Generic PATCH
cm360_patch <- function(endpoint, body) {
  access_token <- get_access_token()
  url <- sprintf("https://dfareporting.googleapis.com/dfareporting/v4/userprofiles/%s/%s", profile_id, endpoint)
  response <- httr::PATCH(
    url = url, 
    httr::add_headers("Authorization" = paste("Bearer", access_token), "Content-Type" = "application/json"),
    body = jsonlite::toJSON(body, auto_unbox = TRUE), encode = "raw"
  )
  if (httr::status_code(response) >= 300) stop("API Error: ", httr::content(response, "text"))
  return(httr::content(response, "parsed"))
}

# Generic GET
cm360_get <- function(endpoint) {
  access_token <- get_access_token()
  url <- sprintf("https://dfareporting.googleapis.com/dfareporting/v4/userprofiles/%s/%s", profile_id, endpoint)
  response <- httr::GET(url = url, httr::add_headers("Authorization" = paste("Bearer", access_token)))
  if (httr::status_code(response) >= 300) stop("API Error: ", httr::content(response, "text"))
  return(httr::content(response, "parsed"))
}

# Upload Asset Function
upload_asset <- function(file_path) {
  file_name <- basename(file_path)
  access_token <- get_access_token()
  init_url <- sprintf("https://dfareporting.googleapis.com/upload/dfareporting/v4/userprofiles/%s/creativeAssets/%s/creativeAssets", profile_id, advertiser_id)
  
  metadata <- list(assetIdentifier = list(name = file_name, type = "HTML_IMAGE"))
  file_size <- file.info(file_path)$size
  mime_type <- mime::guess_type(file_path)
  
  init_response <- httr::POST(
    url = init_url, query = list(uploadType = "resumable"),
    httr::add_headers("Authorization" = paste("Bearer", access_token), "X-Upload-Content-Type" = mime_type, "X-Upload-Content-Length" = as.character(file_size)),
    body = jsonlite::toJSON(metadata, auto_unbox = TRUE), encode = "raw"
  )
  
  upload_url <- httr::headers(init_response)$location
  file_content <- readBin(file_path, "raw", file_size)
  
  upload_response <- httr::PUT(
    url = upload_url, httr::add_headers("Content-Type" = mime_type, "Content-Length" = as.character(length(file_content))),
    body = file_content
  )
  return(httr::content(upload_response, "parsed")$assetIdentifier)
}

create_creative <- function(asset, name, w, h) {
  body <- list(
    name = name, advertiserId = advertiser_id, type = "DISPLAY", active = TRUE,
    size = list(width = w, height = h),
    creativeAssets = list(list(assetIdentifier = asset, role = "PRIMARY"))
  )
  return(cm360_post("creatives", body))
}

# Helper to get all ads (handling pagination)
get_ads <- function(campaign_id) {
  all_ads <- list()
  token <- NULL
  repeat {
    q <- list(campaignIds = campaign_id, active = TRUE)
    if (!is.null(token)) q$pageToken <- token
    qs <- paste(names(q), q, sep="=", collapse="&")
    res <- cm360_get(paste0("ads?", qs))
    
    if (!is.null(res$ads)) all_ads <- c(all_ads, res$ads)
    if (is.null(res$nextPageToken) || res$nextPageToken == "") break
    token <- res$nextPageToken
  }
  return(all_ads)
}

get_ad_details <- function(id) {
  return(cm360_get(paste0("ads/", id)))
}

# =========================================================
# STEP 1: UPLOAD CREATIVES
# =========================================================


files <- list.files(creative_folder, pattern = "\\.(png|jpg|jpeg)$", full.names = TRUE, ignore.case = TRUE)
if(length(files) == 0) stop("No images found in ", creative_folder)

uploaded_creatives <- list()

for (f in files) {
  tryCatch({
    info <- magick::image_info(magick::image_read(f))
    asset <- upload_asset(f)
    creative <- create_creative(asset, tools::file_path_sans_ext(basename(f)), info$width, info$height)
    uploaded_creatives[[length(uploaded_creatives) + 1]] <- list(id = creative$id, name = creative$name, width = info$width, height = info$height)
    message("  ✓ Uploaded: ", creative$name, " (ID: ", creative$id, ")")
  }, error = function(e) message("  ✗ Failed: ", f))
}

# =========================================================
# STEP 2: CREATE CREATIVE GROUPS
# =========================================================


cg1 <- cm360_post("creativeGroups", list(name = creative_group_1_name, advertiserId = advertiser_id, groupNumber = 1))
cg2 <- cm360_post("creativeGroups", list(name = creative_group_2_name, advertiserId = advertiser_id, groupNumber = 2))
message("✓ Group 1 ID: ", cg1$id)
message("✓ Group 2 ID: ", cg2$id)

# =========================================================
# STEP 3: CREATE LANDING PAGE
# =========================================================

lp <- cm360_post("advertiserLandingPages", list(name = paste0("LP_", campaign_name), url = landing_page_url, advertiserId = advertiser_id))
message("✓ Landing Page ID: ", lp$id)

# =========================================================
# STEP 4: CREATE CAMPAIGN
# =========================================================


camp_body <- list(
  name = campaign_name, advertiserId = advertiser_id, startDate = start_date, endDate = end_date,
  defaultLandingPageId = lp$id,
  creativeGroupIds = list(as.character(cg1$id), as.character(cg2$id)) 
)
campaign <- cm360_post("campaigns", camp_body)
message("✓ Campaign ID: ", campaign$id)

# =========================================================
# STEP 5: CREATE PLACEMENT
# =========================================================


unique_sizes <- unique(lapply(uploaded_creatives, function(c) list(width = c$width, height = c$height)))
primary <- unique_sizes[[1]]
others <- if (length(unique_sizes) > 1) unique_sizes[2:length(unique_sizes)] else list()

place_body <- list(
  name = placement_name, campaignId = campaign$id, siteId = site_id, compatibility = "DISPLAY",
  paymentSource = "PLACEMENT_AGENCY_PAID", tagFormats = list("PLACEMENT_TAG_STANDARD"),
  size = primary, pricingSchedule = list(startDate = start_date, endDate = end_date, pricingType = "PRICING_TYPE_CPM")
)
if (length(others) > 0) place_body$additionalSizes <- others
placement <- cm360_post("placements", place_body)
message("✓ Placement ID: ", placement$id)

# =========================================================
# STEP 6: ASSOCIATE CREATIVES TO CAMPAIGN
# =========================================================

for (c in uploaded_creatives) {
  tryCatch({
    cm360_post(paste0("campaigns/", campaign$id, "/campaignCreativeAssociations"), list(creativeId = as.character(c$id)))
    message("  ✓ Associated: ", c$id)
  }, error = function(e) message("  ! Already associated or error: ", c$id))
}

# =========================================================
# STEP 7: CREATE ADS & FORCE GROUPS (ALL ADS)
# =========================================================


groups_to_apply <- list(
  list(creativeGroupId = as.character(cg1$id), creativeGroupNumber = "CREATIVE_GROUP_ONE"),
  list(creativeGroupId = as.character(cg2$id), creativeGroupNumber = "CREATIVE_GROUP_TWO")
)

# --- 7A. CREATE NEW STANDARD AD (IF NEEDED) ---
message("1. Checking for Standard Ad...")
existing_ads <- get_ads(campaign$id)
standard_ad_exists <- any(sapply(existing_ads, function(x) x$type == "AD_SERVING_STANDARD_AD"))

if (!standard_ad_exists) {
  message("   Creating new Standard Ad with Creative Groups pre-configured...")
  
  ad_assignments <- lapply(uploaded_creatives, function(c) {
    list(
      creativeId = as.character(c$id), active = TRUE, 
      clickThroughUrl = list(defaultLandingPage = TRUE), rotation = "EVEN",
      creativeGroupAssignments = groups_to_apply 
    )
  })
  
  ad_body <- list(
    name = ad_name, campaignId = campaign$id, type = "AD_SERVING_STANDARD_AD", active = TRUE,
    startTime = paste0(start_date, "T00:00:00Z"), endTime = paste0(end_date, "T23:59:59Z"),
    placementAssignments = list(list(placementId = placement$id, active = TRUE)),
    creativeRotation = list(type = "CREATIVE_ROTATION_TYPE_RANDOM", creativeAssignments = ad_assignments),
    deliverySchedule = list(impressionRatio = 1, priority = "AD_PRIORITY_01")
  )
  
  new_ad <- cm360_post("ads", ad_body)
  message("   ✓ Created New Ad: ", new_ad$id)
} else {
  message("   Standard Ad already exists. Moving to update phase.")
}

# --- 7B. FORCE UPDATE ALL ADS (STANDARD + DEFAULT) ---
message("\n2. Applying Creative Groups to ALL Ads (including Default)...")

all_current_ads <- get_ads(campaign$id)

for (current_ad in all_current_ads) {
  message("------------------------------------------------")
  message("Processing: ", current_ad$name, " (ID: ", current_ad$id, ")")
  
  full_ad <- get_ad_details(current_ad$id)
  
  if (is.null(full_ad$creativeRotation) || is.null(full_ad$creativeRotation$creativeAssignments)) {
    next
  }
  
  new_assignments <- lapply(full_ad$creativeRotation$creativeAssignments, function(ca) {
    ca$creativeGroupAssignments <- groups_to_apply
    ca$active <- TRUE 
    return(ca)
  })
  
  patch_body <- list(
    creativeRotation = list(
      creativeAssignments = new_assignments
    )
  )
  
  tryCatch({
    endpoint <- sprintf("ads?id=%s", current_ad$id)
    result <- cm360_patch(endpoint, patch_body)
    
    if (!is.null(result$creativeRotation$creativeAssignments[[1]]$creativeGroupAssignments)) {
      message("  ✓ SUCCESS: Updated with 2 groups.")
    } else {
      message("  ? WARNING: Groups were stripped by API.")
    }
  }, error = function(e) {
    message("  ✗ FAILED: ", conditionMessage(e))
  })
}

message("\n========================================")
message("✓✓✓ COMPLETE WORKFLOW FINISHED ✓✓✓")
message("========================================")

Conclusion

By running this R script for Standard Display Campaign, you have successfully automated the creation of a standard display campaign structure: One Campaign, One Placement, and the complete set of Ads (Standard + Defaults).

More importantly, you have solved the “Default Ad” problem. By iterating through every ad in the campaign—including the ones the API created without your permission—you ensure that your Creative Groups and tracking logic are applied universally.. Stay tuned to marketer.tools for the next part of this series!

If you are having issues with the code, please email us at help@marketertools.com—we will try to help you.

Critical Note: File Naming Conventions

The gar_set_client() function depends on the JSON file generated from your Google Cloud Console. If you rotate your keys or change project permissions, you must download a new JSON file and update the path in the script.

Explore Our Tool Library

Frequently Asked Questions (FAQ)

1. Why does the R script for Standard Display Campaign loop through “All Ads” at the end?

When you create a Placement in CM360, the system automatically creates a “Default Ad” for every size assigned to that placement. The R script for Standard Display Campaign must loop through to catch these auto-generated ads and apply your Creative Groups to them; otherwise, your reporting will have gaps.

2. Can I have multiple Placements in this campaign?

This specific R script for Standard Display Campaign is designed for a “1 Campaign : 1 Placement” structure. However, because the code is modular, you could wrap the “Step 5” placement creation block in a loop to generate multiple placements if needed.

3. What happens if I upload images with different sizes?

The R script for Standard Display Campaign uses the unique() function in Step 5. It scans all your uploaded images, finds every unique dimension (e.g., 300×250, 728×90), and adds them all to the Placement’s additionalSizes field automatically.

4. Why do we need magick?

We use magick to read the width and height of your image files before uploading. CM360 requires you to specify the exact dimensions when creating a creative object; magick automates this so you don’t have to type them in.

5. Does this support HTML5 or Video?

No, this specific R script for Standard Display Campaign is optimized for Static Display (Image) creatives. HTML5 and Video require different upload endpoints and asset bundling methods (e.g., ZIP files), which would require a different function structure.

This specific R script for Standard Display Campaign is configured to look for .png, .jpg, and .jpeg files. It creates standard “DISPLAY” creative types. It does not currently support HTML5 zips or Third-Party tags.

Summarize with AI