Mehr Aktualität für eure Shopping-Daten

Wer mit Google Shopping arbeitet, weiß: Gute Kampagnen brauchen aktuelle Produktdaten. Genau dafür ist dieses Google Ads Script gedacht. Es stößt die Aktualisierung eures Merchant Center Feeds stündlich an und sorgt dafür, dass Preis- und Bestandsänderungen schneller in euren Produktdaten ankommen. Google selbst betont, dass korrekte und aktuelle Produktdaten entscheidend für eine saubere Ausspielung und für stabile Produktdarstellung sind.

Preise und Verfügbarkeiten schneller an Google übergeben

In vielen Shops ändern sich Preise, Lagerbestände oder Aktionspreise nicht nur einmal am Tag. Genau dann wird ein häufigeres Update interessant. Mit einem stündlichen Feed-Update verkürzt ihr die Zeit zwischen Änderung im Shop und Aktualisierung im Merchant Center. Das hilft besonders bei Sortimenten mit hoher Dynamik, bei Abverkäufen, Aktionen oder stark schwankender Verfügbarkeit. Google hebt selbst hervor, wie wichtig saubere Preis- und Verfügbarkeitsdaten sind.

Weniger Reibung im Tagesgeschäft

Der praktische Nutzen ist simpel: Ihr müsst nicht darauf warten, dass ein späterer Standardlauf eure Daten nachzieht. Stattdessen bleibt euer Feed enger an dem, was im Shop tatsächlich passiert. Das spart Nacharbeit, reduziert unnötige Kontrolle einzelner Produkte und verbessert die operative Steuerung im Merchant Center. Gerade in aktiven Konten ist das ein echter Vorteil, weil aktuelle Daten die Grundlage für belastbare Shopping-Kampagnen sind.

Besseres Budget für die richtigen Produkte

Je aktueller eure Produktdaten sind, desto sauberer arbeitet auch euer Budget. Produkte mit geänderten Preisen oder veränderter Verfügbarkeit werden schneller auf den aktuellen Stand gebracht. Das hilft dabei, Streuverluste zu reduzieren und Shopping-Traffic stärker auf Produkte zu lenken, die wirklich in der gewünschten Form beworben werden sollen. Google macht klar, dass Preis- und Bestandsdaten mit den tatsächlichen Produktinformationen zusammenpassen sollen.

Saubere Lösung für bestehende Merchant-Center-Setups

Der große Vorteil dieses Ansatzes: Ihr müsst euer bestehendes Setup nicht neu bauen. Das Script ergänzt einen vorhandenen Prozess und macht ihn schneller. Damit ist die Lösung besonders interessant für Spezialisten, die bereits mit Feeds, Merchant Center und Google Ads arbeiten und einfach häufiger aktualisieren wollen. Google Ads Scripts können dafür auf die Shopping Content API zugreifen und sich in bestehende Abläufe einfügen.

Schnell eingerichtet und direkt nutzbar

Auch die Umsetzung ist überschaubar. In Google Ads legt ihr das Script unter den Scripts an, ergänzt die Merchant-Center-Daten und plant die Ausführung regelmäßig ein. Zusätzlich lassen sich Produktdatenquellen im Merchant Center grundsätzlich zeitgesteuert aktualisieren. Das macht den Ansatz gut greifbar und schnell einsetzbar, ohne dass aus einer einfachen Anforderung sofort ein großes Technikprojekt wird.

Besonders sinnvoll bei dynamischen Shops

Stündliche Merchant Center Updates lohnen sich vor allem dann, wenn sich euer Shop im Tagesverlauf spürbar verändert. Dazu gehören häufige Preisänderungen, wechselnde Aktionspreise, knappe Lagerbestände oder Sortimentswechsel im laufenden Betrieb. In genau solchen Szenarien bringt ein häufiger aktualisierter Feed mehr Tempo, mehr Kontrolle und mehr Sicherheit in das Shopping-Setup.

Fazit

Dieses Google Ads Script ist eine praktische Lösung für alle, die ihren Merchant Center Feed stündlich aktualisieren möchten. Der Nutzen ist klar: aktuellere Produktdaten, schnellere Reaktion auf Änderungen im Shop, weniger operative Reibung und ein sauberer abgestimmtes Shopping-Setup. Für spezialisierte Merchant-Center- und Google-Shopping-Setups ist das kein Gimmick, sondern ein sinnvoller Hebel im Alltag.

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…

  • 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
  • Google Ads DemandGen: Der vollständige Praxis-Leitfaden

    Lesen
  • Optimierung von Google Shopping Feeds mit OpenAI’s ChatGPT: Ein umfassender Leitfaden

    Lesen