Hallo Merchant Center Experten,

heute möchte ich euch ein Google Ads Script vorstellen, das euch bei der Verwaltung von Merchant Center Feeds Zeit und Ärger sparen kann. Das Besondere daran ist die Möglichkeit, eure Feeds in Google Ads stündlich zu aktualisieren. Dieses reizvolle Feature klingt zunächst banal, bietet jedoch zahlreiche Vorteile, auf die ich im Folgenden eingehen werde.

Aktualität der Produktdaten

Einer der größten Vorteile dieser Lösung ist die Gewährleistung aktueller Produktdaten in euren Shopping-Kampagnen. Wenn ihr beispielsweise permanente Preisänderungen oder schnelle Bestandsveränderungen habt, sind die stündlichen Updates von unschätzbarem Wert. Ihr vermeidet dadurch enttäuschte Kunden und stellt sicher, dass nur Produkte beworben werden, die tatsächlich noch verfügbar sind.

Effektives Budgetmanagement

Indem Produktfeeds stündlich aktualisiert werden, könnt ihr euer Ad-Budget effizienter nutzen. Bei Produkt-Streichungen oder -Einführungen mitunter innerhalb eines Tages könnt ihr euch schneller an den Markt anpassen und so unnötige Ausgaben reduzieren.

Reaktion auf Wettbewerbsänderungen

Die dynamische Preisgestaltung hat das Online-Shopping intensiviert. Mithilfe stündlicher Updates könnt ihr leicht auf diese Entwicklungen reagieren und beispielsweise eure eigenen Preise relativ zu denjenigen der Konkurrenz kontrolliert anpassen.

Nun, wie kommt ihr also nun in den Genuss der Vorteile dieses Scripts? Hier der Lösungsweg:

1. In eurem Google Ads-Konto navigiert ihr zum Bereich „Bulk-Aktionen“.
2. Wählt „Script“ aus und erstellt ein neues Script.
3. Kopiert nun den bereitgestellten Code für das stündliche Update-Script und fügt ihn hier ein.
4. Tragt im Script die entsprechenden Merchant Center-IDs ein
5. Speichert das Script und plant es so ein, dass es stündlich ausgeführt wird.

Und schon seid ihr fertig! Euer Merchant Center-Feed wird nun mithilfe des Scripts stündlich aktualisiert und ihr könnt davon profitieren.

Abschließend sei gesagt, dass das Google Ads Script für stündliche Updates von Merchant Center Feeds eine einfache, aber hervorragende Möglichkeit ist, eure Online-Marketing-Effizienz zu steigern und euch einen entscheidenden Wettbewerbsvorteil zu sichern!

Probiert es einfach einmal aus – ich bin gespannt auf eure Erfahrungen und Feedback! Happy optimising!

Das 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-Konfiguration:
// Nur bestimmte Feeds:
// { id: "123456", feeds: ["ES-ES", "IT-IT", "DE-DE"] }
// Enthalt-Match - "ES" trifft "Feed ES-ES" UND "Feed ES-EN":
// { id: "123456", feeds: ["ES", "IT", "DE"] }
// Alle Feeds (kein Filter):
// { id: "123456" }
var MCIDS = [
  { id: "123456", feeds: ["DE-", "AT-", "CH-"] }, // example.com - alle Feeds
  { id: "12345687" }                          // somecustomer.de - alle 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 || null;
    } 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: "Übersprungen wegen 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 + " Fehler";
  if (cats.skipped.length > 0) {
    subject += ", " + cats.skipped.length + " übersprungen";
  }
  subject += " | " + now;

  var htmlBody = buildEmailHtml(now, totalFeeds, okCount, errorCount, cats);
  var plainBody = "GMC Feed Update: " + errorCount + " Fehler, " +
    cats.skipped.length + " übersprungen von " + 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 erschöpft", "#d32f2f", cats.dailyQuota);
  }
  if (cats.skipped.length > 0) {
    html += buildErrorSection(
      "Übersprungen (wegen Daily Quota)", "#ff9800", cats.skipped);
  }
  if (cats.perMinuteQuota.length > 0) {
    html += buildErrorSection(
      "Per-Minute Quota (nach " + MAX_RETRIES + " Retries)", "#e65100", cats.perMinuteQuota);
  }
  if (cats.access.length > 0) {
    html += buildErrorSection("Zugriffsfehler", "#9c27b0", cats.access);
  }
  if (cats.other.length > 0) {
    html += buildErrorSection("Sonstige Fehler", "#607d8b", cats.other);
  }

  html += '<div style="padding:12px 20px;color:#999;font-size:12px;border-top:1px solid #ddd">';
  html += 'Automatisch generiert von 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>Gesamt</small></td>';
  html += '<td style="padding:8px;text-align:center;color:#2e7d32">';
  html += '<strong>' + okCount + '</strong><br><small>Erfolgreich</small></td>';
  html += '<td style="padding:8px;text-align:center;color:#d32f2f">';
  html += '<strong>' + errorCount + '</strong><br><small>Fehler</small></td>';
  html += '<td style="padding:8px;text-align:center;color:#ff9800">';
  html += '<strong>' + skippedCount + '</strong><br><small>Übersprungen</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;
}

Das Skript kommt in seiner Originalversion von Nils Rooijmans und wurde von mir zum Multi-Account Script umgebaut.

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 Datenqualität: Health Check Dashboard für E‑Commerce & Performance Marketing erleichtert Debugging

    Lesen
  • MCP Server in Aktion: Hast Du heute schon mit Deinem Google Ads Account geredet?

    Lesen
  • Google Ads Kosten: Was Unternehmen 2026 wirklich einplanen müssen

    Lesen
  • Bessere Daten, bessere Entscheidungen: Datenanreicherung im Server-Side Tracking

    Lesen