Automatisches Preis- und Bestands-Monitoring für Meta Ads via Python & Scripts

Management Summary
Eine Bestands- und Preisüberwachung vergleicht Produktdaten eines Onlineshops mit den zugehörigen Meta-Anzeigen. Das soll Werbung erkennen, die eine Pause braucht, etwa bei ausverkaufter Ware oder nicht mehr passenden Preisangaben. So lassen sich unnötige Ausgaben und veraltete Angebote begrenzen. Das vorgestellte Programm erstellt Änderungsvorschläge, statt Kampagnen unmittelbar umzuschalten. Aktuelle Daten und die eindeutige Verbindung zwischen Produkt und Anzeige sind entscheidend: Fehlende Angaben bedeuten keinen leeren Lagerbestand.
Inhalt
Ein Preis- und Bestandsmonitor kann Anzeigen für nicht verfügbare Produkte erkennen und Änderungen vorbereiten. Dafür braucht der Prozess aktuelle Produktdaten, eine eindeutige Anzeigenzuordnung und klare Regeln für vorhandene Pausen. Ein stündlicher Lauf arbeitet weder in Echtzeit noch ohne Verzögerung; bessere Rendite oder vollständig vermiedene unnötige Ausgaben lassen sich daraus nicht garantieren.
1. Zuerst das richtige Objekt bestimmen
Eine einzelne Produktanzeige kann einer SKU zugeordnet werden, der internen Kennung einer bestimmten Produktvariante im Shop. Eine Anzeigengruppe kann dagegen mehrere Anzeigen und Produkte enthalten. Das Pausieren der gesamten Gruppe wegen einer SKU trifft dann auch verfügbare Angebote. Bei Katalog-Anzeigen gehört die Produktverfügbarkeit in den Katalog, etwa über availability; Datenannahme, Verarbeitung und tatsächliche Auslieferung benötigen eigene Kontrollen.
Eine geprüfte Zuordnung aus Werbekonto, Anzeigen-ID und SKU ist belastbarer als ein Namensfragment. Namen können geändert oder mehrfach verwendet werden. Katalog-Anzeigen und Anzeigen mit mehreren Produkten bleiben außerhalb der unten gezeigten Einzelproduktregel.

