More up-to-date information for your shopping data

Anyone who works with Google Shopping knows that good campaigns need up-to-date product data. This Google Ads script is designed precisely for that purpose. It triggers the hourly update of your Merchant Center feed, ensuring that price and stock changes are reflected in your product data more quickly. Google itself emphasizes that accurate and current product data is crucial for clean ad delivery and stable product display.

Prices and availability information are passed to Google faster

In many online stores, prices, stock levels, or promotional prices change more than once a day. That’s precisely when more frequent updates become beneficial. With an hourly feed update, you shorten the time between changes in your store and updates in the Merchant Center. This is particularly helpful for highly dynamic product ranges, sales, promotions, or items with fluctuating availability. Google itself emphasizes the importance of accurate price and availability data.

Less friction in day-to-day business

The practical benefit is simple: You don’t have to wait for a later standard run to catch up with your data. Instead, your feed stays closer to what’s actually happening in your shop. This saves on rework, reduces unnecessary monitoring of individual products, and improves operational management in the Merchant Center. This is a real advantage, especially for active accounts, because up-to-date data is the foundation for robust shopping campaigns.

A better budget for the right products

The more up-to-date your product data is, the more efficiently your budget will be managed. Products with changed prices or availability are updated more quickly. This helps reduce wasted ad spend and direct shopping traffic more effectively to products that actually deserve to be advertised as intended. Google emphasizes that price and inventory data should align with the actual product information.

Clean solution for existing Merchant Center setups

The major advantage of this approach is that you don’t have to rebuild your existing setup. The script complements an existing process and speeds it up. This makes the solution particularly interesting for specialists who already work with feeds, Merchant Center, and Google Ads and simply want to update more frequently. Google Ads scripts can access the Shopping Content API and integrate into existing workflows.

Quickly set up and ready to use immediately

Implementation is also straightforward. In Google Ads, you create the script under “Scripts,” add the Merchant Center data, and schedule it to run regularly. Additionally, product data sources in the Merchant Center can generally be updated on a schedule. This makes the approach easily accessible and quick to deploy, without turning a simple requirement into a major technical project.

Especially useful for dynamic shops

Hourly Merchant Center updates are particularly beneficial if your shop undergoes significant changes throughout the day. This includes frequent price changes, fluctuating promotional prices, low stock levels, or product range changes during operation. In precisely these scenarios, a more frequently updated feed brings greater speed, control, and security to your shopping setup.

Conclusion

This Google Ads script is a practical solution for anyone who wants to update their Merchant Center feed hourly. The benefits are clear: more up-to-date product data, faster response to changes in the shop, less operational friction, and a more streamlined shopping setup. For specialized Merchant Center and Google Shopping setups, this isn’t just a gimmick, but a valuable tool for everyday use.

The script

// Hourly Updates for Google Merchant Center Main Feeds
// Fetches all enabled datafeeds with reactive retry on per-minute quota errors
// Stops immediately on daily quota exhaustion
// Set the script to "hourly"

var CONTACT_EMAIL = "[email protected]";
var MAX_RETRIES = 3;
var RETRY_DELAY_MS = 45000;

// Merchant configuration:
// Only certain feeds:
// { id: "123456", feeds: ["ES-ES", "IT-IT", "DE-DE"] }
// Containment Match - "ES" meets "Feed ES-ES" AND "Feed ES-EN":
// { id: "123456", feeds: ["ES", "IT", "DE"] }
// All feeds (no filter):
// { id: "123456" }
var MCIDS = [
  { id: "123456", feeds: ["DE-", "AT-", "CH-"] }, // example.com - all feeds
  { id: "12345687" } // somecustomer.de - all feeds
];

function main() {
  var allResults = [];
  for (var m = 0; m  <  MCIDS.length; m++) {
  var merchant = MCIDS[m];
  var results = fetchEnabledProductFeeds(merchant.id, merchant.feeds);
  for (var r = 0; r  <  results.length; r++) {
  allResults.push(results[r]);
  }
  Logger.log(results);
  }

  var problems = filterProblems(allResults);
  if (problems.length > 0) {
  sendErrorEmail(problems, allResults);
  }
}

function fetchEnabledProductFeeds(merchantId, feedFilter) {
  var fetchedFeeds = [];
  var now = getFormattedNow();

  var allFeeds = listAllDatafeeds(merchantId);
  if (allFeeds.error) {
  Logger.log("### ERROR fetching data from merchant: '" + merchantId + "' --> " + allFeeds.error);
  fetchedFeeds.push({
  time: now, merchantId: merchantId,
  feedName: "", feedId: "",
  status: "ERROR", error: allFeeds.error
  });
  return fetchedFeeds;
  }

  if (allFeeds.resources.length === 0) {
  return fetchedFeeds;
  }

  var resources = filterFeedsByName(allFeeds.resources, feedFilter, merchantId);

  for (var i = 0; i  <  resources.length; i++) {
  var feed = resources[i];
  var result = fetchSingleFeed(merchantId, feed, now);
  fetchedFeeds.push(result);

  var logMsg = result.status === "OK" ? "Fetch OK" : "### " + result.status + ": " + result.error; 
  Logger.log([now, merchantId, feed.name, feed.id, logMsg]);

  if (result.status === "DAILY_QUOTA") {
  var remaining = resources.length - i - 1;
  Logger.log("Daily quota exceeded for merchant " + merchantId +
  " - skipping remaining " + remaining + " feeds");
  appendSkippedFeeds(fetchedFeeds, resources, i + 1, now, merchantId);
  break;
  }
  }

  return fetchedFeeds;
}

function listAllDatafeeds(merchantId) {
  var allResources = [];
  var pageToken = null;
  try {
  do {
  var opts = { maxResults: 250 };
  if (pageToken) { opts.pageToken = pageToken; }
  var response = ShoppingContent.Datafeeds.list(merchantId, opts);
  if (response.resources) {
  for (var i = 0; i  <  response.resources.length; i++) {
  allResources.push(response.resources[i]);
  }
  }
  pageToken = response.nextPageToken || zero;
  } while (pageToken);
  } catch (e) {
  return { resources: [], error: "" + e };
  }
  Logger.log("Merchant " + merchantId + ": " + allResources.length + " feeds total (from API)");
  return { resources: allResources, error: null };
}

function appendSkippedFeeds(feedList, resources, startIdx, now, merchantId) {
  for (var j = startIdx; j  <  resources.length; j++) {
  feedList.push({
  time: now, merchantId: merchantId,
  feedName: resources[j].name, feedId: resources[j].id,
  status: "SKIPPED", error: "Skipped due to Daily Quota Limit"
  });
  }
}

function filterFeedsByName(resources, feedFilter, merchantId) {
  if (!feedFilter || feedFilter.length === 0) {
  Logger.log("Merchant " + merchantId + ": processing all " + resources.length + " feeds");
  return resources;
  }

  var filtered = [];
  for (var i = 0; i  <  resources.length; i++) {
  var name = resources[i].name.toLowerCase();
  for (var f = 0; f  <  feedFilter.length; f++) {
  if (name.indexOf(feedFilter[f].toLowerCase()) > -1) {
  filtered.push(resources[i]);
  break;
  }
  }
  }

  Logger.log("Merchant " + merchantId + ": " + filtered.length + " of " +
  resources.length + " feeds selected (filter: [" + feedFilter.join(", ") + "])");
  return filtered;
}

function fetchSingleFeed(merchantId, feed, now) {
  var retries = 0;
  while (true) {
  try {
  ShoppingContent.Datafeeds.fetchnow(merchantId, feed.id);
  return {
  time: now, merchantId: merchantId,
  feedName: feed.name, feedId: feed.id,
  status: "OK", error: ""
  };
  } catch (e) {
  var errorMsg = "" + e;
  if (isDailyQuotaError(errorMsg)) {
  return {
  time: now, merchantId: merchantId,
  feedName: feed.name, feedId: feed.id,
  status: "DAILY_QUOTA", error: errorMsg
  };
  }
  if (isPerMinuteQuotaError(errorMsg) && retries  <  MAX_RETRIES) {
  retries++;
  Logger.log("Per-minute quota hit for '" + feed.name + "' (retry " +
  retries + "/" + MAX_RETRIES + "), waiting " +
  (RETRY_DELAY_MS / 1000) + "s...");
  Utilities.sleep(RETRY_DELAY_MS);
  } else {
  return {
  time: now, merchantId: merchantId,
  feedName: feed.name, feedId: feed.id,
  status: "ERROR", error: errorMsg
  };
  }
  }
  }
}