2. Regeln ohne falsche Gewissheit
- Bestand: Verfügbarer verkaufbarer Bestand muss Reservierungen und den vorgesehenen Umgang mit Vorbestellungen berücksichtigen. Fehlende oder veraltete Daten bedeuten nicht null Bestand. Negative Werte benötigen eine ausdrücklich definierte Behandlung.
- Preis: Varianten, Packungsgröße, Währung, Steuer, Versand und Lieferbarkeit müssen vergleichbar sein. Ein automatisch aus einer Webseite ausgelesener Zahlenwert allein genügt nicht.
- Schwelle: Acht Prozent sind eine Beispielentscheidung, keine belegte allgemeine Grenze für Conversion-Verluste. Bezogen auf den eigenen Preis gilt
(eigener Preis − Vergleichspreis) / eigener Preis. Bei 100 € und 92 € ergibt das genau 8 %; mit dem Vergleichspreis als Nenner wären es rund 8,70 %. - Reaktivierung: Eine manuelle Pause darf nicht durch einen guten Lagerbestand aufgehoben werden. Auch eine vom Monitor verursachte Pause benötigt einen Nachweis, dass keine andere Sperre hinzugekommen ist.
3. Eine isolierte Entscheidungsfunktion
Das folgende Python-Beispiel gibt ausschließlich Vorschläge zurück. Es lädt keinen Produktfeed, liest kein Werbekonto und verändert keine Anzeige. Die vorgelagerte Datenaufbereitung liefert eine geprüfte Einzelproduktzuordnung, den aktuellen konfigurierten Anzeigenstatus und einen expliziten manuellen Sperrzustand. manual_hold=False ist eine bewusst gepflegte Eingabe, kein Meta-API-Feld.
Geldwerte kommen als Text mit Dezimalpunkt, etwa "19.95", Bestand als nicht negative ganze Zahl und Zeitpunkte mit Zeitzone. Das Beispiel begrenzt Preisgrößen auf einen Dezimalexponenten von −6 bis 12 (Werte ab 0,000001 und kleiner als 10.000.000.000.000) und erwartet dreistellige Großbuchstaben als Währungsformat; die tatsächliche ISO-Währung und fachliche Vergleichbarkeit prüft die Datenaufbereitung. Die maximale Datenalterung von einer Stunde ist ebenfalls eine Beispielregel.
from datetime import datetime, timezone
from decimal import Decimal, InvalidOperation
MAX_AGE_SECONDS = 3600 # Example policy, not a Meta requirement.
PRICE_GAP = Decimal("0.08")
def fresh(value, now):
try:
stamp = datetime.fromisoformat(value.replace("Z", "+00:00"))
if stamp.tzinfo is None:
return False
return 0 <= (now - stamp).total_seconds() <= MAX_AGE_SECONDS
except (AttributeError, TypeError, ValueError):
return False
def positive_money(value):
if not isinstance(value, str) or len(value) > 40:
raise ValueError("Money must be a decimal string")
amount = Decimal(value)
if not amount.is_finite() or amount <= 0 or not -6 <= amount.adjusted() <= 12:
raise ValueError("Invalid price")
return amount
def propose(item, ad, now):
"""Pure decision function: no API calls and no status writes."""
if now.tzinfo is None:
raise ValueError("now must include a timezone")
if (ad.get("kind") != "single_product"
or ad.get("sku") != item.get("sku")
or not isinstance(item.get("sku"), str) or not item["sku"]
or not isinstance(ad.get("id"), str) or not ad["id"].isascii()
or not ad["id"].isdigit()):
return "REVIEW", "mapping"
if ad.get("manual_hold") is not False:
return "REVIEW", "manual_or_unknown_hold"
status = ad.get("status")
if status not in {"ACTIVE", "PAUSED"}:
return "REVIEW", "unsupported_status"
stock = item.get("stock")
if type(stock) is not int or stock < 0:
return "REVIEW", "invalid_stock"
if not fresh(item.get("stock_at"), now):
return "REVIEW", "stale_stock"
reason = None
if stock == 0:
reason = "out_of_stock"
else:
if (item.get("comparable") is not True
or item.get("currency") != item.get("competitor_currency")
or not isinstance(item.get("currency"), str)
or len(item["currency"]) != 3
or not item["currency"].isascii()
or not item["currency"].isalpha()
or item["currency"] != item["currency"].upper()
or not fresh(item.get("price_at"), now)
or not fresh(item.get("competitor_at"), now)):
return "REVIEW", "incomparable_or_stale_prices"
try:
own = positive_money(item.get("price"))
other = positive_money(item.get("competitor_price"))
except (ValueError, InvalidOperation):
return "REVIEW", "invalid_price"
# Competitor discount relative to the shop's own price.
if (own - other) / own > PRICE_GAP:
reason = "price_gap"
if reason:
return ("PAUSE_REVIEW" if status == "ACTIVE" else "NO_CHANGE"), reason
if status == "PAUSED":
if ad.get("pause_owner") == "inventory_monitor":
return "RESUME_REVIEW", "conditions_recovered"
return "NO_CHANGE", "pause_not_owned"
return "NO_CHANGE", "conditions_ok"PAUSE_REVIEW bedeutet einen begründeten Pausenvorschlag. RESUME_REVIEW ist ein Prüfauftrag zur möglichen Wiederaufnahme, kein Status ACTIVE. REVIEW kennzeichnet unbrauchbare oder unklare Eingaben; NO_CHANGE löst keine Änderung aus. Das Beispiel betrachtet gültigen Nullbestand unabhängig von fehlenden Vergleichspreisen. Bei positivem Bestand verhindert ein fehlender Preisvergleich eine Freigabeempfehlung.
4. API-Anbindung und tatsächliche Ausführung ergänzen
Für einen produktiven Adapter benötigt die Anwendung passende App-Berechtigungen und zugewiesene Werbekonto-Ressourcen. Eine pauschale Admin-Systemnutzer-Empfehlung ist unnötig. Lesezugriff und Schreibzugriff sind getrennt zu planen; ads_management ermöglicht Änderungen. Auch ein Token ohne festen Ablauftermin kann ungültig werden. Zugangsdaten gehören nicht in Quellcode, Beispielwerte oder Logs.
Die Marketing-API-Version muss ausdrücklich festgelegt und vor Ablauf aktualisiert werden. Die am 24. Oktober 2026 geprüfte Referenz nennt v26.0; ein altes fest eingebautes v20.0 ist keine belastbare Grundlage. Beim Laden einer Anzeigenliste müssen alle Ergebnisseiten verarbeitet werden. Ein größeres limit ersetzt das Abrufen weiterer Ergebnisseiten nicht. Scheitert eine Seite, darf das Teilergebnis nicht als vollständiger erfolgreicher Lauf gelten.
- Vor einer Änderung freigegebene Anzeigen-ID, Kontozuordnung, frische Produktdaten, manuelle Sperren und aktuellen Status erneut prüfen. Eine Vorschlagsdatei kann inzwischen veraltet sein.
- Nur den ausdrücklich ausgewählten Status schreiben. API-Fehler, ungültige JSON-Antworten, Zeitüberschreitungen und Rate-Limits als Fehler behandeln. Nach unklarem Ausgang zuerst den Status lesen, bevor eine Wiederholung erfolgt.
- Erfolgreiche Änderung und anschließenden Statusabgleich dokumentieren. HTTP 200 allein ist kein Auslieferungsnachweis.
effective_statuskann wegen übergeordneter Pausen, Prüfung oder anderer Probleme vom gesetzten Status abweichen. - Eigene Pausen mit Zeitpunkt, vorherigem Status, Regel und Produktdatennachweis in einem dauerhaften Journal festhalten. Wiederaufnahme nur nach zusätzlicher Freigabe; konkurrierende manuelle Änderungen dürfen nicht überschrieben werden.
Automatische Reaktivierung benötigt darüber hinaus einen geregelten Schreibzugriff, eine ausdrückliche Zuständigkeit und eine Strategie für konkurrierende Änderungen. Ein bloßes Feld pause_owner beweist diese Voraussetzungen nicht. Das Beispiel liefert deshalb auch bei einer eigenen Pause lediglich einen Wiederaufnahmevorschlag.
5. Den Monitor selbst überwachen
Ein Zeitplan ersetzt keine Betriebsüberwachung. Zu jedem Lauf gehören Start, Ende, gelesene Seiten, Datenalter, Zahl geprüfter Zuordnungen, Vorschläge und Fehler. Leere Feeds und fehlende Zuordnungen benötigen eine ausdrücklich festgelegte Bewertung. Ein externer Prüfer muss ausbleibende erfolgreiche Läufe erkennen; ein Alarm nur bei Statuswechseln übersieht einen abgestürzten Monitor.
Eine Ausführungssperre lässt nur einen Monitorlauf gleichzeitig zu. Zur Betriebsintegration gehören außerdem eine begrenzte Zahl von Wiederholungen mit Wartezeit, ein sicherer Wechsel der Protokolldateien zur Größenbegrenzung und geprüfte Benachrichtigungen. Diese Komponenten und ein tatsächlicher Meta-Zugriff sind nicht Bestandteil der reinen Entscheidungsfunktion.
Fragen und Antworten
Warum wird ein manuell pausiertes Angebot nicht automatisch aktiviert?
Der Pausengrund kann außerhalb von Bestand und Preis liegen, etwa in einer laufenden Prüfung oder Kampagnenentscheidung. Ein weiterhin vorhandener Bestand widerlegt diesen Grund nicht. Freigabe und Änderungsjournal müssen den aktuellen Zustand berücksichtigen.
Verhindert ein erfolgreicher Lauf alle unnötigen Ausgaben?
Nein. Zwischen Bestandsänderung, Feedübertragung, Verarbeitung, API-Aufruf und tatsächlicher Auslieferung liegen Verzögerungen. Ein Monitor kann diese Lücke verkleinern, aber weder auf null setzen noch einen bestimmten ROAS garantieren. Der ROAS setzt den zugerechneten Umsatz ins Verhältnis zu den Werbekosten. Wirkung und Fehlalarme benötigen Messungen am konkreten Konto.