function isDailyQuotaError(errorMsg) {
  var lower = errorMsg.toLowerCase();
  return lower.indexOf("daily") > -1 && lower.indexOf("quota") > -1;
}

function isPerMinuteQuotaError(errorMsg) {
  var lower = errorMsg.toLowerCase();
  return lower.indexOf("quota") > -1 && lower.indexOf("minute") > -1;
}

function getFormattedNow() {
  return Utilities.formatDate(
  new Date(),
  AdsApp.currentAccount().getTimeZone(),
  "yyyy MMM dd HH:mm"
  );
}

function filterProblems(results) {
  var problems = [];
  for (var i = 0; i  <  results.length; i++) {
  var s = results[i].status;
  if (s === "ERROR" || s === "DAILY_QUOTA" || s === "SKIPPED") {
  problems.push(results[i]);
  }
  }
  return problems;
}

function categorizeProblems(problems) {
  var cats = { dailyQuota: [], perMinuteQuota: [], skipped: [], access: [], other: [] };
  for (var i = 0; i  <  problems.length; i++) {
  var p = problems[i];
  if (p.status === "SKIPPED") {
  cats.skipped.push(p);
  } else if (p.status === "DAILY_QUOTA") {
  cats.dailyQuota.push(p);
  } else if (isPerMinuteQuotaError(p.error)) {
  cats.perMinuteQuota.push(p);
  } else if (isAccessError(p.error)) {
  cats.access.push(p);
  } else {
  cats.other.push(p);
  }
  }
  return cats;
}

function isAccessError(errorMsg) {
  var lower = errorMsg.toLowerCase();
  return lower.indexOf("permission") > -1 ||
  lower.indexOf("access") > -1 ||
  lower.indexOf("denied") > -1;
}

function sendErrorEmail(problems, allResults) {
  var now = getFormattedNow();
  var totalFeeds = allResults.length;
  var okCount = countByStatus(allResults, "OK");
  var cats = categorizeProblems(problems);

  var errorCount = problems.length - cats.skipped.length;
  var subject = "GMC Feed Update | " + errorCount + " Error";
  if (cats.skipped.length > 0) {
  subject += ", " + cats.skipped.length + " skipped";
  }
  subject += " | " + now;

  var htmlBody = buildEmailHtml(now, totalFeeds, okCount, errorCount, cats);
  var plainBody = "GMC Feed Update: " + errorCount + " Error, " +
  cats.skipped.length + " skipped by " + totalFeeds + " Feeds.";

  MailApp.sendEmail({
  to: CONTACT_EMAIL,
  subject: subject,
  body: plainBody,
  htmlBody: htmlBody
  });

  Logger.log("Error email sent to " + CONTACT_EMAIL);
}

function countByStatus(results, status) {
  var count = 0;
  for (var i = 0; i  <  results.length; i++) {
  if (results[i].status === status) { count++; }
  }
  return count;
}

function buildEmailHtml(now, totalFeeds, okCount, errorCount, cats) {
  var skippedCount = cats.skipped.length;
  var html = '<div style="font-family:Arial,sans-serif;max-width:700px;margin:0 auto">';
  html += buildEmailHeader(now);
  html += buildEmailSummary(totalFeeds, okCount, errorCount, skippedCount);

  if (cats.dailyQuota.length > 0) {
  html += buildErrorSection(
  "Daily Quota exhausted", "#d32f2f", cats.dailyQuota);
  }
  if (cats.skipped.length > 0) {
  html += buildErrorSection(
  "Skipped (due to Daily Quota)", "#ff9800", cats.skipped);
  }
  if (cats.perMinuteQuota.length > 0) {
  html += buildErrorSection(
  "Per-Minute Quota (after " + MAX_RETRIES + " Retries)", "#e65100", cats.perMinuteQuota);
  }
  if (cats.access.length > 0) {
  html += buildErrorSection("Access error", "#9c27b0", cats.access);
  }
  if (cats.other.length > 0) {
  html += buildErrorSection("Other errors", "#607d8b", cats.other);
  }

  html += '<div style="padding:12px 20px;color:#999;font-size:12px;border-top:1px solid #ddd">';
  html += 'Automatically generated by GMC Feed Update Script</div></div>';
  return html;
}

function buildEmailHeader(now) {
  var html = '<div style="background:#d32f2f;color:#fff;padding:16px 20px;border-radius:8px 8px 0 0">';
  html += '<h2 style="margin:0">GMC Feed Update Report</h2>';
  html += '<p style="margin:4px 0 0;opacity:0.9">' + now + '</p>';
  html += '</div>';
  return html;
}

function buildEmailSummary(totalFeeds, okCount, errorCount, skippedCount) {
  var html = '<div style="background:#f5f5f5;padding:16px 20px;border-bottom:1px solid #ddd">';
  html += '<table style="width:100%;border-collapse:collapse"><tr>';
  html += '<td style="padding:8px;text-align:center">';
  html += '<strong>' + totalFeeds + '</strong><br><small>Total</small></td>';
  html += '<td style="padding:8px;text-align:center;color:#2e7d32">';
  html += '<strong>' + okCount + '</strong><br><Successful</small></td>';
  html += '<td style="padding:8px;text-align:center;color:#d32f2f">';
  html += '<strong>' + errorCount + '</strong><br><small>Error</small></td>';
  html += '<td style="padding:8px;text-align:center;color:#ff9800">';
  html += '<strong>' + skippedCount + '</strong><br><Skipped</small></td>';
  html += '</tr></table></div>';
  return html;
}

function buildErrorSection(title, color, items) {
  var html = '<div style="padding:16px 20px">';
  html += '<h3 style="color:' + color + ';margin:0 0 8px;border-bottom:2px solid ' +
  color + ';padding-bottom:4px">' + title + ' (' + items.length + ')</h3>';
  html += '<table style="width:100%;border-collapse:collapse;font-size:13px">';
  html += '<tr style="background:#eee">';
  html += '<th style="padding:6px 8px;text-align:left">Merchant</th>';
  html += '<th style="padding:6px 8px;text-align:left">Feed</th>';
  html += '<th style="padding:6px 8px;text-align:left">Feed ID</th>';
  html += '<th style="padding:6px 8px;text-align:left">Details</th></tr>';

  for (var i = 0; i  <  items.length; i++) {
  var bg = i % 2 === 0 ? "#fff" : "#f9f9f9"; 
  html += '<tr style="background:' + bg + '">';
  html += '<td style="padding:6px 8px">' + items[i].merchantId + '</td>';
  html += '<td style="padding:6px 8px">' + items[i].feedName + '</td>';
  html += '<td style="padding:6px 8px">' + items[i].feedId + '</td>';
  html += '<td style="padding:6px 8px;font-size:11px;color:#666;word-break:break-all">' +
  items[i].error + '</td></tr>';
  }

  html += '</table></div>';
  return html;
}

The script comes in its original version from Nils Rooijmans and was rebuilt by me into a multi-account script.

Bernhard prange webmeisterei

SEA-Experte: Bernhard Prange

Bernhard Prange ist Google Ads Freelancer und Tracking-Spezialist mit über 10 Jahren Erfahrung im Performance-Marketing. Sein Fokus liegt auf datengetriebenem Arbeiten: von Google Shopping über Conversion-Tracking bis hin zu serverseitigen Lösungen mit Matomo und BigQuery.

Als Ansprechpartner für Agenturen, E-Commerce-Unternehmen und B2B-Dienstleister verbindet er technisches Know-how mit strategischem Blick auf Marketing und Geschäftsmodelle.

Beiträge, die dich auch interessieren könnten…

  • Google Analytics Data Quality: Health Check Dashboard for E-Commerce & Performance Marketing simplifies debugging

    Lesen
  • MCP Server in action: Have you talked to your Google Ads account today?

    Lesen
  • Google Ads Costs: What Companies Really Need to Plan For in 2026

    Lesen
  • Better data, better decisions: Data enrichment in Server-Side Tracking

    Lesen